写真のバックアップ
デジカメを手に入れた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
留意点など
おわりに
実行するたびに緊張していやな汗がでるが、意図した結果は得られた模様。
色々なところに散らばった写真を年単位で一フォルダに重複無く突っ込むことができたので、それをそのままSmugMugにバックアップ。とりあえず一番コストパフォーマンスの高かったSmugMugを選んでみたが、どこまで安全なストレージなのかは正直なところ分からない。
子供が生まれて最初の一年は、化学的に安定しているというDVD-RWに全てコピーしていた。しかし100GBを越えたあたりから断念。
まだまだ改良の余地があるけど、これが現実解かな。