한국어

EDPS

Cultured Perl: Perl and the Amazon cloud Part1~5

조회 수 6337 추천 수 0 2012.01.06 15:31:52

Cultured Perl: Perl and the Amazon cloud, Part 1

Learn the basics of Amazon's S3 and SimpleDB services by designing a simple photo-sharing site

Teodor Zlatanov (tzz@lifelogs.com), Programmer, Gold Software Systems

Summary:  This five-part series walks you through building a simple photo-sharing Web site using Perl and Apache to access Amazon's Simple Storage Service (S3) and SimpleDB. In this installment, get a feel for the benefits and drawbacks of S3 and SimpleDB by taking a tour of their architectures and starting to design your photo-sharing site.

View more content in this series

Tags for this article:  amazoncloudcultured_perldesarrolloperls3

Date:  31 Mar 2009 
Level:  Intermediate 
PDF:  A4 and Letter (42KB | 11 pages)Get Adobe® Reader® 
Also available in:   Russian  Japanese  Portuguese  Spanish 

Activity:  31484 views 
Comments:   0 (View | Add comment - Sign in)

Average rating 3 stars based on 8 votes Average rating (8 votes)
Rate this article

So you want to learn about two of Amazon's Web services: Amazon S3 (Simple Storage Service) and Amazon SimpleDB. What better way to learn than a hands-on experience? In this case, you'll build a simple photo sharing site.

The goal is not to build a well-designed site; that's been done many times. Besides, putting together a Web site is hard, and the technical side is only part of the equation, so please don't send me complaints like "d00d your teh worst" because sharethewindbeneathmydonkey.com didn't make you a million in the first week. (But if it does, remember me as the one who got you started.)

To get the most from this series

This series requires beginner-level knowledge of HTTP and HTML, as well as intermediate-level knowledge of JavaScript and Perl (inside an Apache mod_perl process). Some knowledge of relational databases, disk storage, and networking will be helpful. The series will get increasingly more technical, so see the Resources section if you need help with any of those topics.

I'll use share.lifelogs.com in this series as the domain name. Let's take a look at Amazon S3.

Amazon S3 overview

I've been a UNIX® administrator for awhile now, and I can tell you that backups and file storage are not simple services. If acronyms such as SAN, NAS, LUN, LVM, RAID, JBOD, IDE, and SCSI don't mean anything to you, then be glad. If they do, you've surely whimpered quietly into your tear-stained napkin at lunch and hoped for a better way to manage data after, say, the third month of restoring from corrupted four-year-old DLT backups. Not that I've ever done that.

IBM and Amazon Web Services

Cloud computing provides a way to develop applications in a virtual environment, where computing capacity, bandwidth, storage, security and reliability aren't issues—you don't need to install the software on your own system. In a virtual computing environment, you can develop, deploy, and manage applications, paying only for the time and capacity you use, while scaling up or down to accommodate changing needs or business requirements.

IBM has partnered with Amazon Web Services to give you access to IBM software products in the Amazon Elastic Compute Cloud (EC2) virtual environment. Our software offerings on EC2 include:

  • DB2® Express-C 9.5
  • Informix® Dynamic Server Developer Edition 11.5
  • WebSphere® Portal Server and Lotus® Web Content Management Standard Edition
  • WebSphere sMash

This is product-level code, with all features and options enabled. Get more information and download the Amazon Machine Images for these products on the IBM developerWorks Cloud Computing Resource Center.

For more cloud computing resources, see the Cloud Computing for Developers space on developerWorks.

