From 2520e7b6c620242deff651b0df50b9c2fbf610e3 Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Sat, 27 Feb 2016 16:33:34 -0800 Subject: [PATCH] Initial commit of Google Cloud Storage File Adapter --- package.json | 1 + src/Adapters/Files/GCSAdapter.js | 96 ++++++++++++++++++++++++++++++++ src/index.js | 14 +++-- 3 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 src/Adapters/Files/GCSAdapter.js diff --git a/package.json b/package.json index 9837376d..22b11e1c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "commander": "^2.9.0", "deepcopy": "^0.6.1", "express": "^4.13.4", + "gcloud": "^0.28.0", "mime": "^1.3.4", "mongodb": "~2.1.0", "multer": "^1.1.0", diff --git a/src/Adapters/Files/GCSAdapter.js b/src/Adapters/Files/GCSAdapter.js new file mode 100644 index 00000000..a5ee4096 --- /dev/null +++ b/src/Adapters/Files/GCSAdapter.js @@ -0,0 +1,96 @@ +// GCSAdapter +// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage +import * as gcloud from 'gcloud'; +import { FilesAdapter } from './FilesAdapter'; + +export class GCSAdapter extends FilesAdapter { + // GCS Project ID and the name of a corresponding Keyfile are required. + // See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication + // for more details. + constructor( + projectId, + keyFilename, + bucket, + { bucketPrefix = '', + directAccess = false } = {} + ) { + super(); + + this._bucket = bucket; + this._bucketPrefix = bucketPrefix; + this._directAccess = directAccess; + + let gcsOptions = { + projectId: projectId, + keyFilename: keyFilename + }; + + this._gcsClient = new gcloud.storage(gcsOptions); + } + + // For a given config object, filename, and data, store a file in GCS. + // Resolves the promise or fails with an error. + createFile(config, filename, data) { + return new Promise((resolve, reject) => { + let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); + // gcloud supports upload(file) not upload(bytes), so we need to stream. + var uploadStream = file.createWriteStream(options); + uploadStream.on('error', (err) => { + return reject(err); + }).on('finish', () => { + // Second call to set public read ACL after object is uploaded. + if (this._directAccess) { + file.makePublic((err, res) => { + if (err !== null) { + return reject(err); + } + resolve(); + }); + } + resolve(); + }); + uploadStream.write(data); + uploadStream.end(); + }); + } + + // Deletes a file with the given file name. + // Returns a promise that succeeds with the delete response, or fails with an error. + deleteFile(config, filename) { + return new Promise((resolve, reject) => { + let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); + file.delete((err, res) => { + if(err !== null) { + return reject(err); + } + resolve(res); + }); + }); + } + + // Search for and return a file if found by filename. + // Returns a promise that succeeds with the buffer result from GCS, or fails with an error. + getFileData(config, filename) { + return new Promise((resolve, reject) => { + let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); + file.download((err, data) => { + if (err !== null) { + return reject(err); + } + resolve(data); + }); + }); + } + + // Generates and returns the location of a file stored in GCS for the given request and filename. + // The location is the direct GCS link if the option is set, + // otherwise we serve the file through parse-server. + getFileLocation(config, filename) { + if (this._directAccess) { + return `https://${this._bucket}.storage.googleapis.com/${this._bucketPrefix + filename}`; + } + return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename)); + } +} + +export default GCSAdapter; diff --git a/src/index.js b/src/index.js index ad715902..6112a3ea 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ import cache from './cache'; import PromiseRouter from './PromiseRouter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { S3Adapter } from './Adapters/Files/S3Adapter'; +import { GCSAdapter } from './Adapters/Files/GCSAdapter'; import { FilesController } from './Controllers/FilesController'; import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; @@ -91,11 +92,11 @@ function ParseServer({ serverURL = requiredParameter('You must provide a serverURL!'), maxUploadSize = '20mb' }) { - + // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; - + if (databaseAdapter) { DatabaseAdapter.setAdapter(databaseAdapter); } @@ -103,7 +104,7 @@ function ParseServer({ if (databaseURI) { DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); } - + if (cloud) { addParseCloud(); if (typeof cloud === 'function') { @@ -125,7 +126,7 @@ function ParseServer({ const pushController = new PushController(pushControllerAdapter); const loggerController = new LoggerController(loggerControllerAdapter); const hooksController = new HooksController(appId, collectionPrefix); - + cache.apps[appId] = { masterKey: masterKey, collectionPrefix: collectionPrefix, @@ -185,7 +186,7 @@ function ParseServer({ if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { routers.push(require('./global_config')); } - + if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { routers.push(new HooksRouter()); } @@ -229,5 +230,6 @@ function getClassName(parseClass) { module.exports = { ParseServer: ParseServer, - S3Adapter: S3Adapter + S3Adapter: S3Adapter, + GCSAdapter: GCSAdapter };