19
19
# along with duplicity; if not, write to the Free Software Foundation,
20
20
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23
23
Provides a common interface to all backends and certain sevices
24
24
intended to be used by the backends themselves.
88
88
path = duplicity.backends.__path__[0]
89
assert path.endswith("duplicity/backends"), duplicity.backends.__path__
89
assert path.endswith(u"duplicity/backends"), duplicity.backends.__path__
91
91
files = os.listdir(path)
94
if fn.endswith("backend.py"):
94
if fn.endswith(u"backend.py"):
96
imp = "duplicity.backends.%s" % (fn,)
96
imp = u"duplicity.backends.%s" % (fn,)
100
100
except Exception:
101
res = "Failed: " + str(sys.exc_info()[1])
102
log.Log(_("Import of %s %s") % (imp, res), log.INFO)
101
res = u"Failed: " + str(sys.exc_info()[1])
102
log.Log(_(u"Import of %s %s") % (imp, res), log.INFO)
107
107
def register_backend(scheme, backend_factory):
109
109
Register a given backend factory responsible for URL:s with the
123
assert callable(backend_factory), "backend factory must be callable"
123
assert callable(backend_factory), u"backend factory must be callable"
125
125
if scheme in _backends:
126
raise ConflictingScheme("the scheme %s already has a backend "
126
raise ConflictingScheme(u"the scheme %s already has a backend "
127
u"associated with it"
130
130
_backends[scheme] = backend_factory
133
133
def register_backend_prefix(scheme, backend_factory):
135
135
Register a given backend factory responsible for URL:s with the
136
136
given scheme prefix.
147
147
global _backend_prefixes
149
assert callable(backend_factory), "backend factory must be callable"
149
assert callable(backend_factory), u"backend factory must be callable"
151
151
if scheme in _backend_prefixes:
152
raise ConflictingScheme("the prefix %s already has a backend "
152
raise ConflictingScheme(u"the prefix %s already has a backend "
153
u"associated with it"
156
156
_backend_prefixes[scheme] = backend_factory
159
159
def strip_prefix(url_string, prefix_scheme):
161
161
strip the prefix from a string e.g. par2+ftp://... -> ftp://...
163
163
return re.sub(r'(?i)^' + re.escape(prefix_scheme) + r'\+', r'', url_string)
166
166
def is_backend_url(url_string):
168
168
@return Whether the given string looks like a backend URL.
170
170
pu = ParsedUrl(url_string)
189
189
global _backends, _backend_prefixes
191
191
pu = ParsedUrl(url_string)
192
assert pu.scheme, "should be a backend url according to is_backend_url"
192
assert pu.scheme, u"should be a backend url according to is_backend_url"
196
196
for prefix in _backend_prefixes:
197
if url_string.startswith(prefix + '+'):
197
if url_string.startswith(prefix + u'+'):
198
198
factory = _backend_prefixes[prefix]
199
199
pu = ParsedUrl(strip_prefix(url_string, prefix))
209
209
return factory(pu)
210
210
except ImportError:
211
raise BackendException(_("Could not initialize backend: %s") % str(sys.exc_info()[1]))
211
raise BackendException(_(u"Could not initialize backend: %s") % str(sys.exc_info()[1]))
214
214
def get_backend(url_string):
216
216
Instantiate a backend suitable for the given URL, or return None
217
217
if the given string looks like a local path rather than a URL.
219
219
Raise InvalidBackendURL if the URL is not a valid URL.
221
221
if globals.use_gio:
222
url_string = 'gio+' + url_string
222
url_string = u'gio+' + url_string
223
223
obj = get_backend_object(url_string)
225
225
obj = BackendWrapper(obj)
253
253
pu = urlparse.urlparse(url_string)
254
254
except Exception:
255
raise InvalidBackendURL("Syntax error in: %s" % url_string)
255
raise InvalidBackendURL(u"Syntax error in: %s" % url_string)
258
258
self.scheme = pu.scheme
259
259
except Exception:
260
raise InvalidBackendURL("Syntax error (scheme) in: %s" % url_string)
260
raise InvalidBackendURL(u"Syntax error (scheme) in: %s" % url_string)
263
263
self.netloc = pu.netloc
264
264
except Exception:
265
raise InvalidBackendURL("Syntax error (netloc) in: %s" % url_string)
265
raise InvalidBackendURL(u"Syntax error (netloc) in: %s" % url_string)
268
268
self.path = pu.path
270
270
self.path = urllib.unquote(self.path)
271
271
except Exception:
272
raise InvalidBackendURL("Syntax error (path) in: %s" % url_string)
272
raise InvalidBackendURL(u"Syntax error (path) in: %s" % url_string)
275
275
self.username = pu.username
276
276
except Exception:
277
raise InvalidBackendURL("Syntax error (username) in: %s" % url_string)
277
raise InvalidBackendURL(u"Syntax error (username) in: %s" % url_string)
278
278
if self.username:
279
279
self.username = urllib.unquote(pu.username)
284
284
self.password = pu.password
285
285
except Exception:
286
raise InvalidBackendURL("Syntax error (password) in: %s" % url_string)
286
raise InvalidBackendURL(u"Syntax error (password) in: %s" % url_string)
287
287
if self.password:
288
288
self.password = urllib.unquote(self.password)
293
293
self.hostname = pu.hostname
294
294
except Exception:
295
raise InvalidBackendURL("Syntax error (hostname) in: %s" % url_string)
295
raise InvalidBackendURL(u"Syntax error (hostname) in: %s" % url_string)
297
297
# init to None, overwrite with actual value on success
300
300
self.port = pu.port
301
301
except Exception: # not raised in python2.7+, just returns None
302
302
# old style rsync://host::[/]dest, are still valid, though they contain no port
303
if not (self.scheme in ['rsync'] and re.search('::[^:]*$', self.url_string)):
304
raise InvalidBackendURL("Syntax error (port) in: %s A%s B%s C%s" %
305
(url_string, (self.scheme in ['rsync']),
306
re.search('::[^:]+$', self.netloc), self.netloc))
303
if not (self.scheme in [u'rsync'] and re.search(u'::[^:]*$', self.url_string)):
304
raise InvalidBackendURL(u"Syntax error (port) in: %s A%s B%s C%s" %
305
(url_string, (self.scheme in [u'rsync']),
306
re.search(u'::[^:]+$', self.netloc), self.netloc))
308
308
# Our URL system uses two slashes more than urlparse's does when using
309
309
# non-netloc URLs. And we want to make sure that if urlparse assuming
310
310
# a netloc where we don't want one, that we correct it.
311
311
if self.scheme not in uses_netloc:
313
self.path = '//' + self.netloc + self.path
313
self.path = u'//' + self.netloc + self.path
315
315
self.hostname = None
316
elif not self.path.startswith('//') and self.path.startswith('/'):
317
self.path = '//' + self.path
316
elif not self.path.startswith(u'//') and self.path.startswith(u'/'):
317
self.path = u'//' + self.path
319
319
# This happens for implicit local paths.
320
320
if not self.scheme:
323
323
# Our backends do not handle implicit hosts.
324
324
if self.scheme in uses_netloc and not self.hostname:
325
raise InvalidBackendURL("Missing hostname in a backend URL which "
326
"requires an explicit hostname: %s"
325
raise InvalidBackendURL(u"Missing hostname in a backend URL which "
326
u"requires an explicit hostname: %s"
329
329
# Our backends do not handle implicit relative paths.
330
if self.scheme not in uses_netloc and not self.path.startswith('//'):
331
raise InvalidBackendURL("missing // - relative paths not supported "
333
"" % (self.scheme, url_string))
330
if self.scheme not in uses_netloc and not self.path.startswith(u'//'):
331
raise InvalidBackendURL(u"missing // - relative paths not supported "
333
u"" % (self.scheme, url_string))
335
335
def geturl(self):
336
336
return self.url_string
339
339
def strip_auth_from_url(parsed_url):
340
"""Return a URL from a urlparse object without a username or password."""
340
u"""Return a URL from a urlparse object without a username or password."""
342
clean_url = re.sub('^([^:/]+://)(.*@)?(.*)', r'\1\3', parsed_url.geturl())
342
clean_url = re.sub(u'^([^:/]+://)(.*@)?(.*)', r'\1\3', parsed_url.geturl())
346
346
def _get_code_from_exception(backend, operation, e):
347
347
if isinstance(e, BackendException) and e.code != log.ErrorCode.backend_error:
349
elif hasattr(backend, '_error_code'):
349
elif hasattr(backend, u'_error_code'):
350
350
return backend._error_code(operation, e) or log.ErrorCode.backend_error
351
elif hasattr(e, 'errno'):
351
elif hasattr(e, u'errno'):
352
352
# A few backends return such errors (local, paramiko, etc)
353
353
if e.errno == errno.EACCES:
354
354
return log.ErrorCode.backend_permission_denied
373
373
except Exception as e:
374
374
# retry on anything else
375
log.Debug(_("Backtrace of previous error: %s")
375
log.Debug(_(u"Backtrace of previous error: %s")
376
376
% exception_traceback())
377
377
at_end = n == globals.num_retries
378
378
code = _get_code_from_exception(self.backend, operation, e)
386
386
return util.escape(f.uc_name)
388
388
return util.escape(f)
389
extra = ' '.join([operation] + [make_filename(x) for x in args if x])
390
log.FatalError(_("Giving up after %s attempts. %s: %s")
389
extra = u' '.join([operation] + [make_filename(x) for x in args if x])
390
log.FatalError(_(u"Giving up after %s attempts. %s: %s")
391
391
% (n, e.__class__.__name__,
392
392
util.uexc(e)), code=code, extra=extra)
394
log.Warn(_("Attempt %s failed. %s: %s")
394
log.Warn(_(u"Attempt %s failed. %s: %s")
395
395
% (n, e.__class__.__name__, util.uexc(e)))
397
397
if isinstance(e, TemporaryLoadException):
398
398
time.sleep(3 * globals.backend_retry_delay) # wait longer before trying again
400
400
time.sleep(globals.backend_retry_delay) # wait a bit before trying again
401
if hasattr(self.backend, '_retry_cleanup'):
401
if hasattr(self.backend, u'_retry_cleanup'):
402
402
self.backend._retry_cleanup()
404
404
return inner_retry
408
408
class Backend(object):
410
410
See README in backends directory for information on how to write a backend.
412
412
def __init__(self, parsed_url):
413
413
self.parsed_url = parsed_url
415
""" use getpass by default, inherited backends may overwrite this behaviour """
415
u""" use getpass by default, inherited backends may overwrite this behaviour """
416
416
use_getpass = True
418
418
def get_password(self):
420
420
Return a password for authentication purposes. The password
421
421
will be obtained from the backend URL, the environment, by
422
422
asking the user, or by some other method. When applicable, the
426
426
return self.parsed_url.password
429
password = os.environ['FTP_PASSWORD']
429
password = os.environ[u'FTP_PASSWORD']
431
431
if self.use_getpass:
432
password = getpass.getpass("Password for '%s@%s': " %
432
password = getpass.getpass(u"Password for '%s@%s': " %
433
433
(self.parsed_url.username, self.parsed_url.hostname))
434
os.environ['FTP_PASSWORD'] = password
434
os.environ[u'FTP_PASSWORD'] = password
439
439
def munge_password(self, commandline):
441
441
Remove password from commandline by substituting the password
442
442
found in the URL, if any, with a generic place-holder.
451
451
return commandline
453
453
def __subprocess_popen(self, args):
455
455
For internal use.
456
456
Execute the given command line, interpreted as a shell command.
457
457
Returns int Exitcode, string StdOut, string StdErr
465
465
return p.returncode, stdout, stderr
467
""" a dictionary for breaking exceptions, syntax is
467
u""" a dictionary for breaking exceptions, syntax is
468
468
{ 'command' : [ code1, code2 ], ... } see ftpbackend for an example """
469
469
popen_breaks = {}
471
471
def subprocess_popen(self, commandline):
473
473
Execute the given command line with error check.
474
474
Returns int Exitcode, string StdOut, string StdErr
480
480
if isinstance(commandline, (types.ListType, types.TupleType)):
481
logstr = ' '.join(commandline)
481
logstr = u' '.join(commandline)
482
482
args = commandline
484
484
logstr = commandline
485
485
args = shlex.split(commandline)
487
487
logstr = self.munge_password(logstr)
488
log.Info(_("Reading results of '%s'") % logstr)
488
log.Info(_(u"Reading results of '%s'") % logstr)
490
490
result, stdout, stderr = self.__subprocess_popen(args)
493
493
ignores = self.popen_breaks[args[0]]
494
494
ignores.index(result)
495
""" ignore a predefined set of error codes """
495
u""" ignore a predefined set of error codes """
497
497
except (KeyError, ValueError):
498
raise BackendException("Error running '%s': returned %d, with output:\n%s" %
499
(logstr, result, stdout + '\n' + stderr))
498
raise BackendException(u"Error running '%s': returned %d, with output:\n%s" %
499
(logstr, result, stdout + u'\n' + stderr))
500
500
return result, stdout, stderr
503
503
class BackendWrapper(object):
505
505
Represents a generic duplicity backend, capable of storing and
506
506
retrieving files.
510
510
self.backend = backend
512
512
def __do_put(self, source_path, remote_filename):
513
if hasattr(self.backend, '_put'):
514
log.Info(_("Writing %s") % util.fsdecode(remote_filename))
513
if hasattr(self.backend, u'_put'):
514
log.Info(_(u"Writing %s") % util.fsdecode(remote_filename))
515
515
self.backend._put(source_path, remote_filename)
517
517
raise NotImplementedError()
519
@retry('put', fatal=True)
519
@retry(u'put', fatal=True)
520
520
def put(self, source_path, remote_filename=None):
522
522
Transfer source_path (Path object) to remote_filename (string)
524
524
If remote_filename is None, get the filename from the last
528
528
remote_filename = source_path.get_filename()
529
529
self.__do_put(source_path, remote_filename)
531
@retry('move', fatal=True)
531
@retry(u'move', fatal=True)
532
532
def move(self, source_path, remote_filename=None):
534
534
Move source_path (Path object) to remote_filename (string)
536
536
Same as put(), but unlinks source_path in the process. This allows the
539
539
if not remote_filename:
540
540
remote_filename = source_path.get_filename()
541
if hasattr(self.backend, '_move'):
541
if hasattr(self.backend, u'_move'):
542
542
if self.backend._move(source_path, remote_filename) is not False:
543
543
source_path.setdata()
545
545
self.__do_put(source_path, remote_filename)
546
546
source_path.delete()
548
@retry('get', fatal=True)
548
@retry(u'get', fatal=True)
549
549
def get(self, remote_filename, local_path):
550
"""Retrieve remote_filename and place in local_path"""
551
if hasattr(self.backend, '_get'):
550
u"""Retrieve remote_filename and place in local_path"""
551
if hasattr(self.backend, u'_get'):
552
552
self.backend._get(remote_filename, local_path)
553
553
local_path.setdata()
554
554
if not local_path.exists():
555
raise BackendException(_("File %s not found locally after get "
556
"from backend") % local_path.uc_name)
555
raise BackendException(_(u"File %s not found locally after get "
556
u"from backend") % local_path.uc_name)
558
558
raise NotImplementedError()
560
@retry('list', fatal=True)
560
@retry(u'list', fatal=True)
563
563
Return list of filenames (byte strings) present in backend
565
565
def tobytes(filename):
566
"Convert a (maybe unicode) filename to bytes"
566
u"Convert a (maybe unicode) filename to bytes"
567
567
if isinstance(filename, unicode):
568
568
# There shouldn't be any encoding errors for files we care
569
569
# about, since duplicity filenames are ascii. But user files
575
if hasattr(self.backend, '_list'):
575
if hasattr(self.backend, u'_list'):
576
576
# Make sure that duplicity internals only ever see byte strings
577
577
# for filenames, no matter what the backend thinks it is talking.
578
578
return [tobytes(x) for x in self.backend._list()]
580
580
raise NotImplementedError()
582
582
def delete(self, filename_list):
584
584
Delete each filename in filename_list, in order if possible.
586
586
assert not isinstance(filename_list, types.StringType)
587
if hasattr(self.backend, '_delete_list'):
587
if hasattr(self.backend, u'_delete_list'):
588
588
self._do_delete_list(filename_list)
589
elif hasattr(self.backend, '_delete'):
589
elif hasattr(self.backend, u'_delete'):
590
590
for filename in filename_list:
591
591
self._do_delete(filename)
593
593
raise NotImplementedError()
595
@retry('delete', fatal=False)
595
@retry(u'delete', fatal=False)
596
596
def _do_delete_list(self, filename_list):
597
597
while filename_list:
598
598
sublist = filename_list[:100]
599
599
self.backend._delete_list(sublist)
600
600
filename_list = filename_list[100:]
602
@retry('delete', fatal=False)
602
@retry(u'delete', fatal=False)
603
603
def _do_delete(self, filename):
604
604
self.backend._delete(filename)
614
614
# Returned dictionary is guaranteed to contain a metadata dictionary for
615
615
# each filename, and all metadata are guaranteed to be present.
616
616
def query_info(self, filename_list):
618
618
Return metadata about each filename in filename_list
621
if hasattr(self.backend, '_query_list'):
621
if hasattr(self.backend, u'_query_list'):
622
622
info = self._do_query_list(filename_list)
625
elif hasattr(self.backend, '_query'):
625
elif hasattr(self.backend, u'_query'):
626
626
for filename in filename_list:
627
627
info[filename] = self._do_query(filename)
631
631
for filename in filename_list:
632
632
if filename not in info or info[filename] is None:
633
633
info[filename] = {}
634
for metadata in ['size']:
634
for metadata in [u'size']:
635
635
info[filename].setdefault(metadata, None)
639
@retry('query', fatal=False)
639
@retry(u'query', fatal=False)
640
640
def _do_query_list(self, filename_list):
641
641
info = self.backend._query_list(filename_list)
646
@retry('query', fatal=False)
646
@retry(u'query', fatal=False)
647
647
def _do_query(self, filename):
649
649
return self.backend._query(filename)
650
650
except Exception as e:
651
code = _get_code_from_exception(self.backend, 'query', e)
651
code = _get_code_from_exception(self.backend, u'query', e)
652
652
if code == log.ErrorCode.backend_not_found:
659
659
Close the backend, releasing any resources held and
660
660
invalidating any file objects obtained from the backend.
662
if hasattr(self.backend, '_close'):
662
if hasattr(self.backend, u'_close'):
663
663
self.backend._close()
665
665
def get_fileobj_read(self, filename, parseresults=None):
667
667
Return fileobject opened for reading of filename on backend
669
669
The file will be downloaded first into a temp file. When the
672
672
if not parseresults:
673
673
parseresults = file_naming.parse(filename)
674
assert parseresults, "Filename not correctly parsed"
674
assert parseresults, u"Filename not correctly parsed"
675
675
tdp = dup_temp.new_tempduppath(parseresults)
676
676
self.get(filename, tdp)
678
return tdp.filtered_open_with_delete("rb")
678
return tdp.filtered_open_with_delete(u"rb")
680
680
def get_data(self, filename, parseresults=None):
682
682
Retrieve a file from backend, process it, return contents.
684
684
fin = self.get_fileobj_read(filename, parseresults)