2 S3Ajax v0.1 - An AJAX wrapper package for Amazon S3
4 http://decafbad.com/trac/wiki/S3Ajax
9 http://pajhome.org.uk/crypt/md5/sha1.js
12 // TODO: Figure out if Safari doesn't support PUT and DELETE
16 // Defeat caching with query params on GET requests?
19 // Default ACL to use when uploading keys.
20 DEFAULT_ACL: 'public-read',
22 // Default content-type to use in uploading keys.
23 DEFAULT_CONTENT_TYPE: 'text/plain',
26 DANGER WILL ROBINSON - Do NOT fill in your KEY_ID and SECRET_KEY
27 here. These should be supplied by client-side code, and not
28 stored in any server-side files. Failure to protect your S3
29 credentials will result in surly people doing nasty things
32 For example, scoop values up from un-submitted form fields like so:
34 S3Ajax.KEY_ID = $('key_id').value;
35 S3Ajax.SECRET_KEY = $('secret_key').value;
37 URL: 'http://s3.amazonaws.com',
41 // Flip this to true to potentially get lots of wonky logging.
45 Get contents of a key in a bucket.
47 get: function(bucket, key, cb, err_cb) {
48 return this.httpClient({
50 resource: '/' + bucket + '/' + key,
51 load: function(req, obj) {
52 if (cb) return cb(req, req.responseText);
54 error: function(req, obj) {
55 if (err_cb) return err_cb(req, obj);
56 if (cb) return cb(req, req.responseText);
62 Head the meta of a key in a bucket.
64 head: function(bucket, key, cb, err_cb) {
65 return this.httpClient({
67 resource: '/' + bucket + '/' + key,
68 load: function(req, obj) {
69 if (cb) return cb(req, req.responseText);
71 error: function(req, obj) {
72 if (err_cb) return err_cb(req, obj);
73 if (cb) return cb(req, req.responseText);
79 Put data into a key in a bucket.
81 put: function(bucket, key, content/*, [params], cb, [err_cb]*/) {
83 // Process variable arguments for optional params.
86 if (typeof arguments[idx] == 'object')
87 params = arguments[idx++];
88 var cb = arguments[idx++];
89 var err_cb = arguments[idx++];
91 if (!params.content_type)
92 params.content_type = this.DEFAULT_CONTENT_TYPE;
94 params.acl = this.DEFAULT_ACL;
96 return this.httpClient({
98 resource: '/' + bucket + '/' + key,
100 content_type: params.content_type,
103 load: function(req, obj) {
104 if (cb) return cb(req);
106 error: function(req, obj) {
107 if (err_cb) return err_cb(req, obj);
108 if (cb) return cb(req, obj);
114 List buckets belonging to the account.
116 listBuckets: function(cb, err_cb) {
117 return this.httpClient({
118 method:'GET', resource:'/',
119 force_lists: [ 'ListAllMyBucketsResult.Buckets.Bucket' ],
120 load: cb, error:err_cb
125 Create a new bucket for this account.
127 createBucket: function(bucket, cb, err_cb) {
128 return this.httpClient({
129 method:'PUT', resource:'/'+bucket, load:cb, error:err_cb
134 Delete an empty bucket.
136 deleteBucket: function(bucket, cb, err_cb) {
137 return this.httpClient({
138 method:'DELETE', resource:'/'+bucket, load:cb, error:err_cb
143 Given a bucket name and parameters, list keys in the bucket.
145 listKeys: function(bucket, params, cb, err_cb) {
146 return this.httpClient({
147 method:'GET', resource: '/'+bucket,
148 force_lists: [ 'ListBucketResult.Contents' ],
149 params:params, load:cb, error:err_cb
154 Delete a single key in a bucket.
156 deleteKey: function(bucket, key, cb, err_cb) {
157 return this.httpClient({
158 method:'DELETE', resource: '/'+bucket+'/'+key, load:cb, error:err_cb
163 Delete a list of keys in a bucket, with optional callbacks
164 for each deleted key and when list deletion is complete.
166 deleteKeys: function(bucket, list, one_cb, all_cb) {
169 // If the list is empty, then fire off the callback.
170 if (!list.length && all_cb) return all_cb();
172 // Fire off key deletion with a callback to delete the
173 // next part of list.
174 var key = list.shift();
175 this.deleteKey(bucket, key, function() {
176 if (one_cb) one_cb(key);
177 _this.deleteKeys(bucket, list, one_cb, all_cb);
182 Perform an authenticated S3 HTTP query.
184 httpClient: function(kwArgs) {
187 // If need to defeat cache, toss in a date param on GET.
188 if (this.DEFEAT_CACHE && ( kwArgs.method == "GET" || kwArgs.method == "HEAD" ) ) {
189 if (!kwArgs.params) kwArgs.params = {};
190 kwArgs.params["___"] = new Date().getTime();
193 // Prepare the query string and URL for this request.
194 var qs = (kwArgs.params) ? '?'+queryString(kwArgs.params) : '';
195 var url = this.URL + kwArgs.resource + qs;
198 // Handle Content-Type header
199 if (!kwArgs.content_type && kwArgs.method == 'PUT')
200 kwArgs.content_type = 'text/plain';
201 if (kwArgs.content_type)
202 hdrs['Content-Type'] = kwArgs.content_type;
204 kwArgs.content_type = '';
206 // Set the timestamp for this request.
207 var http_date = this.httpDate();
208 hdrs['Date'] = http_date;
210 var content_MD5 = '';
212 // TODO: Fix this Content-MD5 stuff.
213 if (kwArgs.content && kwArgs.content.hashMD5) {
214 content_MD5 = kwArgs.content.hashMD5();
215 hdrs['Content-MD5'] = content_MD5;
219 // Handle the ACL parameter
220 var acl_header_to_sign = '';
222 hdrs['x-amz-acl'] = kwArgs.acl;
223 acl_header_to_sign = "x-amz-acl:"+kwArgs.acl+"\n";
226 // Handle the metadata headers
227 var meta_to_sign = '';
229 for (var k in kwArgs.meta) {
230 hdrs['x-amz-meta-'+k] = kwArgs.meta[k];
231 meta_to_sign += "x-amz-meta-"+k+":"+kwArgs.meta[k]+"\n";
235 // Only perform authentication if non-anonymous and credentials available
236 if (kwArgs['anonymous'] != true && this.KEY_ID && this.SECRET_KEY) {
238 // Build the string to sign for authentication.
240 s = kwArgs.method + "\n";
241 s += content_MD5 + "\n";
242 s += kwArgs.content_type + "\n";
243 s += http_date + "\n";
244 s += acl_header_to_sign;
246 s += kwArgs.resource;
248 // Sign the string with our SECRET_KEY.
249 var signature = this.hmacSHA1(s, this.SECRET_KEY);
250 hdrs['Authorization'] = "AWS "+this.KEY_ID+":"+signature;
253 // Perform the HTTP request.
254 var req = getXMLHttpRequest();
255 req.open(kwArgs.method, url, true);
256 for (var k in hdrs) req.setRequestHeader(k, hdrs[k]);
257 req.onreadystatechange = function() {
258 if (req.readyState == 4) {
260 // Pre-digest the XML if needed.
262 if (req.responseXML && kwArgs.parseXML != false)
263 obj = _this.xmlToObj(req.responseXML, kwArgs.force_lists);
265 // Stash away the last request details, if DEBUG active.
267 window._lastreq = req;
268 window._lastobj = obj;
271 // Dispatch to appropriate handler callback
272 if ( (req.status >= 400 || (obj && obj.Error) ) && kwArgs.error)
273 return kwArgs.error(req, obj);
275 return kwArgs.load(req, obj);
279 req.send(kwArgs.content);
284 Turn a simple structure of nested XML elements into a
287 TODO: Handle attributes?
289 xmlToObj: function(parent, force_lists, path) {
292 var is_struct = false;
294 for(var i=0,node; node=parent.childNodes[i]; i++) {
295 if (3 == node.nodeType) {
296 cdata += node.nodeValue;
299 var name = node.nodeName;
300 var cpath = (path) ? path+'.'+name : name;
301 var val = arguments.callee(node, force_lists, cpath);
304 var do_force_list = false;
306 for (var j=0,item; item=force_lists[j]; j++) {
308 do_force_list=true; break;
312 obj[name] = (do_force_list) ? [ val ] : val;
313 } else if (obj[name].length) {
314 // This is a list of values to append this one to the end.
317 // Has been a single value up till now, so convert to list.
318 obj[name] = [ obj[name], val ];
323 // If any subnodes were found, return a struct - else return cdata.
324 return (is_struct) ? obj : cdata;
328 Abstract HMAC SHA1 signature calculation.
330 hmacSHA1: function(data, secret) {
331 // TODO: Alternate Dojo implementation?
332 return b64_hmac_sha1(secret, data)+'=';
336 Return a date formatted appropriately for HTTP Date header.
337 Inspired by: http://www.svendtofte.com/code/date_format/
339 TODO: Should some/all of this go into common.js?
341 httpDate: function(d) {
342 // Use now as default date/time.
343 if (!d) d = new Date();
345 // Date abbreviations.
346 var daysShort = ["Sun", "Mon", "Tue", "Wed",
347 "Thu", "Fri", "Sat"];
348 var monthsShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
349 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
351 // See: http://www.quirksmode.org/js/introdate.html#sol
352 function takeYear(theDate) {
353 var x = theDate.getYear();
355 y += (y < 38) ? 2000 : 1900;
359 // Number padding function
360 function zeropad(num, sz) {
361 return ( (sz - (""+num).length) > 0 ) ?
362 arguments.callee("0"+num, sz) : num;
366 // Difference to Greenwich time (GMT) in hours
367 var os = Math.abs(d.getTimezoneOffset());
368 var h = ""+Math.floor(os/60);
370 h.length == 1? h = "0"+h:1;
371 m.length == 1? m = "0"+m:1;
372 return d.getTimezoneOffset() < 0 ? "+"+h+m : "-"+h+m;
376 s = daysShort[d.getDay()] + ", ";
377 s += d.getDate() + " ";
378 s += monthsShort[d.getMonth()] + " ";
379 s += takeYear(d) + " ";
380 s += zeropad(d.getHours(), 2) + ":";
381 s += zeropad(d.getMinutes(), 2) + ":";
382 s += zeropad(d.getSeconds(), 2) + " ";
388 /* Help protect against errant end-commas */
393 if (!window['queryString']) {
394 // Swiped from MochiKit
395 function queryString(params) {
398 l.push(k+'='+encodeURIComponent(params[k]))
403 if (!window['getXMLHttpRequest']) {
404 // Shamelessly swiped from MochiKit/Async.js
405 function getXMLHttpRequest() {
406 var self = arguments.callee;
407 if (!self.XMLHttpRequest) {
409 function () { return new XMLHttpRequest(); },
410 function () { return new ActiveXObject('Msxml2.XMLHTTP'); },
411 function () { return new ActiveXObject('Microsoft.XMLHTTP'); },
412 function () { return new ActiveXObject('Msxml2.XMLHTTP.4.0'); },
413 function () { return null; }
415 for (var i = 0; i < tryThese.length; i++) {
416 var func = tryThese[i];
418 self.XMLHttpRequest = func;
425 return self.XMLHttpRequest();