#!/usr/bin/env perl # ifuse-photos-to-tmp -- import photos from iPhone/iPad/etc. using ifuse # Copyright (C) 2019 Sean Whitton # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or (at # your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . use 5.028; use strict; use warnings; use Const::Fast; use DateTime; use DateTime::TimeZone::Local; use DateTime::Format::Epoch; use File::Copy; use File::Find; use File::Path qw(make_path remove_tree); use File::Spec::Functions; use File::stat qw(stat); use Image::ExifTool qw(ImageInfo); use Syntax::Keyword::Try; use File::Basename "basename"; # should be absolute paths const my $mount => "$ENV{HOME}/mnt/hermes"; const my $dest => "$ENV{HOME}/tmp"; try { make_path($mount, $dest); } catch { die "could not mkdir $mount and/or $dest"; } unless (grep { /ifuse on $mount/ } `mount`) { system "ifuse $mount"; die "could not mount $mount with ifuse" unless ($? == 0); } my $epoch = DateTime->new(year => 1970, month => 1, day => 1); my $epoch_formatter = DateTime::Format::Epoch->new(epoch => $epoch, ); find(sub { # defend against ImageInfo changing $_ (which obeys dynamic, # not lexical, scoping rules) my $file = $_; # we handle any file, images or not, falling back to mtime if # we can't extract EXIF data return unless (-f $file); my $info = ImageInfo($file, 'FileType', 'CreateDate', 'DateCreated'); my $ext; my $dt; my $no_exif = 0; # a closure, in particular over $info and $dt my $try_apple_exif = sub { my $key = shift; if ($info->{$key} =~ /([0-9]+):([0-9]+):([0-9]+) ([0-9]+):([0-9]+):([0-9]+)/) { $dt = DateTime->new(year => $1, month => $2, day => $3, hour => $4, minute => $5, second => $6); } }; if (exists $info->{FileType} && exists $info->{CreateDate} && $info->{CreateDate} ne "0000:00:00 00:00:00" && $info->{FileType} eq "JPEG") { $ext = "jpg"; $try_apple_exif->("CreateDate"); } elsif (exists $info->{FileType} && exists $info->{DateCreated} && $info->{DateCreated} ne "0000:00:00 00:00:00" && $info->{FileType} eq "PNG") { $ext = "png"; $try_apple_exif->("DateCreated"); } elsif (exists $info->{FileType} && exists $info->{MediaCreateDate} && $info->{MediaCreateDate} ne "0000:00:00 00:00:00" && $info->{FileType} eq "MOV") { $ext = "mov"; $try_apple_exif->("MediaCreateDate"); } elsif (exists $info->{FileType} && exists $info->{CreateDate} && $info->{CreateDate} ne "0000:00:00 00:00:00" && $info->{FileType} eq "MOV") { $ext = "mov"; $try_apple_exif->("CreateDate"); } else { $no_exif = 1; $file =~ /\.([^.]+\z)/; $ext = lc($1) if defined $1; $dt = $epoch_formatter->parse_datetime(stat($file)->mtime); $dt->set_time_zone(DateTime::TimeZone::Local->TimeZone()); } if (defined $ext && defined $dt) { # TODO factor out Local::Rename::suffix_until_unique() my $target = catfile($dest, $dt->strftime('%Y-%m-%d %H.%M.%S') . "." . $ext); my $counter = 1; $target =~ s/\.$ext\z/-1.$ext/ if -e $target; while (-e $target) { $counter++; $target =~ s/-[0-9]+\.$ext\z/-$counter.$ext/; } move($file, $target); say STDERR "fell back to mtime for $file $target" if $no_exif; } else { warn "not touching $file because could not determine target name"; } }, catfile($mount, "DCIM")); # this causes the device to regenerate its knowledge of what photos it # has; otherwise they'll still appear despite the files having been moved unlink glob "$mount/PhotoData/Photos*"; unlink "$mount/PhotoData/com.apple.photos.caches_metadata.plist"; # iOS 13 additionally seems to require us to clear these: unlink glob "$mount/PhotoData/Thumbnails/*.ithmb"; remove_tree(glob("$mount/PhotoData/Thumbnails/V2/DCIM/10*"), "$mount/PhotoData/MISC"); # (source: https://ubuntuforums.org/archive/index.php/t-2203298.html) say "attempting unmount of photos"; patient_unmount($mount); system "ifuse --documents org.videolan.vlc-ios $mount"; die "could not mount $mount with ifuse" unless ($? == 0); foreach my $file (glob qq<"${mount}/Apple CoreAudio format*.caf" "${mount}/Audio Message*.caf">) { my $target = catfile $dest, basename $file; my $counter = 1; my $dir = catfile $ENV{HOME}, qw(annex chats), lc DateTime->now->strftime('%Y/%b'); my $eventual_dest = catfile $dir, basename $target; $target =~ s/\.caf\z/-1.caf/ if -e $target or -e $eventual_dest; $eventual_dest = catfile $dir, basename $target; while (-e $target or -e $eventual_dest) { $counter++; $target =~ s/-[0-9]+\.caf\z/-$counter.caf/; $eventual_dest = catfile $dir, basename $target; } move $file, $target; } say "attempting unmount of voice notes"; patient_unmount($mount); if (fork) { exit; } else { exec "xdg-open", $dest; } sub patient_unmount { my $mount = shift; my $count = 0; while ($count < 3) { system "fusermount -u $mount"; if ($? == 0) { say " unmounted."; last; } else { print "\n" if $count > 0; print "unmount failed; waiting a few seconds .."; sleep 2; $count++; } } die "failed to unmount $mount" unless $? == 0; }