Merge lp:~duplicity-team/duplicity/check-volumes into lp:duplicity/0.6
- check-volumes
- Merge into 0.6-series
Status: | Merged |
---|---|
Merged at revision: | 782 |
Proposed branch: | lp:~duplicity-team/duplicity/check-volumes |
Merge into: | lp:duplicity/0.6 |
Diff against target: |
423 lines (+244/-10) 12 files modified
duplicity-bin (+18/-4) duplicity/backend.py (+29/-0) duplicity/backends/botobackend.py (+19/-0) duplicity/backends/cloudfilesbackend.py (+19/-0) duplicity/backends/giobackend.py (+17/-0) duplicity/backends/localbackend.py (+13/-1) duplicity/backends/u1backend.py (+37/-5) duplicity/commandline.py (+4/-0) duplicity/globals.py (+3/-0) duplicity/log.py (+1/-0) testing/alltests (+1/-0) testing/badupload.py (+83/-0) |
To merge this branch: | bzr merge lp:~duplicity-team/duplicity/check-volumes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Terry | Pending | ||
Review via email: mp+72826@code.launchpad.net |
Commit message
Description of the change
This is the first pass of checking each volume's size as it is uploaded, as discussed in https:/
edso (ed.so) wrote : | # |
Michael Terry (mterry) wrote : | # |
Having added and tested query support to the Ubuntu One, Rackspace, Amazon S3, GIO, and local backends, I'm marking this branch 'ready for review'.
Edso said he'd look into sftp/ftp later as well.
edso (ed.so) wrote : | # |
On 29.08.2011 05:42, Michael Terry wrote:
> Michael Terry has proposed merging lp:~duplicity-team/duplicity/check-volumes into lp:duplicity.
>
> Requested reviews:
> Michael Terry (mterry)
>
> For more details, see:
> https:/
>
> This is the first pass of checking each volume's size as it is uploaded, as discussed in https:/
>
mt,
did you see my comment on
https:/
?
ede
Michael Terry (mterry) wrote : | # |
> did you see my comment on
> https:/
> ?
Yes. The code gracefully handles backends that don't support querying metadata. It only declares a volume corrupt if the backend successfully determined size (or lack of a file altogether).
edso (ed.so) wrote : | # |
On 29.08.2011 14:42, Michael Terry wrote:
>> did you see my comment on
>> https:/
>> ?
>
> Yes. The code gracefully handles backends that don't support querying metadata. It only declares a volume corrupt if the backend successfully determined size (or lack of a file altogether).
ah, i see it now.. sorry for the noise.. ede
Preview Diff
1 | === modified file 'duplicity-bin' | |||
2 | --- duplicity-bin 2011-08-23 13:37:06 +0000 | |||
3 | +++ duplicity-bin 2011-08-29 03:36:23 +0000 | |||
4 | @@ -269,14 +269,28 @@ | |||
5 | 269 | end_block -= 1 | 269 | end_block -= 1 |
6 | 270 | return start_index, start_block, end_index, end_block | 270 | return start_index, start_block, end_index, end_block |
7 | 271 | 271 | ||
9 | 272 | def put(tdp, dest_filename): | 272 | def validate_block(tdp, dest_filename): |
10 | 273 | info = backend.query_info([dest_filename])[dest_filename] | ||
11 | 274 | if 'size' not in info: | ||
12 | 275 | return # backend didn't know how to query size | ||
13 | 276 | size = info['size'] | ||
14 | 277 | if size is None: | ||
15 | 278 | return # error querying file | ||
16 | 279 | if size != tdp.getsize(): | ||
17 | 280 | code_extra = "%s %d %d" % (util.escape(dest_filename), tdp.getsize(), size) | ||
18 | 281 | log.FatalError(_("File %s was corrupted during upload.") % dest_filename, | ||
19 | 282 | log.ErrorCode.volume_wrong_size, code_extra) | ||
20 | 283 | |||
21 | 284 | def put(tdp, dest_filename, vol_num): | ||
22 | 273 | """ | 285 | """ |
23 | 274 | Retrieve file size *before* calling backend.put(), which may (at least | 286 | Retrieve file size *before* calling backend.put(), which may (at least |
24 | 275 | in case of the localbackend) rename the temporary file to the target | 287 | in case of the localbackend) rename the temporary file to the target |
25 | 276 | instead of copying. | 288 | instead of copying. |
26 | 277 | """ | 289 | """ |
27 | 278 | putsize = tdp.getsize() | 290 | putsize = tdp.getsize() |
29 | 279 | backend.put(tdp, dest_filename) | 291 | if globals.skip_volume != vol_num: # for testing purposes only |
30 | 292 | backend.put(tdp, dest_filename) | ||
31 | 293 | validate_block(tdp, dest_filename) | ||
32 | 280 | if tdp.stat: | 294 | if tdp.stat: |
33 | 281 | tdp.delete() | 295 | tdp.delete() |
34 | 282 | return putsize | 296 | return putsize |
35 | @@ -350,8 +364,8 @@ | |||
36 | 350 | sig_outfp.flush() | 364 | sig_outfp.flush() |
37 | 351 | man_outfp.flush() | 365 | man_outfp.flush() |
38 | 352 | 366 | ||
41 | 353 | async_waiters.append(io_scheduler.schedule_task(lambda tdp, dest_filename: put(tdp, dest_filename), | 367 | async_waiters.append(io_scheduler.schedule_task(lambda tdp, dest_filename, vol_num: put(tdp, dest_filename, vol_num), |
42 | 354 | (tdp, dest_filename))) | 368 | (tdp, dest_filename, vol_num))) |
43 | 355 | 369 | ||
44 | 356 | # Log human-readable version as well as raw numbers for machine consumers | 370 | # Log human-readable version as well as raw numbers for machine consumers |
45 | 357 | log.Progress('Processed volume %d' % vol_num, diffdir.stats.SourceFileSize) | 371 | log.Progress('Processed volume %d' % vol_num, diffdir.stats.SourceFileSize) |
46 | 358 | 372 | ||
47 | === modified file 'duplicity/backend.py' | |||
48 | --- duplicity/backend.py 2011-08-06 15:57:54 +0000 | |||
49 | +++ duplicity/backend.py 2011-08-29 03:36:23 +0000 | |||
50 | @@ -361,6 +361,35 @@ | |||
51 | 361 | """ | 361 | """ |
52 | 362 | raise NotImplementedError() | 362 | raise NotImplementedError() |
53 | 363 | 363 | ||
54 | 364 | # Should never cause FatalError. | ||
55 | 365 | # Returns a dictionary of dictionaries. The outer dictionary maps | ||
56 | 366 | # filenames to metadata dictionaries. Supported metadata are: | ||
57 | 367 | # | ||
58 | 368 | # 'size': if >= 0, size of file | ||
59 | 369 | # if -1, file is not found | ||
60 | 370 | # if None, error querying file | ||
61 | 371 | # | ||
62 | 372 | # Returned dictionary is guaranteed to contain a metadata dictionary for | ||
63 | 373 | # each filename, but not all metadata are guaranteed to be present. | ||
64 | 374 | def query_info(self, filename_list, raise_errors=True): | ||
65 | 375 | """ | ||
66 | 376 | Return metadata about each filename in filename_list | ||
67 | 377 | """ | ||
68 | 378 | info = {} | ||
69 | 379 | if hasattr(self, '_query_list_info'): | ||
70 | 380 | info = self._query_list_info(filename_list) | ||
71 | 381 | elif hasattr(self, '_query_file_info'): | ||
72 | 382 | for filename in filename_list: | ||
73 | 383 | info[filename] = self._query_file_info(filename) | ||
74 | 384 | |||
75 | 385 | # Fill out any missing entries (may happen if backend has no support | ||
76 | 386 | # or its query_list support is lazy) | ||
77 | 387 | for filename in filename_list: | ||
78 | 388 | if filename not in info: | ||
79 | 389 | info[filename] = {} | ||
80 | 390 | |||
81 | 391 | return info | ||
82 | 392 | |||
83 | 364 | """ use getpass by default, inherited backends may overwrite this behaviour """ | 393 | """ use getpass by default, inherited backends may overwrite this behaviour """ |
84 | 365 | use_getpass = True | 394 | use_getpass = True |
85 | 366 | 395 | ||
86 | 367 | 396 | ||
87 | === modified file 'duplicity/backends/botobackend.py' | |||
88 | --- duplicity/backends/botobackend.py 2011-04-04 13:01:12 +0000 | |||
89 | +++ duplicity/backends/botobackend.py 2011-08-29 03:36:23 +0000 | |||
90 | @@ -26,6 +26,7 @@ | |||
91 | 26 | from duplicity import log | 26 | from duplicity import log |
92 | 27 | from duplicity.errors import * #@UnusedWildImport | 27 | from duplicity.errors import * #@UnusedWildImport |
93 | 28 | from duplicity.util import exception_traceback | 28 | from duplicity.util import exception_traceback |
94 | 29 | from duplicity.backend import retry | ||
95 | 29 | 30 | ||
96 | 30 | class BotoBackend(duplicity.backend.Backend): | 31 | class BotoBackend(duplicity.backend.Backend): |
97 | 31 | """ | 32 | """ |
98 | @@ -294,6 +295,24 @@ | |||
99 | 294 | self.bucket.delete_key(self.key_prefix + filename) | 295 | self.bucket.delete_key(self.key_prefix + filename) |
100 | 295 | log.Debug("Deleted %s/%s" % (self.straight_url, filename)) | 296 | log.Debug("Deleted %s/%s" % (self.straight_url, filename)) |
101 | 296 | 297 | ||
102 | 298 | @retry | ||
103 | 299 | def _query_file_info(self, filename, raise_errors=False): | ||
104 | 300 | try: | ||
105 | 301 | key = self.bucket.lookup(self.key_prefix + filename) | ||
106 | 302 | if key is None: | ||
107 | 303 | return {'size': -1} | ||
108 | 304 | return {'size': key.size} | ||
109 | 305 | except Exception, e: | ||
110 | 306 | log.Warn("Query %s/%s failed: %s" | ||
111 | 307 | "" % (self.straight_url, | ||
112 | 308 | filename, | ||
113 | 309 | str(e))) | ||
114 | 310 | self.resetConnection() | ||
115 | 311 | if raise_errors: | ||
116 | 312 | raise e | ||
117 | 313 | else: | ||
118 | 314 | return {'size': None} | ||
119 | 315 | |||
120 | 297 | duplicity.backend.register_backend("s3", BotoBackend) | 316 | duplicity.backend.register_backend("s3", BotoBackend) |
121 | 298 | duplicity.backend.register_backend("s3+http", BotoBackend) | 317 | duplicity.backend.register_backend("s3+http", BotoBackend) |
122 | 299 | 318 | ||
123 | 300 | 319 | ||
124 | === modified file 'duplicity/backends/cloudfilesbackend.py' | |||
125 | --- duplicity/backends/cloudfilesbackend.py 2011-02-12 15:11:34 +0000 | |||
126 | +++ duplicity/backends/cloudfilesbackend.py 2011-08-29 03:36:23 +0000 | |||
127 | @@ -26,6 +26,7 @@ | |||
128 | 26 | from duplicity import log | 26 | from duplicity import log |
129 | 27 | from duplicity.errors import * #@UnusedWildImport | 27 | from duplicity.errors import * #@UnusedWildImport |
130 | 28 | from duplicity.util import exception_traceback | 28 | from duplicity.util import exception_traceback |
131 | 29 | from duplicity.backend import retry | ||
132 | 29 | 30 | ||
133 | 30 | class CloudFilesBackend(duplicity.backend.Backend): | 31 | class CloudFilesBackend(duplicity.backend.Backend): |
134 | 31 | """ | 32 | """ |
135 | @@ -140,4 +141,22 @@ | |||
136 | 140 | self.container.delete_object(file) | 141 | self.container.delete_object(file) |
137 | 141 | log.Debug("Deleted '%s/%s'" % (self.container, file)) | 142 | log.Debug("Deleted '%s/%s'" % (self.container, file)) |
138 | 142 | 143 | ||
139 | 144 | @retry | ||
140 | 145 | def _query_file_info(self, filename, raise_errors=False): | ||
141 | 146 | from cloudfiles.errors import NoSuchObject | ||
142 | 147 | try: | ||
143 | 148 | sobject = self.container.get_object(filename) | ||
144 | 149 | return {'size': sobject.size} | ||
145 | 150 | except NoSuchObject: | ||
146 | 151 | return {'size': -1} | ||
147 | 152 | except Exception, e: | ||
148 | 153 | log.Warn("Error querying '%s/%s': %s" | ||
149 | 154 | "" % (self.container, | ||
150 | 155 | filename, | ||
151 | 156 | str(e))) | ||
152 | 157 | if raise_errors: | ||
153 | 158 | raise e | ||
154 | 159 | else: | ||
155 | 160 | return {'size': None} | ||
156 | 161 | |||
157 | 143 | duplicity.backend.register_backend("cf+http", CloudFilesBackend) | 162 | duplicity.backend.register_backend("cf+http", CloudFilesBackend) |
158 | 144 | 163 | ||
159 | === modified file 'duplicity/backends/giobackend.py' | |||
160 | --- duplicity/backends/giobackend.py 2011-06-12 22:25:39 +0000 | |||
161 | +++ duplicity/backends/giobackend.py 2011-08-29 03:36:23 +0000 | |||
162 | @@ -164,3 +164,20 @@ | |||
163 | 164 | self.handle_error(raise_errors, e, 'delete', | 164 | self.handle_error(raise_errors, e, 'delete', |
164 | 165 | target_file.get_parse_name()) | 165 | target_file.get_parse_name()) |
165 | 166 | return | 166 | return |
166 | 167 | |||
167 | 168 | @retry | ||
168 | 169 | def _query_file_info(self, filename, raise_errors=False): | ||
169 | 170 | """Query attributes on filename""" | ||
170 | 171 | target_file = self.remote_file.get_child(filename) | ||
171 | 172 | attrs = gio.FILE_ATTRIBUTE_STANDARD_SIZE | ||
172 | 173 | try: | ||
173 | 174 | info = target_file.query_info(attrs, gio.FILE_QUERY_INFO_NONE) | ||
174 | 175 | return {'size': info.get_size()} | ||
175 | 176 | except Exception, e: | ||
176 | 177 | if isinstance(e, gio.Error): | ||
177 | 178 | if e.code == gio.ERROR_NOT_FOUND: | ||
178 | 179 | return {'size': -1} # early exit, no need to retry | ||
179 | 180 | if raise_errors: | ||
180 | 181 | raise e | ||
181 | 182 | else: | ||
182 | 183 | return {'size': None} | ||
183 | 167 | 184 | ||
184 | === modified file 'duplicity/backends/localbackend.py' | |||
185 | --- duplicity/backends/localbackend.py 2011-06-17 18:22:28 +0000 | |||
186 | +++ duplicity/backends/localbackend.py 2011-08-29 03:36:23 +0000 | |||
187 | @@ -57,7 +57,7 @@ | |||
188 | 57 | code = log.ErrorCode.backend_no_space | 57 | code = log.ErrorCode.backend_no_space |
189 | 58 | extra = ' '.join([util.escape(x) for x in [file1, file2] if x]) | 58 | extra = ' '.join([util.escape(x) for x in [file1, file2] if x]) |
190 | 59 | extra = ' '.join([op, extra]) | 59 | extra = ' '.join([op, extra]) |
192 | 60 | if op != 'delete': | 60 | if op != 'delete' and op != 'query': |
193 | 61 | log.FatalError(str(e), code, extra) | 61 | log.FatalError(str(e), code, extra) |
194 | 62 | else: | 62 | else: |
195 | 63 | log.Warn(str(e), code, extra) | 63 | log.Warn(str(e), code, extra) |
196 | @@ -110,5 +110,17 @@ | |||
197 | 110 | except Exception, e: | 110 | except Exception, e: |
198 | 111 | self.handle_error(e, 'delete', self.remote_pathdir.append(filename).name) | 111 | self.handle_error(e, 'delete', self.remote_pathdir.append(filename).name) |
199 | 112 | 112 | ||
200 | 113 | def _query_file_info(self, filename): | ||
201 | 114 | """Query attributes on filename""" | ||
202 | 115 | try: | ||
203 | 116 | target_file = self.remote_pathdir.append(filename) | ||
204 | 117 | if not os.path.exists(target_file.name): | ||
205 | 118 | return {'size': -1} | ||
206 | 119 | target_file.setdata() | ||
207 | 120 | size = target_file.getsize() | ||
208 | 121 | return {'size': size} | ||
209 | 122 | except Exception, e: | ||
210 | 123 | self.handle_error(e, 'query', target_file.name) | ||
211 | 124 | return {'size': None} | ||
212 | 113 | 125 | ||
213 | 114 | duplicity.backend.register_backend("file", LocalBackend) | 126 | duplicity.backend.register_backend("file", LocalBackend) |
214 | 115 | 127 | ||
215 | === modified file 'duplicity/backends/u1backend.py' | |||
216 | --- duplicity/backends/u1backend.py 2011-08-17 14:25:52 +0000 | |||
217 | +++ duplicity/backends/u1backend.py 2011-08-29 03:36:23 +0000 | |||
218 | @@ -98,17 +98,15 @@ | |||
219 | 98 | import urllib | 98 | import urllib |
220 | 99 | return urllib.quote(url, safe="/~") | 99 | return urllib.quote(url, safe="/~") |
221 | 100 | 100 | ||
223 | 101 | def handle_error(self, raise_error, op, headers, file1=None, file2=None, ignore=None): | 101 | def parse_error(self, headers, ignore=None): |
224 | 102 | from duplicity import log | 102 | from duplicity import log |
225 | 103 | from duplicity import util | ||
226 | 104 | import json | ||
227 | 105 | 103 | ||
228 | 106 | status = int(headers[0].get('status')) | 104 | status = int(headers[0].get('status')) |
229 | 107 | if status >= 200 and status < 300: | 105 | if status >= 200 and status < 300: |
231 | 108 | return | 106 | return None |
232 | 109 | 107 | ||
233 | 110 | if ignore and status in ignore: | 108 | if ignore and status in ignore: |
235 | 111 | return | 109 | return None |
236 | 112 | 110 | ||
237 | 113 | if status == 400: | 111 | if status == 400: |
238 | 114 | code = log.ErrorCode.backend_permission_denied | 112 | code = log.ErrorCode.backend_permission_denied |
239 | @@ -118,6 +116,18 @@ | |||
240 | 118 | code = log.ErrorCode.backend_no_space | 116 | code = log.ErrorCode.backend_no_space |
241 | 119 | else: | 117 | else: |
242 | 120 | code = log.ErrorCode.backend_error | 118 | code = log.ErrorCode.backend_error |
243 | 119 | return code | ||
244 | 120 | |||
245 | 121 | def handle_error(self, raise_error, op, headers, file1=None, file2=None, ignore=None): | ||
246 | 122 | from duplicity import log | ||
247 | 123 | from duplicity import util | ||
248 | 124 | import json | ||
249 | 125 | |||
250 | 126 | code = self.parse_error(headers, ignore) | ||
251 | 127 | if code is None: | ||
252 | 128 | return | ||
253 | 129 | |||
254 | 130 | status = int(headers[0].get('status')) | ||
255 | 121 | 131 | ||
256 | 122 | if file1: | 132 | if file1: |
257 | 123 | file1 = file1.encode("utf8") | 133 | file1 = file1.encode("utf8") |
258 | @@ -222,5 +232,27 @@ | |||
259 | 222 | answer = auth.request(remote_full, http_method="DELETE") | 232 | answer = auth.request(remote_full, http_method="DELETE") |
260 | 223 | self.handle_error(raise_errors, 'delete', answer, remote_full, ignore=[404]) | 233 | self.handle_error(raise_errors, 'delete', answer, remote_full, ignore=[404]) |
261 | 224 | 234 | ||
262 | 235 | @retry | ||
263 | 236 | def _query_file_info(self, filename, raise_errors=False): | ||
264 | 237 | """Query attributes on filename""" | ||
265 | 238 | import json | ||
266 | 239 | import ubuntuone.couch.auth as auth | ||
267 | 240 | from duplicity import log | ||
268 | 241 | remote_full = self.meta_base + self.quote(filename) | ||
269 | 242 | answer = auth.request(remote_full) | ||
270 | 243 | |||
271 | 244 | code = self.parse_error(answer) | ||
272 | 245 | if code is not None: | ||
273 | 246 | if code == log.ErrorCode.backend_not_found: | ||
274 | 247 | return {'size': -1} | ||
275 | 248 | elif raise_errors: | ||
276 | 249 | self.handle_error(raise_errors, 'query', answer, remote_full, filename) | ||
277 | 250 | else: | ||
278 | 251 | return {'size': None} | ||
279 | 252 | |||
280 | 253 | node = json.loads(answer[1]) | ||
281 | 254 | size = node.get('size') | ||
282 | 255 | return {'size': size} | ||
283 | 256 | |||
284 | 225 | duplicity.backend.register_backend("u1", U1Backend) | 257 | duplicity.backend.register_backend("u1", U1Backend) |
285 | 226 | duplicity.backend.register_backend("u1+http", U1Backend) | 258 | duplicity.backend.register_backend("u1+http", U1Backend) |
286 | 227 | 259 | ||
287 | === modified file 'duplicity/commandline.py' | |||
288 | --- duplicity/commandline.py 2011-08-18 18:09:18 +0000 | |||
289 | +++ duplicity/commandline.py 2011-08-29 03:36:23 +0000 | |||
290 | @@ -292,6 +292,10 @@ | |||
291 | 292 | parser.add_option("--fail-on-volume", type="int", | 292 | parser.add_option("--fail-on-volume", type="int", |
292 | 293 | help=optparse.SUPPRESS_HELP) | 293 | help=optparse.SUPPRESS_HELP) |
293 | 294 | 294 | ||
294 | 295 | # used in testing only - skips upload for a given volume | ||
295 | 296 | parser.add_option("--skip-volume", type="int", | ||
296 | 297 | help=optparse.SUPPRESS_HELP) | ||
297 | 298 | |||
298 | 295 | # If set, restore only the subdirectory or file specified, not the | 299 | # If set, restore only the subdirectory or file specified, not the |
299 | 296 | # whole root. | 300 | # whole root. |
300 | 297 | # TRANSL: Used in usage help to represent a Unix-style path name. Example: | 301 | # TRANSL: Used in usage help to represent a Unix-style path name. Example: |
301 | 298 | 302 | ||
302 | === modified file 'duplicity/globals.py' | |||
303 | --- duplicity/globals.py 2011-08-18 18:09:18 +0000 | |||
304 | +++ duplicity/globals.py 2011-08-29 03:36:23 +0000 | |||
305 | @@ -200,6 +200,9 @@ | |||
306 | 200 | # used in testing only - raises exception after volume | 200 | # used in testing only - raises exception after volume |
307 | 201 | fail_on_volume = 0 | 201 | fail_on_volume = 0 |
308 | 202 | 202 | ||
309 | 203 | # used in testing only - skips uploading a particular volume | ||
310 | 204 | skip_volume = 0 | ||
311 | 205 | |||
312 | 203 | # ignore (some) errors during operations; supposed to make it more | 206 | # ignore (some) errors during operations; supposed to make it more |
313 | 204 | # likely that you are able to restore data under problematic | 207 | # likely that you are able to restore data under problematic |
314 | 205 | # circumstances. the default should absolutely always be True unless | 208 | # circumstances. the default should absolutely always be True unless |
315 | 206 | 209 | ||
316 | === modified file 'duplicity/log.py' | |||
317 | --- duplicity/log.py 2011-05-31 18:07:07 +0000 | |||
318 | +++ duplicity/log.py 2011-08-29 03:36:23 +0000 | |||
319 | @@ -189,6 +189,7 @@ | |||
320 | 189 | gio_not_available = 40 | 189 | gio_not_available = 40 |
321 | 190 | source_dir_mismatch = 42 # 41 is reserved for par2 | 190 | source_dir_mismatch = 42 # 41 is reserved for par2 |
322 | 191 | ftps_lftp_missing = 43 | 191 | ftps_lftp_missing = 43 |
323 | 192 | volume_wrong_size = 44 | ||
324 | 192 | 193 | ||
325 | 193 | # 50->69 reserved for backend errors | 194 | # 50->69 reserved for backend errors |
326 | 194 | backend_error = 50 | 195 | backend_error = 50 |
327 | 195 | 196 | ||
328 | === modified file 'testing/alltests' | |||
329 | --- testing/alltests 2009-08-12 17:43:42 +0000 | |||
330 | +++ testing/alltests 2011-08-29 03:36:23 +0000 | |||
331 | @@ -24,3 +24,4 @@ | |||
332 | 24 | finaltest.py | 24 | finaltest.py |
333 | 25 | restarttest.py | 25 | restarttest.py |
334 | 26 | cleanuptest.py | 26 | cleanuptest.py |
335 | 27 | badupload.py | ||
336 | 27 | 28 | ||
337 | === added file 'testing/badupload.py' | |||
338 | --- testing/badupload.py 1970-01-01 00:00:00 +0000 | |||
339 | +++ testing/badupload.py 2011-08-29 03:36:23 +0000 | |||
340 | @@ -0,0 +1,83 @@ | |||
341 | 1 | # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
342 | 2 | # | ||
343 | 3 | # Copyright 2002 Ben Escoto <ben@emerose.org> | ||
344 | 4 | # Copyright 2007 Kenneth Loafman <kenneth@loafman.com> | ||
345 | 5 | # Copyright 2011 Canonical Ltd | ||
346 | 6 | # | ||
347 | 7 | # This file is part of duplicity. | ||
348 | 8 | # | ||
349 | 9 | # Duplicity is free software; you can redistribute it and/or modify it | ||
350 | 10 | # under the terms of the GNU General Public License as published by the | ||
351 | 11 | # Free Software Foundation; either version 2 of the License, or (at your | ||
352 | 12 | # option) any later version. | ||
353 | 13 | # | ||
354 | 14 | # Duplicity is distributed in the hope that it will be useful, but | ||
355 | 15 | # WITHOUT ANY WARRANTY; without even the implied warranty of | ||
356 | 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
357 | 17 | # General Public License for more details. | ||
358 | 18 | # | ||
359 | 19 | # You should have received a copy of the GNU General Public License | ||
360 | 20 | # along with duplicity; if not, write to the Free Software Foundation, | ||
361 | 21 | # Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | ||
362 | 22 | |||
363 | 23 | import config | ||
364 | 24 | import os, unittest, sys | ||
365 | 25 | sys.path.insert(0, "../") | ||
366 | 26 | |||
367 | 27 | config.setup() | ||
368 | 28 | |||
369 | 29 | # This can be changed to select the URL to use | ||
370 | 30 | backend_url = 'file://testfiles/output' | ||
371 | 31 | |||
372 | 32 | class CmdError(Exception): | ||
373 | 33 | """Indicates an error running an external command""" | ||
374 | 34 | return_val = -1 | ||
375 | 35 | def __init__(self, return_val): | ||
376 | 36 | self.return_val = os.WEXITSTATUS(return_val) | ||
377 | 37 | |||
378 | 38 | class BadUploadTest(unittest.TestCase): | ||
379 | 39 | """ | ||
380 | 40 | Test missing volume upload using duplicity binary | ||
381 | 41 | """ | ||
382 | 42 | def setUp(self): | ||
383 | 43 | assert not os.system("tar xzf testfiles.tar.gz > /dev/null 2>&1") | ||
384 | 44 | |||
385 | 45 | def tearDown(self): | ||
386 | 46 | assert not os.system("rm -rf testfiles tempdir temp2.tar") | ||
387 | 47 | |||
388 | 48 | def run_duplicity(self, arglist, options = []): | ||
389 | 49 | """ | ||
390 | 50 | Run duplicity binary with given arguments and options | ||
391 | 51 | """ | ||
392 | 52 | options.append("--archive-dir testfiles/cache") | ||
393 | 53 | cmd_list = ["../duplicity-bin"] | ||
394 | 54 | cmd_list.extend(options + ["--allow-source-mismatch"]) | ||
395 | 55 | cmd_list.extend(arglist) | ||
396 | 56 | cmdline = " ".join(cmd_list) | ||
397 | 57 | if not os.environ.has_key('PASSPHRASE'): | ||
398 | 58 | os.environ['PASSPHRASE'] = 'foobar' | ||
399 | 59 | return_val = os.system(cmdline) | ||
400 | 60 | if return_val: | ||
401 | 61 | raise CmdError(return_val) | ||
402 | 62 | |||
403 | 63 | def backup(self, type, input_dir, options = []): | ||
404 | 64 | """Run duplicity backup to default directory""" | ||
405 | 65 | options = options[:] | ||
406 | 66 | if type == "full": | ||
407 | 67 | options.insert(0, 'full') | ||
408 | 68 | args = [input_dir, "'%s'" % backend_url] | ||
409 | 69 | self.run_duplicity(args, options) | ||
410 | 70 | |||
411 | 71 | def test_missing_file(self): | ||
412 | 72 | """ | ||
413 | 73 | Test basic lost file | ||
414 | 74 | """ | ||
415 | 75 | # we know we're going to fail this one, its forced | ||
416 | 76 | try: | ||
417 | 77 | self.backup("full", "testfiles/dir1", options = ["--skip-volume 1"]) | ||
418 | 78 | assert False # shouldn't get this far | ||
419 | 79 | except CmdError, e: | ||
420 | 80 | assert e.return_val == 44, e.return_val | ||
421 | 81 | |||
422 | 82 | if __name__ == "__main__": | ||
423 | 83 | unittest.main() |
1 === modified file 'duplicity-bin' dest_filename) , tdp.getsize(), size) _("File %s was corrupted during upload.") % dest_filename, volume_ wrong_size, code_extra)
...
8 - def put(tdp, dest_filename):
9 + def validate_block(tdp, dest_filename):
...
16 + if size != tdp.getsize():
17 + code_extra = "%s %d %d" % (util.escape(
18 + log.FatalError(
19 + log.ErrorCode.
if we can't get a file size, we cannot assume that the file is corrupted, probably the backend only does not support it.
i am busy the next few days but will have a look at sftp/ftp implementations next week .. ede/duply.net