mirror of
https://github.com/RsyncProject/rsync.git
synced 2026-02-25 10:55:55 -05:00
126 lines
4.2 KiB
Python
Executable File
126 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# 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!**
|
|
|
|
import os, sys, re, subprocess, shutil
|
|
|
|
ALT_DEST_ARG_RE = re.compile('^--[a-z][^ =]+-dest(=|$)')
|
|
|
|
RSYNC_PROG = '/usr/bin/rsync'
|
|
|
|
def main():
|
|
cmd_args = sys.argv[1:]
|
|
if '--help' in cmd_args:
|
|
usage_and_exit()
|
|
|
|
if len(cmd_args) < 2:
|
|
usage_and_exit(True)
|
|
|
|
dest_dir = cmd_args[-1].rstrip('/')
|
|
if dest_dir == '' or dest_dir.startswith('-'):
|
|
usage_and_exit(True)
|
|
|
|
if not os.path.isdir(dest_dir):
|
|
die(dest_dir, "is not a directory or a symlink to a dir.\nUse --help for help.")
|
|
|
|
bad_args = [ arg for arg in cmd_args if ALT_DEST_ARG_RE.match(arg) ]
|
|
if bad_args:
|
|
die("You cannot use the", ' or '.join(bad_args), "option with atomic-rsync.\nUse --help for help.")
|
|
|
|
symlink_content = os.readlink(dest_dir) if os.path.islink(dest_dir) else None
|
|
|
|
dest_arg = dest_dir
|
|
dest_dir = os.path.realpath(dest_dir) # The real destination dir with all symlinks dereferenced
|
|
if dest_dir == '/':
|
|
die('You must not use "/" as the destination directory.\nUse --help for help.')
|
|
|
|
old_dir = new_dir = None
|
|
if symlink_content is not None and dest_dir.endswith(('-1','-2')):
|
|
if not symlink_content.endswith(dest_dir[-2:]):
|
|
die("Symlink suffix out of sync with dest_dir name:", symlink_content, 'vs', dest_dir)
|
|
num = 3 - int(dest_dir[-1]);
|
|
old_dir = None
|
|
new_dir = dest_dir[:-1] + str(num)
|
|
symlink_content = symlink_content[:-1] + str(num)
|
|
else:
|
|
old_dir = dest_dir + '~old~'
|
|
new_dir = dest_dir + '~new~'
|
|
|
|
cmd_args[-1] = new_dir + '/'
|
|
|
|
if old_dir is not None and os.path.isdir(old_dir):
|
|
shutil.rmtree(old_dir)
|
|
if os.path.isdir(new_dir):
|
|
shutil.rmtree(new_dir)
|
|
|
|
subprocess.run([RSYNC_PROG, '--link-dest=' + dest_dir, *cmd_args], check=True)
|
|
|
|
if old_dir is None:
|
|
atomic_symlink(symlink_content, dest_arg)
|
|
return
|
|
|
|
os.rename(dest_dir, old_dir)
|
|
os.rename(new_dir, dest_dir)
|
|
|
|
|
|
def atomic_symlink(target, link):
|
|
newlink = link + "~new~"
|
|
try:
|
|
os.unlink(newlink); # Just in case
|
|
except OSError:
|
|
pass
|
|
os.symlink(target, newlink)
|
|
os.rename(newlink, link)
|
|
|
|
|
|
def usage_and_exit(use_stderr=False):
|
|
usage_msg = """\
|
|
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 -aiv 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 -aiv 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, --compare-dest, or --copy-dest options (since this script
|
|
uses --link-dest to make the transfer efficient).
|
|
"""
|
|
print(usage_msg, file=sys.stderr if use_stderr else sys.stdout)
|
|
sys.exit(1 if use_stderr else 0)
|
|
|
|
|
|
def die(*args):
|
|
print(*args, file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|
|
# vim: sw=4 et
|