duplicity-team team mailing list archive
-
duplicity-team team
-
Mailing list archive
-
Message #00561
[Merge] lp:~mterry/duplicity/u1 into lp:duplicity
Michael Terry has proposed merging lp:~mterry/duplicity/u1 into lp:duplicity.
Requested reviews:
duplicity-team (duplicity-team)
For more details, see:
https://code.launchpad.net/~mterry/duplicity/u1/+merge/62128
Adds Ubuntu One backend.
--
https://code.launchpad.net/~mterry/duplicity/u1/+merge/62128
Your team duplicity-team is requested to review the proposed merge of lp:~mterry/duplicity/u1 into lp:duplicity.
=== modified file 'duplicity.1'
--- duplicity.1 2011-04-04 15:50:11 +0000
+++ duplicity.1 2011-05-24 13:37:27 +0000
@@ -772,6 +772,12 @@
.PP
s3+http://bucket_name[/prefix]
.PP
+.BI "Ubuntu One"
+.br
+u1://host/volume_path
+.br
+u1+http://volume_path
+.PP
.BI "ssh protocols"
.br
scp://.. or sftp://.. are synonymous for
@@ -1183,6 +1189,14 @@
or HTTP errors when trying to upload files to your newly created
bucket. Give it a few minutes and the bucket should function normally.
+.SH UBUNTU ONE
+Connecting to Ubuntu One requires that you be running duplicity inside of an X
+session so that you can be prompted for your credentials if necessary by the
+Ubuntu One session daemon.
+.PP
+See https://one.ubuntu.com/ for more information about Ubuntu One.
+.PP
+
.SH IMAP
An IMAP account can be used as a target for the upload. The userid may
be specified and the password will be requested.
=== modified file 'duplicity/backend.py'
--- duplicity/backend.py 2011-04-04 15:50:11 +0000
+++ duplicity/backend.py 2011-05-24 13:37:27 +0000
@@ -178,6 +178,7 @@
'hsi',
'rsync',
's3',
+ 'u1',
'scp', 'ssh', 'sftp',
'webdav', 'webdavs',
'http', 'https',
=== added file 'duplicity/backends/u1backend.py'
--- duplicity/backends/u1backend.py 1970-01-01 00:00:00 +0000
+++ duplicity/backends/u1backend.py 2011-05-24 13:37:27 +0000
@@ -0,0 +1,198 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2011 Canonical Ltd
+# Authors: Michael Terry <michael.terry@xxxxxxxxxxxxx>
+#
+# This file is part of duplicity.
+#
+# Duplicity is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# Duplicity is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with duplicity. If not, see <http://www.gnu.org/licenses/>.
+
+import duplicity.backend
+
+def ensure_dbus():
+ # GIO requires a dbus session bus which can start the gvfs daemons
+ # when required. So we make sure that such a bus exists and that our
+ # environment points to it.
+ import atexit
+ import os
+ import subprocess
+ import signal
+ if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
+ output = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE).communicate()[0]
+ lines = output.split('\n')
+ for line in lines:
+ parts = line.split('=', 1)
+ if len(parts) == 2:
+ if parts[0] == 'DBUS_SESSION_BUS_PID': # cleanup at end
+ atexit.register(os.kill, int(parts[1]), signal.SIGTERM)
+ os.environ[parts[0]] = parts[1]
+
+class U1Backend(duplicity.backend.Backend):
+ """
+ Backend for Ubuntu One, through the use of the ubuntone module and a REST
+ API. See https://one.ubuntu.com/developer/ for REST documentation.
+ """
+ def __init__(self, url):
+ duplicity.backend.Backend.__init__(self, url)
+
+ if self.parsed_url.scheme == 'u1+http':
+ # Use the default Ubuntu One host
+ self.parsed_url.hostname = "one.ubuntu.com"
+ else:
+ assert self.parsed_url.scheme == 'u1'
+
+ path = self.parsed_url.path.lstrip('/')
+
+ self.api_base = "https://%s/api/file_storage/v1" % self.parsed_url.hostname
+ self.volume_uri = "%s/volumes/~/%s" % (self.api_base, path)
+ self.meta_base = "%s/~/%s/" % (self.api_base, path)
+ # This next line *should* work, but isn't set up correctly server-side yet
+ #self.content_base = self.api_base
+ self.content_base = "https://files.%s" % self.parsed_url.hostname
+
+ ensure_dbus()
+
+ if not self.login():
+ from duplicity import log
+ log.FatalError(_("Could not obtain Ubuntu One credentials"),
+ log.ErrorCode.backend_error)
+
+ # Create volume in case it doesn't exist yet
+ import ubuntuone.couch.auth as auth
+ answer = auth.request(self.volume_uri, http_method="PUT")
+ self.handle_error('put', answer, self.volume_uri)
+
+ def login(self):
+ from gobject import MainLoop
+ from dbus.mainloop.glib import DBusGMainLoop
+ from ubuntuone.platform.credentials import CredentialsManagementTool
+
+ self.login_success = False
+
+ DBusGMainLoop(set_as_default=True)
+ loop = MainLoop()
+
+ def quit(result):
+ loop.quit()
+ if result:
+ self.login_success = True
+
+ cd = CredentialsManagementTool()
+ d = cd.login()
+ d.addCallbacks(quit)
+ loop.run()
+ return self.login_success
+
+ def quote(self, url):
+ import urllib
+ return urllib.quote(url, safe="/~")
+
+ def handle_error(self, op, headers, file1=None, file2=None):
+ from duplicity import log
+ from duplicity import util
+ import json
+
+ status = headers[0].get('status')
+ if status == '200':
+ return
+
+ if status == '400':
+ code = log.ErrorCode.backend_permission_denied
+ elif status == '404':
+ code = log.ErrorCode.backend_not_found
+ elif status == '500': # wish this were a more specific error
+ code = log.ErrorCode.backend_no_space
+ else:
+ code = log.ErrorCode.backend_error
+
+ file1 = file1.encode("utf8") if file1 else None
+ file2 = file2.encode("utf8") if file2 else None
+ extra = ' '.join([util.escape(x) for x in [file1, file2] if x])
+ extra = ' '.join([op, extra])
+ msg = _("Got status code %s") % status
+ if headers[0].get('content-type') == 'application/json':
+ node = json.loads(headers[1])
+ if node.get('error'):
+ msg = node.get('error')
+ log.FatalError(msg, code, extra)
+
+ def put(self, source_path, remote_filename = None):
+ """Copy file to remote"""
+ import json
+ import ubuntuone.couch.auth as auth
+ import mimetypes
+ if not remote_filename:
+ remote_filename = source_path.get_filename()
+ remote_full = self.meta_base + self.quote(remote_filename)
+ answer = auth.request(remote_full,
+ http_method="PUT",
+ request_body='{"kind":"file"}')
+ self.handle_error('put', answer, source_path.name, remote_full)
+ node = json.loads(answer[1])
+
+ remote_full = self.content_base + self.quote(node.get('content_path'))
+ data = bytearray(open(source_path.name, 'rb').read())
+ size = len(data)
+ content_type = mimetypes.guess_type(source_path.name)[0]
+ content_type = content_type or 'application/octet-stream'
+ headers = {"Content-Length": str(size),
+ "Content-Type": content_type}
+ answer = auth.request(remote_full, http_method="PUT",
+ headers=headers, request_body=data)
+ self.handle_error('put', answer, source_path.name, remote_full)
+
+ def get(self, filename, local_path):
+ """Get file and put in local_path (Path object)"""
+ import json
+ import ubuntuone.couch.auth as auth
+ remote_full = self.meta_base + self.quote(filename)
+ answer = auth.request(remote_full)
+ self.handle_error('get', answer, remote_full, filename)
+ node = json.loads(answer[1])
+
+ remote_full = self.content_base + self.quote(node.get('content_path'))
+ answer = auth.request(remote_full)
+ self.handle_error('get', answer, remote_full, filename)
+ f = open(local_path.name, 'wb')
+ f.write(answer[1])
+ local_path.setdata()
+
+ def list(self):
+ """List files in that directory"""
+ import json
+ import ubuntuone.couch.auth as auth
+ import urllib
+ remote_full = self.meta_base + "?include_children=true"
+ answer = auth.request(remote_full)
+ self.handle_error('list', answer, remote_full)
+ filelist = []
+ node = json.loads(answer[1])
+ if node.get('has_children') == True:
+ for child in node.get('children'):
+ path = urllib.unquote(child.get('path')).lstrip('/')
+ filelist += [path]
+ return filelist
+
+ def delete(self, filename_list):
+ """Delete all files in filename list"""
+ import types
+ import ubuntuone.couch.auth as auth
+ assert type(filename_list) is not types.StringType
+ for filename in filename_list:
+ remote_full = self.meta_base + self.quote(filename)
+ answer = auth.request(remote_full, http_method="DELETE")
+ self.handle_error('delete', answer, remote_full)
+
+duplicity.backend.register_backend("u1", U1Backend)
+duplicity.backend.register_backend("u1+http", U1Backend)
Follow ups