C言語でフォルダ(ディレクトリ)を丸ごとコピーする

※2012年10月1日、Ubuntu 12.10でコンパイルできるように修正。

※より複雑なコードを見たい場合はsnowcpのソースを見てやって下さい。



Linuxで動作する良さ気なサンプルコードを見つけられなかったので自分で書いてみた。実用性は皆無。

コンパイルにはGLibが必要なのでインストールしておく。

sudo apt-get install build-essential libgtk-3-dev

コンパイルするときは以下のように実行する。

gcc *.c -Wall -Wextra -O2 `pkg-config --cflags --libs gtk+-3.0` -std=c99 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64

以下、ソース。4つのファイルに分割してある。

/* a.c */

#include <stdio.h>
#include <limits.h>
#include <stdlib.h>

#include <linux/limits.h>
#include <unistd.h>

#include <glib.h>

void check_dir(char *src);

int main(int argc, char *argv[])
{
  char src[PATH_MAX];
  char dst[PATH_MAX];

  if(argc == 3)
  {
    if(g_file_test(argv[1], G_FILE_TEST_IS_DIR) && g_file_test(argv[2], G_FILE_TEST_IS_DIR))
    {
      if((realpath(argv[1], src) == NULL) || (realpath(argv[2], dst) == NULL))
      {
        fprintf(stderr, "絶対パスの作成に失敗しました\n");
      }
      else if(chdir(dst) == -1)
      {
        fprintf(stderr, "コピー先にアクセスできません\n");
      }
      else
      {
        check_dir(src);
      }
    }
    else
    {
      fprintf(stderr, "引数はフォルダである必要があります\n");
    }
  }
  else
  {
    puts("引数が正しくありません");
  }

  return EXIT_SUCCESS;
}

/* b.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <dirent.h>

#include <glib.h>

void cp_file(char *src, char *dst);

void check_dir(char *src)
{
  DIR *dp;
  char *src_basename = g_path_get_basename(src);

  if(mkdir(src_basename, 0755))
  {
    fprintf(stderr, "フォルダの作成に失敗しました\n");
    return;
  }
  else if(chdir(src_basename) == -1)
  {
    fprintf(stderr, "コピー先にアクセスできません\n");
    return;
  }
  else if((dp = opendir(src)) == NULL)
  {
    fprintf(stderr,"%sを開けません\n", src);

    if(chdir("..") == -1)
    {
      fprintf(stderr, "コピー先にアクセスできません\n");
      exit(EXIT_FAILURE);
    }

    return;
  }

  struct dirent *entry;

  while((entry = readdir(dp)) != NULL)
  {
    /*
     * 引数の最後のNULLを忘れると、missing sentinel in function call、という警告が出る。
     * g_build_pathを使わない場合はstrcatやstrncatを使うことになるか。
     * strlcatはglibcには無いらしいのでg_strlcatを使う手も。
     * いや、sprintf使った方が楽か。
    */
    char *next_file = g_build_path(G_DIR_SEPARATOR_S, src, entry->d_name, NULL);

    if(g_file_test(next_file, G_FILE_TEST_IS_DIR))
    {
      if(strcmp(".", entry->d_name) == 0 || strcmp("..", entry->d_name) == 0)
      {
        continue;
      }

      check_dir(next_file);
    }
    else if(g_file_test(next_file, G_FILE_TEST_IS_REGULAR))
    {
      cp_file(next_file, entry->d_name);
    }

    if(next_file != NULL)
    {
      free(next_file);
      next_file = NULL;
    }
  }

  if(closedir(dp) == -1)
  {
    fprintf(stderr, "コピー先でエラーが発生しました\n");
  }

  if(chdir("..") == -1)
  {
    fprintf(stderr, "コピー先にアクセスできません\n");
    exit(EXIT_FAILURE);
  }
}

/* c.c */

#define FILE_BUF 8192000

#include <stdio.h>

char buf[FILE_BUF];

void ch_time_and_mod(char *src, char *dst);

