Merge pull request #708 from mcdonamp/mcdonald-gcs-adapter
Yet Another FileAdapter: Google Cloud Storage
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
|
||||
if (!adapter)
|
||||
if (!adapter)
|
||||
{
|
||||
if (!defaultAdapter) {
|
||||
return options;
|
||||
@@ -20,7 +19,7 @@ export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
if (adapter.default) {
|
||||
adapter = adapter.default;
|
||||
}
|
||||
|
||||
|
||||
return loadAdapter(adapter, undefined, options);
|
||||
} else if (adapter.module) {
|
||||
return loadAdapter(adapter.module, undefined, adapter.options);
|
||||
@@ -30,7 +29,7 @@ export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
return loadAdapter(adapter.adapter, undefined, adapter.options);
|
||||
}
|
||||
// return the adapter as provided
|
||||
return adapter;
|
||||
return adapter;
|
||||
}
|
||||
|
||||
export default loadAdapter;
|
||||
|
||||
125
src/Adapters/Files/GCSAdapter.js
Normal file
125
src/Adapters/Files/GCSAdapter.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// GCSAdapter
|
||||
// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage
|
||||
import { storage } from 'gcloud';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import requiredParameter from '../../requiredParameter';
|
||||
|
||||
function requiredOrFromEnvironment(env, name) {
|
||||
let environmentVariable = process.env[env];
|
||||
if (!environmentVariable) {
|
||||
requiredParameter(`GCSAdapter requires an ${name}`);
|
||||
}
|
||||
return environmentVariable;
|
||||
}
|
||||
|
||||
function fromEnvironmentOrDefault(env, defaultValue) {
|
||||
let environmentVariable = process.env[env];
|
||||
if (environmentVariable) {
|
||||
return environmentVariable;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export class GCSAdapter extends FilesAdapter {
|
||||
// GCS Project ID and the name of a corresponding Keyfile are required.
|
||||
// Unlike the S3 adapter, you must create a new Cloud Storage bucket, as this is not created automatically.
|
||||
// See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication
|
||||
// for more details.
|
||||
constructor(
|
||||
projectId = requiredOrFromEnvironment('GCP_PROJECT_ID', 'projectId'),
|
||||
keyFilename = requiredOrFromEnvironment('GCP_KEYFILE_PATH', 'keyfile path'),
|
||||
bucket = requiredOrFromEnvironment('GCS_BUCKET', 'bucket name'),
|
||||
{ bucketPrefix = fromEnvironmentOrDefault('GCS_BUCKET_PREFIX', ''),
|
||||
directAccess = fromEnvironmentOrDefault('GCS_DIRECT_ACCESS', false) } = {}) {
|
||||
super();
|
||||
|
||||
this._bucket = bucket;
|
||||
this._bucketPrefix = bucketPrefix;
|
||||
this._directAccess = directAccess;
|
||||
|
||||
let options = {
|
||||
projectId: projectId,
|
||||
keyFilename: keyFilename
|
||||
};
|
||||
|
||||
this._gcsClient = new storage(options);
|
||||
}
|
||||
|
||||
// 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, contentType) {
|
||||
let params = {
|
||||
contentType: contentType || 'application/octet-stream'
|
||||
};
|
||||
|
||||
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(params);
|
||||
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();
|
||||
});
|
||||
} else {
|
||||
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);
|
||||
// Check for existence, since gcloud-node seemed to be caching the result
|
||||
file.exists((err, exists) => {
|
||||
if (exists) {
|
||||
file.download((err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(data);
|
||||
});
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -2,7 +2,7 @@
|
||||
AdaptableController.js
|
||||
|
||||
AdaptableController is the base class for all controllers
|
||||
that support adapter,
|
||||
that support adapter,
|
||||
The super class takes care of creating the right instance for the adapter
|
||||
based on the parameters passed
|
||||
|
||||
@@ -28,30 +28,30 @@ export class AdaptableController {
|
||||
this.validateAdapter(adapter);
|
||||
this[_adapter] = adapter;
|
||||
}
|
||||
|
||||
|
||||
get adapter() {
|
||||
return this[_adapter];
|
||||
}
|
||||
|
||||
|
||||
get config() {
|
||||
return new Config(this.appId);
|
||||
}
|
||||
|
||||
|
||||
expectedAdapterType() {
|
||||
throw new Error("Subclasses should implement expectedAdapterType()");
|
||||
}
|
||||
|
||||
|
||||
validateAdapter(adapter) {
|
||||
if (!adapter) {
|
||||
throw new Error(this.constructor.name+" requires an adapter");
|
||||
}
|
||||
|
||||
|
||||
let Type = this.expectedAdapterType();
|
||||
// Allow skipping for testing
|
||||
if (!Type) {
|
||||
if (!Type) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Makes sure the prototype matches
|
||||
let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => {
|
||||
const adapterType = typeof adapter[key];
|
||||
@@ -64,7 +64,7 @@ export class AdaptableController {
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
|
||||
if (Object.keys(mismatches).length > 0) {
|
||||
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,11 @@ export class FilesController extends AdaptableController {
|
||||
}
|
||||
|
||||
createFile(config, filename, data, contentType) {
|
||||
|
||||
|
||||
let extname = path.extname(filename);
|
||||
|
||||
|
||||
const hasExtension = extname.length > 0;
|
||||
|
||||
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
filename = filename + '.' + mime.extension(contentType);
|
||||
} else if (hasExtension && !contentType) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
|
||||
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { HooksController } from './Controllers/HooksController';
|
||||
@@ -259,4 +260,5 @@ function addParseCloud() {
|
||||
module.exports = {
|
||||
ParseServer: ParseServer,
|
||||
S3Adapter: S3Adapter,
|
||||
GCSAdapter: GCSAdapter
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user