Amazon's S3 (Simple Storage Service) is a distributed storage system. If you're willing to trust Amazon with your data, it makes life quite a bit easier. Of course, you can always run your own backups to be sure. (Security might also be an issue: putting data into S3 means you have to use S3's access control system, which might not fit your authentication and authorization requirements. Check the S3 documentation listed in Resources for details.)

So what do you get with S3? S3 uses a user key (a long, random-looking string) and a user password (another random-looking string) to let you store and retrieve files. You get charged according to Amazon's S3 pricing, which you can find on their Web site. It's not too expensive; when compared to the costs of keeping your own NAS or SAN or local disks, S3 is quite reasonable.

As of early 2009, S3 data is hosted at two Amazon data centers (the US and EU centers) with good network connectivity. If you want to serve your data to large audiences outside the US and EU, you should run tests with a service such as Gomez or Keynote, which are designed to determine worldwide performance. Even within the US and EU, if your business depends on serving data quickly and reliably, you should set up daily performance tests through such a service.

The major problem with a distributed storage system is its update latency. This is the time between the content owner's actions and when those actions propagate. But simple time between actions and propagation isn't the only potential worry; the propagation may not be uniform, so your customers may see different content at different times. Amazon guarantees consistency at the server, meaning that your customers will not see corrupted data, but you should bear this in mind as you evaluate S3. When you upload, modify, or delete an image, don't expect the changes to take effect immediately.

There are Perl libraries for S3 access on the CPAN (see Resources). Net::Amazon::S3 is a good option, but there are many others listed on Amazon's S3 resource page. We won't need to use them, because our S3 integration uses S3 features to bypass any Perl code when content is uploaded. (In addition, there are many good tools for accessing S3—such as JungleDisk or the Firefox S3Fox add-on—that make it easy and convenient to manage your data without Perl.)

An example of Amazon S3

Now, onward to what you get with S3. Files (called objects by S3) are stored in buckets. In each bucket, a file name (its key) has to be unique. You can give files attributes like "color" or "language," but those are not part of the file name.

Let's say you store the picture of the American flag as "images/flag.png" in the bucket "us.images.share.lifelogs.com" and the picture of the German flag as "images/flag.png" in the bucket "de.images.share.lifelogs.com" (they are named the same but in different buckets). Your users can then request http://us.images.share.lifelogs.com.s3.amazonaws.com/images/flag.png to get the American flag or http://de.images.share.lifelogs.com.s3.amazonaws.com/images/flag.png to get the German flag. Furthermore, you can alias de.images.share.lifelogs.com to de.images.share.lifelogs.com.s3.amazonaws.com in DNS (do the same for us.images.share.lifelogs.com), so users will just have to request http://us.images.share.lifelogs.com/images/flag.png or http://de.images.share.lifelogs.com/images/flag.png to get the flags.

Note that bucket names have to be unique across all Amazon S3 accounts, so names like "test" and "default" are no good. Qualify the bucket name with the full domain name if possible. It makes identifying the bucket and using it in DNS easier. Also, bucket names are pretty limited, so don't try to write a novel in them. Stick to the same characters you'd use for a domain name.

S3 is a complex service, so I encourage you to look at the S3 home page before you go on.

Amazon SimpleDB overview

This is the part where professional speakers and college professors yell out a random phrase to wake up that snoring guy in the front row who was up until 3 AM last night doing tequila shots: DATABASES ARE IMPORTANT!

Are you awake?

Sorted, filtered, aggregated, averaged, analyzed, the flood of raw data we face every day can become an unmanageable stream of information. Hosting these databases is a full-time job for IT professionals. They require space, power, backups, and many other resources. Using hosted databases such as SimpleDB may be worthwhile for your business as a financial decision; I will only be explaining the technical side.

A simple database example is a to-do list pinned to the fridge: each item is on a line by itself, and there's a check mark next to some and perhaps others are crossed out. In a traditional relational database, this might be modeled as two tables with two columns each:


Table 1. todo_foreign table
itemstatuscode
(FK to status.statuscode, 
default 0)
call Mom0
call IRS2
get milk1

Table 2. Status table
statuscodestatusdesc
0active
1done
2deleted

"But wait," you say. "What about the date when the item was completed or deleted, who modified it, and what are the data types? This is, after all, why we train wise, powerful database administrators (DBAs). They know all about normal forms and foreign keys and SQL. Surely, you need one of them to look over your design now, right?"

Yes, thank you, Mr. Smarty Pants, but leave my simple example alone. Console yourself later by cuddling up with a copy of "Secrets Of The SQL and RDBMS Gods For Dummies."

Amazon SimpleDB is a widely distributed key-attribute database. It's definitely not for every business, and there are tight restrictions on performance and scalability. Attributes are limited to 1KB each, so your to-do items can't have a name longer than one kilobyte.

Security is also an issue; SimpleDB's access control system is similar to S3's. A social site such as the simple one you will assemble in this series can thrive with SimpleDB as the database back end. Still, you should assess your business requirements, budget, and data storage needs to find out if SimpleDB will fit them all.

The S3 update latency issue I mentioned affects SimpleDB as well. Your updates do not take immediate effect everywhere.

Using our simple database example, the SimpleDB structure would be:


Table 3. SimpleDB todo structure
ItemStatus
call Momactive
call IRSdeleted
get milkdone

So far, so good. This is simpler than the first example, isn't it? Oh, but let's add another item:

get cowactive

Do you see that the status is duplicated? The word active is now stored twice in the database. This can be expensive for large tables in terms of storage and performance. On the other hand, each SimpleDB row is self-sufficient by design. When you get that row, you've got everything it contains. You don't need to look up the status description. With the update latency of SimpleDB, this matters.

More SimpleDB to-do lists

Let's say you add a new status code, waituntiltomorrow, and apply it to one item in the todo_foreign table (Table 1, with the foreign keys). So, now you have two updates (one to the status table, one to todo_foreign). If the status table (Table 2) update happens after the todo_foreign update, you'll have inconsistent data. Remember, SimpleDB doesn't guarantee that your updates will make it out immediately in the order you make them, so besides the performance penalty you'll pay for doing two lookups (one for the item, one for the status code description), you may also have inconsistent data.

Here's the key to SimpleDB: forget about the columns in todo_simple (Table 2). SimpleDB doesn't have columns! It has attributes for each row. Those attributes are not static, so you can add and delete them at will. You want your to-do items to have a creation and a deletion date? Just give them those attributes. In todo_foreign, that would require two columns; the deletion date might be null to indicate that the item is still active. Let's add one more column for the date the item was done. Or maybe it should be a status code only, and use the deleted date as the done date. What do we do?

The SimpleDB way is to just do what you need. You need a creation date? Make a created_date attribute. A deletion date? Assign that attribute only to items that have been deleted. The presence of the attribute tells us that it applies to this item.

Stop thinking in terms of columns. SimpleDB rows are more like Perl's hashes. Every key is a string. Every value is a string or an array of strings. Let's try our design again:


Listing 1. todo_freeform
{ item: "call Mom" }
{ item: "call IRS", deleted_date: "2009-03-01" }
{ item: "get milk", done_date: "2009-03-02" }

Note that SimpleDB has an implicit key called ItemName, which in this case would be the to-do item as a string, like so:


Listing 2. SimpleDB todo list
"call Mom" {  }
"call IRS" { deleted_date: "2009-03-01" }
"get milk" { done_date: "2009-03-02" }

SimpleDB doesn't allow an object without attributes, so give all objects a created_date attribute, like so:


Listing 3. SimpleDB todo list with created_date added
"call Mom" { created_date: "2009-02-01" }
"call IRS" { created_date: "2009-02-01", deleted_date: "2009-03-01" }
"get milk" { created_date: "2009-02-01", done_date: "2009-03-02" }

"But wait," you cry, "Everything really is a string? Data is not rigidly typed? Aaaaah! Doom!"

Yes. Everything is a string. Isn't it wonderful?

Oh, and you can add a deletereason attribute to any deleted item in this table three months after it goes live. It won't break anything, and only new code that knows about it has to use it.

I'll pause here for dramatic effect while the DBAs take a couple of aspirin. Meanwhile, the Perl programmers are giving them glasses of water just because, well, that's the kind of nice people we are.

Moving on with the example. The important part now is figuring out the queries that will give us active, deleted, or done items. This is really simple; you can look at the SimpleDB documentation for all the query options. We'll use the SELECT language. There's also a QUERY language, but SELECTis closer to SQL and thus easier to understand for most readers.


Listing 4. todo_freeform queries
-- get active
select * from todo_freeform where done_date is null and deleted_date is null
-- get deleted
select * from todo_freeform where deleted_date is not null
-- get done
select * from todo_freeform where done_date is not null

There you go. Now let's put SimpleDB and S3 together.

Integrating the services and sharing photos

The next question you're probably asking is, how can I connect SimpleDB and S3? (They are not innately connected except for the access control model they use.) Easy: you can simply store the bucket and name for an S3 object in SimpleDB. In any case, enough with the to-do list; let's start designing the photo-sharing site.

The site needs to store photos in S3 and user comments in SimpleDB. What about user accounts? We need to live with the distributed nature of SimpleDB, which means we will have invalid users sometimes (such as when the user has not been pushed out but the row that refers to that user has). We'll keep users in SimpleDB for this application, though. There will be no dependency on any external databases, as the goal here is to be able to set up the site anywhere quickly, with just some Perl glue running under mod_perl and with the real action happening on S3 and SimpleDB.

First, you'll need a photo table. Records might look like this:


Listing 5. Photo table records, share_photos
"http://developer.amazonwebservices.com/connect/images/amazon/logo_aws.gif" 
{ user: "ted", name: "Amazon Logo"}

"http://images.share.lifelogs.com/funny.jpg" 
{ user: "bob", name: "Funny Picture",  s3bucket: "images.share.lifelogs.com" }

Next, a users table:


Listing 6. Users table, share_users
"ted" { given: "Ted", family: "Zlatanov" }
"bob" { given: "Bob", family: "Leech" }

And comments:


Listing 7. Comments, share_comments
"random-string"
{ 
 url: "http://images.share.lifelogs.com/funny.jpg",
 comment: "Ha ha", 
 posted_when: "2009-03-01T19:00:00+05" 
}

"random-string2"
{ 
 user: "ted",
 url: "http://developer.amazonwebservices.com/connect/images/amazon/logo_aws.gif", 
 comment: "No it doesn't", 
 posted_when: "2009-03-01T20:00:01+05" 
}

"random-string3"
{ 
 url: "http://developer.amazonwebservices.com/connect/images/amazon/logo_aws.gif", 
 comment: "No it doesn't", 
 reply_to: "random-string2", 
 posted_when: "2009-03-01T20:00:01+05" 
}
            

Worth noting

Google offers at least some comparable services; this series is not in any way a comparison between Google's services and Amazon's services. You can find plenty of comparisons online. This series also does not discuss other Amazon offerings, such as the Elastic Compute Cloud (EC2), even though they are interesting and useful and could certainly help in putting together a Web site presence. Finally, there are other distributed key-value databases like CouchDB that compare favorably to SimpleDB; I highly recommend investigating them as well.

We'll thread comments by looking at the reply_to key. Every post will have a random string as the unique key.

Please notice that we've established some conventions here:

  • The lack of a user attribute means the comment is anonymous.
  • The photo URL unites all the comments, so changing the photo URL will not be allowed.
  • S3 objects will have a URL, too, with a bucket name to identify them as S3 objects.
  • Duplicate photo URLs will not be allowed, because the URL is the key of the object.

This is not the final version of the table design (remember, SimpleDB is very flexible), but this is enough to get started.

Wrapup

You've now seen the benefits and drawbacks of S3 and SimpleDB in some detail. Though not by any means complete, this discussion should help you decide if Amazon's S3 and SimpleDB are right for your project.

After providing a simple to-do list example in SimpleDB, you saw how to start designing the data layout of the user, photo, and comment tables. In Part 2, I set up the Web site (Apache with mod_perl) and the libraries to work with S3 and SimpleDB.


----


Cultured Perl: Perl and the Amazon cloud, Part 2

Securely upload data to S3 via HTML form

Teodor Zlatanov (tzz@lifelogs.com), Programmer, Gold Software Systems

Summary:  This five-part series walks you through building a simple photo-sharing Web site using Perl and Apache to access Amazon's Simple Storage Service (S3) and SimpleDB. In this installment, learn how to upload a file into S3 from a Web page through an HTML form to minimize the load on the server, while maintaining a tight security policy.

View more content in this series

Tags for this article:  amazoncloudperl

Date:  08 Apr 2009 
Level:  Intermediate 
PDF:  A4 and Letter (39KB | 10 pages)Get Adobe® Reader® 
Also available in:   Russian  Japanese  Portuguese  Spanish 

Activity:  21356 views 
Comments:   0 (View | Add comment - Sign in)

Average rating 2 stars based on 3 votes Average rating (3 votes)
Rate this article

You can upload a file into Amazon's Simple Storage Service (S3) from a Web page in several ways:

  • From the command line using the appropriate modules from CPAN
  • From the command line using the appropriate modules from Amazon
  • Directly from an HTML form

To get the most from this series

This series requires beginner-level knowledge of HTTP and HTML, as well as intermediate-level knowledge of JavaScript and Perl (inside an Apache mod_perl process). Some knowledge of relational databases, disk storage, and networking will be helpful. The series gets increasingly technical, so see theResources section if you need help with any of those topics.

This article shows you upload a file directly from a HTML form, thus minimizing the load on your server. A successful upload will go to a custom URL that we'll use in later parts of the series to set up the other pieces of the photo-sharing site we're building. We'll use share.lifelogs.com in the series as the domain name.

Uploading to S3

You can upload data into S3 through a form POST. (The PUT HTTP method and the SOAP PutObject call can also be used.) For this article, we'll use the formPOST, because it's simple and does not use server resources (disk, CPU, or bandwidth).

One big problem with uploads to S3 is that the metadata cannot be changed. That may be due to the distributed nature of S3 or to Amazon's wish to keep it simple. On the S3 forums, Amazon has indicated this may change in the future.

In any case, this means that the Content-Type must be set when the content is uploaded, or you'll get the rather unpleasant content type binary/octet-stream set. The other metadata is not too important because Amazon's SimpleDB will be used to track the uploads, and you can store your metadata there. We offer a mostly satisfactory JavaScript solution to the Content-Type problem.

The user name will be part of the successful URL. You could also put it in the metadata for the upload, so it's permanently associated with the file, but then a user can't change his or her name without making you redo all of their S3 uploads some way in order to store new metadata. You could keep user IDs that are independent of the user name and associate the user ID with the S3 object, but that gets needlessly complicated for our purpose—to build a simple photo-sharing site.

We (meaning the Web site operators) have at this point set up a bucket called images.share.lifelogs.com on S3. The bucket has the right access controls to allow public reading. If you have trouble doing this from the Amazon documentation, use a tool like S3Fox, JungleDisk, or any of the many other S3 interfaces to set the bucket up. If you're following along, you also have AWS access and secret keys.

You'll use a policy to control uploads; the policy is a set of rules expressed in the JSON data format. You will sign the policy with your secret Amazon key.

Signing the policy is done with the Digest::HMAC_SHA1 Perl module. You must first convert the policy to Base64, remove all newlines, sign the resulting data, Base64 encode the signature, then turn around three times, touch your toes, pull a hair and burn it, and finally send $1.28 in pennies to Ms. Elle Cowalsky and wait for her to mail you back the signature you should use. Just kidding! Burning the hair is optional. Try this instead:


Listing 1. Signing the upload policy
my $aws_secret_access_key = 'get it from 
 http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';

my $policy = 'entire policy here';
$policy = encode_base64($policy);
$policy =~ s/\n//g;

my $signature = encode_base64(hmac_sha1($policy, $aws_secret_access_key));

$signature =~ s/\n//g;

The S3 upload policy

You'll start with the policy before constructing the form; this means you will decide your security and usability goals before writing HTML, always a good thing.

The policy document is fairly simple:


Listing 2. Upload policy document
{
 "expiration": "3000-01-01T00:00:00Z",
  "conditions": [ 
    {"bucket": "images.share.lifelogs.com"}, 
    {"acl": "public-read"},
    ["starts-with", "$key", ""],
    {"success_action_redirect": "http://share.lifelogs.com/s3uploaded/$user"},
    ["content-length-range", 0, 1048576]
  ]
}

It's all very well explained in the S3 developer documentation (see Resources for a link). The bucket must be as named, the ACL of the bucket must match, the key can start with anything, and on success you'll go to a particular URL. The size of the uploaded document is limited between 0 bytes and 1 megabyte. Note the expiration date (more on that to follow).

The policy's contents will be signed and publicly viewable, so your worst enemy will find it hard to forge its contents. This attribute makes the policy one piece of your site's security—it ensures that uploads to S3 are allowed based on particular criteria and not allowed otherwise. Remember, you pay for the S3 usage, so this is important.

The expiration date is set to 3000 (yes, the year 3000). The point is that this policy, for all practical purposes, does not expire. Instead, you could set the expiration date to 10 minutes in the future, and you'll be assured that the policy can't be used by deleted users more than 10 minutes after their last legitimate access. But then you might get complaints from users who take more than 10 minutes to upload a file and get rejected. So think about what would be a good period to set the expiration date to instead of arbitrarily setting it to some value.

The policy must have conditions for all of the fields specified in the form. This prevents forgery and encourages thorough policy documents.

Now that we've established a policy, let's set up the upload form.

The S3 upload form

Remember we discussed the Content-Type metadata associated with S3 objects and how it has to be set before the object is uploaded? Unfortunately, this does not work well with image uploads because you don't know in advance what the user will upload. JPEG images and PNG images, for instance, have different content types (they are really called MIME types, and there are literally hundreds of common ones).

The solution is some intermediate-level JavaScript. Because you're putting everything inside one Perl script, you have to escape every \ and $character (thus the slightly confusing contents). Look at the generated HTML for the actual JavaScript; you will use the Template Toolkit later in this series to do it right, but the idea was to make the script self-contained and simple. It's straightforward, but unfortunately readability suffered a bit.


Listing 3. s3form.pl
#!/usr/bin/perl

use warnings;
use strict;
use Data::Dumper;
use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
use MIME::Base64;

my $aws_access_key_id     = 'get it from
 http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';
my $aws_secret_access_key = 'get it from 
 http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';

my $user = 'username'; # this is the user name for the upload

my $policy = '{"expiration": "3000-01-01T00:00:00Z",
  "conditions": [ 
    {"bucket": "images.share.lifelogs.com"}, 
    {"acl": "public-read"},
    ["starts-with", "$key", ""],
    ["starts-with", "$Content-Type", ""],
    {"success_action_redirect": "http://share.lifelogs.com/s3uploaded/$user"},
    ["content-length-range", 0, 1048576]
  ]
}';

