diff --git a/README.md b/README.md index 8c05bff..5e368ec 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,39 @@ Google Apps Script wrapper for authenticated REST API requests to Amazon Web Services (AWS). -How to use: +## How To Use -1. Create a new project in Google Scripts -- https://script.google.com +### Paste Source into Project -2. Create a new script file and paste the contents of `aws.js` into it and save. +Add the contents of `aws.js` and `util.js` into new script files in your +project as these are required. Repeat for any additional service wrappers +(i.e. `s3.js`) as desired. -3. Create a second script for project code and configure AWS variable with -`AWS.init` +### Add as a Library -4. Use `AWS.request` with whichever AWS API request you need. Consult AWS +Add the our existing Google Apps Script project as a Library as outlined at +https://developers.google.com/apps-script/guides/libraries + +Project key **MUSi7a5HbvN3pxQdnYlZDjhqZNV8Hxu00** + +Versions of the Google Apps Script project map to tags on this Git repository. + +## Packages + +The AWS portion is the only required section. All other files represent shortcut +functions to simplify interactions with that service. + +Not all services have been wrapped, nor has every function within the service +API been implemented as a function. + +### AWS + +Generic wrapper for API requests to Amazon Web Services that should be +compatible with all AWS services as follows: + +1. Initialize AWS variable using `AWS.init` passing access and secret keys. + +2. Use `AWS.request` with whichever AWS API request you need. Consult AWS documentation to ensure headers and parameters are passed correctly. This function only sets up the `Host`, `X-Amz-Date`, `X-Amz-Target`, and `Authorization` headers by default. @@ -47,3 +70,15 @@ function myFunction() { ... } ``` + +### S3 + +Initialize the S3 variable using `S3.init` similar to how AWS variable is +described above. + +Functions implemented so far: + +* listAllMyBuckets() +* getObject(bucketName, objectName, region) +* putObject(bucketName, objectName, object, region) +* deleteObject(bucketName, objectName, region) diff --git a/aws.test.js b/aws.test.js index c38cbab..3ac5c01 100644 --- a/aws.test.js +++ b/aws.test.js @@ -19,11 +19,17 @@ function testAws() { function testAwsGetObjectExists() { AWS.init(access_key, secret_key); - var contents = AWS.request("s3", region, "GetObject", undefined, "GET", undefined, undefined, name_of_object_that_exists, { "Bucket": bucket_name }); - if (contents == "file") { - Logger.log("testAwsGetObjectExists - PASS"); - } else { - Logger.log("testAwsGetObjectExists - FAIL - contents [" + contents + "] did not match expected result"); + try { + var contents = AWS.request("s3", region, "GetObject", undefined, "GET", undefined, undefined, name_of_object_that_exists, { "Bucket": bucket_name }); + if (contents == "file") { + Logger.log("testAwsGetObjectExists - PASS"); + } else { + Logger.log("testAwsGetObjectExists - FAIL - contents [" + contents + "] did not match expected result"); + } + } catch(e) { + var message = e.toString(); + Logger.log("testAwsGetObjectExists - FAIL - unexpected message [" + message + "]"); + Logger.log(e); } } diff --git a/s3.js b/s3.js new file mode 100644 index 0000000..6c904da --- /dev/null +++ b/s3.js @@ -0,0 +1,172 @@ +/** + * Basic implementation of Amazon S3 REST API for Google Apps Script. + * Wraps the generic AWS request object to simplify calls to S3. + */ +var S3 = (function() { + return { + /** + * Initializes connection to AWS for upcoming S3 requests. Keys are not + * externally once added. + * + * @param {string} accessKey - AWS access key. + * @param {string} secretKey - AWS secret key. + * @throw {Object} Error on missing parameters. + */ + init: function S3(accessKey, secretKey) { + AWS.init(accessKey, secretKey); + }, + /** + * Container for information related to the list of buckets. + * @typedef {Object} BucketList + * @property {Object} owner - Information about the account owner. + * @property {string} owner.id - Bucket owner's canonical user ID. + * @property {string} owner.displayName - Bucket owner's display name. + * @property {Object[]} buckets - Collection of buckets. + * @property {string} buckets[].name - Name of bucket. + * @property {string} buckets[].creationDate - Date the bucket was created. + * Formatted as --
T::.Z + */ + /** + * Returns a list of all buckets owned by the authenticated sender of the + * request. + * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/API/RESTServiceGET.html} + * + * @throws {Object} AWS error on failure + * @return {BucketList} Object listing bucket information. + */ + listAllMyBuckets: function() { + // hardcoding Virginia region for service-level requests + var xml = execute("us-east-1", "ListAllMyBuckets", "GET"); + + // construct object with result contents and return + var result = { + owner: {}, + buckets: [] + }; + var doc = XmlService.parse(xml); + var root = doc.getRootElement(); + root.getChildren().forEach(function(child) { + var tag = child.getName(); + switch (tag) { + case "Owner": + child.getChildren().forEach(function(ownerElement) { + var key = ownerElement.getName(); + key = (key == "ID" ? "id" : lowerFirstCharacter(key)); + result.owner[key] = ownerElement.getText(); + }); + break; + case "Buckets": + child.getChildren().forEach(function(bucket) { + var tmp = {}; + bucket.getChildren().forEach(function(bucketElement) { + var key = lowerFirstCharacter(bucketElement.getName()); + tmp[key] = bucketElement.getText(); + }); + result.buckets.push(tmp); + }); + break; + default: + Logger.log("Unexpected tag [" + tag + "]"); + } + }); + return result; + }, + /** + * Retrieves an object from an S3 bucket. + * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html} + * + * @param {string} bucketName - Name of bucket to access. + * @param {string} objectName - Name of object to fetch (no leading slash). + * @param {string} region - Region of the bucket. + * @throws {Object} AWS error on failure + * @return {Blob|Object} Contents of the accessed object, converted from + * JSON or as a Blob if it was somthing else. + */ + getObject: function(bucketName, objectName, region) { + return execute(region, "GetObject", "GET", bucketName, objectName); + }, + /** + * Adds an object to an S3 bucket. + * See {@link https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html} + * + * @param {string} bucketName - Name of bucket to access. + * @param {string} objectName - Name of object to create (no leading slash). + * @param {string} object - Byte sequence to be the object's content. + * @param {string} region - Region of the bucket. + * @throws {Object} AWS error on failure + * @return void + */ + putObject: function(bucketName, objectName, object, region) { + // if object is not a blob, wrap it in one + var notBlob = !(typeof object.copyBlob == 'function' && + typeof object.getDataAsString == 'function' && + typeof object.getContentType == 'function' + ); + if (notBlob) { + object = Utilities.newBlob(JSON.stringify(object), "application/json"); + object.setName(objectName); + } + + var content = object.getDataAsString(); + var contentType = object.getContentType(); + var contentMD5 = getContentMD5(content); + + var headers = { + "Content-Type": contentType, + "Content-MD5": contentMD5 + }; + + return execute(region, "PutObject", "PUT", bucketName, objectName, content, headers); + }, + /** + * Deletes an object from an S3 bucket. + * @see {@link https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html} + * + * @param {string} bucketName - Name of bucket to access. + * @param {string} objectName - Name of object to delete (no leading slash). + * @param {string} region - Region of the bucket. + * @throws {Object} AWS error on failure + * @return void + */ + deleteObject: function(bucketName, objectName, region) { + return execute(region, "DeleteObject", "DELETE", bucketName, objectName); + } + }; + + /** + * Generates an MD5 hash of the contents or returns the empty string if no + * parameter value provided. + * + * @param {string} content - Content to be hashed. + * @return {string} MD5 hash of the provided parameter. + */ + function getContentMD5(content) { + if (content && content.length > 0) { + return Utilities.base64Encode(Utilities.computeDigest( + Utilities.DigestAlgorithm.MD5, content, Utilities.Charset.UTF_8)); + } else { + return ""; + } + } + + /** + * Triggers the AWS request based on the parameters provided, transforming + * values as needed. + * + * @param {string} region - Region of the bucket. + * @param {string} action - API action to call + * @param {string} method - HTTP method (e.g. GET, POST). + * @param {string} bucketName - Name of bucket to access. + * @param {string} objectName - Name of object to affect (no leading slash) + * @param {(string|object)} payload - Payload to send. Defults to ''. + * @param {Object} headers - Headers to attach to the request. Properties + * 'Host' and 'X-Amz-Date' are populated in this method. + * @throws {Object} AWS error on failure. + * @return {string} AWS server response to the request. + */ + function execute(region, action, method, bucketName, objectName, payload, headers) { + var uri = "/" + (objectName || ""); + var options = { "Bucket": bucketName }; + return AWS.request("s3", region, action, undefined, method, payload, headers, uri, options); + } +})(); diff --git a/s3.test.js b/s3.test.js new file mode 100644 index 0000000..ce2f3b9 --- /dev/null +++ b/s3.test.js @@ -0,0 +1,89 @@ +// Set the following to values that make sense for testing +var access_key = ""; +var secret_key = ""; +var bucket_name = ""; +var region = ""; +var name_of_object_that_exists = ""; // do NOT include leading slash +var name_of_object_that_does_not_exist = ""; // do NOT include leading slash +var name_of_file_create = ""; // do NOT include leading slash + +// Initialize connection: +// S3.init(access_key, secret_key); + +function testS3() { + testS3ListAllBuckets(); + testS3GetObjectExists(); + testS3GetObjectNotExist(); + testS3PutObject(); + testS3DeleteObject(); +} + +function testS3ListAllBuckets() { + try { + S3.init(access_key, secret_key); + var result = S3.listAllMyBuckets(); + Logger.log("testS3ListAllBuckets - Result:"); + Logger.log(result); + Logger.log("testS3ListAllBuckets - PASS"); + } catch(e) { + var message = e.toString(); + Logger.log("testS3ListAllBuckets - FAIL - unexpected message [" + message + "]"); + Logger.log(e); + } +} + +function testS3GetObjectExists() { + try { + S3.init(access_key, secret_key); + var contents = S3.getObject(bucket_name, name_of_object_that_exists, region); + if (contents == "file") { + Logger.log("testS3GetObjectExists - PASS"); + } else { + Logger.log("testS3GetObjectExists - FAIL - contents [" + contents + "] did not match expected result"); + } + } catch(e) { + var message = e.toString(); + Logger.log("testS3GetObjectExists - FAIL - unexpected message [" + message + "]"); + Logger.log(e); + } +} + +function testS3GetObjectNotExist() { + try { + S3.init(access_key, secret_key); + S3.getObject(bucket_name, name_of_object_that_does_not_exist, region); + Logger.log("testS3GetObjectNotExist - FAIL - exception should have been thrown"); + } catch(e) { + var message = e.toString(); + if (message == "AWS Error - NoSuchKey: The specified key does not exist.") { + Logger.log("testS3GetObjectNotExist - PASS"); + } else { + Logger.log("testS3GetObjectNotExist - FAIL - unexpected message [" + message + "]"); + Logger.log(e); + } + } +} + +function testS3PutObject() { + try { + S3.init(access_key, secret_key); + S3.putObject(bucket_name, name_of_file_create, { "data": "contents" }, region); + Logger.log("testS3PutObject - PASS"); + } catch(e) { + var message = e.toString(); + Logger.log("testS3PutObject - FAIL - unexpected message [" + message + "]"); + Logger.log(e); + } +} + +function testS3DeleteObject() { + try { + S3.init(access_key, secret_key); + S3.deleteObject(bucket_name, name_of_file_create, region); + Logger.log("testS3DeleteObject - PASS"); + } catch(e) { + var message = e.toString(); + Logger.log("testS3DeleteObject - FAIL - unexpected message [" + message + "]"); + Logger.log(e); + } +} diff --git a/util.js b/util.js new file mode 100644 index 0000000..291f4ac --- /dev/null +++ b/util.js @@ -0,0 +1,12 @@ +/** + * Lowercases the first character of the given string. If the argument is not + * a string, the argument is returned unmodified. + * + * @param {string} name - String to be modified. + * @return {string} New string with lowercased first letter. + */ +function lowerFirstCharacter(name) { + return (typeof name == "string" + ? name.charAt(0).toLowerCase() + name.slice(1) + : name); +}