← Back to team overview

duplicity-team team mailing list archive

[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