70 votes

Comment copier un fichier sous Unix en utilisant C ?

Je cherche l'équivalent Unix de l'outil Win32 CopyFile Je ne veux pas réinventer la roue en écrivant ma propre version.

6 votes

Pour ne pas réinventer la roue, compilez GNU coreutils, à ma connaissance, il possède une bibliothèque statique pour copier les fichiers dans son arbre de construction, utilisée par cp et d'autres. Il supporte le sparseness et la vache btrfs.

2 votes

Sous-ensemble linux : stackoverflow.com/questions/7463689/

68voto

caf Points 114951

Il n'est pas nécessaire d'appeler des API non portables telles que sendfile ou de payer des services publics externes. La même méthode qui a fonctionné dans les années 70 fonctionne encore aujourd'hui :

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int cp(const char *to, const char *from)
{
    int fd_to, fd_from;
    char buf[4096];
    ssize_t nread;
    int saved_errno;

    fd_from = open(from, O_RDONLY);
    if (fd_from < 0)
        return -1;

    fd_to = open(to, O_WRONLY | O_CREAT | O_EXCL, 0666);
    if (fd_to < 0)
        goto out_error;

    while (nread = read(fd_from, buf, sizeof buf), nread > 0)
    {
        char *out_ptr = buf;
        ssize_t nwritten;

        do {
            nwritten = write(fd_to, out_ptr, nread);

            if (nwritten >= 0)
            {
                nread -= nwritten;
                out_ptr += nwritten;
            }
            else if (errno != EINTR)
            {
                goto out_error;
            }
        } while (nread > 0);
    }

    if (nread == 0)
    {
        if (close(fd_to) < 0)
        {
            fd_to = -1;
            goto out_error;
        }
        close(fd_from);

        /* Success! */
        return 0;
    }

  out_error:
    saved_errno = errno;

    close(fd_from);
    if (fd_to >= 0)
        close(fd_to);

    errno = saved_errno;
    return -1;
}

1 votes

@Caf : OMG....g.o.t.o..... :) Ton code est plus sain que le mien de toute façon... ;) L'ancienne boucle avec lecture/écriture est la plus portable... +1 de ma part...

25 votes

Je trouve que l'utilisation contrôlée de goto peut être utile pour consolider le chemin de traitement des erreurs en un seul endroit.

12 votes

Non utilisable pour un usage général. La copie d'un fichier est plus qu'un simple flux de données. Qu'en est-il des fichiers épars ou des attributs étendus ? C'est encore une fois pourquoi l'API de Windows, aussi moche soit-elle, bat Linux.

28voto

Mahmoud Al-Qudsi Points 14815

Il n'existe pas de fonction CopyFile équivalente intégrée dans les API. Mais sendfile peut être utilisé pour copier un fichier en mode noyau, ce qui est une solution plus rapide et meilleure (pour de nombreuses raisons) que d'ouvrir un fichier, de le lire en boucle dans un tampon et d'écrire la sortie dans un autre fichier.

Mise à jour :

À partir de la version 2.6.33 du noyau Linux, la limitation exigeant la sortie de sendfile pour être un socket a été levée et le code original fonctionnerait à la fois sur Linux et - cependant, à partir de OS X 10.9 Mavericks, sendfile sur OS X exige maintenant que la sortie soit un socket et le code ne fonctionnera pas !

L'extrait de code suivant devrait fonctionner sur la plupart des systèmes OS X (à partir de 10.5), (Free)BSD, et Linux (à partir de 2.6.33). L'implémentation est "zero-copy" pour toutes les plateformes, ce qui signifie que tout est fait dans l'espace noyau et qu'il n'y a pas de copie de tampons ou de données dans et hors de l'espace utilisateur. C'est à peu près la meilleure performance que vous pouvez obtenir.

#include <fcntl.h>
#include <unistd.h>
#if defined(__APPLE__) || defined(__FreeBSD__)
#include <copyfile.h>
#else
#include <sys/sendfile.h>
#endif

