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
+#
+# 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 warnings;
 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];
-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) {
-    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 ', @_);
-    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 '/') {
-    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) {
-       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';
@@ -44,31 +62,61 @@ if (system($RSYNC, "--link-dest=$dest_dir", @ARGV)) {
     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;
 
+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
 {
-    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
---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
-    exit $ret;
 }