At Canonical's Online Services we can do cool stuff on fridays. We do cool
stuff all week, actually, but on fridays we can do crazier cool stuff.
So, today, I ripped off a great service offered by http://calepin.co
and implemented a prototype blog-through-Ubuntu-One web application.
Of course, it's powered by Nikola,
The code is absolute nonsense, and it needs to be looked at by someone
who understands Django, OAuth, OpenID, and programming in general better
than I do, but hey, it does work (for a very loose definition of "work").
It's called Shoreham and no, you can't have it yet.
As a teaser, here's a video. With a pony.
In the near future I will do a better post about this explaining the code, etc.
One of the nice things about working at Canonical is that we produce open source software.
I, specifically, work in the team that does the desktop clients for Ubuntu One which is a really cool job, and a really cool piece
of software. However, one thing not enough people know, is that we offer damn nice APIs
for developers. We have to, since all our client code is open source, so we need those
APIs for ourselves.
So, here is a small tutorial about using some of those APIs. I did it using Python and
PyQt for several reasons:
Both are great tools for prototyping
Both have good support for the required stuff (DBus, HTTP, OAuth)
It's what I know and enjoy. Since I did this code on a sunday, I am
not going to use other things.
Having said that, there is nothing python-specific or Qt-specific in the code. Where
I do a HTTP request using QtNetwork, you are free to use libsoup, or whatever.
So, on to the nuts and bolts. The main pieces of Ubuntu One, from a infrastructure
perspective, are Ubuntu SSO Client, that handles user registration and login, and
SyncDaemon, which handles file synchronization.
To interact with them, on Linux, they offer DBus interfaces. So, for example, this is
a fragment of code showing a way to get the Ubuntu One credentials (this would normally
be part of an object's __init__):
# Get the session busbus=dbus.SessionBus():::# Get the credentials proxy and interfaceself.creds_proxy=bus.get_object("com.ubuntuone.Credentials","/credentials",follow_name_owner_changes=True)# Connect to signals so you get a call when something# credential-related happensself.creds_iface=dbus.Interface(self.creds_proxy,"com.ubuntuone.CredentialsManagement")self.creds_proxy.connect_to_signal('CredentialsFound',self.creds_found)self.creds_proxy.connect_to_signal('CredentialsNotFound',self.creds_not_found)self.creds_proxy.connect_to_signal('CredentialsError',self.creds_error)# Call for credentialsself._credentials=Noneself.get_credentials()
You may have noticed that get_credentials doesn't actually return the credentials. What
it does is, it tells SyncDaemon to fetch the credentials, and then, when/if they are there,
one of the signals will be emitted, and one of the connected methods will be called. This
is nice, because it means you don't have to worry about your app blocking while SyncDaemon
is doing all this.
But what's in those methods we used? Not much, really!
defget_credentials(self):# Do we have them already? If not, get'emifnotself._credentials:self.creds_proxy.find_credentials()# Return what we've got, could be Nonereturnself._credentialsdefcreds_found(self,data):# Received credentials, save them.print"creds_found",dataself._credentials=data# Don't worry about get_quota yet ;-)ifnotself._quota_info:self.get_quota()defcreds_not_found(self,data):# No credentials, remove old ones.print"creds_not_found",dataself._credentials=Nonedefcreds_error(self,data):# No credentials, remove old ones.print"creds_error",dataself._credentials=None
So, basically, self._credentials will hold a set of credentials, or None. Congratulations, we
are now logged into Ubuntu One, so to speak.
So, let's do something useful! How about asking for how much free space there is in
the account? For that, we can't use the local APIs, we have to connect to the servers, who
are, after all, the ones who decide if you are over quota or not.
Access is controlled via OAuth. So, to access the API, we need to sign our requests. Here
is how it's done. It's not particularly enlightening, and I did not write it, I just use it:
defsign_uri(self,uri,parameters=None):# Without credentials, return unsigned URLifnotself._credentials:returnuriifisinstance(uri,unicode):uri=bytes(iri2uri(uri))print"uri:",urimethod="GET"credentials=self._credentialsconsumer=oauth.OAuthConsumer(credentials["consumer_key"],credentials["consumer_secret"])token=oauth.OAuthToken(credentials["token"],credentials["token_secret"])ifnotparameters:_,_,_,_,query,_=urlparse(uri)parameters=dict(cgi.parse_qsl(query))request=oauth.OAuthRequest.from_consumer_and_token(http_url=uri,http_method=method,parameters=parameters,oauth_consumer=consumer,token=token)sig_method=oauth.OAuthSignatureMethod_HMAC_SHA1()request.sign_request(sig_method,consumer,token)print"SIGNED:",repr(request.to_url())returnrequest.to_url()
And how do we ask for the quota usage? By accessing the https://one.ubuntu.com/api/quota/ entry point
with the proper authorization, we would get a JSON dictionary with total and used space.
So, here's a simple way to do it:
# This is on __init__self.nam=QtNetwork.QNetworkAccessManager(self,finished=self.reply_finished):::defget_quota(self):"""Launch quota info request."""uri=self.sign_uri(QUOTA_API)url=QtCore.QUrl()url.setEncodedUrl(uri)self.nam.get(QtNetwork.QNetworkRequest(url))
Again, see how get_quota doesn't return the quota? What happens is that get_quota will
launch a HTTP request to the Ubuntu One servers, which will, eventually, reply with the data.
You don't want your app to block while you do that. So, QNetworkAccessManager will call
self.reply_finished when it gets the response:
What else would be nice to have? How about getting a call whenever the status of syncdaemon
changes? For example, when sync is up to date, or when you get disconnected? Again, those are
DBus signals we are connecting in our __init__:
self.status_proxy=bus.get_object('com.ubuntuone.SyncDaemon','/status')self.status_iface=dbus.Interface(self.status_proxy,dbus_interface='com.ubuntuone.SyncDaemon.Status')self.status_iface.connect_to_signal('StatusChanged',self.status_changed)# Get the status as of right nowself._last_status=self.process_status(self.status_proxy.current_status())
The process_status function is boring code to convert the info from
syncdaemon's status into a human-readable thing like "Sync is up-to-date". So we
store that in self._last_status and update the menu.
What menu? Well, a QSystemTrayIcon's context menu! What you have read are the main pieces
you need to create something useful: a Ubuntu One tray app you can use in KDE, XFCE or openbox.
Or, if you are on unity and install sni-qt, a Ubuntu One app indicator!