Skip to content

Commit

Permalink
S3 wrapper (#1)
Browse files Browse the repository at this point in the history
Implemented a wrapper for S3 requests using the existing AWS object.
Updated README to reflect new wrapper utilization.
  • Loading branch information
losthismind authored Mar 1, 2019
1 parent 3eabbe9 commit e82c93b
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 11 deletions.
47 changes: 41 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
16 changes: 11 additions & 5 deletions aws.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
172 changes: 172 additions & 0 deletions s3.js
Original file line number Diff line number Diff line change
@@ -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 <YYYY>-<MM>-<DD>T<HH>:<MM>:<SS>.<sss>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);
}
})();
89 changes: 89 additions & 0 deletions s3.test.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 12 additions & 0 deletions util.js
Original file line number Diff line number Diff line change
@@ -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);
}

0 comments on commit e82c93b

Please sign in to comment.