$policy = encode_base64($policy);
$policy =~ s/\n//g;

my $signature = encode_base64(hmac_sha1($policy, $aws_secret_access_key));

$signature =~ s/\n//g;
print <<EOHIPPUS;
<html> 
  <head>
    <title>S3 POST Form</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
    <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js" type="text/javascript"></script>
  </head>

  <body> 
  <script language="JavaScript">
function submitUploaderForm()
{
 var form = \$('uploader'); // note the escapes we do from Perl
 var file = form['file'];
 var ct = form['Content-Type'];

 var filename = ''+\$F(file); // note the escapes we do from Perl
 var f = filename.toLowerCase(); // always compare against the lowercase version

 if (!navigator['mimeTypes'])
 {
  alert("Sorry, your browser can't tell us what type of file you're uploading.");
  return false;
 }

 var type = \$A(navigator.mimeTypes).detect(function(m)
 {
  // does any of the suffixes match?
  // note the escapes we do from Perl
  return m.type.length > 3 && m.type.match('/') &&
       \$A(m.suffixes.split(',')).detect(function(suffix)
  {
    return f.match('\\.' + suffix.toLowerCase() + '\$'); 
      // note the escapes we do from Perl
  });
 });

 if (type && type['type'])
 {
  ct.value = type.type;
  return true;
 }

 alert("Sorry, we don't know the type for file " + filename);
 return false;
}
</script>
    <form id="uploader" action="https://images.share.lifelogs.com.s3.amazonaws.com/"
     method="post" enctype="multipart/form-data"
     onSubmit="return submitUploaderForm();">
      <input type="hidden" name="key" value="\${filename}">
      <input type="hidden" name="AWSAccessKeyId" value="$aws_access_key_id"> 
      <input type="hidden" name="acl" value="public-read">
      <input type="hidden" name="success_action_redirect"
       value="http://share.lifelogs.com/s3uploaded/$user">
      <input type="hidden" name="policy" value="$policy">
      <input type="hidden" name="Content-Type" value="image/jpeg">
      <input type="hidden" name="signature" value="$signature">
      Select File to upload to S3: 
      <input name="file" type="file"> 
      <br> 
      <input type="submit" value="Upload File to S3"> 
    </form> 
  </body>
</html>
EOHIPPUS

Sorry to anyone offended by the inelegant JavaScript, but it should work in most modern browsers. Basically, we intercept the submit button and return true only if we know the type of the uploaded file.

Editor's note: You can download this script. The two lines in bold in Listing 3 shouldn't actually break this way, but our display width is limited. If you copy and paste the script from the article, restore each to one line. They are correctly laid out in the script you can download.

The upload form: JavaScript and reasoning

You load Prototype from the Google API site (see Resources). You can host it yourself if you're paranoid, obsessive, or a control freak ("just because you're paranoid doesn't mean they are not after you").

Grab the file name from the form using Prototype utilities, then look at the file name (in lower case). For each MIME type known to the browser, you use the Prototype detect() array method to find the first match to the following conditions:

  • The type must be longer than three characters.
  • It must contain a / character.
  • Any of the suffixes for the MIME type must match the file name.

The three-character check and the / character check are due to the fact that in Firefox, at least, there's a "*" MIME type that will match anything. Because that's not useful, you want to be able to skip it and (we hope!) any other MIME types that are useless for our purposes.

You iterate through the suffixes using the JavaScript split() string method, which produces an array of pieces. So if the suffixes are jpg,jpeg, then you'll iterate over those two split on the comma. Again, you use the Prototype detect() array method to find the first match to the file name among those suffixes. Compare the lower-case version of the suffix with the lower-case version of the file.

If you're confused, read up on Prototype and JavaScript in general. For now you can just assume this works for most common cases. It may break for older or uncommon Web browsers, and, of course, it won't work if the user has disabled JavaScript. That's life. We're just looking for something that works for the majority of visitors.

If the type detection fails, so do we. Although it'll show a message to the user, this could be done better. You could try some guesses, for example, and maybe just fall back to image/jpeg if you don't know the type. Refining this is up to you. The function returns false if this happens, which will abort the upload.

Note that with a successful upload, you are redirecting to a URL that contains the user name. Refer to the section "Uploading to S3" for the reasons why.

Finally, this script has Perl, JavaScript, and HTML mixed into one interesting bunch (imagine a sports car with sails and a rudder, playing the saxophone). Examples written for the express purpose of showing a particular technique should not be your guideline to style and architecture. I dearly hope you don't copy and paste the included script without at least thinking about breaking it into template pieces and refactoring it. Later in this series, I promise to show you how that will work in the context of a whole mod_perl Web site.

Wrapup

You've now seen how to set up an HTML upload form to upload files directly to S3. Perl, JavaScript, and HTML were employed. A single script using the Prototype JavaScript library, the Perl MIME::Base64 and Digest::HMAC_SHA1 modules, and inline JavaScript and HTML, is presented with some caveats. The script will upload a single file to S3 for a given user, redirecting to a particular successful URL on the share.lifelogs.com site in the end.

Part 3 will show how the successful URL will create a SimpleDB record for the uploaded file. You'll also discover how to create, edit, and delete comments as SimpleDB records on a photo for a particular user. Parts 4 and 5 will put it all together in a mod_perl Web site for you. Stay tuned.


----


Cultured Perl: Perl and the Amazon cloud, Part 3

Upload images and create, edit, and delete comments

Teodor Zlatanov, Programmer, Gold Software Systems

Summary:  This five-part series walks you through building a simple photo-sharing Web site using Perl and Apache to access Amazon's Simple Storage Service (S3) and SimpleDB. In this installment, follow your site's interaction with SimpleDB by learning how the URL creates a SimpleDB record for the uploaded file. Also learn how to create, edit, and delete comments as SimpleDB records on a photo for a particular user.

View more content in this series

Tags for this article:  amazoncloudperl

Date:  14 Jun 2009 
Level:  Intermediate 
PDF:  A4 and Letter (41KB | 11 pages)Get Adobe® Reader® 
Also available in:   Russian  Japanese  Portuguese  Spanish 

Activity:  23965 views 
Comments:   0 (View | Add comment - Sign in)

Average rating 2 stars based on 3 votes Average rating (3 votes)
Rate this article

It's been awhile since my last installment, so here's the story so far:

  • Part 1 explained the S3/SimpleDB architectures and how to use them through practical examples.
  • Part 2 showed how to upload a file into S3 from a Web page through an HTML form, minimizing the load on the server.

To get the most from this series

