rsync wrapper for pushing incremental backups
Revision | 8da194cd0eaea126a9bee0bc85cef0d471a13631 (tree) |
---|---|
Zeit | 2019-11-13 15:17:56 |
Autor | Frank Tobin <ftobin@neve...> |
Commiter | Frank Tobin |
support storing permissions
@@ -5,15 +5,19 @@ | ||
5 | 5 | |
6 | 6 | Author: Frank Tobin |
7 | 7 | Email: ftobin@neverending.org |
8 | +Author URL: https://www.neverending.org/ | |
8 | 9 | License: Eclipse Public License 2.0 (https://opensource.org/licenses/EPL-2.0) |
9 | 10 | """ |
10 | 11 | |
11 | 12 | import os |
12 | 13 | import re |
13 | 14 | import sys |
15 | +import lzma | |
14 | 16 | import time |
15 | 17 | import argparse |
16 | 18 | import subprocess |
19 | +import tempfile | |
20 | +import shutil | |
17 | 21 | |
18 | 22 | parser = argparse.ArgumentParser(description="Perform file-based incremental rsync push backup with hard-links") |
19 | 23 |
@@ -24,10 +28,9 @@ | ||
24 | 28 | help="pass-thru options to rsync. Use '=' syntax, and include prefixing dashes. Example: --rsync-opt=--partial-dir=/home/user/rsync-partial") |
25 | 29 | parser.add_argument("--quiet", "-q", action="store_true", default=False, |
26 | 30 | help="emit less output") |
27 | -parser.add_argument("--backup-prefix", default="backup-", | |
28 | - help="Prefix used in directory snapshots; default: 'backup-'") | |
29 | -parser.add_argument("--compare-n-backups", type=int, default=20, | |
30 | - help="How many previous backups to consider hard-linking against; default: 20 (rsync max)") | |
31 | + | |
32 | +backup_prefix = "backup-" | |
33 | +compare_n_backups = 20 | |
31 | 34 | |
32 | 35 | args = parser.parse_args() |
33 | 36 |
@@ -40,33 +43,41 @@ | ||
40 | 43 | dest_account = None |
41 | 44 | dest_path = args.dest |
42 | 45 | |
43 | -mkdir_cmd = ["mkdir", "-p", dest_path] | |
44 | -if dest_account: | |
45 | - mkdir_cmd = ['ssh', dest_account] + mkdir_cmd | |
46 | +def ssh_prefix(cmd): | |
47 | + if dest_account: | |
48 | + cmd = ['ssh', dest_account] + cmd | |
49 | + return cmd | |
50 | + | |
51 | +def rsync_quietness(cmd): | |
52 | + if args.quiet: | |
53 | + cmd.append("--quiet") | |
54 | + else: | |
55 | + cmd.extend(["-v", "--progress"]) | |
56 | + return cmd | |
57 | + | |
58 | +mkdir_cmd = ssh_prefix(["mkdir", "-p", dest_path]) | |
46 | 59 | subprocess.run(mkdir_cmd, check=True) |
47 | 60 | |
48 | 61 | # check what backups already exist |
49 | -check_backups_cmd = ["ls", f"{dest_path}/"] | |
50 | -if dest_account: | |
51 | - check_backups_cmd = ['ssh', dest_account] + check_backups_cmd | |
62 | +check_backups_cmd = ssh_prefix(["ls", f"{dest_path}/"]) | |
63 | + | |
52 | 64 | proc = subprocess.run(check_backups_cmd, |
53 | 65 | stdout=subprocess.PIPE, encoding="utf-8", check=True) |
54 | - | |
55 | 66 | link_dests = proc.stdout.splitlines() |
56 | -link_dests = list(filter(lambda x: re.search(args.backup_prefix, x), link_dests)) | |
57 | -link_dests = link_dests[-args.compare_n_backups:] | |
67 | +link_dests = list(filter(lambda x: re.search(backup_prefix, x), link_dests)) | |
68 | +link_dests = link_dests[-compare_n_backups:] | |
58 | 69 | link_dest_args = list(map(lambda x: f"--link-dest=../{x}", link_dests)) |
59 | 70 | |
60 | -my_dest = time.strftime(f'{args.backup_prefix}%Y%m%d-%H:%M') | |
71 | +timestamp_str = time.strftime('%Y%m%d-%H%M') | |
72 | +my_dest = time.strftime(f'{backup_prefix}{timestamp_str}') | |
61 | 73 | |
62 | 74 | cmd = ["rsync", "--human-readable", "-a", "--hard-links", |
63 | 75 | "--delete", "--compress-level", "9", |
64 | 76 | "-e", "ssh" |
65 | 77 | ] |
66 | 78 | |
67 | -if not args.quiet: | |
68 | - cmd.extend(["-v", "--progress"]) | |
69 | 79 | |
80 | +cmd = rsync_quietness(cmd) | |
70 | 81 | cmd.extend(args.rsync_opt) |
71 | 82 | cmd.extend(link_dest_args) |
72 | 83 |
@@ -75,4 +86,26 @@ | ||
75 | 86 | |
76 | 87 | if not args.quiet: |
77 | 88 | print(*cmd) |
78 | -os.execvp(cmd[0], cmd) | |
89 | +subprocess.run(cmd, check=False) # don't let permissions problems get in the way | |
90 | + | |
91 | +with tempfile.NamedTemporaryFile(prefix="rsnappush-perms.") as permissions_file: | |
92 | + compressed_perms_file = lzma.open(permissions_file.name, mode="w") | |
93 | + | |
94 | + get_perms_cmd = ["getfacl", "-R", "--absolute-names", args.source] | |
95 | + | |
96 | + with subprocess.Popen(get_perms_cmd, stdout=subprocess.PIPE) as proc: | |
97 | + shutil.copyfileobj(proc.stdout, compressed_perms_file) | |
98 | + | |
99 | + compressed_perms_file.close() | |
100 | + | |
101 | + mkdir_cmd = ssh_prefix(["mkdir", "-p", os.path.join(dest_path, "permissions")]) | |
102 | + subprocess.run(mkdir_cmd) | |
103 | + perms_rsync = ["rsync", "--human-readable", "-a", "-e", "ssh"] | |
104 | + perms_rsync = rsync_quietness(perms_rsync) | |
105 | + perms_rsync.extend(args.rsync_opt) | |
106 | + | |
107 | + perms_rsync.extend([permissions_file.name, | |
108 | + os.path.join(args.dest, 'permissions', f'permissions-{timestamp_str}.xz')]) | |
109 | + if not args.quiet: | |
110 | + print(*perms_rsync) | |
111 | + subprocess.run(perms_rsync, check=True) |