Your browser is very old. You might enjoy surfing the web more if you used something newer like:

Google Chrome

Even Firefox would be OK.

If you're being forced at gunpoint to use Internet Explorer, you should at least upgrade it. Version 8 is tolerable and 9 will be OK when it comes out.

Picasa data API fun: Creating albums and uploading images

Today, we’re going to have some fun with the Picasa Data API by learning how to create Picasa albums and upload photos to them.

We’re going to do everything with vanilla-PHP and cURL but it’s worth mentioning that the Zend Framework has a set of classes for doing all of this stuff and more.

While it does look like Google is moving in the direction of using oAuth for authentication, they still support username/password client logins which are, frankly, a ton easier to deal with.

I’ve written about Google client logins before and the code I mentioned previously will still work with the Picasa Data API. Basically, we provide a username and password to Google. They respond with an Auth token which we then pass as an HTTP header to all future requests to the API.

Like pretty much all Google Data APIs, doing things with Picasa mostly involves sending XML packets via HTTP POST. It’s just a matter of crafting the appropriate XML for what you’re trying to do.

Creating a Picasa Album

Here’s a simple example which creates an album called “Test album from PHP”. The below code assume we’re already authenticated and have a valid Auth token. You’ll also notice that we need to have the userId of the Picasa user we’re authenticated as.

    $authHeader = 'Authorization:  GoogleLogin auth="' . $authToken . '"';
    $feedUrl = "https://picasaweb.google.com/data/feed/api/user/$userId";

    $rawXml = "<entry xmlns='http://www.w3.org/2005/Atom'
                    xmlns:media='http://search.yahoo.com/mrss/'
                    xmlns:gphoto='http://schemas.google.com/photos/2007'>
                  <title type='text'>Test album from PHP</title>
                  <summary type='text'>This is a test album</summary>
                  <gphoto:location>Louisville</gphoto:location>
                  <gphoto:access>public</gphoto:access>
                  <gphoto:timestamp>1152255600000</gphoto:timestamp>
                  <category scheme='http://schemas.google.com/g/2005#kind'
                    term='http://schemas.google.com/photos/2007#album'></category>
                </entry>";

    curl_setopt($ch, CURLOPT_URL, $feedUrl);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);  

    $data = array($xml);

    $options = array(
                CURLOPT_SSL_VERIFYPEER=> false,
                CURLOPT_POST=> true,
                CURLOPT_RETURNTRANSFER=> true,
                CURLOPT_HEADER=> true,
                CURLOPT_FOLLOWLOCATION=> true,
                CURLOPT_POSTFIELDS=> $rawXml,
                CURLOPT_HTTPHEADER=> array('GData-Version:  2', $authHeader, 'Content-Type:  application/atom+xml')
            );
    curl_setopt_array($ch, $options);

    $ret = curl_exec($ch);
    curl_close($ch);

Uploading an image without metadata

Here’s an example that uploads an image to the album we created previously. You’ll notice that we also have to specify an albumId in addition to the userId. You can get album id by retrieving the list of albums for a particular user or at the time when you create a new album. Creating a new album returns an XML response with all kinds of detailed information about the album.

In this case, we don’t need any XML, just the URL to the album we want to load to.

Then it’s just a matter of getting the binary data of the image and sending it via POST to the album URL.

NOTE: Don’t try to Base64-encode the image data. Just POST it as-is. Attempting to encode the image data will give you Bad Request: Not an image errors.

    $albumUrl = "https://picasaweb.google.com/data/feed/api/user/$userId/albumid/$albumId";
    $imgName = $_SERVER['DOCUMENT_ROOT'] . '/picasa/cute_baby_kitten.jpg';

    //Do a binary-read of the image file we want to upload.
    $fileSize = filesize($imgName);
    $fh = fopen($imgName, 'rb');
    $imgData = fread($fh, $fileSize);
    fclose($fh);

    //The Slug header is optional and is used to specify the name of the image in the album
    $header = array('GData-Version:  2', $authHeader, 'Content-Type: image/jpeg', 'Content-Length: ' . $fileSize, 'Slug: cute_baby_kitten.jpg');
    $data = $imgData;

    $ret = "";
    $ch  = curl_init($albumUrl);
    $options = array(
            CURLOPT_SSL_VERIFYPEER=> false,
            CURLOPT_POST=> true,
            CURLOPT_RETURNTRANSFER=> true,
            CURLOPT_HEADER=> true,
            CURLOPT_FOLLOWLOCATION=> true,
            CURLOPT_POSTFIELDS=> $data,
            CURLOPT_HTTPHEADER=> $header
        );
    curl_setopt_array($ch, $options);
    $ret = curl_exec($ch);
    curl_close($ch);

