Skip to main content

Upload to Ubuntu One using curl

2012-04-15: I found it easier to work with signed URLs instead of a separate header so the blog post was updated to use these.

I like to find new ways to use Ubuntu One as easily as possible. I have to admit that the way described in headless Ubuntu One article is not quite reliable since the file is read completely into the memory prior to being transferred. For large files this can be a problem. There are ways to make this work for streaming, but while investigating this I found out that we can use curl for uploads and downloads, without any python wrapper.

Basically,

curl -H "Content-Type: text/plain"
    -T filename `oauth-sign -m PUT -f url http://.../~/Folder/filename`

But in order to make it work this way, you need just a tiny bit of preparation.

First of all, you must know that all Ubuntu One clients do not use your password for authentication. Instead, a mechanism called OAuth is used. This means that for every connected device a separate set of credentials is being assigned allowing you to invalidate the access whenever you need and keep the really secret password to yourself.

OAuth is not natively supported by curl but since this is a standard, there are multiple solutions you can use to generate the signed URL as in the example, there are no proprietary extensions or cookie storage.

If you have ruby, installing oauth gem will give you an oauth script. In case you are after the python one, you can grab my implementation from ubuntuone-scripts branch.

First you need to get the tokens, we are using pretty much the same ubuntuone-sso-login.py script I mentioned in the article about the REST client, however in order to make it simpler to pass the arguments to other application, i added the --format (-f) option, which does the following:

$ python ubuntuone-sso-login.py --format shell
Ubuntu SSO Login: sso-test-2@rtg.in.ua
Password:
export OAUTH_CONSUMER_KEY=RwLrAWeWF
export OAUTH_CONSUMER_SECRET=fGztsAJHqfWFirGBwXNhiEinrfqCvDVtEHJ
export OAUTH_TOKEN=HFvMpWiwuOcGOsSSRbkaHZHMARjHZrfYYRa
export OAUTH_TOKEN_SECRET=BjnVKEhQqCaTcHWBEywVGMhlqvtTBzFFeykYC

Copy the lines with export to some shell script file, e.g. ~/.ubuntuone-credentials, we will re-use the names further.

Now create the signed URL with ruby implementation: We are requesting account information, so the URL is https://one.ubuntu.com/api/account/.

$ SIGNED_URL=`oauth --consumer-key $OAUTH_CONSUMER_KEY
        --consumer-secret $OAUTH_CONSUMER_SECRET
        --method GET --uri https://one.ubuntu.com/api/account/
        --secret $OAUTH_TOKEN_SECRET
        --token $OAUTH_TOKEN -v -Q sign | grep 'OAuth Request URI:'
                                    | sed 's/OAuth Request URI: //'`

The weird grep/sed dance is needed to get the URL to the format we will use later, you will not need to do this in python oauth-sign version.

$ curl $SIGNED_URL
{"username": "https://login.ubuntu.com/+id/RwLWeWF", "openid":
"https://login.ubuntu.com/+id/RwLWeWF", "last_name": "Roman"
...

Great, it works. Now let’s work with the actual files.

According to the docs the file root is https://files.one.ubuntu.com/ and PUT is being used to upload the files.

Please note the following paragraph in the docs -

Note also that content_paths may not be rooted under the API root, and may change without warning, so they must be read from the API and not hardcoded.

The current way assumes that the content path is /content/~/path/to/volume/path/to/file.txt, this works for now, but may change in the future.

First let’s construct the URL we will be using

$ URL=https://files.one.ubuntu.com/content/~/Ubuntu%20One/hello.txt

Now we need to get the signed URL. Let’s use the python version. Since it reads the environment variables, there is no need to specify the token:

$ SIGNED_URL=`oauth-sign -f url -m PUT $URL`

Yes, that’s it.

Now create a hello.txt file locally and then upload it with curl, specifying -T makes curl use “PUT” request and transfer the file without buffering it in the memory:

$ echo "Hello, Ubuntu One" > hello.txt
$ curl -T hello.txt -H "Content-Type: text/plain" $SIGNED_URL

{"kind": "file", "public_url": null, "hash":
"sha1:b50a416c31dbf3620b72cd7841980541805db44e", ...

Great! Now let’s get the file:

$ curl `oauth-sign -f url $URL`
Hello, Ubuntu One

Please note that I am specifying Content-Type header too. Usually when you ask curl to upload with --data-binary, it sets the Content-Type to application/octet-stream. This is a mandatory header for Ubuntu One servers. In case you omit the header, you will receive an error.

I have some SQL databases of text messages, expenses and a test openfire installation. I upload the backup regularly to Ubuntu One with the following simple script. I put the OAUTH exports to ~/.ubuntuone-credentials file which gets sourced.

#!/bin/bash

URL=https://files.one.ubuntu.com/content/~/Ubuntu%20One

source ~/.ubuntuone/credentials

for DB in webapp openfire; do
    RESULT=fridge-$DB.sql.gz

    LOCALFILE=`mktemp /tmp/backup-XXXXXX`

    pg_dump -h localhost -U backup $DB | gzip > $LOCALFILE

    REMOTE=$URL/$RESULT

    SIGNED_URL=`~/bin/oauth-sign -f url -m PUT $REMOTE`

    HEADERS=`mktemp /tmp/headers-XXXXXX`
    OUTPUT=`mktemp /tmp/output-XXXXXX`

    STATUS=`curl --silent --fail --dump-header $HEADERS
        -T "$LOCALFILE"
        -H "Content-Type: application/octet-stream"
        -w '%{http_code}' $SIGNED_URL -o $OUTPUT`
    rv=$?
    if [ "$rv" != "0" -o ( "$STATUS" != "201" -a "$STATUS" != "200" ) ]; then
        echo "Backup of $DB failed!"
        echo "The server replied with:"
        echo
        cat $HEADERS
        echo
        cat $OUTPUT
    fi

    rm $OUTPUT
    rm $HEADERS
    rm $LOCALFILE

done