void cp_file(char *src, char *dst)
{
  FILE *src_f;
  FILE *dst_f;
  int rcount = 0;
  int wcount = 0;

  printf("%s\n", dst);

  if((src_f = fopen(src, "rb")) == NULL)
  {
    fprintf(stderr, "%sを開けません\n", src);
    return;
  }

  setvbuf(src_f, NULL, _IOFBF, FILE_BUF);

  if((dst_f = fopen(dst, "wb")) == NULL)
  {
    fprintf(stderr, "%sを開けません\n", dst);
    fclose(src_f);
    return;
  }

  setvbuf(dst_f, NULL, _IOFBF, FILE_BUF);

  while((rcount = fread(buf, 1, FILE_BUF, src_f)) > 0)
  {
    if((wcount = fwrite(buf, 1, rcount, dst_f)) != rcount)
    {
      fprintf(stderr, "%sに書き込む際にエラーが発生しました\n", dst);
      break;
    }
  }

  if(fclose(src_f) == EOF)
  {
    fprintf(stderr, "コピー元ファイルを閉じる際のエラー\n");
  }
  
  if(fclose(dst_f) == EOF)
  {
    fprintf(stderr, "コピー先ファイルを閉じる際のエラー\n");
  }

  ch_time_and_mod(src, dst);
}

/* d.c */

#define _GNU_SOURCE

#include <stdio.h>

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

void ch_time_and_mod(char *src, char *dst)
{
  struct stat stat_buf;

  if(stat(src, &stat_buf) == -1)
  {
    fprintf(stderr, "%s : ファイル情報取得エラー\n", dst);
    return;
  }
  else
  {
    if(chmod(dst, stat_buf.st_mode) == -1)
    {
      fprintf(stderr, "%s : ファイル情報変更エラー\n", dst);
    }
    else
    {
      struct timespec time_s[2];

      time_s[0].tv_sec = stat_buf.st_atime;
      time_s[1].tv_sec = stat_buf.st_mtime;

      time_s[0].tv_nsec = stat_buf.st_atim.tv_nsec;
      time_s[1].tv_nsec = stat_buf.st_mtim.tv_nsec;

      /* 第四引数は0 or AT_SYMLINK_NOFOLLOW */
      if(utimensat(AT_FDCWD, dst, time_s, AT_SYMLINK_NOFOLLOW) == -1)
      {
        fprintf(stderr, "%s : ファイル情報変更エラー\n", dst);
      }
    }
  }
}

関連:C言語でファイルをコピーする(マルチスレッド&ダイレクトI/O編)







以下は修正前の古いコード。







※2010年9月14日。ちょっとだけ修正。

ググってみたけどLinuxで動作する良さ気なサンプルコードを見つけられなかったので自分で書いてみた。実用性は皆無。2GB以上のファイルを扱うには#define _FILE_OFFSET_BITS 64が必要とか、lstat()じゃなくてlstat64()を使うとか、()utime()は廃止予定とか、勉強になった。

ソースコード

/*
gcc -Wall -std=c99 -D_FILE_OFFSET_BITS=64 `pkg-config --cflags --libs glib-2.0` -o dir_copy dir_copy.c

コンパイル時のオプションではなくdefineを使用した場合は
gcc -Wall -std=c99 `pkg-config --cflags --libs glib-2.0` -o dir_copy dir_copy.c

gcc -Wall -std=gnu99 `pkg-config --cflags --libs glib-2.0` -o dir_copy dir_copy.c
とした場合は、#define __USE_ATFILE が要らない
*/

#define _FILE_OFFSET_BITS 64

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <limits.h>

#define __USE_ATFILE
#include <fcntl.h>
#undef __USE_MISC

#include <sys/stat.h>
#include <sys/time.h>
#include <unistd.h>
#include <utime.h>
#include <glib.h>

#define IO_BUF   8192000
#define FILE_BUF 8192000
#define PATH_LEN 8192

/*
ヘッダファイルをincludeし忘れたり、プロトタイプ宣言が無い外部関数を呼び出した場合、
引数や戻り値の型がintになるらしい(C99の場合?古いCではプロトタイプ宣言は無いことも多いらしい)。

↓することでwarningが出なくなるが、悪手。ちゃんとヘッダをincludeすればいいだけの話。
*/
char *realpath(const char *path, char *resolved_path);

void file_cp(char *from, char *to);
void check_dir(char *from);

char buf[IO_BUF];       /*ファイルを読み書きする際のバッファ*/

