Added a fully atomic update if the user has setup a symlink
[rsync/rsync.git] / support / atomic-rsync
index 4173abe..9e620cf 100755 (executable)
@@ -1,40 +1,58 @@
 #!/usr/bin/perl
 #!/usr/bin/perl
+#
+# This script lets you update a hierarchy of files in an atomic way by
+# first creating a new hierarchy using rsync's --link-dest option, and
+# then swapping the hierarchy into place.  **See the usage message for
+# more details and some important caveats!**
 
 use strict;
 
 use strict;
+use warnings;
 use Cwd 'abs_path';
 
 use Cwd 'abs_path';
 
-my $RSYNC = '/usr/bin/rsync';
+my $RSYNC_PROG = '/usr/bin/rsync';
+my $RM_PROG = '/bin/rm';
 
 my $dest_dir = $ARGV[-1];
 
 my $dest_dir = $ARGV[-1];
-usage(1) if $dest_dir eq '' || $dest_dir =~ /^--/;
+&usage if !defined $dest_dir || $dest_dir =~ /(^-|^$)/ || grep(/^--help/, @ARGV);
+$dest_dir =~ s{(?<=.)/+$} {};
 
 if (!-d $dest_dir) {
 
 if (!-d $dest_dir) {
-    print STDERR "$dest_dir is not a directory.\n\n";
-    usage(1);
+    die "$dest_dir is not a directory.\nUse --help for help.\n";
 }
 
 }
 
-if (@_ = grep(/^--(link|compare)-dest/, @ARGV)) {
+if (@_ = grep(/^--[a-z]+-dest\b/, @ARGV)) {
     $_ = join(' or ', @_);
     $_ = join(' or ', @_);
-    print STDERR "You may not use $_ as an rsync option.\n\n";
-    usage(1);
+    die "You cannot use the $_ option with atomic-rsync.\nUse --help for help.\n";
 }
 
 }
 
+my $symlink_content = readlink $dest_dir; # undef when a real dir
+
+my $dest_arg = $dest_dir;
+# This gives us the real destination dir, with all symlinks dereferenced.
 $dest_dir = abs_path($dest_dir);
 if ($dest_dir eq '/') {
 $dest_dir = abs_path($dest_dir);
 if ($dest_dir eq '/') {
-    print STDERR 'You must not use "/" as the destination directory.', "\n\n";
-    usage(1);
+    die qq|You must not use "/" as the destination directory.\nUse --help for help.\n|;
 }
 
 }
 
-my $old_dir = "$dest_dir~old~";
-my $new_dir = $ARGV[-1] = "$dest_dir~new~";
-
-if (-d $old_dir) {
-    rename($old_dir, $new_dir) or die "Unable to rename $old_dir to $new_dir: $!";
+my($old_dir, $new_dir);
+if (defined $symlink_content && $dest_dir =~ /-([12])$/) {
+    my $num = 3 - $1;
+    $old_dir = undef;
+    ($new_dir = $dest_dir) =~ s/-[12]$/-$num/;
+    $symlink_content =~ s/-[12]$/-$num/;
+} else {
+    $old_dir = "$dest_dir~old~";
+    $new_dir = "$dest_dir~new~";
 }
 
 }
 