Uploading an image including metadata

This is a little tricker.

The documentation shows an example request as looking like this

Content-Type: multipart/related; boundary="END_OF_PART"
Content-Length: 423478347
MIME-version: 1.0

Media multipart posting
--END_OF_PART
Content-Type: application/atom+xml


Real cat wants attention too.

  

--END_OF_PART
Content-Type: image/jpeg

...binary image data...
--END_OF_PART--

Unfortunately, they don’t give any clear examples about how to actually craft this kind of POST request.

Here’s the basics. This bit?

Content-Type: multipart/related; boundary="END_OF_PART"
Content-Length: 423478347
MIME-version: 1.0

That’s your HTTP header. The string END_OF_PART is just how you tell the server when one content section has ended and the next section starts. It just needs to be a unique string.

The Content-Length is also tricky. I just assumed that it was calculated by adding the length of the metadata XML and the image file size. I spent a fair amount of time banging my on that until I found this snippet from the YouTube Data API.

Here’s the key takeaway:

To calculate the proper Content-Length, you need to count the full string length of the POST request. However, in addition to the XML component and the file binary, a direct upload request also defines a boundary string that separates the different parts of the request. So the calculation of the Content-Length needs to account for the size of the XML and file binary as well as of the inserted boundary strings and newlines.

Here’s a code example of uploading an image including metadata.

    $albumUrl = "https://picasaweb.google.com/data/feed/api/user/$userId/albumid/$albumId";
    $imgName = $_SERVER['DOCUMENT_ROOT'] . '/picasa/cute_baby_kitten.jpg';

    $rawImgXml = '
Real cat wants attention too.

                  
                ';

    $fileSize = filesize($imgName);
    $fh = fopen($imgName, 'rb');
    $imgData = fread($fh, $fileSize);
    fclose($fh);

    $dataLength = strlen($rawImgXml) + $fileSize;
    $data = "";
    $data .= "\nMedia multipart posting\n";
    $data .= "--P4CpLdIHZpYqNn7\n";
    $data .= "Content-Type: application/atom+xml\n\n";
    $data .= $rawImgXml . "\n";
    $data .= "--P4CpLdIHZpYqNn7\n";
    $data .= "Content-Type: image/jpeg\n\n";
    $data .= $imgData . "\n";
    $data .= "--P4CpLdIHZpYqNn7--";

    $header = array('GData-Version:  2', $authHeader, 'Content-Type: multipart/related; boundary=P4CpLdIHZpYqNn7;', 'Content-Length: ' . strlen($data), 'MIME-version: 1.0');

    $ret = "";
    $ch  = curl_init($albumUrl);
    $options = array(
            CURLOPT_SSL_VERIFYPEER=> false,
            CURLOPT_POST=> true,
            CURLOPT_RETURNTRANSFER=> true,
            CURLOPT_HEADER=> true,
            CURLOPT_FOLLOWLOCATION=> true,
            CURLOPT_POSTFIELDS=> $data,
            CURLOPT_HTTPHEADER=> $header
        );
    curl_setopt_array($ch, $options);
    $ret = curl_exec($ch);
    curl_close($ch);

The request headers look like this:

Array
(
    [0] => GData-Version:  2
    [1] => Authorization:  GoogleLogin auth="THISISAVALIDAUTHCODE"
    [2] => Content-Type: multipart/related;boundary=P4CpLdIHZpYqNn7
    [3] => Content-Length: 179951
    [4] => MIME-version: 1.0
)

And the actual request body ends up looking like this:

Media multipart posting
--P4CpLdIHZpYqNn7
Content-Type: application/atom+xml


Real cat wants attention too.

              
            
--P4CpLdIHZpYqNn7
Content-Type: image/jpeg

IMAGE DATA GOES HERE
--P4CpLdIHZpYqNn7--

