UbuntuOneFilesIntegration

Practical Ubuntu One Files Integration - Instructors: Michael Terry

   1 === ChanServ changed the topic of #ubuntu-classroom to: Welcome to the Ubuntu Classroom - https://wiki.ubuntu.com/Classroom || Support in #ubuntu || Upcoming Schedule: http://is.gd/8rtIi || Questions in #ubuntu-classroom-chat || Event: App Developer Week - Current Session: Practical Ubuntu One Files Integration - Instructors: mterry
   2 [18:00] <ClassBot> Logs for this session will be available at http://irclogs.ubuntu.com/2011/09/09/%23ubuntu-classroom.html following the conclusion of the session.
   3 [18:01] <dpm> talking of which, next up mterry will be talking about connecting your apps to the cloud with ubuntu one :)
   4 [18:01] <mterry> Hello everybody!  Thanks njpatel!
   5 [18:01] <mterry> So I'm Michael Terry, an Ubuntu developer as well as the developer of Deja Dup, a backup program
   6 [18:02] <mterry> I recently added support for Ubuntu One to my program, and I thought I'd share how that went, and some simple examples of how to connect to Ubuntu One Files
   7 [18:02] <mterry> I have lots of notes for this session here: https://wiki.ubuntu.com/mterry/UbuntuOneFilesNotes11.10
   8 [18:02] <mterry> Which you may be interested in going through as I talk
   9 [18:02] <mterry> Please ask questions any old time
  10 [18:03] <mterry> So, for this session (and for my purposes with Deja Dup), I only needed simple file functionality
  11 [18:03] <mterry> get, put, list, delete basically
  12 [18:03] <mterry> So we'll go through each of those basic ideas to help anyone else that's interested in integrating do so easily
  13 [18:03] <mterry> We'll use Python, since that's most convenient
  14 [18:04] <mterry> So let's create together a simple python script that can do basic file operations with U1
  15 [18:04] <mterry> You'll need an updated ubuntuone-couch package from Ubuntu 11.10
  16 [18:04] <mterry> I've backported it in my PPA
  17 [18:04] <mterry> For this sessoin
  18 [18:04] <mterry> So if you want to play along at home, but are stuck on Ubuntu 11.04, please do the following:
  19 [18:05] <mterry> sudo add-apt-repository ppa:mterry/ppa2
  20 [18:05] <mterry> sudo apt-get update
  21 [18:05] <mterry> sudo apt-get upgrade
  22 [18:05] <mterry> That will give you a new ubuntuone-couch
  23 [18:05] <mterry> So to start, let's write a super simple Python script that can just accept an argument
  24 [18:05] <mterry> ===
  25 [18:05] <mterry> #!/usr/bin/python
  26 [18:05] <mterry> import sys
  27 [18:05] <mterry> if len(sys.argv) <= 1:
  28 [18:05] <mterry>   print "Need more arguments"
  29 [18:05] <mterry>   sys.exit(1)
  30 [18:05] <mterry> print sys.argv[1:]
  31 [18:05] <mterry> ===
  32 [18:05] <mterry> Very basic.  We'll augment this with more in a second
  33 [18:06] <mterry> Save that as u1file.py
  34 [18:06] <mterry> And open a terminal in that same directory
  35 [18:06] <mterry> The first thing you have to do when interacting with U1 is make sure the user is logged in
  36 [18:06] <mterry> There is a helper library for that in ubuntuone.platform.credentials
  37 [18:06] <mterry> It's designed to work with Twisted and be asynchronous
  38 [18:07] <mterry> But we just want simple synchronous behavior for now
  39 [18:07] <mterry> So I'll show you a function that will fake synchronousity by opening an event loop and waiting for login to finish
  40 [18:07] <mterry> ===
  41 [18:07] <mterry> #!/usr/bin/python
  42 [18:07] <mterry> import sys
  43 [18:07] <mterry> _login_success = False
  44 [18:07] <mterry> def login():
  45 [18:07] <mterry>   from gobject import MainLoop
  46 [18:07] <mterry>   from dbus.mainloop.glib import DBusGMainLoop
  47 [18:07] <mterry>   from ubuntuone.platform.credentials import CredentialsManagementTool
  48 [18:07] <mterry>   global _login_success
  49 [18:07] <mterry>   _login_success = False
  50 [18:07] <mterry>   DBusGMainLoop(set_as_default=True)
  51 [18:07] <mterry>   loop = MainLoop()
  52 [18:07] <mterry>   def quit(result):
  53 [18:07] <mterry>     global _login_success
  54 [18:08] <mterry>     loop.quit()
  55 [18:08] <mterry>     if result:
  56 [18:08] <mterry>             _login_success = True
  57 [18:08] <mterry>   cd = CredentialsManagementTool()
  58 [18:08] <mterry>   d = cd.login()
  59 [18:08] <mterry>   d.addCallbacks(quit)
  60 [18:08] <mterry>   loop.run()
  61 [18:08] <mterry>   if not _login_success:
  62 [18:08] <mterry>     sys.exit(1)
  63 [18:08] <mterry> if len(sys.argv) <= 1:
  64 [18:08] <mterry>   print "Need more arguments"
  65 [18:08] <mterry>   sys.exit(1)
  66 [18:08] <mterry> if sys.argv[1] == "login":
  67 [18:08] <mterry>   login()
  68 [18:08] <mterry> ===
  69 [18:08] <mterry> This may be easier to see on the wiki page https://wiki.ubuntu.com/mterry/UbuntuOneFilesNotes11.10#Logging_In
  70 [18:08] <mterry> The important bit is from ubuntuone.platform.credentials import CredentialsManagementTool
  71 [18:08] <mterry> followed by
  72 [18:08] <mterry>    cd = CredentialsManagementTool()
  73 [18:08] <mterry>    d = cd.login()
  74 [18:08] <mterry> The rest is just wrappers to support the event loop
  75 [18:08] <mterry> And to support calling "python u1file.py login"
  76 [18:09] <mterry> So try running that now, and you should see a neat little U1 login screen
  77 [18:09] <mterry> Unless...  You've already used U1, in which case, nothing happens (because login() doesn't do anything in that case)
  78 [18:09] <mterry> So let's add a logout function for testing purposes
  79 [18:09] <mterry> ===
  80 [18:09] <mterry> def logout():
  81 [18:09] <mterry>   from gobject import MainLoop
  82 [18:09] <mterry>   from dbus.mainloop.glib import DBusGMainLoop
  83 [18:09] <mterry>   from ubuntuone.platform.credentials import CredentialsManagementTool
  84 [18:09] <mterry>   DBusGMainLoop(set_as_default=True)
  85 [18:09] <mterry>   loop = MainLoop()
  86 [18:09] <mterry>   def quit(result):
  87 [18:09] <mterry>     loop.quit()
  88 [18:09] <mterry>   cd = CredentialsManagementTool()
  89 [18:09] <mterry>   d = cd.clear_credentials()
  90 [18:09] <mterry>   d.addCallbacks(quit)
  91 [18:09] <mterry>   loop.run()
  92 [18:09] <mterry> if sys.argv[1] == "logout":
  93 [18:10] <mterry>   logout()
  94 [18:10] <mterry> ===
  95 [18:10] <mterry> Now add that to your script and you can call "python u1file.py logout" to go back to a clean slate
  96 [18:10] <mterry> OK.  So we have a skeleton script that can talk to U1, but it doesn't do anything yet!
  97 [18:10] <mterry> Let's upload a file
  98 [18:10] <mterry> Oh, whoops
  99 [18:10] <mterry> First, we have to make sure we create a volume
 100 [18:10] <mterry> In U1-speak, a volume is a folder that can be synchronized between the user's computers
 101 [18:11] <mterry> By default, new volumes are not synchronized anywhere
 102 [18:11] <mterry> But let's create a testing volume so that we can upload files without screwing anything up
 103 [18:11] <mterry> Note that the "Ubuntu One" volume always exists
 104 [18:11] <mterry> Creating a volume is a simple enough call:
 105 [18:11] <mterry> ===
 106 [18:11] <mterry> def create_volume(path):
 107 [18:11] <mterry>   import ubuntuone.couch.auth as auth
 108 [18:11] <mterry>   import urllib
 109 [18:11] <mterry>   base = "https://one.ubuntu.com/api/file_storage/v1/volumes/~/"
 110 [18:11] <mterry>   auth.request(base + urllib.quote(path), http_method="PUT")
 111 [18:11] <mterry> if sys.argv[1] == "create-volume":
 112 [18:11] <mterry>   login()
 113 [18:12] <mterry>   create_volume(sys.argv[2])
 114 [18:12] <mterry> ===
 115 [18:12] <mterry> You'll see that we make a single PUT request to a specially crafted URL
 116 [18:12] <mterry> There is no error handling in my snippets of code.  I'll get into how to handle errors at the end
 117 [18:12] <mterry> Add that to your u1file.py, and now you can call "python u1file.py create-volume testing"
 118 [18:12] <mterry> If you open http://one.ubuntu.com/files/ you should be able to see the new volume
 119 [18:13] <mterry> Congratulations if so!
 120 [18:13] <mterry> You'll also note that I included a call to login() before creating the volume
 121 [18:13] <mterry> This was to ensure that the user was logged in first
 122 [18:13] <mterry> You'll also note that I made this weird auth.request call
 123 [18:14] <mterry> This is a wrapper function provided by ubuntuone-couch that handles the OAuth signature required by U1 to securely identify the user
 124 [18:14] <mterry> This is why you had to log in first
 125 [18:14] <mterry> And the 11.10 version has some important fixes, which is why I backported it for this session
 126 [18:14] <mterry> OK, *now* let's upload a file
 127 [18:14] <mterry> (any questions?)
 128 [18:14] <mterry> Uploading is a two-step process
 129 [18:15] <mterry> First, we tell the server we want to create a new file
 130 [18:15] <mterry> Then the server tells us a URL path to upload the contents to
 131 [18:15] <mterry> I'll give you the code then we can talk about it
 132 [18:15] <mterry> ===
 133 [18:15] <mterry> def put(local, remote):
 134 [18:15] <mterry>   import json
 135 [18:15] <mterry>   import ubuntuone.couch.auth as auth
 136 [18:15] <mterry>   import mimetypes
 137 [18:15] <mterry>   import urllib
 138 [18:15] <mterry>   # Create remote path (which contains volume path)
 139 [18:15] <mterry>   base = "https://one.ubuntu.com/api/file_storage/v1/~/"
 140 [18:15] <mterry>   answer = auth.request(base + urllib.quote(remote),
 141 [18:15] <mterry>                         http_method="PUT",
 142 [18:15] <mterry>                         request_body='{"kind":"file"}')
 143 [18:15] <mterry>   node = json.loads(answer[1])
 144 [18:15] <mterry>   # Read info about local file
 145 [18:16] <mterry>   data = bytearray(open(local, 'rb').read())
 146 [18:16] <mterry>   size = len(data)
 147 [18:16] <mterry>   content_type = mimetypes.guess_type(local)[0]
 148 [18:16] <mterry>   content_type = content_type or 'application/octet-stream'
 149 [18:16] <mterry>   headers = {"Content-Length": str(size),
 150 [18:16] <mterry>              "Content-Type": content_type}
 151 [18:16] <mterry>   # Upload content of local file to content_path from original response
 152 [18:16] <mterry>   base = "https://files.one.ubuntu.com"
 153 [18:16] <mterry>   url = base + urllib.quote(node.get('content_path'), safe="/~")
 154 [18:16] <mterry>   auth.request(url, http_method="PUT",
 155 [18:16] <mterry>                headers=headers, request_body=data)
 156 [18:16] <mterry> if sys.argv[1] == "put":
 157 [18:16] <mterry>   login()
 158 [18:16] <mterry>   put(sys.argv[2], sys.argv[3])
 159 [18:16] <mterry> ===
 160 [18:16] <mterry> There are three parts to this
 161 [18:16] <mterry> First is the request to create a file
 162 [18:16] <mterry> We give a URL path and PUT a specially crafted message "{'kind':'file'}"
 163 [18:16] <mterry> Then, we read the local content
 164 [18:16] <mterry> And push it to where the server told us to
 165 [18:16] <mterry> (this is the "content_path" bit)
 166 [18:16] <mterry> The response from the server (and the specially crafted message we gave it) is called JSON
 167 [18:17] <mterry> It's a special format for encoding data structures as strings
 168 [18:17] <mterry> Looks very Python-y
 169 [18:17] <mterry> The 'json' module has support for reading and writing it
 170 [18:17] <mterry> As you can see
 171 [18:17] <mterry> We also use a different base URL for uploading the content
 172 [18:17] <mterry> We use "files.one.ubuntu.com"
 173 [18:18] <mterry> So now, let's try this new code out:
 174 [18:18] <mterry> "python u1file.py put u1file.py testing/u1file.py"
 175 [18:18] <mterry> This will upload our script to the new testing volume we created
 176 [18:18] <mterry> Again, you can visit the U1 page in your browser and refresh it to see if it was created
 177 [18:19] <mterry> If so, congrats!
 178 [18:19] <mterry> Also note that we had to specify the content length and content type
 179 [18:19] <mterry> These are mandatory
 180 [18:19] <mterry> I calculated both in my example (using the mimetypes module)
 181 [18:19] <mterry> But if you already know the mimetype, you can skip that bit of course
 182 [18:20] <mterry> OK, let's try downloading the script we just uploaded
 183 [18:20] <mterry> This is very similar, but uses GET requests instead of PUT ones
 184 [18:20] <mterry> Again, two step process
 185 [18:20] <mterry> We first get the metadata about the file, which tells us the content_path
 186 [18:20] <mterry> And then we get the content
 187 [18:20] <mterry> ===
 188 [18:20] <mterry> def get(remote, local):
 189 [18:20] <mterry>   import json
 190 [18:20] <mterry>   import ubuntuone.couch.auth as auth
 191 [18:20] <mterry>   import urllib
 192 [18:20] <mterry>   # Request metadata
 193 [18:20] <mterry>   base = "https://one.ubuntu.com/api/file_storage/v1/~/"
 194 [18:20] <mterry>   answer = auth.request(base + urllib.quote(remote))
 195 [18:20] <mterry>   node = json.loads(answer[1])
 196 [18:20] <mterry>   # Request content
 197 [18:21] <mterry>   base = "https://files.one.ubuntu.com"
 198 [18:21] <mterry>   url = base + urllib.quote(node.get('content_path'), safe="/~")
 199 [18:21] <mterry>   answer = auth.request(url)
 200 [18:21] <mterry>   f = open(local, 'wb')
 201 [18:21] <mterry>   f.write(answer[1])
 202 [18:21] <mterry> if sys.argv[1] == "get":
 203 [18:21] <mterry>   login()
 204 [18:21] <mterry>   get(sys.argv[2], sys.argv[3])
 205 [18:21] <mterry> ===
 206 [18:21] <mterry> Nothing ground breaking there
 207 [18:21] <mterry> Again, we hit files.one.ubuntu.com for the content
 208 [18:21] <mterry> And again, there is no error checking here
 209 [18:21] <mterry> We'll get to that later
 210 [18:21] <mterry> Let's try to download that script we uploaded
 211 [18:21] <mterry> "python u1file.py get testing/u1file.py /tmp/u1file.py"
 212 [18:22] <mterry> This will put it in /tmp/u1file.py
 213 [18:22] <mterry> Now let's see what we downloaded
 214 [18:22] <mterry> "less /tmp/u1file.py"
 215 [18:22] <mterry> It should look right
 216 [18:22] <mterry> So we can create volumes, upload, and download files
 217 [18:23] <mterry> Big things left to do are list files, query metadata, and delete files
 218 [18:23] <mterry> Let's start with listing
 219 [18:23] <mterry> ===
 220 [18:23] <mterry> def get_children(path):
 221 [18:23] <mterry>   import json
 222 [18:23] <mterry>   import ubuntuone.couch.auth as auth
 223 [18:23] <mterry>   import urllib
 224 [18:23] <mterry>   # Request children metadata
 225 [18:23] <mterry>   base = "https://one.ubuntu.com/api/file_storage/v1/~/"
 226 [18:23] <mterry>   url = base + urllib.quote(path) + "?include_children=true"
 227 [18:23] <mterry>   answer = auth.request(url)
 228 [18:23] <mterry>   # Create file list out of json data
 229 [18:23] <mterry>   filelist = []
 230 [18:23] <mterry>   node = json.loads(answer[1])
 231 [18:23] <mterry>   if node.get('has_children') == True:
 232 [18:23] <mterry>     for child in node.get('children'):
 233 [18:23] <mterry>       child_path = urllib.unquote(child.get('path')).lstrip('/')
 234 [18:23] <mterry>       filelist += [child_path]
 235 [18:23] <mterry>   print filelist
 236 [18:23] <mterry> if sys.argv[1] == "list":
 237 [18:23] <mterry>   login()
 238 [18:23] <mterry>   get_children(sys.argv[2])
 239 [18:23] <mterry> ===
 240 [18:23] <mterry> This is very similar to downloading a file
 241 [18:23] <mterry> But we add "?include_children=true" to the end of the request URL
 242 [18:24] <mterry> Then we grab the list of children from the JSON data returned
 243 [18:25] <mterry> black_puppydog has noted that my ubuntuone-couch backport has a bug preventing it from working right
 244 [18:25] <mterry> I will prepare a new package
 245 [18:25] <mterry> But you can fix it by doing the following
 246 [18:26] <mterry> sudo gedit /usr/share/pyshared/ubuntuone-couch/ubuntuone/couch/auth.py
 247 [18:26] <mterry> Search for ", disable_ssl_certificate_validation=True" near the bottom
 248 [18:26] <mterry> And remove it
 249 [18:26] <mterry> Sorry, I really thought I had tested with that
 250 [18:27] <mterry> I've uploaded a fixed package, but it will take a few minutes to build
 251 [18:27] <mterry> So to download the complete file we've got so far...
 252 [18:27] <mterry> grab it here: https://wiki.ubuntu.com/mterry/UbuntuOneFilesNotes11.10?action=AttachFile&do=view&target=6.py
 253 [18:28] <mterry> I'll give everyone a few seconds to catch up
 254 [18:28] <mterry> Save that 6.py file as u1file.py
 255 [18:28] <mterry> And do the following commands to get to the same state:
 256 [18:28] <mterry> python u1file.py login
 257 [18:28] <mterry> python u1file.py create-volume testing
 258 [18:29] <mterry> python u1file.py put u1file.py testing/u1file.py
 259 [18:29] <mterry> python u1file.py get testing/u1file.py /tmp/u1file.py
 260 [18:29] <mterry> python u1file.py list testing
 261 [18:29] <mterry> Really sorry about that
 262 [18:30] <mterry> Note that if you are working on a project that needs to work in 11.04 but you still want this functionality
 263 [18:30] <mterry> You can just locally make a copy of ubuntuone-couch's auth.py file and use it in your project (as long as the license is compatible of course)
 264 [18:31] <mterry> OK, I'm going to wait just a moment longer to let people catch up and re-read the file now that it will actually work when they run it
 265 [18:31] <mterry> So when you run "python u1file.py list testing" you should get a list of all the files you put there
 266 [18:32] <mterry> Which I expect will just be the one u1file.py file
 267 [18:32] <mterry> So now, let's see if we can't get a bit more info about that file
 268 [18:32] <mterry> Sometimes you'll want to query file metadata
 269 [18:32] <mterry> This is very much like downloading
 270 [18:32] <mterry> But without getting the actual contents
 271 [18:32] <mterry> ===
 272 [18:32] <mterry> def query(path):
 273 [18:32] <mterry>   import json
 274 [18:32] <mterry>   import ubuntuone.couch.auth as auth
 275 [18:32] <mterry>   import urllib
 276 [18:32] <mterry>   # Request metadata
 277 [18:32] <mterry>   base = "https://one.ubuntu.com/api/file_storage/v1/~/"
 278 [18:32] <mterry>   url = base + urllib.quote(path)
 279 [18:32] <mterry>   answer = auth.request(url)
 280 [18:33] <mterry>   node = json.loads(answer[1])
 281 [18:33] <mterry>   # Print interesting info
 282 [18:33] <mterry>   print 'Size:', node.get('size')
 283 [18:33] <mterry> if sys.argv[1] == "query":
 284 [18:33] <mterry>   login()
 285 [18:33] <mterry>   query(sys.argv[2])
 286 [18:33] <mterry> ===
 287 [18:33] <mterry> Adding that to your file will let you call "python u1file.py query testing/u1file.py"
 288 [18:33] <mterry> You should see the size in bytes
 289 [18:33] <mterry> There is a bit more metadata available (try inserting a "print node" in there to see it all)
 290 [18:33] <mterry> And the last big file operation we'll cover is the easiest
 291 [18:34] <mterry> Deleting files
 292 [18:34] <mterry> ===
 293 [18:34] <mterry> def delete(path):
 294 [18:34] <mterry>   import ubuntuone.couch.auth as auth
 295 [18:34] <mterry>   import urllib
 296 [18:34] <mterry>   base = "https://one.ubuntu.com/api/file_storage/v1/~/"
 297 [18:34] <mterry>   auth.request(base + urllib.quote(path), http_method="DELETE")
 298 [18:34] <mterry> if sys.argv[1] == "delete":
 299 [18:34] <mterry>   login()
 300 [18:34] <mterry>   delete(sys.argv[2])
 301 [18:34] <mterry> ===
 302 [18:34] <mterry> That's simple.  Merely an HTTP DELETE request to the metadata URL
 303 [18:34] <mterry> This covers the basic file operations you'd want to do
 304 [18:34] <mterry> I promised I'd talk about error handling
 305 [18:34] <mterry> So behind the scenes, this is all done using HTTP
 306 [18:35] <mterry> And the responses you get back from the server are all in HTTP
 307 [18:35] <mterry> So it makes sense that to check what kind of response you got, you'd use HTTP status codes
 308 [18:35] <mterry> You may be familiar with these
 309 [18:35] <mterry> To look at a status code, with the above examples, you'd do something like:
 310 [18:36] <mterry> answer = auth.request(...)
 311 [18:36] <mterry> status = int(answer[0].get('status'))
 312 [18:36] <mterry> answer is a tuple of 2
 313 [18:36] <mterry> The first bit is HTTP headers
 314 [18:36] <mterry> The second is the HTTP body
 315 [18:36] <mterry> So we're asking for the 'status' HTTP header here
 316 [18:36] <mterry> Any number in the 200s is an "operation succeeded" message
 317 [18:36] <mterry> There are a few important status codes to be aware of
 318 [18:36] <mterry> 400 is "permission denied"
 319 [18:37] <mterry> 404 is "file not found"
 320 [18:37] <mterry> 503 is "servers busy, please try again in a bit"
 321 [18:37] <mterry> 507 is "out of space"
 322 [18:37] <mterry> You may also just receive a boring old 500 status
 323 [18:37] <mterry> This is like an "internal error" message
 324 [18:37] <mterry> Which isn't very helpful, but usually you are also given an Oops ID to go with it
 325 [18:38] <mterry> oops_id = answer[0].get('x-oops-id')
 326 [18:38] <mterry> If you give this to the U1 server folks, they can tell you what happened and fix the bug
 327 [18:38] <mterry> So if you're going to print a message for the user, include that so that when they report the bug, you'll have the Oops-ID to hand over
 328 [18:39] <ClassBot> black_puppydog asked: how about checksums? this is needed for example in dejadup, right?
 329 [18:40] <mterry> One piece of metadata is "hash"
 330 [18:40] <mterry> That the server will give you
 331 [18:40] <mterry> I actually have not used that, so I don't know what checksum algorithm it uses
 332 [18:41] <mterry> But you can also just download the file and see (which is what Deja Dup does)
 333 [18:41] <mterry> See https://one.ubuntu.com/developer/files/store_files/cloud/
 334 [18:41] <mterry> For a list of other metadata pieces you can get from the server
 335 [18:41] <mterry> That also has other useful info.  It's the official documentation for this stuff
 336 [18:42] <mterry> If anyone is interested, the Deja Dup code is actually in duplicity, a command line tool that Deja Dup is a wrapper for
 337 [18:42] <mterry> http://bazaar.launchpad.net/~duplicity-team/duplicity/0.6-series/view/head:/duplicity/backends/u1backend.py
 338 [18:42] <mterry> That's real code in use right now
 339 [18:43] <mterry> If you ever have a problem playing with this stuff, the folks in #ubuntuone are very helpful
 340 [18:43] <mterry> With Oops that you run into or whatever
 341 [18:44] <mterry> And that's all I have!  I'll hang around for questions if there are any
 342 [18:46] <ClassBot> black_puppydog asked: this file you used here, shouldn't that be some sort of library?
 343 [18:46] <mterry> black_puppydog, yeah, it very well could be
 344 [18:46] <mterry> You mean, some sort of library supported by the U1 folks to make this all easier?
 345 [18:46] <mterry> Well...  They've already provided a lot of the code around it.  I think their intention is to focus on providing the best generic API (the web HTTP one) that all sorts of devices and languages can use.
 346 [18:47] <mterry> I think they'd be happy to see an awesome Python wrapper library, but I don't think they want to maintain and promote one such library at the expense of others
 347 [18:47] <mterry> This is close, it would just need much better error handling and such
 348 [18:47] <mterry> But I also don't want to maintain it  :)
 349 [18:48] <mterry> But really, it's not *that* much code.  A bit boiler plate, true
 350 [18:48] <mterry> ubuntuone-couch takes care of most of the icky parts that are hard to do well (OAuth authentication)
 351 [18:49] <mterry> Most languages have REST and OAuth libraries that can be used in conjunction to talk to the servers
 352 [18:50] <ClassBot> There are 10 minutes remaining in the current session.
 353 [18:51] <mterry> black_puppydog makes a good point in the chat channel.  The duplicity code has better error handling than I've presented.  So it may be a better jumping-off point to just steal wholesale than the script we've built here
 354 [18:52] <mterry> Note that it is licensed GPL-2+
 355 [18:52] <mterry> So if that's not appropriate, maybe just whip something similar up yourself

MeetingLogs/appdevweek1109/UbuntuOneFilesIntegration (last edited 2011-09-12 15:37:34 by bl18-252-24)