int OSCopyFile(const char* source, const char* destination)
{    
    int input, output;    
    if ((input = open(source, O_RDONLY)) == -1)
    {
        return -1;
    }    
    if ((output = creat(destination, 0660)) == -1)
    {
        close(input);
        return -1;
    }

    //Here we use kernel-space copying for performance reasons
#if defined(__APPLE__) || defined(__FreeBSD__)
    //fcopyfile works on FreeBSD and OS X 10.5+ 
    int result = fcopyfile(input, output, 0, COPYFILE_ALL);
#else
    //sendfile will work with non-socket output (i.e. regular file) on Linux 2.6.33+
    off_t bytesCopied = 0;
    struct stat fileinfo = {0};
    fstat(input, &fileinfo);
    int result = sendfile(output, input, &bytesCopied, fileinfo.st_size);
#endif

    close(input);
    close(output);

    return result;
}

EDITAR : Remplacé l'ouverture de la destination par l'appel à creat() comme nous voulons le drapeau O_TRUNC à spécifier. Voir le commentaire ci-dessous.

4 votes

Selon la page de manuel, l'argument de sortie de sendfile doit être un socket. Êtes-vous sûr que cela fonctionne ?

0 votes

Le prototype de ma page man (OS X) : int sendfile(int fd, int s, off_t offset, off_t *len, struct sf_hdtr *hdtr, int flags); Le paramètre de sortie est fd - descripteur de fichier. En tout cas, je l'ai testé rapidement (d'où la version non-C++ mise à jour) et ça a marché :)

1 votes

Pour Linux, Jay Conrod a raison - les out_fd de sendfile pouvait être un fichier ordinaire dans les noyaux 2.4, mais il doit maintenant prendre en charge l'option sendpage API interne du noyau (ce qui signifie essentiellement pipe ou socket). sendpage est implémenté différemment sur différents UNIX - il n'y a pas de sémantique standard pour cela.

22voto

plinth Points 26817

Il est facile d'utiliser fork/execl pour lancer cp qui fera le travail pour vous. Cette méthode présente des avantages par rapport à system en ce sens qu'elle n'est pas sujette à une attaque de type Bobby Tables et que vous n'avez pas besoin d'assainir les arguments au même degré. De plus, puisque system() vous demande de rassembler l'argument de la commande, vous ne risquez pas d'avoir un problème de dépassement de tampon dû à une vérification bâclée de sprintf().

L'avantage d'appeler directement cp au lieu de l'écrire est de ne pas avoir à se soucier de l'existence d'éléments du chemin cible dans la destination. Faire cela dans le code "roll-you-own" est source d'erreurs et fastidieux.

J'ai écrit cet exemple en ANSI C et je me suis contenté de la gestion des erreurs la plus élémentaire, à part cela, il s'agit d'un code simple.

void copy(char *source, char *dest)
{
    int childExitStatus;
    pid_t pid;
    int status;
    if (!source || !dest) {
        /* handle as you wish */
    }

    pid = fork();

    if (pid == 0) { /* child */
        execl("/bin/cp", "/bin/cp", source, dest, (char *)0);
    }
    else if (pid < 0) {
        /* error - couldn't start process - you decide how to handle */
    }
    else {
        /* parent - wait for child - this has all error handling, you
         * could just call wait() as long as you are only expecting to
         * have one child process at a time.
         */
        pid_t ws = waitpid( pid, &childExitStatus, WNOHANG);
        if (ws == -1)
        { /* error - handle as you wish */
        }

        if( WIFEXITED(childExitStatus)) /* exit code in childExitStatus */
        {
            status = WEXITSTATUS(childExitStatus); /* zero is normal exit */
            /* handle non-zero as you wish */
        }
        else if (WIFSIGNALED(childExitStatus)) /* killed */
        {
        }
        else if (WIFSTOPPED(childExitStatus)) /* stopped */
        {
        }
    }
}

1 votes

+1 pour un autre long, détaillé, slog. Cela vous fait vraiment apprécier la forme "vecteur"/liste de system() en perl. Hmm. Peut-être qu'une fonction de type système avec un tableau d'argv serait sympa à avoir ?!?

0 votes

