js/S3Ajax.js
author deusx
Sun Apr 23 03:52:09 2006 +0000 (4 years ago)
branchS3Ajax
changeset 11 7006d3de0ce0
parent 58af45ef3b03c
permissions -rw-r--r--
[svn r760] [wiki:S3Ajax]: Added an anonymous option to the wiki
     1 /**
     2     S3Ajax v0.1 - An AJAX wrapper package for Amazon S3
     3 
     4     http://decafbad.com/trac/wiki/S3Ajax
     5     l.m.orchard@pobox.com
     6     Share and Enjoy.
     7 
     8     Requires:
     9         http://pajhome.org.uk/crypt/md5/sha1.js
    10 */
    11 
    12 // TODO: Figure out if Safari doesn't support PUT and DELETE
    13 
    14 S3Ajax = {
    15 
    16     // Defeat caching with query params on GET requests?
    17     DEFEAT_CACHE: false,
    18 
    19     // Default ACL to use when uploading keys.
    20     DEFAULT_ACL: 'public-read',
    21     
    22     // Default content-type to use in uploading keys.
    23     DEFAULT_CONTENT_TYPE: 'text/plain',
    24 
    25     /**
    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
    30         on your tab.
    31 
    32         For example, scoop values up from un-submitted form fields like so:
    33 
    34         S3Ajax.KEY_ID     = $('key_id').value;
    35         S3Ajax.SECRET_KEY = $('secret_key').value;
    36     */
    37     URL:        'http://s3.amazonaws.com',
    38     KEY_ID:     '',
    39     SECRET_KEY: '',
    40 
    41     // Flip this to true to potentially get lots of wonky logging.
    42     DEBUG: false,
    43 
    44     /**
    45         Get contents of a key in a bucket.
    46     */
    47     get: function(bucket, key, cb, err_cb) {
    48         return this.httpClient({
    49             method:   'GET',
    50             resource: '/' + bucket + '/' + key,
    51             load: function(req, obj) {
    52                 if (cb)     return cb(req, req.responseText);
    53             },
    54             error: function(req, obj) {
    55                 if (err_cb) return err_cb(req, obj);
    56                 if (cb)     return cb(req, req.responseText);
    57             }
    58         })
    59     },
    60 
    61     /**
    62         Head the meta of a key in a bucket.
    63     */
    64     head: function(bucket, key, cb, err_cb) {
    65         return this.httpClient({
    66             method:   'HEAD',
    67             resource: '/' + bucket + '/' + key,
    68             load: function(req, obj) {
    69                 if (cb)     return cb(req, req.responseText);
    70             },
    71             error: function(req, obj) {
    72                 if (err_cb) return err_cb(req, obj);
    73                 if (cb)     return cb(req, req.responseText);
    74             }
    75         })
    76     },
    77 
    78     /**
    79         Put data into a key in a bucket.
    80     */
    81     put: function(bucket, key, content/*, [params], cb, [err_cb]*/) {
    82 
    83         // Process variable arguments for optional params.
    84         var idx = 3;
    85         var params = {};
    86         if (typeof arguments[idx] == 'object')
    87             params = arguments[idx++];
    88         var cb     = arguments[idx++];
    89         var err_cb = arguments[idx++];
    90 
    91         if (!params.content_type) 
    92             params.content_type = this.DEFAULT_CONTENT_TYPE;
    93         if (!params.acl)
    94             params.acl = this.DEFAULT_ACL;
    95 
    96         return this.httpClient({
    97             method:       'PUT',
    98             resource:     '/' + bucket + '/' + key,
    99             content:      content,
   100             content_type: params.content_type,
   101             meta:         params.meta,
   102             acl:          params.acl,
   103             load: function(req, obj) {
   104                 if (cb)     return cb(req);
   105             },
   106             error: function(req, obj) {
   107                 if (err_cb) return err_cb(req, obj);
   108                 if (cb)     return cb(req, obj);
   109             }
   110         });
   111     },
   112 
   113     /**
   114         List buckets belonging to the account.
   115     */
   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 
   121         });
   122     },
   123 
   124     /**
   125         Create a new bucket for this account.
   126     */
   127     createBucket: function(bucket, cb, err_cb) {
   128         return this.httpClient({ 
   129             method:'PUT', resource:'/'+bucket, load:cb, error:err_cb 
   130         });
   131     },
   132 
   133     /**
   134         Delete an empty bucket.
   135     */
   136     deleteBucket: function(bucket, cb, err_cb) {
   137         return this.httpClient({ 
   138             method:'DELETE', resource:'/'+bucket, load:cb, error:err_cb 
   139         });
   140     },
   141 
   142     /**
   143         Given a bucket name and parameters, list keys in the bucket.
   144     */
   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
   150         });
   151     },
   152 
   153     /**
   154         Delete a single key in a bucket.
   155     */
   156     deleteKey: function(bucket, key, cb, err_cb) {
   157         return this.httpClient({
   158             method:'DELETE', resource: '/'+bucket+'/'+key, load:cb, error:err_cb
   159         });
   160     },
   161 
   162     /**
   163         Delete a list of keys in a bucket, with optional callbacks
   164         for each deleted key and when list deletion is complete.
   165     */
   166     deleteKeys: function(bucket, list, one_cb, all_cb) {
   167         var _this = this;
   168         
   169         // If the list is empty, then fire off the callback.
   170         if (!list.length && all_cb) return all_cb();
   171 
   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);
   178         });
   179     },
   180 
   181     /**
   182         Perform an authenticated S3 HTTP query.
   183     */
   184     httpClient: function(kwArgs) {
   185         var _this = this;
   186         
   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();
   191         }
   192 
   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;
   196         var hdrs = {};
   197 
   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;
   203         else
   204             kwArgs.content_type = '';
   205 
   206         // Set the timestamp for this request.
   207         var http_date = this.httpDate();
   208         hdrs['Date']  = http_date;
   209 
   210         var content_MD5 = '';
   211         /*
   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;
   216         }
   217         */
   218 
   219         // Handle the ACL parameter
   220         var acl_header_to_sign = '';
   221         if (kwArgs.acl) {
   222             hdrs['x-amz-acl'] = kwArgs.acl;
   223             acl_header_to_sign = "x-amz-acl:"+kwArgs.acl+"\n";
   224         }
   225         
   226         // Handle the metadata headers
   227         var meta_to_sign = '';
   228         if (kwArgs.meta) {
   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";
   232             }
   233         }
   234 
   235         // Only perform authentication if non-anonymous and credentials available
   236         if (kwArgs['anonymous'] != true && this.KEY_ID && this.SECRET_KEY) {
   237 
   238             // Build the string to sign for authentication.
   239             var s; 
   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;
   245             s += meta_to_sign;
   246             s += kwArgs.resource;
   247 
   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;
   251         }
   252 
   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) {
   259 
   260                 // Pre-digest the XML if needed.
   261                 var obj = null;
   262                 if (req.responseXML && kwArgs.parseXML != false)
   263                     obj = _this.xmlToObj(req.responseXML, kwArgs.force_lists);
   264 
   265                 // Stash away the last request details, if DEBUG active.
   266                 if (_this.DEBUG) {
   267                     window._lastreq = req;
   268                     window._lastobj = obj;
   269                 }
   270                 
   271                 // Dispatch to appropriate handler callback
   272                 if ( (req.status >= 400 || (obj && obj.Error) ) && kwArgs.error)
   273                     return kwArgs.error(req, obj);
   274                 else
   275                     return kwArgs.load(req, obj);
   276 
   277             }
   278         }
   279         req.send(kwArgs.content);
   280         return req;
   281     },
   282 
   283     /**
   284         Turn a simple structure of nested XML elements into a 
   285         JavaScript object.
   286 
   287         TODO: Handle attributes?
   288     */
   289     xmlToObj: function(parent, force_lists, path) {
   290         var obj = {};
   291         var cdata = '';
   292         var is_struct = false;
   293 
   294         for(var i=0,node; node=parent.childNodes[i]; i++) {
   295             if (3 == node.nodeType) { 
   296                 cdata += node.nodeValue;
   297             } else {
   298                 is_struct = true;
   299                 var name  = node.nodeName;
   300                 var cpath = (path) ? path+'.'+name : name;
   301                 var val   = arguments.callee(node, force_lists, cpath);
   302 
   303                 if (!obj[name]) {
   304                     var do_force_list = false;
   305                     if (force_lists) {
   306                         for (var j=0,item; item=force_lists[j]; j++) {
   307                             if (item == cpath) {
   308                                 do_force_list=true; break;
   309                             }
   310                         }
   311                     }
   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.
   315                     obj[name].push(val);
   316                 } else {
   317                     // Has been a single value up till now, so convert to list.
   318                     obj[name] = [ obj[name], val ];
   319                 }
   320             }
   321         }
   322 
   323         // If any subnodes were found, return a struct - else return cdata.
   324         return (is_struct) ? obj : cdata;
   325     },
   326 
   327     /**
   328         Abstract HMAC SHA1 signature calculation.
   329     */
   330     hmacSHA1: function(data, secret) {
   331         // TODO: Alternate Dojo implementation?
   332         return b64_hmac_sha1(secret, data)+'=';
   333     },
   334     
   335     /**
   336         Return a date formatted appropriately for HTTP Date header.
   337         Inspired by: http://www.svendtofte.com/code/date_format/
   338 
   339         TODO: Should some/all of this go into common.js?
   340     */
   341     httpDate: function(d) {
   342         // Use now as default date/time.
   343         if (!d) d = new Date();
   344 
   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"];
   350 
   351         // See: http://www.quirksmode.org/js/introdate.html#sol
   352         function takeYear(theDate) {
   353             var x = theDate.getYear();
   354             var y = x % 100;
   355             y += (y < 38) ? 2000 : 1900;
   356             return y;
   357         };
   358 
   359         // Number padding function
   360         function zeropad(num, sz) { 
   361             return ( (sz - (""+num).length) > 0 ) ? 
   362                 arguments.callee("0"+num, sz) : num; 
   363         };
   364         
   365         function gmtTZ(d) {
   366             // Difference to Greenwich time (GMT) in hours
   367             var os = Math.abs(d.getTimezoneOffset());
   368             var h = ""+Math.floor(os/60);
   369             var m = ""+(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;
   373         };
   374 
   375         var s;
   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) + " ";
   383         s += gmtTZ(d);
   384 
   385         return s;
   386     },
   387 
   388     /* Help protect against errant end-commas */
   389     EOF: null
   390 
   391 };
   392 
   393 if (!window['queryString']) {
   394     // Swiped from MochiKit
   395     function queryString(params) {
   396         var l = [];
   397         for (k in params) 
   398             l.push(k+'='+encodeURIComponent(params[k]))
   399         return l.join("&");
   400     }
   401 }
   402 
   403 if (!window['getXMLHttpRequest']) {
   404     // Shamelessly swiped from MochiKit/Async.js
   405     function getXMLHttpRequest() {
   406         var self = arguments.callee;
   407         if (!self.XMLHttpRequest) {
   408             var tryThese = [
   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; }
   414             ];
   415             for (var i = 0; i < tryThese.length; i++) {
   416                 var func = tryThese[i];
   417                 try {
   418                     self.XMLHttpRequest = func;
   419                     return func();
   420                 } catch (e) {
   421                     // pass
   422                 }
   423             }
   424         }
   425         return self.XMLHttpRequest();
   426     }
   427 }
   428