296
300
# Replace the full network location with the stripped copy.
297
301
return parsed_url.geturl().replace(parsed_url.netloc, straight_netloc, 1)
300
# Decorator for backend operation functions to simplify writing one that
301
# retries. Make sure to add a keyword argument 'raise_errors' to your function
302
# and if it is true, raise an exception on an error. If false, fatal-log it.
305
for n in range(1, globals.num_retries):
307
kwargs = {"raise_errors" : True}
308
return fn(*args, **kwargs)
309
except Exception as e:
310
log.Warn(_("Attempt %s failed: %s: %s")
311
% (n, e.__class__.__name__, str(e)))
312
log.Debug(_("Backtrace of previous error: %s")
313
% exception_traceback())
314
if isinstance(e, TemporaryLoadException):
315
time.sleep(30) # wait longer before trying again
317
time.sleep(10) # wait a bit before trying again
318
# Now try one last time, but fatal-log instead of raising errors
319
kwargs = {"raise_errors" : False}
320
return fn(*args, **kwargs)
323
# same as above, a bit dumber and always dies fatally if last trial fails
324
# hence no need for the raise_errors var ;), we really catch everything here
325
# as we don't know what the underlying code comes up with and we really *do*
326
# want to retry globals.num_retries times under all circumstances
328
def _retry_fatal(self, *args):
303
def get_code_from_exception(backend, e):
304
if isinstance(e, BackendException) and e.code != log.ErrorCode.backend_error:
306
elif hasattr(backend, '_error_code'):
307
return backend._error_code(e) or log.ErrorCode.backend_error
309
return log.ErrorCode.backend_error
311
def _retry(self, num_retries, fn, *args):
312
for n in range(1, num_retries):
314
return fn(self, *args)
315
except FatalBackendException as e:
316
# die on fatal errors
318
except Exception as e:
319
# retry on anything else
320
log.Warn(_("Attempt %s failed. %s: %s")
321
% (n, e.__class__.__name__, str(e)))
322
log.Debug(_("Backtrace of previous error: %s")
323
% exception_traceback())
324
if get_code_from_exception(self, e) == log.ErrorCode.backend_not_found:
325
# If we tried to do something, but the file just isn't there,
328
if isinstance(e, TemporaryLoadException):
329
time.sleep(1)#90) # wait longer before trying again
331
time.sleep(1)#30) # wait a bit before trying again
332
if hasattr(self, '_retry_cleanup'):
333
self._retry_cleanup()
335
def retry(fatal=True):
336
# Decorators with arguments introduce a new level of indirection. So we
337
# have to return a decorator function (which itself returns a function!)
338
def handle_fatal_error(backend, e, n):
339
code = get_code_from_exception(backend, e)
340
def make_filename(f):
341
if isinstance(f, path.ROPath):
342
return util.escape(f.name)
344
return util.escape(f)
345
extra = ' '.join([fn.__name__] + [make_filename(x) for x in args if x])
346
log.FatalError(_("Giving up after %s attempts. %s: %s")
347
% (n, e.__class__.__name__,
348
str(e)), code=code, extra=extra)
351
def inner_retry(self, *args):
331
352
for n in range(1, globals.num_retries):
334
354
return fn(self, *args)
335
except FatalBackendError as e:
355
except FatalBackendException as e:
336
356
# die on fatal errors
338
358
except Exception as e:
339
359
# retry on anything else
340
log.Warn(_("Attempt %s failed. %s: %s")
341
% (n, e.__class__.__name__, str(e)))
342
360
log.Debug(_("Backtrace of previous error: %s")
343
361
% exception_traceback())
344
time.sleep(10) # wait a bit before trying again
345
# final trial, die on exception
346
self.retry_count = n+1
347
return fn(self, *args)
348
except Exception as e:
349
log.Debug(_("Backtrace of previous error: %s")
350
% exception_traceback())
351
log.FatalError(_("Giving up after %s attempts. %s: %s")
352
% (self.retry_count, e.__class__.__name__, str(e)),
353
log.ErrorCode.backend_error)
362
at_end = n == globals.num_retries
363
if get_code_from_exception(self, e) == log.ErrorCode.backend_not_found:
364
# If we tried to do something, but the file just isn't there,
368
handle_fatal_error(self, e, n)
370
log.Warn(_("Attempt %s failed. %s: %s")
371
% (n, e.__class__.__name__, str(e)))
373
if isinstance(e, TemporaryLoadException):
374
time.sleep(1)#90) # wait longer before trying again
376
time.sleep(1)#30) # wait a bit before trying again
377
if hasattr(self, '_retry_cleanup'):
378
self._retry_cleanup()
358
383
class Backend(object):
360
385
Represents a generic duplicity backend, capable of storing and
361
386
retrieving files.
363
Concrete sub-classes are expected to implement:
388
See README in backends directory for information on how to write a backend.
376
391
def __init__(self, parsed_url):
377
392
self.parsed_url = parsed_url
379
def put(self, source_path, remote_filename = None):
394
def __do_put(self, source_path, remote_filename):
395
if hasattr(self, '_put'):
396
log.Info(_("Writing %s") % remote_filename)
397
self._put(source_path, remote_filename)
399
raise NotImplementedError()
402
def put(self, source_path, remote_filename=None):
381
404
Transfer source_path (Path object) to remote_filename (string)
383
406
If remote_filename is None, get the filename from the last
384
407
path component of pathname.
386
raise NotImplementedError()
409
if not remote_filename:
410
remote_filename = source_path.get_filename()
411
self.__do_put(source_path, remote_filename)
388
def move(self, source_path, remote_filename = None):
414
def move(self, source_path, remote_filename=None):
390
416
Move source_path (Path object) to remote_filename (string)
392
418
Same as put(), but unlinks source_path in the process. This allows the
393
419
local backend to do this more efficiently using rename.
395
self.put(source_path, remote_filename)
421
if not remote_filename:
422
remote_filename = source_path.get_filename()
423
if not hasattr(self, '_move') or not self._move(source_path, remote_filename):
424
self.__do_put(source_path, remote_filename)
398
428
def get(self, remote_filename, local_path):
399
429
"""Retrieve remote_filename and place in local_path"""
400
raise NotImplementedError()
430
if hasattr(self, '_get'):
431
self._get(remote_filename, local_path)
432
if not local_path.exists():
433
raise BackendException(_("File %s not found locally after get "
434
"from backend") % util.ufn(local_path.name))
437
raise NotImplementedError()
404
442
Return list of filenames (byte strings) present in backend
435
490
# if None, error querying file
437
492
# Returned dictionary is guaranteed to contain a metadata dictionary for
438
# each filename, but not all metadata are guaranteed to be present.
439
def query_info(self, filename_list, raise_errors=True):
493
# each filename, and all metadata are guaranteed to be present.
494
def query_info(self, filename_list):
441
496
Return metadata about each filename in filename_list
444
if hasattr(self, '_query_list_info'):
445
info = self._query_list_info(filename_list)
446
elif hasattr(self, '_query_file_info'):
499
if hasattr(self, '_query_list'):
500
info = self._do_query_list(filename_list)
501
elif hasattr(self, '_query'):
447
502
for filename in filename_list:
448
info[filename] = self._query_file_info(filename)
503
info[filename] = self._do_query(filename)
450
505
# Fill out any missing entries (may happen if backend has no support
451
506
# or its query_list support is lazy)
452
507
for filename in filename_list:
453
if filename not in info:
508
if filename not in info or info[filename] is None:
454
509
info[filename] = {}
510
for metadata in ['size']:
511
info[filename].setdefault(metadata, None)
516
def _do_query_list(self, filename_list):
517
info = self._query_list(filename_list)
523
def _do_query(self, filename):
524
return self._query(filename)
528
Close the backend, releasing any resources held and
529
invalidating any file objects obtained from the backend.
531
if hasattr(self, '_close'):
458
534
""" use getpass by default, inherited backends may overwrite this behaviour """
459
535
use_getpass = True