This series requires beginner-level knowledge of HTTP and HTML, as well as intermediate-level knowledge of JavaScript and Perl (inside an Apache mod_perl process). Some knowledge of relational databases, disk storage, and networking will be helpful. The series gets increasingly technical, so see theResources section if you need help with any of those topics.

Now we'll really start diving into the photo-sharing site's interaction with Amazon's SimpleDB by learning how the successful URL creates a SimpleDB record for the uploaded file and learning how to create, edit, and delete comments as SimpleDB records on a photo for a particular user. (Remember, we are not comparing SimpleDB to Google's BigTable or to standalone solutions like CouchDB.)

As in previous installments, I use share.lifelogs.com as the domain name.

But first, the database structure

Referring back to Part 1, we set up a table structure like the one shown in Listing 1:


Listing 1. The table structure from Part 1
share_photos:
"http://developer.amazonwebservices.com/connect/images/amazon/logo_aws.gif" 
{ user: "ted", name: "Amazon Logo"}

"http://images.share.lifelogs.com/funny.jpg" 
{ user: "bob", name: "Funny Picture",  s3bucket: "images.share.lifelogs.com" }

share_users:
"ted" { given: "Ted", family: "Zlatanov" }
"bob" { given: "Bob", family: "Leech" }

share_comments:
"random-string"
{ 
 url: "http://images.share.lifelogs.com/funny.jpg",
 comment: "Ha ha", 
 posted_when: "2009-03-01T19:00:00+05" 
}

"random-string2"
{ 
 user: "ted",
 url: "http://developer.amazonwebservices.com/connect/images/amazon/logo_aws.gif", 
 comment: "No it doesn't", 
 posted_when: "2009-03-01T20:00:01+05" 
}

"random-string3"
{ 
 url: "http://developer.amazonwebservices.com/connect/images/amazon/logo_aws.gif", 
 comment: "No it doesn't", 
 reply_to: "random-string2", 
 posted_when: "2009-03-01T20:00:01+05" 
}

In this version of the structure, you can store all the comments in a thread inside one comment structure, but that would make it dangerous to allow two users to delete or edit a comment in the same thread at the same time.

As I noted in Part 1, this structure is subject to change. One big advantage of SimpleDB is its flexibility, so let's use that. "A foolish consistency is the hobgoblin of little minds," as Emerson said. Of course, he also said "I hate quotations." Damn those waffling transcendentalists.

I found the SimpleDB scratchpad less useful than making the calls directly from code, but perhaps you'll like it. Look in the Resources section for the scratchpad URL. You can also purchase several SimpleDB management tools, use the free one for Firefox, or try out the typica shell. It's all in theResources section; go forth and manage.

Uploading and modifying images

First of all, note that SimpleDB allows multiple values for a single key. So the image URL I mentioned or the comment text could be arrays instead of single values. We won't use this feature, preferring instead to keep things simple with one value per key.

Let's start with the image update. Every time an upload is done, you'll run code to add the new image to the share_photos table. Part 2 showed you the S3 upload form; and next time we'll connect that form to the code written here.

For now, let's write a simple script to add an image. You're given a user name, a URL, an image name, and an optional S3 bucket name. The S3 bucket will be just a field in the table; the URL will be sufficient to display and use the image. We'd like to reject uploads with a duplicate URL. But can we?

The problem with a distributed data store is that the data you just put into it may not have made it out to the edge of the network. It's like calling Europe from Japan (or doing anything where you experience the latency of the network or voice signal): there's a slight delay after every sentence while the other side synchronizes with you. You can start talking without pause, but then the other side's answers will start overlapping with your words and you'll feel like a Real Manager In An Important Meeting. So while latency is good for a forceful discourse, it's not so nice if you want a civilized conversation.

Similarly, when you put data into the SimpleDB system, there's a pause while the data flows out to a data center. Now, this part has not been confirmed by Amazon, but I've been told it becomes part of galactic consciousness and improves life for slug-like beings in the Runu system (three parsecs off Betelgeuse, then hang a left and keep going until you see it). So you're doing a favor for slug-like beings in the Runu system every time you use SimpleDB. And you thought this article was not educational.

Anyhow, in parallel with improving galactic consciousness, your data goes live and then your queries will see it. But if you made queries before all this happened, not only will galactic consciousness not improve, you'll get old data. So it's not as simple as working with a classic database like DB2 in which your data goes live and there's ACID ensuring your transactions are committed when the database says they are. With SimpleDB and other "eventually consistent" databases, you just have to live with the uncertainty.

The point is, updating the images is not so simple. We'd like to reject uploads with a duplicate URL, but that's not always possible. Imagine Alice uploads http://horsey.com/wilbur.png and Bob also uploads http://horsey.com/wilbur.png at the same time. If Alice's upload goes in first and Bob doesn't see it, Bob's upload will overwrite it. So what do we do?

First of all, you might ask, what's the harm? Users are inconvenienced but it's not a huge deal. Also, it's not a terribly likely occurrence. Well, we want happy users and, if we are obsessive about quality, it will bother us to our death bed to release something so obviously broken.

Rather than carrying unhappiness to the grave, we'll modify our table design for images, as shown in Listing 2:


Listing 2. The modified table design
share_photos:

"random-string10"
{ url: "http://developer.amazonwebservices.com/connect/images/amazon/logo_aws.gif",
        user: "ted", name: "Amazon Logo"}

"random-string11"
{ url: "http://images.share.lifelogs.com/funny.jpg", user: "bob", name: "Funny Picture",
        s3bucket: "images.share.lifelogs.com" }

The random-strings you've seen so far will be UUIDs. It's not perfect, but at least our images won't collide if the URLs are the same. But wait...what about image comments? Easy; we'll just change the foreign key as shown in Listing 3:


Listing 3. Changing the foreign key
share_comments:

"random-string3"
{ 
 image_id: "random-string10", 
 comment: "No it doesn't", 
 reply_to: "random-string2", 
 posted_when: "2009-03-01T20:00:01+05" 
}

Now we have to be aware that there may be multiple entries in share_photos with the same URL, but otherwise the system seems to be okay.

Understand that rather than showing you the artificially ready final version of the tables, we're working through them together. This lets me showcase the flexibility of SimpleDB and also showcase agile development at its best: invent, test, refine, repeat. Rather than planning everything, we plan just enough to get us to the next stage, though:

  • We don't forget about the larger picture at any time.
  • Architectural decisions are not made or changed as lightly as task-specific decisions.

So an image upload is simple, right? Just add a new entry to SimpleDB with the given URL, image name, and user name. A S3 bucket is optional. This is done with a PutAttributes call.

Modifying an image is equally easy, but let's just allow changing the name for now. This is also done with PutAttributes.

Adding and modifying comments

Refer to the previous section for the share_comments table. Simple stuff, really: Adding a comment will require the comment text, image ID, and optionally the parent comment ID and a user name. Modifying a comment will allow changes only to the comment text for now.

The standalone script

I've included a standalone Perl script (simple_go.pl; you can get it from the Download section at the bottom of this article) to do the tasks I've listed (add and modify image, add and modify comment). It won't create the domains, so you'll need to create the SimpleDB domains share_photos.share.lifelogs.com and share_comments.share.lifelogs.com externally. This is trivial using any of the SimpleDB management tools. Note that the --domain switch will change share.lifelogs.com to something else for the full domain name (stored in $full_domain).

The script uses the CPAN Data::UUID module to generate new unique identifiers.

The script is pretty cavalier about handling errors, choosing to die() at every opportunity. This is lazy and despicable, and you should not do it unless you're writing articles and want to show the world the utter depravity of programmers that write articles.

You're welcome.

The last tasks are to submit a SELECT statement and to delete an item. I'll show you how to implement them here because they are simple and you'll need them later.

To list images, you call the script as shown in Listing 4:


Listing 4. Listing images
./simple_go.pl -l -i --ak=accesskey --sk=secretkey

A word of caution: make sure the machine you run this on is not used by others. They can look at the list of processes and see your Amazon secret key. Similarly, if you are in a shell that keeps history, your secret key will end up in your history file. A better approach is to pass a file name and get the password from that file, but in the interest of simplicity, I did not implement it.

To list comments:


Listing 5. Listing comments
./simple_go.pl -l -c --ak=accesskey --sk=secretkey

Pretty simple so far. Internally the script calls the $service->select() method, parses the results, and prints the data with show_list(). Everything is done with the assumption that a key has just one value (note we specify Replace=true in the put() method), so this is not a general-purpose SimpleDB script.

Why not a general-purpose script? We don't need it. Let's get a simple solution working now. If we need multiple values, we can adapt the script later or just write new code. This script is the playground for creating our Web site's database structures.

Don't be tempted to use this script in the real site either ("I'll just call system() and let the errors go to a log file"). Yes, it's a few hundred lines of code and it works, but every prototype must be discarded without regrets so a real program can be written. Let's not make an exception for this one even if we've grown somewhat attached to its one-space indents and haphazard (er, "creative") layout.

Back to the script. Creating a new image is simple (bucket is optional):


Listing 6. Creating a new image
./simple_go.pl -i --ak=accesskey --sk=secretkey -u ted --url="any url"
  --name="any name you like" --bucket=mybucket

Editing the image name (-l -i gave us an ID of 25EC17B8-0F6B-11DE-A1A1-944E07F9DEC1). This now creates an image with a unique UUID:


Listing 7. Creating a new image with a unique UUID
./simple_go.pl -i --ak=accesskey --sk=secretkey --name="new name"
  --id=25EC17B8-0F6B-11DE-A1A1-944E07F9DEC1

Similarly, creating a comment is easy (user and refcommentid are optional):


Listing 8. Creating a comment
./simple_go.pl -c --ak=accesskey --sk=secretkey -u ted --refimageid="any image ID"
  --text="the text" --refcommentid='any comment ID'

