#!/usr/local/bin/perl # Fix short file names in a windows backup # Usage: sfn-fix /mnt/windows C: [orig-mdir-output] # # Create the list of file names like this: # (Edit /etc/mtools.conf if necessary to set up the drives) # sudo mdir -a -/ C: > /mnt/windows/sfn.txt # sudo mdir -a -/ D: > /mnt/windows2/sfn.txt # use strict; use warnings; sub parse_mdir($\%\%); sub sfn_generate($); $| = 1; (my $myname = $0) =~ s|(.*/)*||; # strip path component from name my $Usage = "Usage: $myname /mnt/windows C: [orig-mdir-output] \n"; # Check two or three arguments: die $Usage if ((@ARGV < 2) || (@ARGV > 3)); my $root = $ARGV[0]; die $Usage unless -d $root; my $drive = $ARGV[1]; die $Usage unless $drive =~ /^[a-zA-Z]:/; my $old = ""; if (@ARGV == 3) { $old = $ARGV[2]; } else { $old = "$root/sfn.txt"; } die $Usage unless -f $old; my $mdir = "sudo /usr/bin/mdir -/"; my %old_lfn = (); # Original sfn->lfn map my %old_sfn = (); # Original lfn->sfn map my %cur_lfn = (); # Current sfn->lfn map my %cur_sfn = (); # Current lfn->sfn map open(MDIR, $old) or die "Can't open $old: $!\n"; parse_mdir(\*MDIR, %old_lfn, %old_sfn); close(MDIR); open(MDIR, "$mdir $drive|") or die "Can't run $mdir: $!\n"; parse_mdir(\*MDIR, %cur_lfn, %cur_sfn); close(MDIR); # prefix.ext short file bases that have already been processed # (or can't be processed because there are too many files): my %done = (); my ($printed, $base, $ext, @todo, $pp, $i); FILE: foreach my $dir (sort keys %old_lfn) { %done = (); $printed = 0; $pp = ""; foreach my $lfn (sort keys %{$old_sfn{$dir}}) { $old = $old_sfn{$dir}{$lfn}; # Check if file still exists: next unless defined($cur_sfn{$dir}{$lfn}); # Skip if old and new sfns are the same: next if ($cur_sfn{$dir}{$lfn} eq $old_sfn{$dir}{$lfn}); # Skip if this prefix has already been processed: $base = sfn_generate($lfn); next if $done{$base}++; @todo = (); # Count how many files currently have this prefix: foreach my $l (sort keys %{$cur_sfn{$dir}}) { push(@todo, $l) if (sfn_generate($l) eq $base); } next if (@todo > 9); # Too many files with this prefix.ext print "\n$dir:\n" unless $printed++; # Check that all the required names (old_sfn) # are of the form prefix~N.ext # (otherwise we can't fix them). ($base, $ext) = ($base =~ /^(.*)\.(.*)$/); foreach my $l (@todo) { next unless defined($old_sfn{$dir}{$l}); if ($old_sfn{$dir}{$l} !~ /^\Q$base\E\~\d\.$ext$/) { print "!!! $l --> $old_sfn{$dir}{$l} can't be done (patt=$base~\\d\\.$ext)!\n"; next FILE; } } # Fix the @todo files so that they have the right short names # but keep the same long file names: # (1) Move the files to a spare prefix # (2) Create 9 dummy files (so that ~1 to ~9 are accessable) # (3) For each file with an old sfn, delete the appropriate dummy # and move the file to its original lfn (so it gets the right sfn) # (4) Delete remaining dummy files # (5) Move remaining @todo files back to their original lfn if ($pp eq "") { $pp = "aa"; $pp++ while (grep { /^$pp/ } keys %{$cur_lfn{$dir}}); print "Spare Prefix = <$pp>\n"; } chdir "$root/$dir" or die "Can't chdir to $root/$dir: $!\n"; # (1) Move files to spare prefix: foreach my $file (@todo) { rename $file, "$pp-$file" or warn "rename $file => $pp-$file failed: $!\n"; } # (2) Create dummy files: foreach my $i (1..9) { open(DUMMY, ">$base.$ext-dummy-file-$i") or warn "Can't create $base.$ext-dummy-file-$i: $!\n"; close(DUMMY); } # (3) Delete a dummy and move a file back to give # file the right sfn: foreach my $file (@todo) { next unless defined($old_sfn{$dir}{$file}); $old_sfn{$dir}{$file} =~ /^\Q$base\E\~(\d)\.$ext$/ or warn "Short name $old_sfn{$dir}{$file} of $file doesn't match $base~?.$ext!\n"; $i = $1; unlink("$base.$ext-dummy-file-$i") or warn "unlink $base.$ext-dummy-file-$i failed: $!\n"; print "$file: $cur_sfn{$dir}{$file} -> $old_sfn{$dir}{$file}\n"; rename("$pp-$file", $file); # Should slot in to right sfn } # (4) Delete any remaining dummy files foreach my $i (1..9) { unlink "$base.$ext-dummy-file-$i"; } # (5) Move any remaining @todo files back to original name: foreach my $file (@todo) { rename "$pp-$file", $file; } } } # Create the six character short file name prefix plus the three character # extension (if any) from a long file name: sub sfn_generate($) { local ($_) = @_; my ($pre, $ext); # Convert to lower case: tr/A-Z/a-z/; # Delete spaces: s/\s+//g; # Initial/trailing periods: s/^\.//; s/\.$//; # Delete all but last period: 1 while (s/\.(.*)\./$1\./); # Translate illegal chars to "_": s/[\+\,\;\=\[\]]/_/g; if (/^(.+)\.(.+)$/) { $pre = substr($1, 0, 6); $ext = substr($2, 0, 3); } else { $pre = substr($_, 0, 6); $ext = ""; } return("$pre.$ext"); } sub parse_mdir($\%\%) { my ($fh, $sfn_lfn, $lfn_sfn) = @_; local $_; my $dir = ""; my ($sfn, $lfn, $pre, $ext); while (<$fh>) { next if /^ Volume/; next if /^$/; next if /^\s*\d+ files?\s+[0-9 ]+bytes?$/; next if /^\.\.?\s+