From 05c2246f7bf75fb454d0f2034ea7859840581163 Mon Sep 17 00:00:00 2001 From: Patrick Pelletier Date: Sun, 31 Jan 2016 22:27:33 -0800 Subject: [PATCH 1/2] Add support for saving files to AWS S3 --- FilesAdapter.js | 1 + GridStoreAdapter.js | 12 ++++++- S3Adapter.js | 79 +++++++++++++++++++++++++++++++++++++++++++++ files.js | 6 +--- package.json | 1 + 5 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 S3Adapter.js diff --git a/FilesAdapter.js b/FilesAdapter.js index 7b952ed031..427e20d9bb 100644 --- a/FilesAdapter.js +++ b/FilesAdapter.js @@ -5,6 +5,7 @@ // Adapter classes must implement the following functions: // * create(config, filename, data) // * get(config, filename) +// * location(config, req, filename) // // Default is GridStoreAdapter, which requires mongo // and for the API server to be using the ExportAdapter diff --git a/GridStoreAdapter.js b/GridStoreAdapter.js index 3168de066a..0d1e896578 100644 --- a/GridStoreAdapter.js +++ b/GridStoreAdapter.js @@ -4,6 +4,7 @@ // Requires the database adapter to be based on mongoclient var GridStore = require('mongodb').GridStore; +var path = require('path'); // For a given config object, filename, and data, store a file // Returns a promise @@ -32,7 +33,16 @@ function get(config, filename) { }); } +// Generates and returns the location of a file stored in GridStore for the +// given request and filename +function location(config, req, filename) { + return (req.protocol + '://' + req.get('host') + + path.dirname(req.originalUrl) + '/' + req.config.applicationId + + '/' + encodeURIComponent(filename)); +} + module.exports = { create: create, - get: get + get: get, + location: location }; diff --git a/S3Adapter.js b/S3Adapter.js new file mode 100644 index 0000000000..e4930e0391 --- /dev/null +++ b/S3Adapter.js @@ -0,0 +1,79 @@ +// S3Adapter +// +// Stores Parse files in AWS S3. + +var AWS = require('aws-sdk'); +var path = require('path'); + +var DEFAULT_REGION = "us-east-1"; +var DEFAULT_BUCKET = "parse-files"; + +// Creates an S3 session. +// Providing AWS access and secret keys is mandatory +// Region and bucket will use sane defaults if omitted +function S3Adapter(accessKey, secretKey, options) { + options = options || {}; + + this.region = options.region || DEFAULT_REGION; + this.bucket = options.bucket || DEFAULT_BUCKET; + this.bucketPrefix = options.bucketPrefix || ""; + this.directAccess = options.directAccess || false; + + s3Options = { + accessKeyId: accessKey, + secretAccessKey: secretKey, + params: {Bucket: this.bucket} + }; + AWS.config.region = this.region; + this.s3 = new AWS.S3(s3Options); +} + +// For a given config object, filename, and data, store a file in S3 +// Returns a promise containing the S3 object creation response +S3Adapter.prototype.create = function(config, filename, data) { + var self = this; + var params = { + Key: self.bucketPrefix + filename, + Body: data, + }; + if (self.directAccess) { + params.ACL = "public-read" + } + + return new Promise(function(resolve, reject) { + self.s3.upload(params, function(err, data) { + if (err !== null) return reject(err); + resolve(data); + }); + }); +} + +// Search for and return a file if found by filename +// Returns a promise that succeeds with the buffer result from S3 +S3Adapter.prototype.get = function(config, filename) { + var self = this; + var params = {Key: self.bucketPrefix + filename}; + + return new Promise(function(resolve, reject) { + self.s3.getObject(params, function(err, data) { + if (err !== null) return reject(err); + resolve(data.Body); + }); + }); +} + +// Generates and returns the location of a file stored in S3 for the given request and +// filename +// The location is the direct S3 link if the option is set, otherwise we serve +// the file through parse-server +S3Adapter.prototype.location = function(config, req, filename) { + if (this.directAccess) { + return ('https://' + this.bucket + '.s3.amazonaws.com' + '/' + + this.bucketPrefix + filename); + } + return (req.protocol + '://' + req.get('host') + + path.dirname(req.originalUrl) + '/' + req.config.applicationId + + '/' + encodeURIComponent(filename)); +} + +module.exports = S3Adapter; diff --git a/files.js b/files.js index 2c36e3417c..e2575a5d7e 100644 --- a/files.js +++ b/files.js @@ -7,7 +7,6 @@ var bodyParser = require('body-parser'), middlewares = require('./middlewares.js'), mime = require('mime'), Parse = require('parse/node').Parse, - path = require('path'), rack = require('hat').rack(); var router = express.Router(); @@ -44,10 +43,7 @@ var processCreate = function(req, res, next) { FilesAdapter.getAdapter().create(req.config, filename, req.body) .then(() => { res.status(201); - var location = (req.protocol + '://' + req.get('host') + - path.dirname(req.originalUrl) + '/' + - req.config.applicationId + '/' + - encodeURIComponent(filename)); + var location = FilesAdapter.getAdapter().location(req.config, req, filename); res.set('Location', location); res.json({ url: location, name: filename }); }).catch((error) => { diff --git a/package.json b/package.json index bed46504fb..c8dd3b94b1 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ }, "license": "BSD-3-Clause", "dependencies": { + "aws-sdk": "~2.2.33", "bcrypt-nodejs": "0.0.3", "body-parser": "~1.12.4", "deepcopy": "^0.5.0", From 2008c4dce90c1a4ca2fe425a6cbfe01579445315 Mon Sep 17 00:00:00 2001 From: Patrick Pelletier Date: Mon, 1 Feb 2016 10:30:01 -0800 Subject: [PATCH 2/2] Use ES6 => instead of self = this ack --- S3Adapter.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/S3Adapter.js b/S3Adapter.js index e4930e0391..aeea44efb6 100644 --- a/S3Adapter.js +++ b/S3Adapter.js @@ -31,17 +31,16 @@ function S3Adapter(accessKey, secretKey, options) { // For a given config object, filename, and data, store a file in S3 // Returns a promise containing the S3 object creation response S3Adapter.prototype.create = function(config, filename, data) { - var self = this; var params = { - Key: self.bucketPrefix + filename, + Key: this.bucketPrefix + filename, Body: data, }; - if (self.directAccess) { + if (this.directAccess) { params.ACL = "public-read" } - - return new Promise(function(resolve, reject) { - self.s3.upload(params, function(err, data) { + + return new Promise((resolve, reject) => { + this.s3.upload(params, function(err, data) { if (err !== null) return reject(err); resolve(data); }); @@ -51,11 +50,10 @@ S3Adapter.prototype.create = function(config, filename, data) { // Search for and return a file if found by filename // Returns a promise that succeeds with the buffer result from S3 S3Adapter.prototype.get = function(config, filename) { - var self = this; - var params = {Key: self.bucketPrefix + filename}; + var params = {Key: this.bucketPrefix + filename}; - return new Promise(function(resolve, reject) { - self.s3.getObject(params, function(err, data) { + return new Promise((resolve, reject) => { + this.s3.getObject(params, (err, data) => { if (err !== null) return reject(err); resolve(data.Body); });