use strict;
use Getopt::Long; use Fcntl;
use Fcntl qq(:mode);
use POSIX qw(tmpnam);
use DB_File;
use File::Basename;
use MacOSX::File;
use MacOSX::File::Copy;
use MacOSX::File::Info;
my $true= (1==1); my $false= !$true;
my $ofNone= 0; my $ofFirst= 1;
my $ofSecond= 2;
my $ofBoth= $ofFirst | $ofSecond;
my $mode= 2; my $uid= 4;
my $gid= 5;
my $size= 7;
my $atime= 8;
my $mtime= 9;
my @indices= ($mode, $uid, $gid, $size, $mtime);
my $indices= @indices; my $sigMode= 0; my $sigUid= 1;
my $sigGid= 2;
my $sigSize= 3;
my $sigMtime= 4;
my $attAtime= 3;
my $unknownGroup= 99; my $dSyncDBFileName= ".dsync.db";
my $dSyncConflictDirectoryName= "Conflicts.dsync/";
my $versionString= "dsync 0.9.4\n"
. "<http://www.ayradyss.org/programs/current.html#dsync>";
my $me= $0 or "dsync"; my $quiet= $false; my $debug= $false; my $help= $false; my $version= $false; my $simulation= $false; my $remote= $false; my $master= ""; my $snapshot= ""; my $lax= ""; my $count= 0; my %ignoreItems= ();
my %ignoreSpecialItems= map { $_ => 1 }
( $dSyncDBFileName,
'.DS_Store',
'.FBCIndex',
'.FBCLockFolder',
'.Trashes',
'AppleShare PDS',
'Desktop DB',
'Desktop DF',
'TheFindByContentFolder',
'TheVolumeSettingsFolder',
'Temporary Items',
);
my %ignoreSpecialPaths= map { $_ => 1 }
( '/tmp',
'/dev',
'/var/run',
'/var/tmp',
'/private/tmp',
'/private/var/tmp',
'/private/var/vm',
'/private/var/run',
'/Network',
'/Volumes',
'/automount',
'/.vol',
);
sub reverse { my $b cmp my $a; }
Initialize(); Run(@ARGV);
exit;
sub Initialize
{
my @ignore= ();
if (@ARGV)
{ GetOptions("n|simulation" => \$simulation,
"quiet" => \$quiet,
"debug" => \$debug,
"master=s" => \$master,
"s|snapshot=s" => \$snapshot,
"x|ignore=s" => \@ignore,
"remote" => \$remote,
"lax=s" => \$lax,
"V|version" => \$version,
"H|help" => \$help,
);
if ($quiet and $debug)
{ print "Debugging mode trumps quiet mode...\n";
$quiet= $false;
}
%ignoreItems= map { $_ => 1 } @ignore;
}
else
{ Usage();
}
}
sub Run
{
if ($help)
{ print "Initialized: ready for help mode.\n" if $debug;
Help();
}
elsif ($version)
{ print "Initialized: ready for version mode.\n" if $debug;
Version();
}
else
{ my @directories= ();
select(STDOUT); $|= 1;
if ($master)
{ my $target= shift or Usage();
ConfirmDirectory(\$master, $false)
or die "Master directory <$master> does not seem to exist... scuttling\n";
ConfirmDirectory(\$target, !$simulation)
or die "Cannot access target directory <$target>... scuttling\n";
print("Initialized: ready to replicate master.\n") if $debug;
Synchronize($master, $target);
}
elsif ($lax)
{ my $target= shift or Usage();
ConfirmDirectory(\$lax, $false)
or die "Local directory <$lax> does not seem to exist... scuttling\n";
ConfirmDirectory(\$target, !$simulation)
or die "Cannot access target directory <$target>... scuttling\n";
print("Initialized: ready for lax merging.\n") if $debug;
Synchronize($lax, $target);
}
elsif ($snapshot)
{ my $database= "";
ConfirmDirectory(\$snapshot, $false)
or die "Snapshot directory <$snapshot> does not seem to exist... scuttling\n";
print("Initialized: ready to take a snapshot.\n") if $debug;
if ($database= Snapshot($snapshot, "snapshot"))
{
unless ($simulation)
{
copy($database, $snapshot.$dSyncDBFileName)
or die "Failed to copy snapshot to <$snapshot$dSyncDBFileName>: ",
&MacOSX::File::strerr, "\n";
print "\nSnapshot taken <$snapshot$dSyncDBFileName>!\n" unless $quiet;
}
print "\n" if $debug; ClearTmpStorage($database);
}
}
else
{ my $firstDirectory= shift or Usage();
my $secondDirectory= shift or Usage();
ConfirmDirectory(\$firstDirectory, $false)
or die "Cannot access first directory <$firstDirectory>... scuttling\n";
ConfirmDirectory(\$secondDirectory, !$simulation)
or die "Cannot access second directory <$secondDirectory>... scuttling\n";
print("Initialized: ready to merge!\n") if $debug;
Synchronize($firstDirectory, $secondDirectory);
}
}
}
sub ConfirmDirectory
{
my $directory= shift; my $create= shift;
unless (-d $$directory)
{ return $false unless $create;
print "Attempting to create <$$directory>\n" if $debug;
if (mkdir($$directory, 0700))
{ $master= $true; } else
{ warn "Failed to create directory <$$directory>: $!\n";
return $false;
}
}
$$directory= $$directory . "/" unless $$directory=~ /\/$/;
$$directory=~ s/\/+/\//g;
return $true;
}
sub Synchronize
{
my %attributes= (); my %copy= (); my %delete= (); my %reset= (); my $fakeSnapshot= ""; my %firstDirectoryAttributes= (); my %secondDirectoryAttributes= (); my ($primarySnapshot, $secondarySnapshot, $firstDirectory, $secondDirectory )= ChooseSnapshot(shift, shift);
if ($simulation)
{ $fakeSnapshot= tmpnam();
if (-e $primarySnapshot)
{ if (copy($primarySnapshot, $fakeSnapshot))
{ CopyAttributes($primarySnapshot, $fakeSnapshot, $false)
or die "Could not replicate snapshot attributes;\n",
"\tthe simulation would not be accurate.\n";
}
else
{ die "Failed to replicate snapshot <$primarySnapshot>\n",
"\tas <$fakeSnapshot>: ", &MacOSX::File::strerr, "\n";
}
}
$primarySnapshot= $fakeSnapshot;
}
tie(%attributes, 'DB_File', $primarySnapshot, O_CREAT|O_RDWR, 0600, $DB_HASH)
or die "Cound not access snapshot <$primarySnapshot>: $!\n";
my $firstDirectoryTmpStorage= Snapshot($firstDirectory, $master ? "master" : "primary");
my $secondDirectoryTmpStorage= Snapshot($secondDirectory, "secondary");
tie(%firstDirectoryAttributes, 'DB_File', $firstDirectoryTmpStorage,
O_RDWR, 0600, $DB_HASH)
or die "Could not access <$firstDirectory> listings: $!\n";
tie(%secondDirectoryAttributes, 'DB_File', $secondDirectoryTmpStorage,
O_RDWR, 0600, $DB_HASH)
or die "Could not access <$secondDirectory> listings: $!\n";
%delete= FindDeleted(\%attributes,
\%firstDirectoryAttributes, \%secondDirectoryAttributes);
%copy= FindChanged(\%delete, \%attributes,
\%firstDirectoryAttributes, \%secondDirectoryAttributes);
DeleteItems(\%attributes, \%delete, \%reset, $firstDirectory, $secondDirectory)
if keys %delete;
CopyItems(\%attributes, \%copy, \%reset,
\%firstDirectoryAttributes, \%secondDirectoryAttributes,
$firstDirectory, $secondDirectory)
if keys %copy;
untie(%firstDirectoryAttributes); untie(%secondDirectoryAttributes);
print "\n" if $debug; ClearTmpStorage($firstDirectoryTmpStorage, $secondDirectoryTmpStorage);
CleanUp(\%attributes, \%reset, $firstDirectory, $secondDirectory)
if keys %reset;
untie(%attributes); if ($simulation)
{ ClearTmpStorage($fakeSnapshot);
}
else
{ if (copy($primarySnapshot, $secondarySnapshot))
{ CopyAttributes($primarySnapshot, $secondarySnapshot, $false);
}
else
{ warn "Failed to replicate snapshot <$primarySnapshot>\n",
"\tas <$secondarySnapshot>: ", &MacOSX::File::strerr, "\n";
}
}
}
sub Snapshot
{
my $directory= shift; my $designation= shift; my %attributes= (); my $directoryTmpStorage= tmpnam();
print "Setting up temporary storage <$directoryTmpStorage> "
. "for <$directory> listings...\n" if $debug;
tie(%attributes, 'DB_File', $directoryTmpStorage,
O_CREAT|O_RDWR|O_EXCL, 0600, $DB_HASH)
or die "Could not create temporary storage for <$directory> listings: $!\n";
$count= 0; unless ($quiet)
{
print "\nScanning $designation directory <$directory>...";
}
foreach my $subNode (ListDirectory($directory))
{ ScanNode($directory, "$subNode", \%attributes);
}
print "\n" unless $quiet;
untie(%attributes);
return $directoryTmpStorage; }
sub ScanNode
{
my $root= shift; my $node= shift; my $changes= shift; my @attributes= ();
return if ($ignoreSpecialItems{basename($node)});
return if ($ignoreItems{$node} or $ignoreItems{$root.$node});
printf("\n%10d:", $count) if ($count % 8192 == 0) and !$debug and !$quiet;
print "." if ($count % 128 == 0) and !$debug and !$quiet;
$count++;
printf("\n%10d: Checking <$root$node>\n", $count) if $debug;
@attributes= Attributes($root.$node);
printf("%12s[%08x %08x %08x %08x %08x]\n", "", @attributes) if $debug;
if (S_ISDIR(@attributes[$sigMode]))
{ unless ($ignoreSpecialPaths{$root.$node})
{ foreach my $subNode (ListDirectory($root.$node))
{ ScanNode($root, "$node/$subNode", $changes);
}
}
}
elsif (S_ISLNK(@attributes[$sigMode]))
{ return if $lax; @attributes[$sigMode]&= 0xff00;
@attributes[$sigUid]= 0;
@attributes[$sigGid]= 0;
printf("%12s[%08x %08x %08x %08x %08x]\n", "", @attributes) if $debug;
}
${$changes}{$node}= pack("N$indices", @attributes);
}
sub Attributes
{
my $item= shift; my @attributes= ();
if (@attributes= (lstat($item))[@indices])
{
@attributes[$sigSize]= 0 if -d _;
return @attributes;
}
else
{
die "Cannot get information about <$item>: $!\n";
}
}
sub ListDirectory
{
my $path= shift; my @nodes= ();
if (opendir(DIR, $path))
{ @nodes= grep(!/^\.(?:\.?$|_)/o, readdir(DIR));
closedir(DIR);
}
else
{ warn "Could not list contents of <$path>: $!\n";
}
return @nodes;
}
sub ChooseSnapshot
{
my $firstDirectory= shift; my $secondDirectory= shift;
unless ($master or $lax)
{ my $firstSignature= 0; my $firstMtime= 0; my $secondSignature= 0; my $secondMtime= 0; my @attributes= ();
print "\nGetting information about <$firstDirectory$dSyncDBFileName>\n" if $debug;
if (@attributes= lstat($firstDirectory.$dSyncDBFileName))
{ @attributes[$sigGid]= 0; @attributes[$sigUid]= 0;
@attributes[$sigMode]= 0;
$firstSignature= pack("N$indices", @attributes[@indices]);
printf("%12s[%08x %08x %08x %08x %08x]\n\n", "",
unpack("N$indices", $firstSignature))
if $debug;
$firstMtime= @attributes[$mtime];
}
else
{
TrackingAdvice($firstDirectory);
}
print "Getting information about <$secondDirectory$dSyncDBFileName>\n" if $debug;
if (@attributes= lstat($secondDirectory.$dSyncDBFileName))
{ @attributes[$sigGid]= 0; @attributes[$sigUid]= 0;
@attributes[$sigMode]= 0;
$secondSignature= pack("N$indices", @attributes[@indices]);
printf("%12s[%08x %08x %08x %08x %08x]\n\n", "",
unpack("N$indices", $secondSignature)) if $debug;
$secondMtime= @attributes[$mtime];
}
else
{
TrackingAdvice($secondDirectory);
}
unless ($firstSignature eq $secondSignature)
{ print "Snapshot signatures do not match!\n" if $debug;
if ($firstMtime > $secondMtime)
{
print "Will use <$secondDirectory$dSyncDBFileName>\n\n" if $debug;
return ($secondDirectory.$dSyncDBFileName, $firstDirectory.$dSyncDBFileName,
$firstDirectory, $secondDirectory);
}
else
{
print "Will use <$firstDirectory$dSyncDBFileName>\n\n" if $debug;
return ($firstDirectory.$dSyncDBFileName, $secondDirectory.$dSyncDBFileName,
$secondDirectory, $firstDirectory);
}
}
}
print "Will use <$firstDirectory$dSyncDBFileName>\n\n" if $debug;
return ($firstDirectory.$dSyncDBFileName, $secondDirectory.$dSyncDBFileName,
$firstDirectory, $secondDirectory);
}
sub FindDeleted
{
my $snapshot= shift; my $first= shift; my $second= shift; my %delete= (); my $deleteStatus;
foreach my $item (keys %$snapshot)
{ $deleteStatus= $ofNone;
$deleteStatus|= $ofFirst unless defined ${$first}{$item};
$deleteStatus|= $ofSecond unless defined ${$second}{$item};
if ($deleteStatus == $ofFirst)
{ $delete{$item}= $deleteStatus;
print "Flagged for removal from secondary: <$item>\n" if $debug;
}
elsif ($deleteStatus == $ofSecond)
{ $delete{$item}= $deleteStatus;
if ($master)
{ print "Flagged for restoration to secondary: <$item>\n" if $debug;
}
else
{ print "Flagged for removal from primary: <$item>\n" if $debug;
}
}
elsif ($deleteStatus == $ofBoth)
{ print "Gone from both sides: <$item>\n" if $debug;
delete(${$snapshot}{$item});
}
}
print "\n" if $debug; return %delete;
}
sub FindChanged
{
my $delete= shift; my $snapshot= shift; my $first= shift; my $second= shift; my %copy= (); my @differences= (); my $payload= $false;
foreach my $item (keys %$first)
{ if (defined ${$snapshot}{$item})
{
next if (${$snapshot}{$item} eq ${$first}{$item});
@differences= Differences(${$snapshot}{$item}, ${$first}{$item}, $false);
$payload= shift @differences;
}
else
{ $payload= $true;
@differences= ("new item");
}
if (@differences)
{ print $payload ? "Payload" : "Attributes",
" (", join(", ", @differences), ")",
" changed in the ", $master ? "master" : "primary",
" directory: <$item>\n" if $debug;
$copy{$item}= [($ofFirst, $payload, @differences)];
}
}
foreach my $item (keys %$second)
{ if (defined ${$snapshot}{$item})
{
next if (${$snapshot}{$item} eq ${$second}{$item});
@differences= Differences(${$snapshot}{$item}, ${$second}{$item}, $lax);
$payload= shift @differences;
}
else
{ $payload= $true;
@differences= ("new item");
}
if (@differences)
{ print $payload ? "Payload" : "Attributes",
" (", join(", ", @differences), ")",
" changed in the secondary directory: <$item>\n" if $debug;
if (defined $copy{$item})
{ if (${$first}{$item} eq ${$second}{$item})
{ $copy{$item}= [($ofNone)];
}
else
{ $copy{$item}= [($master ? $ofFirst: $ofBoth, $payload, @differences)];
}
}
else
{ if ($master)
{ if (defined ${$first}{$item})
{ $copy{$item}= [($ofFirst, $payload, @differences)]
}
elsif (defined ${$second}{$item})
{ ${$delete}{$item}= $ofFirst;
}
}
else
{ $copy{$item}= [($ofSecond, $payload, @differences)];
}
}
}
}
if ($master)
{
foreach my $item (keys %$delete)
{ if (${$delete}{$item} == $ofSecond)
{ delete(${$delete}{$item}); $copy{$item}= [($ofFirst, $true, "restore missing")];
}
}
}
else
{ foreach my $item (keys %$delete)
{ next unless defined $copy{$item};
${$delete}{$item}= $ofNone; }
}
print "\n" if $debug; return %copy;
}
sub DeleteItems
{
my $snapshot= shift; my $items= shift; my $reset= shift; my $firstDirectory= shift; my $secondDirectory= shift; my $status;
unless ($quiet)
{
if ($simulation)
{ print "\nItems that would have been removed:\n";
}
else
{ print "\nRemoving items:\n";
}
}
foreach my $item (reverse sort (keys %$items))
{ $status= ${$items}{$item};
if ($status == $ofFirst)
{ print "\t[ rm] <$item>\n" unless $quiet;
unless ($simulation)
{ print "\t\tRemoving <$secondDirectory$item>\n\n" if $debug;
if (Delete($secondDirectory.$item))
{ delete(${$snapshot}{$item});
${$reset}{dirname($item)}|= $ofSecond;
delete(${$reset}{$item})
if exists(${$reset}{$item});
}
}
}
elsif ($status == $ofSecond)
{ print "\t[rm ] <$item>\n" unless $quiet;
unless ($simulation)
{ print "\t\tRemoving <$firstDirectory$item>\n\n" if $debug;
if (Delete($firstDirectory.$item))
{ delete(${$snapshot}{$item});
${$reset}{dirname($item)}|= $ofFirst;
delete(${$reset}{$item})
if exists(${$reset}{$item});
}
}
}
elsif ($status == $ofNone)
{ print "\t[keep] <$item>\n" unless $quiet;
print "\t\tA scheduled copy trumps this deletion of <$item>\n\n"
if $debug;
}
}
}
sub CopyItems
{
my $snapshot= shift; my $items= shift; my $reset= shift; my $first= shift; my $second= shift; my $firstDirectory= shift; my $secondDirectory= shift; my $status; my $payload;
unless ($quiet)
{
if ($simulation)
{ print "\nItems that would have been updated:\n";
}
else
{ print "\nUpdating items:\n";
}
}
foreach my $item (sort (keys %$items))
{ my @changes= @{${$items}{$item}};
$status= shift(@changes);
$payload= shift(@changes);
if ((-d $firstDirectory.$item) and (-d $secondDirectory.$item))
{ @changes= grep(!/time|size/, @changes);
$payload= $false;
}
if ($status == $ofFirst)
{ unless ($quiet)
{
if (@changes)
{ print "\t[ -> ] <$item> (", join(", ", @changes),
$payload ? "; copy data" : "", ")\n";
}
else
{ print "\t[ -> ] <$item> (defer reset)\n";
}
}
unless ($simulation)
{ unless (@changes)
{ ${$reset}{$item}|= $ofSecond;
}
else
{ ${$reset}{dirname($item)}|= $ofSecond
if Copy($item, $firstDirectory, $secondDirectory,
${$first}{$item}, ${$second}{$item}, $payload);
}
${$snapshot}{$item}= ${$first}{$item};
}
}
elsif ($status == $ofSecond)
{ unless ($quiet)
{
if (@changes)
{ print "\t[ <- ] <$item> (", join(", ", @changes),
$payload ? "; copy data" : "", ")\n";
}
else
{ print "\t[ <- ] <$item> (defer reset)\n";
}
}
unless ($simulation)
{ unless (@changes)
{ ${$reset}{$item}|= $ofFirst;
}
else
{ ${$reset}{dirname($item)}|= $ofFirst
if Copy($item, $secondDirectory, $firstDirectory,
${$first}{$item}, ${$second}{$item}, $payload);
}
${$snapshot}{$item}= ${$second}{$item};
}
}
elsif ($status == $ofNone)
{ print "\t[ == ] <$item>\n" if $debug;
unless ($simulation)
{ print "\t\tUpdating snapshot -- items are the same\n\n" if $debug;
${$snapshot}{$item}= ${$first}{$item};
}
}
else
{ unless ($quiet)
{
if (@changes)
{ print "\t[-><-] <$item> (", join(", ", @changes),
$payload ? "; copy data" : "", ")\n";
}
else
{ print "\t[ -> ] <$item> (defer reset)\n";
}
}
unless ($simulation)
{ unless (@changes)
{ ${$reset}{$item}|= $ofSecond;
}
else
{ ${$reset}{dirname($item)}|= $ofBoth
if ResolveConflict($item, $firstDirectory, $secondDirectory,
${$first}{$item}, ${$second}{$item}, $payload);
}
${$snapshot}{$item}= pack("N$indices", Attributes($firstDirectory.$item));
}
}
}
}
sub ResolveConflict
{
my $item= shift; my $sourceDirectory= shift; my $destinationDirectory= shift; my $sourceSignature= shift; my $destinationSignature= shift; my $payload= shift; my $success= $true;
unless ((-d $sourceDirectory.$item) and (-d $destinationDirectory.$item))
{ if ($payload)
{ if ($success= MoveToConflictDirectory($destinationDirectory.$item))
{ print "\t\tCopying <$sourceDirectory$item>\n",
"\t\t\tto <$destinationDirectory$item>\n" if $debug;
$success= copy($sourceDirectory.$item, $destinationDirectory.$item)
or warn "Could not copy <$sourceDirectory$item>",
" to <$destinationDirectory$item>: ", &MacOSX::File::strerr, "\n";
}
}
}
if ($success)
{ print "\t\tUpdating attributes on <$destinationDirectory$item>\n",
"\t\t\tto match those of <$sourceDirectory$item>\n" if $debug;
CopyAttributes($sourceDirectory.$item, $destinationDirectory.$item, !$remote);
}
print "\n" if $debug; return $success;
}
sub MoveToConflictDirectory
{
my $item= shift; my $directory= dirname($item) . "/";
my $name= basename($item);
my $success= $true;
$success= mkdir($directory.$dSyncConflictDirectoryName, 0700)
unless (-d $directory.$dSyncConflictDirectoryName);
if ($success and -e $directory.$dSyncConflictDirectoryName.$name)
{ $success= MoveToConflictDirectory($directory.$dSyncConflictDirectoryName.$name);
}
if ($success and !$simulation)
{ print "\t\tPreserving <$item)>\n",
"\t\t\tas <$directory.$dSyncConflictDirectoryName.$name>\n"
if $debug;
$success= move($item, $directory.$dSyncConflictDirectoryName.$name);
warn "Could not move <$item> to",
" <$directory$dSyncConflictDirectoryName$name>: $!\n"
unless $success;
}
return $success;
}
sub Copy
{
my $item= shift; my $sourceDirectory= shift; my $destinationDirectory= shift; my $sourceSignature= shift; my $destinationSignature= shift; my $payload= shift; my $link= $false; my $success= $true;
if (-l $sourceDirectory.$item)
{ my $symLink= ""; my $recreate= $false; $link= $true;
if ($symLink= readlink($sourceDirectory.$item))
{
if (-e $destinationDirectory.$item)
{
if (-l $destinationDirectory.$item)
{ if ($symLink ne readlink($destinationDirectory.$item))
{ $success= unlink($destinationDirectory.$item)
or warn "Cound not delete old symbolic link ",
"<$destinationDirectory.$item>: $!\n";
$recreate= $success;
}
else
{ $recreate= $false; }
}
else
{ $success= MoveToConflictDirectory($destinationDirectory.$item);
$recreate= $success;
}
}
else
{
$recreate = $true;
}
$success= symlink($symLink, $destinationDirectory.$item)
or warn "Could not save a new symbolic link as ",
"<$destinationDirectory$item>: $!\n"
if $recreate;
}
else
{ $success= $false;
warn "Cound not read source symbolic link <$sourceDirectory$item>: $!\n";
}
}
elsif (-d $sourceDirectory.$item)
{ unless (-d $destinationDirectory.$item)
{ $success= unlink($destinationDirectory.$item)
if -l $destinationDirectory.$item; $success= MoveToConflictDirectory($destinationDirectory.$item)
if -f $destinationDirectory.$item;
$success= mkdir($destinationDirectory.$item) if $success;
warn "Failed to create directory <$destinationDirectory$item>: $!\n"
unless $success;
}
}
elsif (-f $sourceDirectory.$item)
{ if (-e $destinationDirectory.$item and ! -f $destinationDirectory.$item)
{ $success= MoveToConflictDirectory($destinationDirectory.$item);
$payload= $true;
}
if ($payload and $success)
{ print "\t\tCopying <$sourceDirectory$item>\n",
"\t\t\tto <$destinationDirectory$item>\n" if $debug;
$success= copy($sourceDirectory.$item, $destinationDirectory.$item)
or warn "Could not copy <$sourceDirectory$item>",
" to <$destinationDirectory$item>: ", &MacOSX::File::strerr, "\n";
}
}
else
{ print "\t\tFailed to deal with <$sourceDirectory$item>\n" if $debug;
$success= $false;
}
if ($success)
{ print "\t\tUpdating attributes on <$destinationDirectory$item>\n",
"\t\t\tto match those of <$sourceDirectory$item>\n" if $debug;
CopyAttributes($sourceDirectory.$item, $destinationDirectory.$item, !$remote)
unless $link;
}
print "\n" if $debug; return $success;
}
sub Differences
{
my @firstAttributes= unpack("N$indices", shift);
my @secondAttributes= unpack("N$indices", shift);
my $lax= shift;
my @differences= (); my $payload= $false;
if (@firstAttributes[$sigSize] != @secondAttributes[$sigSize])
{ push(@differences, "size");
$payload= $true;
}
return $payload, @differences if $lax;
if (@firstAttributes[$sigMtime] != @secondAttributes[$sigMtime])
{ push(@differences, "time");
$payload= $true;
}
if (S_IFMT(@firstAttributes[$sigMode]) != S_IFMT(@secondAttributes[$sigMode]))
{ push(@differences, "type");
$payload= $true;
}
if (@firstAttributes[$sigMode] & 07777 != @secondAttributes[$sigMode] & 07777)
{ push(@differences, "permissions")
unless $remote;
}
if (@firstAttributes[$sigUid] != @secondAttributes[$sigUid])
{ push(@differences, "owner");
}
if (@firstAttributes[$sigGid] != @secondAttributes[$sigGid])
{ my $falsePositive= ($remote and
((@firstAttributes[$sigGid] == $unknownGroup) or
(@secondAttributes[$sigGid] == $unknownGroup)));
push(@differences, "group") unless $falsePositive;
print "Group mismatch: <", @firstAttributes[$sigGid], "> v. <",
@secondAttributes[$sigGid], ">; lax=<$lax>\n" unless ($falsePositive or !$debug);
}
return $payload, @differences;
}
sub CopyAttributes
{
my $original= shift; my $target= shift; my $permissions= shift; my $info; my @attributes= (); my $success= $true;
$success= setfinfo(getfinfo($original), $target)
or warn "Could not replicate FileInfo attributes from <$original>",
" to <$target>: ", &MacOSX::File::strerr, "\n";
if (@attributes= (lstat($original))[@indices])
{ if ($permissions)
{
unless (chmod(@attributes[$sigMode] & 07777, $target))
{
$success= $false;
warn "Failed to replicate permissions from <$original> to <$target>: $!\n";
}
if (@attributes[$sigGid] == $unknownGroup)
{ @attributes[$sigGid]= (lstat($target))[$gid];
}
unless (chown(@attributes[$sigUid], @attributes[$sigGid], $target))
{
$success= $false;
warn "Failed to replicate ownership from <$original> to <$target>: $!\n";
}
}
unless (utime(@attributes[$attAtime], @attributes[$sigMtime], $target))
{
$success= $false;
warn "Failed to replicate time stamps from <$original> to <$target>: $!\n";
}
}
else
{
warn "Could not read attributes for <$original>: $!\n";
$success= $false;
}
return $success;
}
sub Delete
{
my $item= shift; my $success= $true;
if (-f $item)
{ if ($success= unlink($item))
{ my $frassItem= dirname($item) . "._" . basename($item);
if (-f "$frassItem")
{
unlink("$frassItem")
or warn "Failed to remove file <$frassItem>: $!\n";
}
}
else
{ warn "Failed to remove file <$item>: $!\n";
}
}
elsif (-l $item)
{ $success= unlink($item);
warn "Failed to remove symbolic link <$item>: $!\n"
unless $success;
}
elsif (-d $item)
{ $success= DeleteDirectory($item);
}
else
{ $success= $false;
}
return $success;
}
sub DeleteDirectory
{
my $directory= shift; my $success= $true;
foreach my $item (ListDirectory($directory))
{ $success= Delete("$directory/$item")
or last;
}
$success= rmdir($directory)
or warn "Failed to remove directory <$directory>: $!\n"
if $success;
return $success;
}
sub CleanUp
{
my $snapshot= shift; my $reset= shift; my $firstDirectory= shift; my $secondDirectory= shift; my $originalMtime= 0; my $now= time();
print "\nCleaning up:\n" unless $quiet;
foreach my $item (reverse sort (keys %$reset))
{ print "\t[rset] <$item>\n" unless $quiet;
$originalMtime= (unpack("N$indices", ${$snapshot}{$item}))[$sigMtime];
if (${$reset}{$item} && $ofFirst)
{ printf("\t\tUpdating modification time of <$firstDirectory$item>\n",
"\t\t\tto [%08x]\n", $originalMtime) if $debug;
utime($now, $originalMtime, $firstDirectory.$item)
or warn "Failed to reset time stamps on <$firstDirectory$item>\n";
}
if (${$reset}{$item} && $ofSecond)
{ printf("\t\tUpdating modification time of <$secondDirectory$item>\n",
"\t\t\tto [%08x]\n", $originalMtime) if $debug;
utime($now, $originalMtime, $secondDirectory.$item)
or warn "Failed to reset time stamps on <$secondDirectory$item>\n";
}
print "\n" if $debug; }
}
sub ClearTmpStorage
{
for (@_)
{ print "Removing temporary storage file <$_>\n" if $debug;
unlink($_)
or warn "Cound not remove temporary storage file <$_>: $!\n";
}
}
sub TrackingAdvice
{
my $directory= shift;
die "\nCould not access a saved snapshot for <$directory>\n",
"Changes may not be accurately tracked --\n",
"Re-run $me in \"master\" or \"snapshot\" mode.\n",
"\n",
"Usage: $me --master <master_directory> <target_directory>\n",
"Usage: $me --snapshot <snapshot_directory>\n",
"\n\n",
"Consult documentation or run $me in \"help\" mode for more information.\n",
"\n",
"Usage: $me --help\n",
"\n";
}
sub Usage
{
die "\nYou did not invoke $me properly!\n",
"\n",
"Usage: $me <first_directory> <second_directory>\n",
"Usage: $me --master <master_directory> <target_directory>\n",
"Usage: $me --snapshot <snapshot_directory>\n",
"Usage: $me --help\n",
"\n";
}
sub Help
{
print "\nWithin Help...\n" if $debug;
system("perldoc $me")
and die "Problems accessing perldoc... Try the following line at the prompt:\n",
"perldoc $me";
}
sub Version
{
print "\nWithin Version...\n" if $debug;
print "\n$versionString\n\n";
}
__END__
=pod
=head1 NAME
dsync -- synchronize contents of two directories
=head1 SYNOPSIS
B<dsync> [B<-n>] [B<-q> | B<-d>] [B<-x> I<pathname>]... I<pathname> I<pathname>
B<dsync> [B<-n>] [B<-q> | B<-d>] [B<-x> I<pathname>]... [B<-s> I<pathname>]
B<dsync> [B<-n>] [B<-q> | B<-d>] [B<-x> I<pathname>]... [B<-m> I<pathname>] I<pathname>
B<dsync> B<-h>
B<dsync> B<-v>
(See the OPTIONS section for alternate option syntax with long option names.)
=head1 DESCRIPTION
B<dsync> synchronizes contents of two directories. B<dsync> remembers
state between invocations and tries to track and reconcile changes
between successive runs and between independent synchronizations
of multiple targets (local and backup, local and remote, etc.).
While B<dsync> will normally consider bidirectional changes,
it may also mirror one directory to another if a master directory
is specified. Such may be used for backups or to force conflict
resolution.
Alternatively, B<dsync> may be invoked to simply update state of a given
directory in order to reset state from a previous run or
to update a given directory to current.
Options
=over 4
=item B<-n>, B<--simulation>
Merely print what will be done, but don't actually do it;
directories and state snapshots remain unaffected.
=item B<-q>, B<--quiet>
Suppress all but the most critical output.
=item B<-d>, B<--debug>
Describe state and actions at appropriate points during execution.
=item B<-x> I<pathname>, B<--ignore> I<pathname>
Ignore indicated item during processing. Repeat this option to list
multiple items.
=item B<-r>, B<--remote>
Hint that a directory being synchronized is on a remote volume
and that not all file attributes are accurate or accessible.
Forgo some attribute related operations but still reflect them
in the saved snapshot.
=item B<-l> I<pathname>, B<--lax> I<pathname>
Option to ignore any attribute changes on the secondary side.
This behavior may be useful when backing up to foreign file systems
as attrbiutes such as permissions, ownership, time, and type
may not be accurately preserved. Only changes in size of files
on the secondary side matter.
This option is more extreme than B<--remote>
=item B<-s> I<pathname>, B<--snapshot> I<pathname>
Take a snapshot of the indicated directory.
=item B<-m> I<pathname>, B<--master> I<pathname>
Treat the indicated directory as the master copy and mirror its
content in the secondary directory.
=item B<-v>, B<-V>, B<--version>
Print version information and exit.
=item B<-h>, B<-H>, B<--help>
Print this terse manual and exit.
=back
=head1 EXAMPLE
To synchronize a local Documents directory and a remotely mounted
analog, invoke via:
C<sudo dsync ~user/Documents /Volumes/user/Documents>
assuming that the user has identically named accounts on both machines
and that the user's remote home directory was mounted locally.
=head1 FILES
=over 4
=item .dsync.db
Berkeley DB Hash file used to store states of files between executions.
=back
=head1 REQUIRES
Perl 5.6, Getopt::Long, Fcntl, POSIX, DB_File, File::Basename, MacOSX::File,
MacOSX::File::Copy, MacOSX::File::Info
=head1 SEE ALSO
perl(1)
=head1 BUGS
=over 4
This is an early release; it has not been tested comprehensively.
Send bug reports, questions, and requests to dsync@ayradyss.org.
Mac OS X obfuscates some file attributes and prevents updates
to volumes mounted via AFP. Dsync requires the B<--remote> hint in order
to ignore fallacious attributes and to skip operations that would
otherwise fail. A future version may support a special "snapshot
restore" mode that will allow Dsync to properly reset attributes
when invoked locally after a remote synchronization run.
=back
=head1 AUTHOR
Igor S. Livshits <dsync@ayradyss.org>
=head1 COPYRIGHT
Copyright (C) 2006 Igor S. Livshits
Use and distribute this tool as per the Artistic License
=cut