-if (system($RSYNC, "--link-dest=$dest_dir", @ARGV)) {
+$ARGV[-1] = "$new_dir/";
+
+system($RM_PROG, '-rf', $old_dir) if defined $old_dir && -d $old_dir;
+system($RM_PROG, '-rf', $new_dir) if -d $new_dir;
+
+if (system($RSYNC_PROG, "--link-dest=$dest_dir", @ARGV)) {
     if ($? == -1) {
     if ($? == -1) {
-       print "failed to execute $RSYNC: $!\n";
+       print "failed to execute $RSYNC_PROG: $!\n";
     } elsif ($? & 127) {
        printf "child died with signal %d, %s coredump\n",
            ($? & 127),  ($? & 128) ? 'with' : 'without';
     } elsif ($? & 127) {
        printf "child died with signal %d, %s coredump\n",
            ($? & 127),  ($? & 128) ? 'with' : 'without';
@@ -44,31 +62,61 @@ if (system($RSYNC, "--link-dest=$dest_dir", @ARGV)) {
     exit $?;
 }
 
     exit $?;
 }
 
-rename($dest_dir, $old_dir) or die "Unable to rename $new_dir to $old_dir: $!";
+if (!defined $old_dir) {
+    atomic_symlink($symlink_content, $dest_arg);
+    exit;
+}
+
+rename($dest_dir, $old_dir) or die "Unable to rename $dest_dir to $old_dir: $!";
 rename($new_dir, $dest_dir) or die "Unable to rename $new_dir to $dest_dir: $!";
 
 exit;
 
 rename($new_dir, $dest_dir) or die "Unable to rename $new_dir to $dest_dir: $!";
 
 exit;
 
+sub atomic_symlink
+{
+    my($target, $link) = @_;
+    my $newlink = "$link~new~";
+
+    unlink($newlink); # Just in case
+    symlink($target, $newlink) or die "Unable to symlink $newlink -> $target: $!\n";
+    rename($newlink, $link) or die "Unable to rename $newlink to $link: $!\n";
+}
+
 
 sub usage
 {
 
 sub usage
 {
-    my($ret) = @_;
-    my $fh = $ret ? *STDERR : *STDOUT;
-    print $fh <<EOT;
-Usage: atomic-rsync [RSYNC-OPTIONS] HOST:SOURCE DEST
-
-This script allows you to pull some files into DEST on the local system
-(which must exist) in an atomic manner.  It does this by first pulling
-files to DEST~new~ (using hard-links to unchanged files in order to keep
-the space requirements down), and then, at the end of the transfer, it
-renames DEST to DEST~old~ and renames DEST~new~ to DEST to effect the
-atomic update.  The DEST~old~ hierarchy will be preserved until the next
-run of this script, at which point it will be renamed to DEST~new~ and
-used in the copy.
+    die <<EOT;
+Usage: atomic-rsync [RSYNC-OPTIONS] HOST:/SOURCE/DIR/ /DEST/DIR/
+       atomic-rsync [RSYNC-OPTIONS] HOST::MOD/DIR/ /DEST/DIR/
+
+This script lets you update a hierarchy of files in an atomic way by first
+creating a new hierarchy (using hard-links to leverage the existing files),
+and then swapping the new hierarchy into place.  You must be pulling files
+to a local directory, and that directory must already exist.  For example:
+
+    mkdir /local/files-1
+    ln -s files-1 /local/files
+    atomic-rsync -av host:/remote/files/ /local/files/
+
+If /local/files is a symlink to a directory that ends in -1 or -2, the
+copy will go to the alternate suffix and the symlink will be changed to
+point to the new dir.  This is a fully atomic update.  If the destination
+is not a symlink (or not a symlink to a *-1 or a *-2 directory), this
+will instead create a directory with "~new~" suffixed, move the current
+directory to a name with "~old~" suffixed, and then move the ~new~
+directory to the original destination name (this double rename is not
+fully atomic, but is rapid).  In both cases, the prior destintaion
+directory will be preserved until the next update, at which point it
+will be deleted.
+
+In all likelihood, you do NOT want to specify this command:
+
+    atomic-rsync -av host:/remote/files /local/
+
+... UNLESS you want the entire /local dir to be swapped out!
 
 See the "rsync" command for its list of options.  You may not use the
 
 See the "rsync" command for its list of options.  You may not use the
---link-dest or --compare-dest options (since this script uses --link-dest
-to effect the atomic transfer).  Also, DEST cannot be "/".
+--link-dest, --compare-dest, or --copy-dest options (since this script
+uses --link-dest to make the transfer efficient).
 EOT
 EOT
-    exit $ret;
 }
 }