I recently finished up a branch adding support to the backup program duplicity for Ubuntu One. I thought I’d write up how I did it, because some things weren’t obvious.
My needs were simple: Python versions of get
, put
, delete
, and list
operations.
Code snippets under GPL-2+ license. These don’t have any error handling, but that’s simple enough to add. Note that for the list
and put
snippets you will need to either use trunk or a release greater than 0.2.0 of the ubuntuone-couch Python module.
If parts of the snippets are confusing, it may help to consult the official documentation.
Log In
First, you have to log the user into Ubuntu One. There’s a utility class in ubuntuone.platform.credentials
to help with this. It’s designed to be asynchronous, but for our simple purposes, we’ll fake synchronicity with a main loop.
_login_success = False
def login(self):
from gobject import MainLoop
from dbus.mainloop.glib import DBusGMainLoop
from ubuntuone.platform.credentials import CredentialsManagementTool
global _login_success
_login_success = False
DBusGMainLoop(set_as_default=True)
loop = MainLoop()
def quit(result):
global _login_success
loop.quit()
if result:
_login_success = True
cd = CredentialsManagementTool()
d = cd.login()
d.addCallbacks(quit)
loop.run()
return _login_success
Create Volume
Another piece of set up before we can get to the good stuff is to create a volume if needed. A volume is just a directory in Ubuntu One that can be synchronized (but isn’t by default). It’s OK to attempt to create a volume multiple times, but Ubuntu One will report back an error if you try to create a nested volume.
Note the auth.request
call. This adds the required OAuth header to our request so that we can successfully authorize with the server. This is why we must be logged in first, to have access to the credentials that auth.request
uses.
def create_volume(path):
import ubuntuone.couch.auth as auth
import urllib
base = "https://one.ubuntu.com/api/file_storage/v1/volumes/~/"
return auth.request(base + urllib.quote(path), http_method="PUT")
Delete
Alright, we can get down to brass tacks now that we are logged in and have a volume in which to work. Let’s start simple with a file delete request.
def delete(path):
import ubuntuone.couch.auth as auth
import urllib
base = "https://one.ubuntu.com/api/file_storage/v1/~/"
return auth.request(base + urllib.quote(path), http_method="DELETE")
That was easy!
List
Listing involves requesting the metadata for the parent directory / volume and explicitly asking for information about its children.
def list(path):
import json
import ubuntuone.couch.auth as auth
import urllib
base = "https://one.ubuntu.com/api/file_storage/v1/~/"
url = base + urllib.quote(path) + "?include_children=true"
answer = auth.request(url)
filelist = []
node = json.loads(answer[1])
if node.get('has_children') == True:
for child in node.get('children'):
child_path = urllib.unquote(child.get('path')).lstrip('/')
filelist += [child_path]
return filelist
Get
This is a little tricky. You have to first ask for metadata about the file to get the actual content path. Then you have to hit files.one.ubuntu.com to get the data itself.
Note we don’t quote the twiddle in the content_path field. It needs to be unquoted.
def get(remote, local):
import json
import ubuntuone.couch.auth as auth
import urllib
base = "https://one.ubuntu.com/api/file_storage/v1/~/"
answer = auth.request(base + urllib.quote(remote))
node = json.loads(answer[1])
base = "https://files.one.ubuntu.com"
url = base + urllib.quote(node.get('content_path'), safe="~")
answer = auth.request(url)
f = open(local, 'wb')
f.write(answer[1])
Put
Last, but not least, putting a file on the server. Note that you must specify a content type and length.
def put(local, remote):
import json
import ubuntuone.couch.auth as auth
import mimetypes
import urllib
base = "https://one.ubuntu.com/api/file_storage/v1/~/"
answer = auth.request(base + urllib.quote(remote),
http_method="PUT",
request_body='{"kind":"file"}')
node = json.loads(answer[1])
data = bytearray(open(local, 'rb').read())
size = len(data)
content_type = mimetypes.guess_type(local)[0]
content_type = content_type or 'application/octet-stream'
headers = {"Content-Length": str(size),
"Content-Type": content_type}
base = "https://files.one.ubuntu.com"
url = base + urllib.quote(node.get('content_path'), safe="~")
return auth.request(url, http_method="PUT",
headers=headers, request_body=data)
Odds and Ends
Hope that was helpful!
You should be able to figure out how to do other things you’ll need from these snippets and the official documentation. Note that as of this writing, parts of that documentation are out of date. For the moment, trust my snippets over the documentation! I’m told the documentation is being updated.
Note that in terms of HTTP error codes, Ubuntu One seems to return 500 for out-of-space. And if you request a malformed path (like, with two slashes in it), it likes to give back an error code with a login page as an HTML payload.