Again, the comment ID will be a unique UUID. Editing a comment's text is done like so (-l -c gave us an ID of 4BE2EA0A-0F6B-11DE-976B-A542FC6BD07C):


Listing 9. Creating a comment with a unique UUID
./simple_go.pl -c --ak=accesskey --sk=secretkey --text="the text"
  --id=4BE2EA0A-0F6B-11DE-976B-A542FC6BD07C

Finally, deleting images or comments works like so:


Listing 10. Deleting images and comments
./simple_go.pl --delete -i --ak=accesskey --sk=secretkey
  --id=25EC17B8-0F6B-11DE-A1A1-944E07F9DEC1
./simple_go.pl --delete -c --ak=accesskey --sk=secretkey
  --id=4BE2EA0A-0F6B-11DE-976B-A542FC6BD07C

Wrapup

This installment showed how images and comments are created, edited, and deleted in the SimpleDB database that backs the photo-sharing site we're building.

We established the schema (loosely) and implemented a tool to add, list, modify, and delete images and comments. We settled on UUIDs as the primary key for both images and comments to prevent the unlikely scenario of two users uploading the same image URL at the same time.

We also established that we would use only a single value per key since currently our schema does not require more and we're aiming for simplicity. This shortcoming may need to be addressed later in code, but for now we will live with it.

In Part 4, you see how to start putting it all together in a mod_perl Web site.


----


Cultured Perl: Perl and the Amazon cloud, Part 4

Dive into the full mod_perl site's code base

Teodor Zlatanov, Programmer, Gold Software Systems

Summary:  This five-part series walks you through building a simple photo-sharing Web site using Perl and Apache to access Amazon's Simple Storage Service (S3) and SimpleDB. In this installment, examine the full mod_perl site's code base, including how to configure the top level, what to do with the handlers, and how to set up external dependencies.

View more content in this series

Tags for this article:  amazoncloudperl

Date:  14 Jun 2009 
Level:  Intermediate 
PDF:  A4 and Letter (54KB | 15 pages)Get Adobe® Reader® 
Also available in:   Russian  Japanese  Portuguese  Spanish 

Activity:  25331 views 
Comments:   0 (View | Add comment - Sign in)

Average rating 1 star based on 3 votes Average rating (3 votes)
Rate this article

In this installment, prepare to witness a full mod_perl site (code only; templates are in the next part). Now our formerly relaxed pace (a saunter or a canter, if you like) will become a full gallop as we race on our mod_perl horse through the Plains Of Strained Metaphors.

To get the most from this series

This series requires beginner-level knowledge of HTTP and HTML, as well as intermediate-level knowledge of JavaScript and Perl (inside an Apache mod_perl process). Some knowledge of relational databases, disk storage, and networking will be helpful. The series gets increasingly technical, so see theResources section if you need help with any of those topics.

I strongly encourage you to read the source code. The site is functional, but many details are not fully explained in this series because I expect you either understand them or can learn what you didn't understand. Your local or remote bookstore and search engines are your friends.

In particular, setting up a full mod_perl site and using the Template Toolkit are broad topics that have been covered many times and so are not explained at all here. The best way to learn is to work through every question and obstacle until the site is running. This series presents you with the engine, wheels, body, etc.—it's up to you to get gas and get the car running.

As before, I use share.lifelogs.com in this series as the domain name. Remember to change it as needed for your own environment.

Top-level configuration

You need to have a working Apache server with mod_perl support (so set one up). Insert the following section in your Apache httpd.conf file, as shown in Listing 1:


Listing 1. Providing mod_perl support to Apache configuration file share.httpd.conf
<VirtualHost 1.2.3.4:80>
        ServerName      share.lifelogs.com
        DocumentRoot    /var/www/html
        ErrorLog        /var/log/apache/error-share.log
        PerlRequire     /home/tzz/mod_perl_require_share.pl
        <Location />
                SetHandler      perl-script
                PerlHandler     SharePerlHandler
        </Location>
        SetEnv AWS_KEY 'my-AWS-key'
        SetEnv AWS_SECRET_KEY 'my-secret-AWS-key'
</VirtualHost>

Everything lives under /home/tzz, as you can see.

Important things here:

  • There is a specific error log (so you can watch errors with this site in isolation).
  • You pass the Amazon developer keys in the process environment. This way, the Perl source code will not have them in case the source code leaks out somehow. (The Web server configuration is normally more secure than source code.)

Note that everything is handled through SharePerlHandler, every request on share.lifelogs.com! This is probably not what you want in a production environment.

The PerlRequire directive just set up an environment, doing nothing special. Again, everything is under /home/tzz.

Listing 2 shows the mod_perl_require_share Perl file.


Listing 2. The mod_perl_require_share.pl file
#!/usr/bin/perl -w

use strict;

use lib '/home/tzz';

use SharePerlHandler;

1;

The mod_perl handler

The mod_perl handler is entirely in the SharePerlHandler.pm file. It has several sections, broadly speaking: setup, main handler, comment and photo handlers, general utilities, and SimpleDB utilities.

The general and SimpleDB utilities could have lived in their own module, but for simplicity I left everything in one place. The comment and photo handlers and the SimpleDB utility functions are mostly from the simple_go.pl script (see Downloads) with some small changes.

Let's start with the setup. As I walk you through each section, I'll explain my decisions; more often than not you'll hear "simplicity" as the reason I did things in a particular way. Making a good Web site is hard, so take everything you find here as a rough template to be filtered through your needs and budget rather than a finished design you can pop into production. The fact that it works may, in fact, be a distraction, but I couldn't resist the temptation to get it working.

Setting up external dependencies

Listing 3 shows the general setup for the SharePerlHandler.pm file:


Listing 3. General setup for SharePerlHandler.pm
package SharePerlHandler;
use Apache::Constants qw(:common REDIRECT);
use strict;
use Carp qw/verbose cluck carp croak confess/;
use Data::Dumper;
use Apache::Request;
use Template;
use POSIX;
use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
use MIME::Base64;
use Data::UUID;				# generates unique IDs
use lib '/home/tzz/amazon-simpledb-2007-11-07-perl-library/src/';
use Amazon::SimpleDB::Client;

SharePerlHandler.pm depends on many modules. First of all, it uses the strict module, which is essential to good Perl programming. I wouldn't put something in production that didn't run under use strict. Also:

  • The Carp modules provide better errors.
  • Data::Dumper is for general debugging.
  • POSIX is for many functions I find I need frequently.
  • Digest::HMAC_SHA1 and MIME::Base64 are for the Amazon S3 upload policy.
  • The Template module is the Template Toolkit, which will let us put HTML pages with some dynamic content together quickly.
  • Data::UUID is for generating unique IDs.
  • Apache::Request and Apache::Constants are for mod_perl interaction with the Apache server.
  • Finally, Amazon::SimpleDB::Client is from Amazon and lets us interact with SimpleDB.

If you don't know how to install these modules from CPAN, it's done with cpan -e 'install MODULE' (if you don't know this, you're probably in way over your head right now...).

We don't use the Net::Amazon::S3 modules here, although we could have (in Part 2 I said we would). They were just not needed due to several architectural decisions in the interest of simplicity; more on this when I talk about uploads.

Listing 4 shows the global setup for SharePerlHandler.pm.


Listing 4. The global setup for SharePerlHandler.pm
use constant IMAGE_MODE   => 0;
use constant COMMENT_MODE => 1;

use constant VERBOSE => 1; # can also be done through the environment or some other way

my $template = Template->new( {
			       INCLUDE_PATH => '/home/tzz/templates/share',
			       RECURSION => 1,
			      }
			    );

my $uuid = Data::UUID->new();

You'll need constants to represent the image mode versus the comment mode for the basic SimpleDB operations, so define them here. The VERBOSEconstant can be replaced by any other method to control verbosity on your server. Remember, the more dynamic the control over verbosity, the more expensive it is (because the server needs to check it every time).

Next, you get yourself a brand new $templates object (which will load templates from /home/tzz/templates/share and will recurse). Finally, you get a UUID generator you can use everywhere.

The main handler

All right, this is the big one, the place where magic happens. PAY ATTENTION! (Awake? Good!) Take a thoughtful look at Listing 5.


Listing 5. More global setup
sub handler
{
 my $r = shift @_;
 my $q = Apache::Request->new($r,
                        POST_MAX => 4096,
                        DISABLE_UPLOADS => 1);

 my $user = (rand 2 > 1) ? 'bob' : 'ted'; 
                              # pick a user randomly between bob and ted
                              # (50% chance each)

 handle_photo($q);   
                              # always try to delete, add, or edit a URL
                              # if it's passed as a parameter
 handle_comment($q); 
                              # always try to delete, add, or edit a comment
                              # if it's passed as a parameter
 
 my $uri = $q->uri();
 my $tfile = $uri;
 $tfile =~ s,^/,,;            # remove the starting "/" in the name if it exists
 $tfile =~ s,/$,,;            # remove the ending "/" in the name if it exists
 $tfile =~ s,/,_,g;           # "/" in the file name becomes "_" so all the
                              # templates can be in one directory

 $tfile = 'index' unless length $tfile;	
                              # make the URI "index" if it's
                              # empty (e.g. someone hit the / URI)
 
 if ($tfile =~ m/\.html$/)
 {
  $tfile =~ s/html$/tmpl/;    # map ANYTHING.html to ANYTHING.tmpl
 }
 else
 {
  $tfile .= '.tmpl';          # map ANYTHING to ANYTHING.tmpl
 }

 my $policy = '';
 my $signature = '';

 if ($tfile eq 'upload.tmpl')
 {
  $template->process('policy.tmpl',
                 {
                  username => $user,
                 },
                 \$policy) || croak($template->error());

  my $key = $ENV{AWS_SECRET_KEY};
  $policy = encode_base64($policy);
  $policy =~ s/\n//g;
  $signature = encode_base64(hmac_sha1($policy, $key));
  $signature =~ s/\n//g;
 }
 
 $q->send_http_header('text/html; charset=utf-8');
 my $output = '';
 
 $template->process($tfile,
     {
      request   => $q,
      username  => $user,
      policy    => $policy,
      signature => $signature,
      env       => \%ENV,
      params    => \%{$q->param()},
      fimages   => sub { return list_simpledb(sprintf('SELECT * from `%s`',
                   simpledb_image_domain_name())) },
      fcomments => \&get_comments,
     },
     \$output) || croak($template->error());
	
 print $output;
 return OK;
}