void file_cp(char *from, char *to)
{
  struct stat     stat_buf;          /*ファイルの状態を入れる構造体*/
/*  struct utimbuf  times;*/             /*タイムスタンプ変更用構造体。utime用*/
  struct timespec time_s[2];         /*タイムスタンプ変更用構造体。utimensat用*/
  FILE            *ffrom;
  FILE            *tto;
  int             rcount;
  int             wcount;

  printf("%sのコピーを開始します\n", to);

  if((ffrom = fopen(from, "rb")) == NULL)
  {
    fprintf(stderr, "\n");
    printf("コピー元ファイルを開けません\n");
    return;
  }

  setvbuf(ffrom, NULL, _IOFBF, FILE_BUF);

  if((tto = fopen(to, "wb")) == NULL)
  {
    fprintf(stderr, "\n");
    printf("コピー先ファイルを開けません\n");
    return;
  }

  setvbuf(tto, NULL, _IOFBF, FILE_BUF);

  while((rcount = fread(buf, 1, IO_BUF, ffrom)) > 0)
  {
    if((wcount = fwrite(buf, 1, rcount, tto)) != rcount)
    {
      fprintf(stderr, "\n");
      printf("ファイル書き込み時にエラーが発生しました\n");
      exit(1);
    }
  }

  if(fclose(ffrom) == EOF)
  {
    printf("コピー元ファイルを閉じる際のエラー\n");
    exit(1);
  }
  
  if(fclose(tto) == EOF)
  {
    printf("コピー先ファイルを閉じる際のエラー\n");
    exit(1);
  }

  /*元ファイルの状態を取得*/
  lstat64(from, &stat_buf);
  chmod(to, stat_buf.st_mode);
  time_s[0].tv_sec  = stat_buf.st_atime;
  time_s[0].tv_nsec = stat_buf.st_atimensec;
  time_s[1].tv_sec  = stat_buf.st_mtime;
  time_s[1].tv_nsec = stat_buf.st_mtimensec;
  /*第四引数は0かAT_SYMLINK_NOFOLLOW*/
  utimensat(AT_FDCWD, to, time_s, AT_SYMLINK_NOFOLLOW);
/*
  times.actime = stat_buf.st_atime;
  times.modtime = stat_buf.st_mtime;
  utime(to, &times);
*/
}

/*引数は絶対パス*/
void check_dir(char *from)
{
  char           current_dir[PATH_LEN];     /*CWD*/
  char           *separator = "/";          /*パスの区切り文字. Windowsなら\*/
  char           *from_basename;            /*コピー元ファイル名・フォルダ名*/
  char           *next_file;
  DIR            *dp;
  struct dirent  *entry;
  struct stat    stat_buf;
  struct utimbuf times;                     /*タイムスタンプ変更用構造体*/
  gboolean       gb;

  from_basename = g_path_get_basename(from);

  if(mkdir(from_basename, 0755))
  {
    puts("mkdirに失敗しました");
    return;
  }

  chdir(from_basename);
  getcwd(current_dir, sizeof(current_dir));
  printf("cwd is : %s \n", current_dir);

  if((dp = opendir(from)) == NULL)
  {
    fprintf(stderr,"ディレクトリを開けません : %s\n", from);
    return;
  }

  while((entry = readdir(dp)) != NULL)
  {
    if(next_file != NULL)
    {
      free(next_file);
      next_file = NULL;
    }

    /*
    引数の最後のNULLを忘れると、missing sentinel in function call、という警告が出る。
    g_build_pathを使わない場合はstrcatやstrncatを使うことになるか。
    strlcatはglibcには無いらしいのでg_strlcatを使う手も。
    いや、sprintf使った方が楽か。
    */
    next_file = g_build_path(separator, from, entry->d_name, NULL);

    gb = g_file_test(next_file, G_FILE_TEST_IS_DIR);
    if(gb)
    {
      if(strcmp(".", entry->d_name) == 0 || strcmp("..", entry->d_name) == 0)
      {
        continue;
      }

      check_dir(next_file);
    }

    else
    {
      file_cp(next_file, entry->d_name);
    }
  }

  chdir("..");

  lstat(from, &stat_buf);
  times.actime  = stat_buf.st_atime;
  times.modtime = stat_buf.st_mtime;
  utime(from_basename, &times);

  if(next_file != NULL)
  {
    free(next_file);
    next_file = NULL;
  }

  closedir(dp);
}

int main(int argc, char *argv[])
{
  gboolean gb;
  char    current_dir[PATH_LEN];
  char    from[PATH_LEN];
  char    to[PATH_LEN];

  if(argc == 3)
  {
    gb = g_file_test(argv[1], G_FILE_TEST_IS_DIR);
    if(gb)
    {
      gb = g_file_test(argv[2], G_FILE_TEST_IS_DIR);
      if(gb)
      {
        if((realpath(argv[1], from) == NULL) || (realpath(argv[2], to) == NULL))
        {
          puts("絶対パスの作成に失敗しました");
          exit(1);
        }
        getcwd(current_dir, sizeof(current_dir));
        printf("cwd is : %s \n", current_dir);
        printf("from   : %s\n", from);
        printf("to     : %s\n", to);

        chdir(to);
        check_dir(from);
      }
    }
  }

  else
  {
    puts("引数が正しくありません");
  }

  return 0;
}