Featured image of post タイムスタンプ比較で差分検出

タイムスタンプ比較で差分検出

タイムスタンプ比較による差分検出でバックアップを高速化。mtimeを活用した効率的なファイル同期の実装方法を解説します。

前回は、すべてのファイルを無条件にコピーする「全バックアップ」ツールを作成しました。しかし、ファイル数が多い環境では時間がかかりすぎるという課題がありました。

今回は、バックアップツールの基本機能である 「差分バックアップ」 を実装することで、劇的な高速化を目指します。

前回: シンプルなバックアップから始める | 目次 | 次回: 処理フローを整理する骨格

差分検出の仕組み

差分バックアップの考え方は単純です。「変更されたファイルだけをコピーする」です。では、どうやって「変更された」と判断するのでしょうか?

最も一般的な方法は、ファイルの 更新日時(mtime: modification time) を比較することです。

  1. コピー元(Source)とコピー先(Dest)のファイルが存在するか確認
  2. 存在しないならコピー(新規ファイル)
  3. 存在するなら、両者の mtime を比較
  4. Sourceの方が新しければコピー(更新ファイル)
  5. それ以外は何もしない(スキップ)

実装:タイムスタンプ比較

前回の simple_backup.pl を改良して、timestamp_backup.pl を作ってみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env perl
use strict;
use warnings;
use Path::Tiny;
use File::Copy qw(copy);
use Time::HiRes qw(gettimeofday tv_interval);

my $source_dir = path($ARGV[0] or die "Usage: $0 <source_dir> <dest_dir>");
my $dest_dir   = path($ARGV[1] or die "Usage: $0 <source_dir> <dest_dir>");

die "Source directory not found!" unless $source_dir->is_dir;

print "Start DIFF backup from $source_dir to $dest_dir\n";
my $t0 = [gettimeofday];

my $count = 0;
my $skip_count = 0;
my $iterator = $source_dir->iterator({ recurse => 1 });

while (my $path = $iterator->()) {
    next if $path->is_dir;

    my $rel_path = $path->relative($source_dir);
    my $to_path  = $dest_dir->child($rel_path);

    # 差分判定ロジック
    my $should_copy = 0;

    if (! $to_path->exists) {
        # 新規ファイル
        $should_copy = 1;
    }
    elsif ($path->stat->mtime > $to_path->stat->mtime) {
        # 更新されている(Sourceの方が新しい)
        $should_copy = 1;
    }
    else {
        # 変更なし
        $skip_count++;
    }

    if ($should_copy) {
        $to_path->parent->mkpath unless $to_path->parent->exists;
        copy($path, $to_path) or die "Failed to copy $path: $!";
        
        # 重要: コピー先のmtimeをコピー元と同じにする
        # File::Copy だけでは mtime が「コピーした時刻」になってしまうことがあるため、
        # utime で明示的に揃えるのが確実です(OSやファイルシステムによる)。
        utime($path->stat->atime, $path->stat->mtime, $to_path);
        
        $count++;
    }
}

my $elapsed = tv_interval($t0);
printf "Backup completed! Copied: %d, Skipped: %d in %.2f seconds.\n", $count, $skip_count, $elapsed;

効果測定

さっそくベンチマークを取ってみましょう。

1回目(初回バックアップ)

1
2
3
$ perl timestamp_backup.pl ./my_photos ./backup_diff
Start DIFF backup from ./my_photos to ./backup_diff
Backup completed! Copied: 1500, Skipped: 0 in 5.30 seconds.

初回は全ファイルコピーなので、前回と同じくらいの時間がかかります。

2回目(差分バックアップ)

1
2
3
$ perl timestamp_backup.pl ./my_photos ./backup_diff
Start DIFF backup from ./my_photos to ./backup_diff
Backup completed! Copied: 0, Skipped: 1500 in 0.08 seconds.

なんと 0.08秒! 前回は5秒かかっていたので、約60倍の高速化です。これが差分バックアップの威力です。

新たな課題:ロジックの複雑化

高速化には成功しましたが、コードを見てみると少し気になる点が出てきました。while ループの中にある if 文です。

1
2
3
4
5
6
7
    if (! $to_path->exists) {
        $should_copy = 1; # 新規
    }
    elsif ($path->stat->mtime > $to_path->stat->mtime) {
        $should_copy = 1; # 更新
    }
    # ...

今は「タイムスタンプ比較」だけですが、もし以下のような要望が出たらどうなるでしょうか?

  • 「ファイルサイズが違う場合もコピーしたい」
  • 「MD5ハッシュ値で厳密に比較したい」
  • 「特定の拡張子は除外したい」
  • 「古いファイルを削除する機能(同期)も欲しい」

これらをすべて今の while ループの中に if/elsif で追加していくと、あっという間にコードは読みづらく、バグの温床になります。「コピーする・しない」の判断ロジックと、「再帰的にファイルを巡回する」ロジックが混ざり合っているのが原因です。

次回予告

この問題を解決するために、次回からいよいよデザインパターンを導入します。

まずは Template Method パターン を使って、バックアップ処理の「骨格(フロー)」と「具体的な中身」をきれいに分離し、見通しの良いコードにリファクタリングしていきましょう。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。