Wow, that's a long function. It's almost too long; I would probably extract the pieces that can stand on their own ("refactor" it, as the cool kids say these days) if I had to add even a little more logic. It serves nicely to show how the main handler might look, though.

First the function gets the request object. Then it sets up a random user name ("bob" or "ted"); usually you'd have your own way to get this, perhaps through cookies, or you can let Apache handle authentication and authorization for you.

I said in Part 1 that I was going to have a users table in SimpleDB, but it complicated the site too much, so I dropped it. Looking up users in SimpleDB is not simple, because I needed to provide a way to sign up and to manage user information. It made the code too big, so please forgive this omission—it's definitely possible to keep the users table in SimpleDB.

Okay, next you'll want to handle any photo or comment parameters. For example, if you see the deletecommentid parameter, you try to delete that comment ID. We'll dig into the photo and comment parameter handlers later.

Next you have to manage the actual request. This is done with a simple-minded mapping that transforms "any/request/here.html" into "any_request_here.tmpl" and then requests that template. We make sure index.tmpl is served for the "/" request.

Any URIs that don't have a corresponding template will produce no data and in fact will throw an error. This is not a production technique, let's be clear, but it sets up a Web site in a few lines, so it's quite useful when the goal is brevity and simplicity in a demo.

Next, if the template file is going to be "upload.tmpl", you know you'll need to generate an upload policy for S3, and so you do that using the policy.tmpl file. The user name is passed to that template, which is very similar to the one in Part 2 of this series. Listing 6 shows you a policy template.


Listing 6. Sample code listing at maximum width
{"expiration": "3000-01-01T00:00:00Z",
  "conditions": [
    {"bucket": "images.share.lifelogs.com"}, 
    {"acl": "public-read"},
    ["starts-with", "$key", ""],
    ["starts-with", "$Content-Type", ""],
    ["starts-with", "$success_action_redirect",
     "http://share.lifelogs.com/s3uploaded?user=[% username %]"],
    ["content-length-range", 0, 1048576]
  ]
}

The major difference here is that instead of making the user name part of the success URL, you make it a parameter because it makes the image parameter handler much simpler. More on that to come.