Then, if all goes well, you’ll end up with something like this:

There are lots of neat things you can do with the Picasa data API.

Hopefully the above will be enough to get you rolling with them.

SVN working copy is “nested”?

Have you ever gone to commit something and TortoiseSVN tells you that part of your working copy is “nested”?

You know what that means?

It means that the nested folder contains the Subversion meta-information for a different working copy! I find this usually happens when you copy a directory into your working copy from somewhere else and forget to delete all of the .svn directories.

The interesting thing is that, despite this warning, Subversion will happily commit any changes you’ve made in that directory.

The problem is that it commits them to whatever repository that directory came from, not to your repository.

We had a situation at work today where part of a working copy was nested and we thought we’d lost the only copy of a modified image as a result. Luckily we were able to pull the correct image from the revision log of the other working copy.

So how do you fix a nested working copy? It’s really easy.

1. Delete all of the .svn directories from the nested directory.
2. Add the directory into subversion (svn add).
3. Commit the add (svn ci).
4. Profit!

My one, feeble attempt at being a cracker of software.

Ah, the heady days of fall-1994. I was 16, hopelessly obsessed with my computer, and my dorm room had dial-up Internet access.

This meant my roommates and I spent far more time hacking (in the good way), and downloading and playing Mac (System 7 for life!) games than attending class.

The gaming went through a number of phases.

Phase 1: Myst. There were four of us in the room and one of the four (not me) almost flunked out because of his Myst obsession.

Phase 2: Civilization. I got hooked on this in high school. In the end, I think we all lost equal amounts of time to this one.

Phase 3: Network games of Spectre. 3D tanks! Virtual-reality-esque! This was back when Lawnmower Man was cool. (OK, Lawnmower Man was never cool but I still loved it).

Phase 4: Network games of Marathon (OK, technically this one didn’t come around until early 1995.)

For the network games, we bribed a facilities maintenance person to drill a hole through my closet so we could run Ethernet cable into the room next door.

Phase 5: Emulation! I was mainly a fan of 8-bit Nintendo emulators with the occasional Sega Genesis* game thrown in for good measure.

*I loved my Sega Genesis in high school. I, unfortunately, sold it to my then-girlfriend’s little sister who proceeded to never pay me for it. When said-girlfriend and I parted ways, any hopes of collecting my money disappeared.

My preferred Nintendo emulator at the time was a little Macintosh shareware app called iNES. There’s still a iNES emulator in active development that’s cross platform, I have no idea if it’s the same one I used.

This old version, iNES 7.7, had a copy-protection scheme where it would lock itself down after a certain number of uses unless you paid for it.

Thanks to Zen and the Art of Resource Editing, I spent a fair amount of time poking around in the guts of my Macintosh LC and its various applications.

It turned out that the mechanism by which iNES locked itself was pretty simple. First, it made a modification to the iNES preferences file, then made the file locked and invisible. Second, it added a special resource to the resource fork of the iNES application which flagged it as “expired”.

After manually unlocking iNES a bunch of times using ResEdit, I decided to learn enough Mac programming to write a little program to do the unlocking for me.

The app itself was pretty simple:

  1. Popup a File Open dialog so the iNES application can be located
  2. Unlock and delete the preferences file. This was annoying because you had to reenter all of your prefs the next time you launched the program. My goal for phase two was try to preserve the existing prefs and just remove whatever bit it was that locked the app.
  3. Open the resource fork of iNES and delete the offending resource. I was especially proud of the function that did this part because it was called URiNES(). You’re never too old for potty jokes, right?

My first Macintosh application! I was very product of my 3l33t h4x0r skillz and, of course, wanted to show off.

I figured the best thing to do was to share the app with the guy who ran the emulator site where I had been downloading iNES (and ROMs) from. Little did I know that this same fellow was the guy who wrote and maintained iNES!

As you can imagine, he was less than thrilled with my accomplish and I, of course, felt like a giant moron.

In the end, I swore to him that I would never distribute my little crack to anyone and I also purchased a licensed copy of iNES.

Given that this was almost 20 years ago and that version of iNES doesn’t exist, I think it’s probably safe to share the source of my one and only attempt at being a software cracker.
UnPirate iNES: http://pastebin.com/YJQd916j