Merge lp:~mterry/duplicity/u1 into lp:duplicity/0.6

Proposed by Michael Terry
Status: Merged
Approved by: Kenneth Loafman
Approved revision: 727
Merged at revision: 733
Proposed branch: lp:~mterry/duplicity/u1
Merge into: lp:duplicity/0.6
Diff against target: 246 lines (+213/-0)
3 files modified
duplicity.1 (+14/-0)
duplicity/backend.py (+1/-0)
duplicity/backends/u1backend.py (+198/-0)
To merge this branch: bzr merge lp:~mterry/duplicity/u1
Reviewer Review Type Date Requested Status
Kenneth Loafman Approve
Review via email: mp+62128@code.launchpad.net

Description of the change

Adds Ubuntu One backend.

To post a comment you must log in.
Revision history for this message
Michael Terry (mterry) wrote :

Note that this requires trunk of the ubuntuone-couch module. So a little difficult to test right now.

Revision history for this message
Kenneth Loafman (kenneth-loafman) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'duplicity.1'
2--- duplicity.1 2011-04-04 15:50:11 +0000
3+++ duplicity.1 2011-05-24 13:43:15 +0000
4@@ -772,6 +772,12 @@
5 .PP
6 s3+http://bucket_name[/prefix]
7 .PP
8+.BI "Ubuntu One"
9+.br
10+u1://host/volume_path
11+.br
12+u1+http://volume_path
13+.PP
14 .BI "ssh protocols"
15 .br
16 scp://.. or sftp://.. are synonymous for
17@@ -1183,6 +1189,14 @@
18 or HTTP errors when trying to upload files to your newly created
19 bucket. Give it a few minutes and the bucket should function normally.
20
21+.SH UBUNTU ONE
22+Connecting to Ubuntu One requires that you be running duplicity inside of an X
23+session so that you can be prompted for your credentials if necessary by the
24+Ubuntu One session daemon.
25+.PP
26+See https://one.ubuntu.com/ for more information about Ubuntu One.
27+.PP
28+
29 .SH IMAP
30 An IMAP account can be used as a target for the upload. The userid may
31 be specified and the password will be requested.
32
33=== modified file 'duplicity/backend.py'
34--- duplicity/backend.py 2011-04-04 15:50:11 +0000
35+++ duplicity/backend.py 2011-05-24 13:43:15 +0000
36@@ -178,6 +178,7 @@
37 'hsi',
38 'rsync',
39 's3',
40+ 'u1',
41 'scp', 'ssh', 'sftp',
42 'webdav', 'webdavs',
43 'http', 'https',
44
45=== added file 'duplicity/backends/u1backend.py'
46--- duplicity/backends/u1backend.py 1970-01-01 00:00:00 +0000
47+++ duplicity/backends/u1backend.py 2011-05-24 13:43:15 +0000
48@@ -0,0 +1,198 @@
49+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
50+#
51+# Copyright 2011 Canonical Ltd
52+# Authors: Michael Terry <michael.terry@canonical.com>
53+#
54+# This file is part of duplicity.
55+#
56+# Duplicity is free software; you can redistribute it and/or modify it
57+# under the terms of the GNU General Public License as published by the
58+# Free Software Foundation; either version 2 of the License, or (at your
59+# option) any later version.
60+#
61+# Duplicity is distributed in the hope that it will be useful, but
62+# WITHOUT ANY WARRANTY; without even the implied warranty of
63+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
64+# General Public License for more details.
65+#
66+# You should have received a copy of the GNU General Public License
67+# along with duplicity. If not, see <http://www.gnu.org/licenses/>.
68+
69+import duplicity.backend
70+
71+def ensure_dbus():
72+ # GIO requires a dbus session bus which can start the gvfs daemons
73+ # when required. So we make sure that such a bus exists and that our
74+ # environment points to it.
75+ import atexit
76+ import os
77+ import subprocess
78+ import signal
79+ if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
80+ output = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE).communicate()[0]
81+ lines = output.split('\n')
82+ for line in lines:
83+ parts = line.split('=', 1)
84+ if len(parts) == 2:
85+ if parts[0] == 'DBUS_SESSION_BUS_PID': # cleanup at end
86+ atexit.register(os.kill, int(parts[1]), signal.SIGTERM)
87+ os.environ[parts[0]] = parts[1]
88+
89+class U1Backend(duplicity.backend.Backend):
90+ """
91+ Backend for Ubuntu One, through the use of the ubuntone module and a REST
92+ API. See https://one.ubuntu.com/developer/ for REST documentation.
93+ """
94+ def __init__(self, url):
95+ duplicity.backend.Backend.__init__(self, url)
96+
97+ if self.parsed_url.scheme == 'u1+http':
98+ # Use the default Ubuntu One host
99+ self.parsed_url.hostname = "one.ubuntu.com"
100+ else:
101+ assert self.parsed_url.scheme == 'u1'
102+
103+ path = self.parsed_url.path.lstrip('/')
104+
105+ self.api_base = "https://%s/api/file_storage/v1" % self.parsed_url.hostname
106+ self.volume_uri = "%s/volumes/~/%s" % (self.api_base, path)
107+ self.meta_base = "%s/~/%s/" % (self.api_base, path)
108+ # This next line *should* work, but isn't set up correctly server-side yet
109+ #self.content_base = self.api_base
110+ self.content_base = "https://files.%s" % self.parsed_url.hostname
111+
112+ ensure_dbus()
113+
114+ if not self.login():
115+ from duplicity import log
116+ log.FatalError(_("Could not obtain Ubuntu One credentials"),
117+ log.ErrorCode.backend_error)
118+
119+ # Create volume in case it doesn't exist yet
120+ import ubuntuone.couch.auth as auth
121+ answer = auth.request(self.volume_uri, http_method="PUT")
122+ self.handle_error('put', answer, self.volume_uri)
123+
124+ def login(self):
125+ from gobject import MainLoop
126+ from dbus.mainloop.glib import DBusGMainLoop
127+ from ubuntuone.platform.credentials import CredentialsManagementTool
128+
129+ self.login_success = False
130+
131+ DBusGMainLoop(set_as_default=True)
132+ loop = MainLoop()
133+
134+ def quit(result):
135+ loop.quit()
136+ if result:
137+ self.login_success = True
138+
139+ cd = CredentialsManagementTool()
140+ d = cd.login()
141+ d.addCallbacks(quit)
142+ loop.run()
143+ return self.login_success
144+
145+ def quote(self, url):
146+ import urllib
147+ return urllib.quote(url, safe="/~")
148+
149+ def handle_error(self, op, headers, file1=None, file2=None):
150+ from duplicity import log
151+ from duplicity import util
152+ import json
153+
154+ status = headers[0].get('status')
155+ if status == '200':
156+ return
157+
158+ if status == '400':
159+ code = log.ErrorCode.backend_permission_denied
160+ elif status == '404':
161+ code = log.ErrorCode.backend_not_found
162+ elif status == '500': # wish this were a more specific error
163+ code = log.ErrorCode.backend_no_space
164+ else:
165+ code = log.ErrorCode.backend_error
166+
167+ file1 = file1.encode("utf8") if file1 else None
168+ file2 = file2.encode("utf8") if file2 else None
169+ extra = ' '.join([util.escape(x) for x in [file1, file2] if x])
170+ extra = ' '.join([op, extra])
171+ msg = _("Got status code %s") % status
172+ if headers[0].get('content-type') == 'application/json':
173+ node = json.loads(headers[1])
174+ if node.get('error'):
175+ msg = node.get('error')
176+ log.FatalError(msg, code, extra)
177+
178+ def put(self, source_path, remote_filename = None):
179+ """Copy file to remote"""
180+ import json
181+ import ubuntuone.couch.auth as auth
182+ import mimetypes
183+ if not remote_filename:
184+ remote_filename = source_path.get_filename()
185+ remote_full = self.meta_base + self.quote(remote_filename)
186+ answer = auth.request(remote_full,
187+ http_method="PUT",
188+ request_body='{"kind":"file"}')
189+ self.handle_error('put', answer, source_path.name, remote_full)
190+ node = json.loads(answer[1])
191+
192+ remote_full = self.content_base + self.quote(node.get('content_path'))
193+ data = bytearray(open(source_path.name, 'rb').read())
194+ size = len(data)
195+ content_type = mimetypes.guess_type(source_path.name)[0]
196+ content_type = content_type or 'application/octet-stream'
197+ headers = {"Content-Length": str(size),
198+ "Content-Type": content_type}
199+ answer = auth.request(remote_full, http_method="PUT",
200+ headers=headers, request_body=data)
201+ self.handle_error('put', answer, source_path.name, remote_full)
202+
203+ def get(self, filename, local_path):
204+ """Get file and put in local_path (Path object)"""
205+ import json
206+ import ubuntuone.couch.auth as auth
207+ remote_full = self.meta_base + self.quote(filename)
208+ answer = auth.request(remote_full)
209+ self.handle_error('get', answer, remote_full, filename)
210+ node = json.loads(answer[1])
211+
212+ remote_full = self.content_base + self.quote(node.get('content_path'))
213+ answer = auth.request(remote_full)
214+ self.handle_error('get', answer, remote_full, filename)
215+ f = open(local_path.name, 'wb')
216+ f.write(answer[1])
217+ local_path.setdata()
218+
219+ def list(self):
220+ """List files in that directory"""
221+ import json
222+ import ubuntuone.couch.auth as auth
223+ import urllib
224+ remote_full = self.meta_base + "?include_children=true"
225+ answer = auth.request(remote_full)
226+ self.handle_error('list', answer, remote_full)
227+ filelist = []
228+ node = json.loads(answer[1])
229+ if node.get('has_children') == True:
230+ for child in node.get('children'):
231+ path = urllib.unquote(child.get('path')).lstrip('/')
232+ filelist += [path]
233+ return filelist
234+
235+ def delete(self, filename_list):
236+ """Delete all files in filename list"""
237+ import types
238+ import ubuntuone.couch.auth as auth
239+ assert type(filename_list) is not types.StringType
240+ for filename in filename_list:
241+ remote_full = self.meta_base + self.quote(filename)
242+ answer = auth.request(remote_full, http_method="DELETE")
243+ self.handle_error('delete', answer, remote_full)
244+
245+duplicity.backend.register_backend("u1", U1Backend)
246+duplicity.backend.register_backend("u1+http", U1Backend)

Subscribers

People subscribed via source and target branches

to all changes: