git @ Cat's Eye Technologies ellsync / 5cc52e9
When executing system commands, don't use shell expansion. Chris Pressey 1 year, 6 months ago
3 changed file(s) with 41 addition(s) and 18 deletion(s). Raw diff Collapse all Expand all
147147 is for example owned by another user and not world-readable, it will abort.
148148 `ellsync` does not currently detect this properly. It should be made to handle
149149 it gracefully, if possible.
150 * Better test case for `--thorough`.
151 * When executing system commands, don't use shell expansion.
152150 * (Aspirational) Ability to convert the backup router to a `dot` file (`graphviz`)
153151 so that the relationships between the streams can be easily visualized.
154152
166164 A bash tab-completion script is included in the `script` directory. It enables
167165 tab-completion of both subcommand names, and stream names in the `sync` subcommand.
168166
167 Internally, shell expansion is no longer used when executing system commands, and
168 several new tests have been added to the test suite.
169
169170 ### 0.4
170171
171172 The `:` in a backup stream identifier is optional, when no subdirectory is being
1313 return dirname
1414
1515
16 def run_command(cmd):
17 sys.stdout.write(cmd + '\n')
16 def run_command(argv):
17 def pretty(s):
18 return '"{}"'.format(s) if ' ' in s else s
19 sys.stdout.write(' '.join([pretty(a) for a in argv]) + '\n')
1820 try:
19 p = Popen(cmd, shell=True, stderr=STDOUT, stdout=PIPE, encoding='utf-8')
21 p = Popen(argv, stderr=STDOUT, stdout=PIPE, encoding='utf-8')
2022 decode_line = lambda line: line
2123 except TypeError:
2224 # python 2.x
23 p = Popen(cmd, shell=True, stderr=STDOUT, stdout=PIPE)
25 p = Popen(argv, stderr=STDOUT, stdout=PIPE)
2426 decode_line = lambda line: line.decode('utf-8')
2527 pipe = p.stdout
2628 for line in p.stdout:
5557 to_dir = os.path.join(to_dir, subdir)
5658 sync_directories(from_dir, to_dir, options)
5759 if options.apply:
58 run_command('sync')
60 run_command(['sync'])
5961
6062
6163 def sync_directories(from_dir, to_dir, options):
6668 if not os.path.isdir(d):
6769 raise ValueError("Directory '{}' is not present".format(d))
6870
69 dry_run = not options.apply
70 dry_run_option = '--dry-run ' if dry_run else ''
71 checksum_option = '--checksum ' if options.thorough else ''
72 cmd = 'rsync {}{}--archive --verbose --delete "{}" "{}"'.format(dry_run_option, checksum_option, from_dir, to_dir)
73 run_command(cmd)
71 argv = ['rsync']
72 if not options.apply:
73 argv.append('--dry-run')
74 if options.thorough:
75 argv.append('--checksum')
76 argv.extend(['--archive', '--verbose', '--delete', from_dir, to_dir])
77 run_command(argv)
7478
7579
7680 def rename(router, options):
7171 main(['backup.json', 'sync', 'basic:'])
7272 self.assertFalse(os.path.exists('cache/thing'))
7373 output = sys.stdout.getvalue()
74 self.assertEqual(output.split('\n')[0], 'rsync --dry-run --archive --verbose --delete "canonical/" "cache/"')
74 self.assertEqual(output.split('\n')[0], 'rsync --dry-run --archive --verbose --delete canonical/ cache/')
7575 self.assertIn('DRY RUN', output)
7676
7777 def test_sync_apply(self):
7979 self.assertTrue(os.path.exists('cache/thing'))
8080 output = sys.stdout.getvalue()
8181 self.assertEqual(output.split('\n')[:4], [
82 'rsync --archive --verbose --delete "canonical/" "cache/"',
82 'rsync --archive --verbose --delete canonical/ cache/',
8383 'sending incremental file list',
8484 'thing',
8585 ''
9494 self.assertFalse(os.path.exists('cache/thing'))
9595 output = sys.stdout.getvalue()
9696 self.assertEqual(output.split('\n')[:4], [
97 'rsync --archive --verbose --delete "canonical/subdir/" "cache/subdir/"',
97 'rsync --archive --verbose --delete canonical/subdir/ cache/subdir/',
9898 'sending incremental file list',
9999 'stuff',
100100 ''
110110 output = sys.stdout.getvalue()
111111 lines = [l for l in output.split('\n') if l.startswith('rsync')]
112112 self.assertEqual(lines, [
113 'rsync --dry-run --archive --verbose --delete "canonical3/" "cache3/"',
114 'rsync --dry-run --archive --verbose --delete "canonical/" "cache/"',
113 'rsync --dry-run --archive --verbose --delete canonical3/ cache3/',
114 'rsync --dry-run --archive --verbose --delete canonical/ cache/',
115115 ])
116116
117117 def test_sync_thorough(self):
119119 output = sys.stdout.getvalue()
120120 lines = [l for l in output.split('\n') if l.startswith('rsync')]
121121 self.assertEqual(lines, [
122 'rsync --dry-run --checksum --archive --verbose --delete "canonical/" "cache/"',
122 'rsync --dry-run --checksum --archive --verbose --delete canonical/ cache/',
123 ])
124
125 def test_sync_with_spaces_in_dirnames(self):
126 check_call("mkdir -p 'can onical'", shell=True)
127 check_call("mkdir -p 'ca che'", shell=True)
128 router = {
129 'spaced': {
130 'from': 'can onical',
131 'to': 'ca che',
132 },
133 }
134 with open('backup.json', 'w') as f:
135 f.write(json.dumps(router))
136 main(['backup.json', 'sync', 'spaced'])
137 output = sys.stdout.getvalue()
138 lines = [l for l in output.split('\n') if l.startswith('rsync')]
139 self.assertEqual(lines, [
140 'rsync --dry-run --archive --verbose --delete "can onical/" "ca che/"',
123141 ])
124142
125143 def test_rename(self):