Now you sign the policy and move on to sending the HTTP headers (good work there, Apache!). Next, generate the necessary output with quite a few parameters, as follows:

  • request, the Apache request
  • username, the random user name
  • policy, the S3 upload policy (could be blank).
  • signature, the S3 policy signature (could be blank)
  • env, the process environment (don't do this in production!)
  • params, the parameters, for example from a POST or a GET request
  • fimages, a function to return all images
  • fcomments, a function to return all comments, keyed by image ID

That's it for the general handler. All the other magic happens in the comment and image parameter handlers and in the templates themselves. Let's continue our top-down journey.

The image and comment handlers are called for every request. If they find parameters that look applicable, they will do an operation: add, modify, or delete an image or a comment. The templates, which we'll study after SharePerlHandler.pm, contain these parameters in POST forms.

But don't jump ahead, there's plenty here to keep you entertained—shoddy code, dodgy architecture, and the hope you'll find a bug worth posting on Twitter ("haha @tzlatanov sux teh worst check this out map used in void context and omg b0rken templates obv not a real h4ck3r mod_perl issolastmllnm").

The comment parameter handler

Listing 7 shows off the comment parameter handler.


Listing 7. The comment parameter handler
sub handle_comment
{
 my $q = shift @_;

 my $user           = $q->param('user');
 my $imageid   	  = $q->param('refimageid');
 my $comment   	  = $q->param('comment');
 my $refcommentid   = $q->param('refcommentid');
 my $commentid 	  = $q->param('commentid');
 my $deleteid 	  = $q->param('deletecommentid');

 my $result;
 
 if (defined $deleteid) # delete
 {
  $result = delete_simpledb($deleteid, COMMENT_MODE);
 }
 elsif (defined $commentid && defined $comment) # edit
 {
  my %q = (
         comment => $comment,
        );

  put_simpledb($commentid, COMMENT_MODE, %q);
  $result = get_simpledb($commentid, COMMENT_MODE);
 }
 elsif (defined $imageid && defined $comment) # new comment
 {
  my %q = (
         image_id => $imageid,
         comment => $comment,
        );

  $q{reply_to} = $refcommentid if defined $refcommentid;
  $q{user} = $user if defined $user;

  my $id = new_uuid();
  put_simpledb($id, COMMENT_MODE, %q);
  $result = get_simpledb($id, COMMENT_MODE);
 }

 $q->param()->{'result'} = $result;
}

The comment parameter handler has three possible modes, depending on the parameters passed to it. The modes are mutually exclusive. In every case, the result query parameter is set appropriately to indicate success if it's defined. Thus, the templates can later check if it's set and act accordingly. As the site grows, you would probably want to add other parameters such as last_operation or error_message.

If the deletecommentid parameter is set, the handler calls delete_simpledb with the appropriate values. This is the simplest, unconditional mode.

The next mode modifies a comment. It does not check that the comment exists already, so an incorrect ID here would create a new comment. Checking is easy but expensive (you have to make an extra call to SimpleDB, which is slow since it's a whole HTTP round trip plus the processing time on Amazon's side).

Note that because each comment has its own ID, editing is easy. Without individual comment records, you could have grouped comments as an image attribute (an array of strings, each one a comment), but then it would have been much harder to edit or delete an individual comment. Essentially, you would have had to implement your own record structure within a comment to represent an ID, a posting user, etc.

The editing mode is triggered by the presence of the commentid and comment query parameters, which state the edit target's UUID and the new contents of the comment, respectively. The result is the retrieval of the same UUID back from SimpleDB, so you could check here to see if the resulting comment field is the one you wanted (otherwise something went wrong). I don't check all this here in the interest of simplicity.

The final mode, creating a new comment, is triggered by the imageid and comment parameters. Optionally, it will take a reference UUID (when the comment is a reply to another one) and a user name (when the comment is not anonymous). Just like the editing mode, you just put_simpledb the attributes and then get them back without checking if the fields were modified correctly.

The image parameter handler

This handler is very similar to the comment handler, so if you slept through it all, go back and pay attention.

The image URL, if not passed, is constructed from the S3 key and bucket. This way you have a consistent interface to handle success redirects after S3 uploads. Listing 8 shows off the image parameter handler.


Listing 8. The image parameter handler
sub handle_photo
{
 my $q = shift @_;

 my $user     = $q->param('user');
 my $name     = $q->param('name');
 my $bucket   = $q->param('bucket');
 my $key      = $q->param('key');
 my $url      = $q->param('url');
 my $editid   = $q->param('imageid');
 my $deleteid = $q->param('deleteimageid');

 # set the URL from the S3 key and bucket if necessary
 if (!defined $url && defined $key && defined $bucket)
 {
  $url = sprintf("http://%s.s3.amazonaws.com/%s", $bucket, $key)
 }

 my $result;

 if (defined $deleteid) # delete
 {
  $result = delete_simpledb($deleteid, IMAGE_MODE);
 }
 elsif (defined $name && defined $editid) # editing an image name
 {
  my %q = (
         name => $name,
        );

  put_simpledb($editid, IMAGE_MODE, %q);
  $result = get_simpledb($editid, IMAGE_MODE);
 }
 elsif (defined $url && defined $name && defined $user) # adding a new one
 {
  my %q = (
         url => $url,
         name => $name,
         user => $user,
        );

  $q{bucket} = $bucket if defined $bucket;

  my $id = new_uuid();
  put_simpledb($id, IMAGE_MODE, %q);
  $result = get_simpledb($id, IMAGE_MODE);
 }

 $q->param()->{'result'} = $result;
}

Deleting the image is triggered by the deleteimageid parameter. Editing an image's name is done with the name and imageid parameters. Creating a new image is done with a URL, a user name, and an image name. The image bucket is optional and should only show up if this handler is called after a S3 upload success redirects to our site.

Remember from the policy, the S3 success redirect has the user name as part of the URL as a parameter. It will also have the key and bucket, so all you have to do is create an image from them.

Utility functions

Let's look at the truly miscellaneous functions.


Listing 9. Truly misc functions
sub qlog
{
 printf STDERR @_;
 print STDERR "\n";
}

sub new_uuid
{
 return $uuid->to_string($uuid->create());
}

sub simpledb_image_domain_name
{
 return simpledb_domain_name(IMAGE_MODE);
}

sub simpledb_comment_domain_name
{
 return simpledb_domain_name(COMMENT_MODE);
}

sub simpledb_domain_name
{
 return sprintf "%s.share.lifelogs.com",
  (shift == IMAGE_MODE) ? 'share_photos' : 'share_comments';
}

qlog is nice when you don't want to write printf STDERR every time. It also writes a line ending for you. Whether miscellaneous output should be going to the Apache error log is up to you. Also:

  • new_uuid is for generating a new UUID.
  • simpledb_domain_namesimpledb_image_domain_name, and simpledb_comment_domain_name use the mode parameter (which can be IMAGE_MODE or COMMENT_MODE) to provide a SimpleDB domain.

SimpleDB utility functions

The SimpleDB utility functions are almost exactly like the ones in Part 3 (simple_go.pl). Download them from the Downloads section, below. I'll list the changes:

  • get_comments is a new function to get all comments, keyed by image ID and then either the parent comment ID or the word noparent.
  • qlog is used instead of print, and the VERBOSE constant is used instead of $verbose.
  • The service is initialized for every request using the environment AWS_KEY and AWS_SECRET_KEY keys.
  • The mode, as $mode, is passed around instead of being global.
  • On errors, instead of using die(), you handle them as gracefully as possible.
  • The domain name is obtained with simpledb_domain_name instead of a global variable.
  • The functions were renamed because "get" and "put" are not good names in a namespace that does not serve a single purpose, as the old simple_go.pl script did.

I should mention again that this code will only do single values per attribute. If any attribute has an array of strings, only one of them will be returned. When attributes are written, only one value will remain even if there was an array of values before. The code is significantly simpler because of this, but it's also not suitable for general-purpose SimpleDB usage.

Wrapup

You've now taken a tour of the code for a full mod_perl site, using the bits we wrote in Parts 2 and 3 of this series (the download files for those parts are available below as well). The resulting site uses the Template Toolkit, S3, and SimpleDB to provide image uploads, browsing (threaded), editing and comment adding (anonymous or not), and deleting.

In Part 5, you'll get familiar with the template version of the site.


----


Cultured Perl: Perl and the Amazon cloud, Part 5

You've seen the code; now scan the full mod_perl site's templates

Teodor Zlatanov, Programmer, Gold Software Systems

Summary:  This five-part series walks you through building a simple photo-sharing Web site using Perl and Apache to access Amazon's Simple Storage Service (S3) and SimpleDB. In this installment, examine the full mod_perl site's templates, including one for indexing, three for uploading (general, S3 forms, and URL additions), one for image and comment browsing, and one to browse comments recursively for an image (or threading down).

View more content in this series

Tags for this article:  amazoncloudperl

Date:  23 Jun 2009 
Level:  Intermediate 
PDF:  A4 and Letter (43KB | 11 pages)Get Adobe® Reader® 
Also available in:   Russian  Japanese  Portuguese  Spanish 

Activity:  25269 views 
Comments:   0 (View | Add comment - Sign in)

Average rating 2 stars based on 5 votes Average rating (5 votes)
Rate this article

In this last installment, prepare yourself to witness a full mod_perl site (templates this time; the code base was in Part 4). Again, I strongly encourage you to read the source code. The site is functional, but many details are not fully explained in this series because I expect you either understand them or can learn what you didn't understand. Your local or remote bookstore and search engines are your friends.

To get the most from this series

This series requires beginner-level knowledge of HTTP and HTML, as well as intermediate-level knowledge of JavaScript and Perl (inside an Apache mod_perl process). Some knowledge of relational databases, disk storage, and networking will be helpful. The series is fairly technical, so see theResources section if you need help with any of those topics.

In particular, setting up a full mod_perl site and using the Template Toolkit are broad topics, so I won't explain it all in this series. The best way to learn is to work through every question and obstacle until the site is running. Remember, I've given you the engine, wheels, body, etc.—now you need to get gas and get the car running.

I use share.lifelogs.com in this series as the domain name. Remember to change it as needed for your own environment.

index.tmpl

We'll start with the templates from the top down (policy.tmpl was discussed in Part 4). For explanations of the Template Toolkit syntax, refer to theResources section. I'll try to explain the tricky bits.

The index.tmpl is a simple HTML page. Only thing to note here is that all the URIs are relative, so this and all the other templates will work with any domain.


Listing 1. The index.tmpl, simple HTML
<html>
  <head>
<title>Share Pictures</title>
</head>
<body>
<h1>Share Pictures</h1>

You can
<a href="/upload">upload or add images</a>
or
<a href="/browse">browse images and comments</a>.

<address>
Contact <a href="mailto:tzz@bu.edu">Ted Zlatanov</a> if you have lots
of money you're trying to get out of Nigeria.  The breath
is <strike>baited</strike>bated.
</address>
</body>
</html>

upload.tmpl

Now we're ready for the good stuff. JavaScript, HTML, and Template Toolkit language in one place. If this doesn't make a Web designer cry, I don't know what will.


Listing 2. The upload.tmpl, enough to make a Web designer shed tears of joy
<html> 
  <head>
    <title>Upload Page For [% username %]</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
    <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js" type="text/javascript"></script>
  </head>

  <body> 
  <script language="JavaScript">
function OnSubmitForm()
{
 var form = $('uploader');
 var file = form['file'];
 var ct   = form['Content-Type'];
 var name = form['name'].value;

 if (!name || name.length < 1)
 {
  alert("Sorry, you can't upload without a name.");
  return false;
 }

 var filename = ''+$F(file);
 var f = filename.toLowerCase(); // always compare against the lowercase version

 if (!navigator['mimeTypes'])
 {
  alert("Sorry, your browser can't tell us what type of file you're uploading.");
  return false;
 }

 var type = $A(navigator.mimeTypes).detect(function(m)
 {
  // does any of the suffixes match?
  return m.type.length > 3 && m.type.match('/') &&
   $A(m.suffixes.split(',')).detect(function(suffix)
  {
    return f.match('\.' + suffix.toLowerCase() + '$');
  });
 });

 if (!type || !type['type'])
 {
  type = { type : prompt("Enter your own MIME type, we couldn't find one through
                          the browser", "image/jpeg") };
 }

 if (type && type['type'])
 {
  ct.value = type.type;

  // fix up the redirect if we're about to submit
  var sar  = form['success_action_redirect'];

  sar.value = sar.value + escape(name);
  return true;
 }

 alert("Sorry, we don't know the type for file " + filename);
 return false;
}
</script>
<h1>Hi, [% username %]</h1>
    <form id="uploader" action="https://images.share.lifelogs.com.s3.amazonaws.com/"
          method="post" enctype="multipart/form-data" onSubmit="return OnSubmitForm();">
      <input type="hidden" name="key" value="${filename}">
      <input type="hidden" name="AWSAccessKeyId" value="[% env.AWS_KEY %]"> 
      <input type="hidden" name="acl" value="public-read">
      <input type="hidden" name="success_action_redirect"
             value="http://share.lifelogs.com/s3uploaded?user=[% username %]&name=">
      <input type="hidden" name="policy" value="[% policy %]">
      <input type="hidden" name="Content-Type" value="image/jpeg">
      <input type="hidden" name="signature" value="[% signature %]">
      Select File to upload to S3:
      <input name="file" type="file"> 
      <br>
      Enter a Name:
      <input name="name" type="text"> 
      <br> 
      <input type="submit" value="Upload File to S3"> 
    </form> 
    <form id="adder" action="/urluploaded" method="post" enctype="multipart/form-data">
      <input type="hidden" name="user" value="[% username %]">
      Enter a URL:
      <input name="url" type="text"> 
      <br>
      Enter a Name:
      <input name="name" type="text"> 
      <br>
      <input type="submit" value="Add URL"> 
    </form> 
  </body>
</html>

The upload page shows two upload dialogs. They both add an image, but the second one is much simpler. In the second one, the user fills in the URL and name for the image, and that gets POSTed to /urluploaded, which is just urluploaded.tmpl. The image parameter handler will be automatically invoked when that template is displayed. The user name is obtained from the server and is a form-hidden POST parameter.

The first form is the really complicated one. Fortunately, you can read Part 2 of this series, which explained all about S3 uploads, so you shouldn't be surprised by any of it.

The major changes in s3form.pl from Part 2 (and included again in the Downloads section):

  • success_action_redirect passes the user name and image name as parameters. The policy is adjusted to required only a portion of this string, up to the user name but not including the name.
  • The policy and signature are passed from the server.
  • The AWS access and secret keys are passed in the env hash from the server.
  • The OnSubmitForm function requires a name and adds it to the success_action_redirect form field as a parameter, escaped (note that the name is demanded before the MIME type is determined, but it's added to the URL only immediately before the form is POSTed).
  • The OnSubmitForm function fails gracefully if the MIME type can't be found, allowing the users to specify their own.

s3uploaded.tmpl

On a S3 upload, this template is used.


Listing 3. The s3uploaded.tmpl for successful and unsuccessful uploads
[% success = params.result %]
<html> 
  <head>
    <title>[% IF success %]Successful[% ELSE %]Unsuccessful[% END %]
              Upload Page For [% params.user %]</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
  </head>
  <body> 
    [% IF success %]Congratulations[% ELSE %]Sorry[% END %], [% params.user %].
       You have [% IF success %]successfully[% ELSE %]unsuccessfully[% END %]
       uploaded [% params.key %] to S3 bucket [% params.bucket %]
       named [% params.name %].<p>
      (etag is [% params.etag %] but I doubt you care.)
    <p>
[% IF success %]
      <a href="http://[% params.bucket %].s3.amazonaws.com/[% params.key %]">
   Your new upload is probably here.  Let's see if it displays already.
   <img src="http://[% params.bucket %].s3.amazonaws.com/[% params.key %]">
      </a>
[% END %]
    <p>
      You can now go back to <a href="/upload">uploading</a> or
      <a href="/">the main page</a>.
  </body>
</html>

Through some nasty Template Toolkit IF-ELSE constructs and the result parameter, the page handles successful and unsuccessful uploads. The success is regarding the SimpleDB addition; the image has always been uploaded to S3 if you get to this point. Backing out the image from S3 on a SimpleDB failure is left as an exercise for the reader (nyah, nyah).

urluploaded.tmpl

On a URL addition, this template is used.


Listing 4. The urluploaded.tmpl, the dream of code reuse
[% success = params.result %]
<html> 
  <head>
    <title>[% IF success %]Successful[% ELSE %]Unsuccessful[% END %]
              URL add Page For [% params.user %]</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
  </head>
  <body> 
    [% IF success %]Congratulations[% ELSE %]Sorry[% END %], [% params.user %].
       You have [% IF success %]successfully[% ELSE %]unsuccessfully[% END %]
       added [% params.url %] named [% params.name %].<p>
    <p>
[% IF success %]
      <a href="[% params.url %]">
   The URL you added is, perhaps, visible here.
   <img src="[% params.url %]">
      </a>
[% END %]
    <p>
      You can now go back to <a href="/upload">uploading</a>
      or <a href="/">the main page</a>.
  </body>
</html>

Besides the obviously bad HTML, this is similar to the s3uploaded.tmpl. Code reuse is obviously not an issue in this writer's dream world.

browse.tmpl

This template browses images and comments.


Listing 5. The browse.tmpl, images and comments in a single shot
[% SET images = fimages() %]
[% SET comments = fcomments() %]
<html> 
  <head>
    <title>Browse</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
  </head>
  <body> 
    <ul>
      [% FOR ik IN images.keys %]
      <li>
         [% SET image = images.$ik %]
         [% image.name %]<br>
         <img src="[% image.url %]"><br>
         [% IF image.bucket %](in S3)[% END %]<br>
         uploaded by [% image.user %]<br>
         <form action="/browse" method="post" enctype="multipart/form-data">
           <input type="hidden" name="deleteimageid" value="[% ik %]">
           <input type="submit" value="Delete"> 
         </form> 
         <form action="/browse" method="post" enctype="multipart/form-data">
           <input type="hidden" name="imageid" value="[% ik %]">
           Change Image Name:
           <input name="name" type="text" value="[% image.name|html %]"> 
           <input type="submit" value="Rename"> 
         </form>
         [% INCLUDE comments.tmpl ik=ik comments=comments %]
         <form action="/browse" method="post" enctype="multipart/form-data">
           <input type="hidden" name="user" value="[% username %]">
           <input type="hidden" name="refimageid" value="[% ik %]">
           Enter a Comment (as user [% username %]):
           <input name="comment" type="text"> 
           <input type="submit" value="Comment"> 
         </form> 
         <form action="/browse" method="post" enctype="multipart/form-data">
           <input type="hidden" name="refimageid" value="[% ik %]">
           Enter Anonymous Comment:
           <input name="comment" type="text"> 
           <input type="submit" value="Comment"> 
         </form> 
      </li>
      [% END %]
    </ul>
  </body>
</html>

With this, you get all the images and comments in one shot. This is not efficient and will almost certainly fail horribly in a real Web site once you get a few thousand images and comments on them. You need to set up pagination on the images using SimpleDB's nice NextToken paging scheme (which the SimpleDB utility functions presented here do not use efficiently) or something of your own, and then only get the comments for the images you're about to display.

You really want to avoid getting comments unless you need them. SimpleDB requests are expensive. This is why this template passed the comments down to the comments.tmpl template every time.

The template uses a Template Toolkit FOR loop to iterate over the images, unsorted (if you need sorting, it's better to have the Perl code do it outside the Template Toolkit environment). The image key is required here to select the comments that match it. For each image, you show the image name, URL, S3 status, owner. Then you show forms to delete the image, change its name, or post a comment anonymously or as a user. It's pretty straightforward.

Finally (well, in the middle, but we're not linear thinkers, right?) the comments.tmpl template is INCLUDEd with some parameters—ik is the image key and comments is the comment list—meaning that whatever that template generates will get dropped right in the middle of our image list for each image.

comments.tmpl

This template browses comments recursively for an image (threading down).


Listing 6. Threading down with the comments.tmpl
[% IF parent %]
 [% SET thread = comments.$ik.$parent %]
[% ELSE %]
 [% SET thread = comments.$ik.noparent %]
[% END %]

<ul>
[% FOR ck IN thread.keys %]
 [% SET comment = thread.$ck %]
  <li>[% comment.comment %] (by [% IF comment.user %][% comment.user %]
      [% ELSE %]Anonymous[% END %])<br>
    <form action="/browse" method="post" enctype="multipart/form-data">
      <input type="hidden" name="deletecommentid" value="[% ck %]">
      <input type="submit" value="Delete"> 
    </form> 
    <form action="/browse" method="post" enctype="multipart/form-data">
      <input type="hidden" name="commentid" value="[% ck %]">
      Edit Comment:
      <input name="comment" type="text" value="[% comment.comment|html %]"> 
      <input type="submit" value="Edit"> 
    </form>
    <form action="/browse" method="post" enctype="multipart/form-data">
      <input type="hidden" name="user" value="[% username %]">
      <input type="hidden" name="refimageid" value="[% ik %]">
      <input type="hidden" name="refcommentid" value="[% ck %]">
      Enter a Comment (as user [% username %]):
      <input name="comment" type="text"> 
      <input type="submit" value="Comment"> 
    </form> 
    <form action="/browse" method="post" enctype="multipart/form-data">
      <input type="hidden" name="refimageid" value="[% ik %]">
      <input type="hidden" name="refcommentid" value="[% ck %]">
      Enter Anonymous Comment:
      <input name="comment" type="text"> 
      <input type="submit" value="Comment"> 
    </form> 
    [% INCLUDE comments.tmpl ik=ik comments=comments parent=ck %]
  </li>
[% END %]
</ul>

I've saved the best, nastiest code for last.

Given an image key, this template finds all the comments for that image whose parent equals a particular comment key.

For each comment of interest (with the given parent or without a parent as the case may be), the template shows the comment itself followed by HTML forms to delete the comment, edit it, or enter a new comment (under a user name or anonymously). This is almost the same as the forms in the browse.tmpl except they include a refcommentid parameter.

Now, and this is the tricky or horrible part, depending on whether you merely study Computer Science or teach it, this template includes itself again for every comment it finds, setting parent to the value of the comment key each time. So you have recursion in a pseudo-language (Template Toolkit) to generate recursion in a layout language (HTML) running under Perl.

Wrapup

This five-part series is complete. You've now seen the templates for a full mod_perl site—using the pieces we wrote in Parts 2 and 3 and the code generated from Part 4. This site uses the Template Toolkit, S3, and SimpleDB to provide image uploads, browsing, editing, deleting, as well as comment adding (anonymous or not), browsing (threaded), and deleting.


Downloads

DescriptionNameSizeDownload method
SimpleDB utility functionssimpledb_utility.zip3KBHTTP
Sample script (from Part 2)s3form.zip2KBHTTP
Sample script (from Part 3)simple_go.zip4KBHTTP

Information about download methods


URL 

http://www.ibm.com/developerworks/linux/library/l-amazon-perl-1/index.html

http://www.ibm.com/developerworks/linux/library/l-amazon-perl-2/index.html

http://www.ibm.com/developerworks/linux/library/l-amazon-perl-3/index.html

http://www.ibm.com/developerworks/linux/library/l-amazon-perl-4/index.html

http://www.ibm.com/developerworks/linux/library/l-amazon-perl-5/index.html

List of Articles
번호 제목 글쓴이 날짜 조회 수
47 “개발자라면 즉시 설치!” 크롬 확장 프로그램 10가지 WindBoy 2015-03-23 104
46 Dell의 교육용 크롬북 11 “거칠게 뛰노는 아이들을 위한 튼튼한 노트북" WindBoy 2015-02-24 97
45 클라우드에서 동작하는 빅 데이터 애널리틱스 ‘조이엔트 만타’ WindBoy 2013-06-29 1549
44 무료 아마존 웹 서비스, 100% 알뜰하게 사용하는 방법 WindBoy 2013-04-14 2956
43 고해상도 크롬북의 사향 WindBoy 2013-02-24 2273
42 3년차 N드라이브, 네이버 촘촘히 엮었네 [1] WindBoy 2013-01-24 2889
41 클라우스 보안 WindBoy 2012-12-10 2357
40 뜬구름 같던 클라우드를 KT에서 현실화 하고 있네 WindBoy 2012-10-15 2357
39 오라클 “늦게 배운 클라우드, 자신 있다” WindBoy 2012-07-24 2478
38 국정원과 방통위의 황당한 클라우드 이야기 WindBoy 2012-07-04 2480
37 코앞에 다가온 웹하드 등록제, 알고 있나요 WindBoy 2012-05-02 2777
36 아마존 클라우드에 대한 오해 5가지 WindBoy 2012-04-25 2848
35 아마존이 말하는 클라우드의 5대 조건 WindBoy 2012-04-25 2976
34 간단하게 알아본 클라우드로 스마트한 문서관리, 하나도 어렵지 않아요~ WindBoy 2012-02-20 5261
33 최적의 클라우드 컴퓨팅 플랫폼 찾기 WindBoy 2012-01-06 2963
32 클라우드 컴퓨팅 – 비즈니스 전략에 큰 영향을 미치는 플랫폼 WindBoy 2012-01-06 3030
31 Amazon Web Services를 사용한 클라우드 컴퓨팅 Part 1~5 WindBoy 2012-01-06 3801
30 Amazon 클라우드에 Linux 애플리케이션 마이그레이션하기 WindBoy 2012-01-06 5305
29 Cultured Perl: Amazon S3의 스토리지 관리 WindBoy 2012-01-06 3939
» Cultured Perl: Perl and the Amazon cloud Part1~5 WindBoy 2012-01-06 6337