Wednesday, October 26, 2011

Amazon S3 API


I had the simple task of writing the access to the “cloud storage”. It turns out that simple is not the correct description, plus the documentation is not the best there is.
The first task is easy – Create the bucket:
public bool AddBucket(string name)
{
try
{
Amazon.S3.Model.PutBucketRequest req = new Amazon.S3.Model.PutBucketRequest().WithBucketName(name);
client.PutBucket(req);
return true;
} catch {
return false;
}
}

Next task was the upload. At first I thought “why buzzer with the multi part upload, I need a max of 500MB”. After the first test, I had it. Multi part upload is safer for the large files (i.e. 10MB+) because it reduces the amounts of timeouts encountered.
The second issue, there’s no method to stream the bytes directly to the multi part upload.
Anyway I ended up with a send data function of this type:
public bool SendData(byte[] data, string key, string name)
{
string tmp_file = System.IO.Path.GetTempFileName();
bool res = false;
try
{
System.IO.File.WriteAllBytes(tmp_file, data);
if (data.Length < 5000000)
{
Amazon.S3.Model.PutObjectRequest req = new Amazon.S3.Model.PutObjectRequest().WithBucketName(name);
req.FilePath = tmp_file;
req.Key = key;
req.Timeout = int.MaxValue;
req.AddHeader("x-amz-acl", "public-read");
req.PutObjectProgressEvent += new EventHandler(req_PutObjectProgressEvent);
client.PutObject(req);
} else {
Amazon.S3.Transfer.TransferUtilityUploadRequest req = new Amazon.S3.Transfer.TransferUtilityUploadRequest().WithBucketName(name);
req.FilePath = tmp_file;
req.Key = key;
req.AddHeader("x-amz-acl", "public-read");
req.UploadProgressEvent += new EventHandler(req_UploadProgressEvent);
Amazon.S3.Transfer.TransferUtilityConfig config = new Amazon.S3.Transfer.TransferUtilityConfig();
config.MinSizeBeforePartUpload = 5000000;
config.DefaultTimeout = 1200000;
Amazon.S3.Transfer.TransferUtility utility = new Amazon.S3.Transfer.TransferUtility(client, config);
utility.Upload(req);
}
res = true;
}
catch (Amazon.S3.AmazonS3Exception amazonS3Exception)
{
if (amazonS3Exception.ErrorCode != null && amazonS3Exception.ErrorCode.Equals("NoSuchBucket"))
{
if (AddBucket(name))
{
res = SendData(data, key, name);
}
}
}
finally
{
System.IO.File.Delete(tmp_file);
}
return res;
}
Notes:
1.       I’m using 2 types of upload, each one raises a different event for progress (though they’ve got the same parameters inside).
2.       If the error is “bucket does not exist” a new one is created and the upload is re-initiated.
3.       The requests have a line “req.AddHeader("x-amz-acl", "public-read");”, this line sets the access to public. Without it you’ll receive a “403 – Forbidden” error message when accessing from the browser.
4.       I don’t set the ContentType property, if you want an automatic use of the file from the browser - set it.
5.       Incorrect “key” will result in an access denied error, the official document doesn’t say it, but there are limitations to the characters used (found it the hard way).
6.       Last, and probably the most important, in order to send the binary data I save it temporarily to a file.

2 more tasks remain – activating the web access, and doing the cleanup.
Activating the website:
public bool AddWebsiteConfig(string name)
{
try
{
Amazon.S3.Model.PutBucketWebsiteRequest putRequest = new Amazon.S3.Model.PutBucketWebsiteRequest().WithBucketName(name);
Amazon.S3.Model.WebsiteConfiguration config = new Amazon.S3.Model.WebsiteConfiguration();
config.IndexDocumentSuffix = "index.html";
putRequest.WithWebsiteConfiguration(config);
client.PutBucketWebsite(putRequest);
return true;
} catch {
return false;
}
}
The most straight forward method in this post. The only thing to pay attention to is the fact that “name” must be lower-case only, and the address is according to the region. Just as documented.

The last task is Delete. It should have been an easy one:
public bool DeleteBucket(string name)
{
try
{
Amazon.S3.Model.DeleteBucketRequest req = new Amazon.S3.Model.DeleteBucketRequest().WithBucketName(name);
client.DeleteBucket(req);
return true;
} catch {
return false;
}
}

As it turned out, the bucket must be cleared first. For the clearing a “Listing” procedure is needed.
The result is:
public bool DeleteBucket(string name)
{
try
{
Amazon.S3.Model.ListObjectsRequest list_req = new Amazon.S3.Model.ListObjectsRequest().WithBucketName(name);
do
{
using (Amazon.S3.Model.ListObjectsResponse response = client.ListObjects(list_req))
{
if (!response.IsTruncated)
{
list_req = null;
}
foreach (Amazon.S3.Model.S3Object entry in response.S3Objects)
{
Amazon.S3.Model.DeleteObjectRequest del_req = new Amazon.S3.Model.DeleteObjectRequest().WithBucketName(name);
del_req.Key = entry.Key;
client.DeleteObject(del_req);
}
}
} while (list_req != null);
Amazon.S3.Model.DeleteBucketRequest req = new Amazon.S3.Model.DeleteBucketRequest().WithBucketName(name);
client.DeleteBucket(req);
return true;
} catch {
return false;
}
Note: if you need a simple list put the following condition for fetching the next page:
if (response.IsTruncated)
{
list_req.Marker = response.NextMarker;
} else {
list_req = null;
}

I hope this post will solve some of the problems I had...