空想犬猫記

※当日記では、犬も猫も空想も扱っておりません。(旧・エト記)

写真のバックアップ

デジカメを手に入れた2000年くらいから、何とか写真を失わずにバックアップしてきたが、子供ができてからその量が激増し、300GBを越えようとしている。統一的な写真のバックアップの必要性を感じ色々検討してみた。Macを使っていた頃はiPhotoを利用し、Windowsに移ってからはまた別のソフトで写真をインポートしていた。重複したファイルも沢山ある。
またiPhotoは、インポート時にタイムスタンプを勝手に変更するという問題が(少なくとも私が使っていたバージョンには)あって好きじゃなかったし、そのほかにも色々と問題があって、スクリプトで前処理して直した後、結局自分で管理することにした。

写真(と動画)の管理のポリシー

  • マスターは、カメラからインポートした生データ。加工したものは基本的にバックアップ対象としない。
  • マスターのバックアップをクラウド上に1つ、ハードディスクに2つ
  • 年単位で、写真、動画各1つずつフォルダを作成
例)D:\Family\Photos\2012、Family\Movies\2012
  • ファイル名は「YYYY-mm-dd-HHMMSS-(sha1ハッシュ).(拡張子)」とする。ハッシュを使うのは、カメラから同じファイルを複数回インポートするのを安全に避けるため。タイムスタンプのみだと、連続撮影で時間がかぶることがあるし、タイムスタンプとオリジナルのファイル名の組み合わせだと、サマータイムなどでローカル時間がシフトしたときに同じファイルを重複して読み込んでしまう問題がある。そうかといってUTCをファイル名に使うと、それはそれで使い勝手が悪い。ファイルの一意性だけであればハッシュ値をファイル名にすればよいが、何らかの拍子にタイムスタンプが吹っ飛んだり、スクリプトでいじったり、エクスプローラでソートするときの利便性を考え、日付をプレフィックスとして追加する。
例)2009-01-25-104914-f3eb46588af6efd7650cca8db5ad30b8f16e47fe.avi
  • ファイル名および拡張子は小文字に正規化する

Rubyによる実装

実装してみた。Cygwin上でインポート元のフォルダをカレントディレクトリにあわせ、以下のスクリプトを実行すると、BACKUP_BASE_DIR以下に次々にファイルを放り込む仕組み。is_copyを制御することで、コピーか移動かを調整できる。

require 'find'
require 'digest/sha1'
require 'fileutils'
require File.join(File.dirname(__FILE__), 'conf')

#------------------------------------------------------------------------------

def sha1(path)
  Digest::SHA1.hexdigest(File.open(path, "rb").read)
end

#------------------------------------------------------------------------------

def move_to_family(dir, opt = {:is_noop => true, :is_copy => true})
  dir = File.expand_path(dir)
  Find.find(dir) do |path|
    next if File.directory?(path)
    next if File.basename(path).match(/^\./)
    [['Photos', /\.(jpg|png)$/i],
     ['Movies', /\.(mts|avi|mov|m4v|mp4|divx|flv)$/i]].each do |med, reg_ext|
      next unless path.match(reg_ext)
      ext = $1.downcase
      mtime = File.mtime(path)
      str = sprintf("#{BACKUP_BASE_DIR}/Family/#{med}/%s-%s.#{ext.downcase}",
                    mtime.strftime("%Y/%Y-%m-%d-%H%M%S"),
                    sha1(path))

      if File.exist?(str) && !FileUtils.cmp(str, path)
        raise "SHA1 collision detected. #{path}"
      end

      if path == str
        raise "source == target!!!"
      end

      if opt[:is_copy]
        if File.file?(str)
          puts "SKIP #{path} => #{str}"
          next
        end
        FileUtils.cp(path, str, {:preserve => true, :verbose => true, :noop => opt[:is_noop]})
      else
        FileUtils.mkdir_p(File.dirname(str)) unless opt[:is_noop]
        print '*' if File.file?(str)
        FileUtils.mv(path, str, {:verbose => true, :noop => opt[:is_noop]})
      end
    end
  end
end

#------------------------------------------------------------------------------

if $0 == __FILE__
  move_to_family('.', {:is_noop => true, :is_copy => false})
end

留意点など

  • 運用上タイムスタンプが狂うことは無いはずなので「タイムスタンプが異なるがハッシュ値が同じ」ファイルは、何か予期しないことが起こっている。念のため異なるファイルとして扱う。サマータイムタイムゾーンがシフトするケースがまさにこれなのだが、あまり凝ったことをすると色々負担になるのでしない。
  • is_noop = true でテストした後、出力が正しそうだったら is_noop = false にして本番。自分の目でパスの指定間違いなど、万が一のミスをチェックする。

おわりに

実行するたびに緊張していやな汗がでるが、意図した結果は得られた模様。

色々なところに散らばった写真を年単位で一フォルダに重複無く突っ込むことができたので、それをそのままSmugMugにバックアップ。とりあえず一番コストパフォーマンスの高かったSmugMugを選んでみたが、どこまで安全なストレージなのかは正直なところ分からない。

子供が生まれて最初の一年は、化学的に安定しているというDVD-RWに全てコピーしていた。しかし100GBを越えたあたりから断念。

まだまだ改良の余地があるけど、これが現実解かな。