2 # archive_photos archives photos into a directory structure
3 # and is released under the terms of the GNU GPL version 3, or any
4 # later version, at your option. See the file README and COPYING for
6 # Copyright 2013 by Don Armstrong <don@donarmstrong.com>.
17 archive_photos - archives photos into a directory structure
21 archive_photos [options] path/to/photos [additional photos]
24 --recurse, -r recurse into subdirectories (default)
25 --archive-dir, -a directory to archive into (~/media/photos)
26 --debug, -d debugging level (Default 0)
27 --help, -h display this help
28 --man, -m display manual
34 =item B<--recurse, -r>
36 Recurse into subdirectories (default; use --no-recurse to disable
38 =item B<--archive-dir, -a>
40 Directory to archive to (default is ~/media/photos)
44 Debug verbosity. (Default 0)
48 Display brief usage information.
66 use Image::ExifTool qw(ImageInfo);
67 use POSIX qw(strftime);
69 use File::Path qw(make_path);
70 use File::Basename qw(dirname basename);
71 use Date::Parse qw(str2time);
72 use IPC::System::Simple qw(capturex);
73 use Digest::SHA qw(sha256_hex);
75 my %options = (debug => 0,
78 archive_dir => User->Home."/media/photos",
83 'archive_dir|archive-dir=s',
85 'debug|d+','help|h|?','man|m');
87 pod2usage() if $options{help};
88 pod2usage({verbose=>2}) if $options{man};
90 $DEBUG = $options{debug};
94 push @USAGE_ERRORS,"You must give at least one directory";
97 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
101 # loads files into @files as appropriate
103 if (-d $_ and not $options{recurse}) {
104 $File::Find::prune = 1;
107 if (-f $_ and $_ =~ /\.(?:NEF|JPG|MOV)$/i) {
108 push @files,$File::Find::name;
114 for my $arg (@ARGV) {
122 # search all of the directories and store the files in @files;
123 find(\&file_pusher,@dirs) if @dirs;
125 # find already existing files
127 find(sub { if (-f $_ or -l $_) {$existing_files{$_} = $File::Find::name;} },$options{archive_dir});
129 for my $file (@files) {
130 my $file_basename=basename($file);
131 # find out when the photo was shot
132 my $info = ImageInfo($file);
133 if (not defined $info->{CreateDate}) {
134 print STDERR "No date information for $file\n";
137 my $epoch = str2time($info->{CreateDate});
138 my $dir = strftime('%Y/%m_%B/%Y_%m_%d/',localtime($epoch));
139 if (not -d $options{archive_dir}.'/'.$dir) {
140 make_path($options{archive_dir}.'/'.$dir) or
141 die "Unable to make dir $!";
143 my $end_location = $options{archive_dir}.'/'.$dir.$file_basename;
144 if (-e $end_location) {
145 print STDERR "$file already exists in $end_location\n";
146 } elsif (exists $existing_files{$file_basename} and
147 are_the_same_files({name => $existing_files{$file_basename}},
148 {name => $file, info => $info})) {
149 print STDERR "$file already exists in $existing_files{$file_basename}\n";
151 print STDERR "copying $file to $end_location\n";
152 copy($file,$end_location);
156 # return true if the files are close enough that we should consider it
158 sub are_the_same_files{
160 # load the exif information if we can
164 if (not exists $f->{info} or not defined $f->{info}) {
165 $f->{info} = ImageInfo($f->{name});
171 # if the files both exist, compare some exif information
172 if (defined $f1->{info} and $f2->{info}) {
173 if ($f1->{info}{CreateDate} eq $f2->{info}{CreateDate} and
174 $f1->{info}{Serial} eq $f2->{info}{Serial}
181 # ok; can't compare exif. Compare sha256 sums
184 # this is probably a git annex file
185 $f->{sha256} = git_annex_sha256($f->{name});
186 } elsif (-e $f->{name}) {
187 $f->{sha256} = calc_sha256($f->{name})
190 # if the sha256 are equal, they're the same. if not, or if we
191 # can't compare, assume they're different.
192 if (defined $f1->{sha256} and
193 defined $f2->{sha256} and
194 $f1->{sha256} eq $f2->{sha256}
205 my $sha = Digest::SHA->new(256);
207 return($sha->hexdigest());
210 sub git_annex_sha256 {
212 my $info = capturex('git','-C',dirname($fn),'annex','info',basename($fn));
213 my ($sha256) = $info =~ /key:.+SHA256E-[^-]+--([^\.]+)/;