... après tout, elle a été implémentée il y a 17 ans dans la glibc, et est devenue une fonction standard 10 ans avant que votre réponse ne soit écrite ...

4voto

t0mm13b Points 21031

Il existe un moyen de le faire, sans recourir à l'option system vous devez incorporer un wrapper comme celui-ci :

#include <sys/sendfile.h>
#include <fcntl.h>
#include <unistd.h>

/* 
** http://www.unixguide.net/unix/programming/2.5.shtml 
** About locking mechanism...
*/

int copy_file(const char *source, const char *dest){
   int fdSource = open(source, O_RDWR);

   /* Caf's comment about race condition... */
   if (fdSource > 0){
     if (lockf(fdSource, F_LOCK, 0) == -1) return 0; /* FAILURE */
   }else return 0; /* FAILURE */

   /* Now the fdSource is locked */

   int fdDest = open(dest, O_CREAT);
   off_t lCount;
   struct stat sourceStat;
   if (fdSource > 0 && fdDest > 0){
      if (!stat(source, &sourceStat)){
          int len = sendfile(fdDest, fdSource, &lCount, sourceStat.st_size);
          if (len > 0 && len == sourceStat.st_size){
               close(fdDest);
               close(fdSource);

               /* Sanity Check for Lock, if this is locked -1 is returned! */
               if (lockf(fdSource, F_TEST, 0) == 0){
                   if (lockf(fdSource, F_ULOCK, 0) == -1){
                      /* WHOOPS! WTF! FAILURE TO UNLOCK! */
                   }else{
                      return 1; /* Success */
                   }
               }else{
                   /* WHOOPS! WTF! TEST LOCK IS -1 WTF! */
                   return 0; /* FAILURE */
               }
          }
      }
   }
   return 0; /* Failure */
}

L'échantillon ci-dessus (le contrôle d'erreur est omis !) emploie open , close y sendfile .

Editar: Comme caf a mis en évidence un condition de course peut se produire entre le open y stat alors j'ai pensé rendre cela un peu plus robuste... Gardez à l'esprit que le mécanisme de verrouillage varie d'une plateforme à l'autre... sous Linux, ce mécanisme de verrouillage avec lockf suffirait. Si vous voulez rendre ce système portable, utilisez la fonction #ifdef des macros pour faire la distinction entre les différentes plates-formes/compilateurs... Merci caf pour avoir repéré cela... Il y a un lien vers un site qui a donné "routines de verrouillage universel". aquí .

0 votes

Je ne suis pas sûr à 100% du prototype sendfile, je pense que je me suis trompé dans l'un des paramètres... gardez cela à l'esprit... :)

0 votes

Vous avez une condition de course - le fichier que vous avez ouvert en tant que fdSource et le fichier que vous avez stat()ed ne sont pas nécessairement les mêmes.

0 votes

@caf : Pouvez-vous donner plus de détails comme je le regarde et comment peut-il y avoir une condition de course ? Je modifierai la réponse en conséquence...merci de me le faire savoir....

1voto

Roboprog Points 2005
sprintf( cmd, "/bin/cp -p \'%s\' \'%s\'", old, new);

system( cmd);

Ajouter quelques contrôles d'erreurs...

Sinon, ouvrir les deux et boucler sur la lecture/écriture, mais ce n'est probablement pas ce que vous voulez.

...

MISE À JOUR pour répondre à des préoccupations valables en matière de sécurité :

Plutôt que d'utiliser "system()", faites un fork/wait, et appelez execv() ou execl() dans le fils.

execl( "/bin/cp", "-p", old, new);

0 votes

Cela ne fonctionne pas pour les fichiers dont le nom contient des espaces (ou des guillemets, des barres obliques inversées, des signes de dollar, etc. J'utilise assez souvent des espaces dans les noms de fichiers.

0 votes

Aïe. C'est vrai. Ajoute des guillemets simples autour des noms de fichiers dans le sprintf().

0 votes

D'accord, c'est un fromage suisse (voir les préoccupations de sécurité valables dans les commentaires ailleurs), mais si vous avez un environnement relativement contrôlé, cela peut être utile.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X