Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ lib-cov

# Coverage directory used by tools like istanbul
coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
Expand Down
27 changes: 27 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type StorageOptions } from '@google-cloud/storage';
interface OtherOptions {
bucketPrefix?: string;
directAccess?: boolean;
}
interface GCSAdapterOptions extends StorageOptions {
bucket?: string;
bucketPrefix?: string;
directAccess?: boolean;
}
type GCSAdapterArgs = [] | [GCSAdapterOptions] | [string, string | undefined, string | undefined, OtherOptions?];
declare class GCSAdapter {
private readonly _bucket;
private readonly _bucketPrefix;
private readonly _directAccess;
private _gcsClient;
constructor(...args: GCSAdapterArgs);
createFile(filename: string, data: Buffer | string, contentType?: string): Promise<void>;
deleteFile(filename: string): Promise<unknown>;
getFileData(filename: string): Promise<Buffer>;
getFileLocation(config: {
mount: string;
applicationId: string;
}, filename: string): string;
private filePath;
}
export default GCSAdapter;
279 changes: 164 additions & 115 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,67 @@
'use strict';
// GCSAdapter
// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage
const { Storage } = require('@google-cloud/storage');

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const storage_1 = require("@google-cloud/storage");
function requiredOrFromEnvironment(options, key, env) {
options[key] = options[key] || process.env[env];
if (!options[key]) {
throw `GCSAdapter requires an ${key}`;
}
return options;
options[key] = options[key] || process.env[env];
if (!options[key]) {
throw new Error(`GCSAdapter requires an ${key}`);
}
return options;
}

function fromEnvironmentOrDefault(options, key, env, defaultValue) {
options[key] = options[key] || process.env[env] || defaultValue;
return options;
function stringFromEnvironmentOrDefault(options, key, env, defaultValue) {
if (options[key] === undefined) {
options[key] = process.env[env] !== undefined ? process.env[env] : defaultValue;
}
return options;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
function booleanFromEnvironmentOrDefault(options, key, env, defaultValue) {
if (typeof options[key] !== 'boolean') {
options[key] = process.env[env] === 'true' || defaultValue;
}
return options;
}
function contentTypeOrDefault(contentType) {
const resolvedContentType = contentType || 'application/octet-stream';
if (/[\r\n]/.test(resolvedContentType)) {
throw new Error('GCSAdapter contentType cannot contain line breaks');
}
return resolvedContentType;
}
function validateFilePath(filePath) {
if (!filePath || filePath.startsWith('/') || filePath.includes('\\')) {
throw new Error('GCSAdapter filename must be a relative Google Cloud Storage object name');
}
if (filePath.split('/').some((part) => part === '.' || part === '..')) {
throw new Error('GCSAdapter filename cannot contain relative path segments');
}
return filePath;
}
function encodeGCSObjectName(filePath) {
return filePath.split('/').map(encodeURIComponent).join('/');
}

function optionsFromArguments(args) {
let options = {};
let projectIdOrOptions = args[0];
if (typeof projectIdOrOptions == 'string') {
options.projectId = projectIdOrOptions;
options.keyFilename = args[1];
options.bucket = args[2];
let otherOptions = args[3];
if (otherOptions) {
options.bucketPrefix = otherOptions.bucketPrefix;
options.directAccess = otherOptions.directAccess;
let options = {};
const projectIdOrOptions = args[0];
if (typeof projectIdOrOptions === 'string') {
options.projectId = projectIdOrOptions;
options.keyFilename = args[1];
options.bucket = args[2];
const otherOptions = args[3];
if (otherOptions) {
options.bucketPrefix = otherOptions.bucketPrefix;
options.directAccess = otherOptions.directAccess;
}
}
} else {
options = Object.assign({}, projectIdOrOptions);
}
options = fromEnvironmentOrDefault(options, 'projectId', 'GCP_PROJECT_ID', undefined);
options = fromEnvironmentOrDefault(options, 'keyFilename', 'GCP_KEYFILE_PATH', undefined);
options = requiredOrFromEnvironment(options, 'bucket', 'GCS_BUCKET');
options = fromEnvironmentOrDefault(options, 'bucketPrefix', 'GCS_BUCKET_PREFIX', '');
options = fromEnvironmentOrDefault(options, 'directAccess', 'GCS_DIRECT_ACCESS', false);
return options;
else {
options = { ...projectIdOrOptions };
}
options = stringFromEnvironmentOrDefault(options, 'projectId', 'GCP_PROJECT_ID');
options = stringFromEnvironmentOrDefault(options, 'keyFilename', 'GCP_KEYFILE_PATH');
options = requiredOrFromEnvironment(options, 'bucket', 'GCS_BUCKET');
options = stringFromEnvironmentOrDefault(options, 'bucketPrefix', 'GCS_BUCKET_PREFIX', '');
options = booleanFromEnvironmentOrDefault(options, 'directAccess', 'GCS_DIRECT_ACCESS', false);
return options;
}

/*
supported options

Expand All @@ -48,89 +71,115 @@ supported options
{ bucketPrefix / 'GCS_BUCKET_PREFIX' defaults to ''
directAccess / 'GCS_DIRECT_ACCESS' defaults to false
*/
function GCSAdapter() {
let options = optionsFromArguments(arguments);

this._bucket = options.bucket;
this._bucketPrefix = options.bucketPrefix;
this._directAccess = options.directAccess;

this._gcsClient = new Storage(options);
}

GCSAdapter.prototype.createFile = function (filename, data, contentType) {
let params = {
metadata: {
contentType: contentType || 'application/octet-stream'
class GCSAdapter {
constructor(...args) {
const options = optionsFromArguments(arguments);
this._bucket = options.bucket;
this._bucketPrefix = options.bucketPrefix;
this._directAccess = options.directAccess;
this._gcsClient = new storage_1.Storage(options);
}
};

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();
createFile(filename, data, contentType) {
let filePath;
let resolvedContentType;
try {
filePath = this.filePath(filename);
resolvedContentType = contentTypeOrDefault(contentType);
}
catch (err) {
return Promise.reject(err);
}
return new Promise((resolve, reject) => {
const file = this._gcsClient.bucket(this._bucket).file(filePath);
// gcloud supports upload(file) not upload(bytes), so we need to stream.
const uploadStream = file.createWriteStream({
metadata: {
contentType: resolvedContentType
}
});
uploadStream.on('error', (err) => {
reject(err);
}).on('finish', () => {
// Second call to set public read ACL after object is uploaded.
if (this._directAccess) {
file.makePublic((err) => {
if (err !== null) {
reject(err);
return;
}
resolve();
});
}
else {
resolve();
}
});
uploadStream.write(data);
uploadStream.end();
});
} else {
resolve();
}
});
uploadStream.write(data);
uploadStream.end();
});
}

GCSAdapter.prototype.deleteFile = function (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.
GCSAdapter.prototype.getFileData = function (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);
}
deleteFile(filename) {
let filePath;
try {
filePath = this.filePath(filename);
}
catch (err) {
return Promise.reject(err);
}
return new Promise((resolve, reject) => {
const file = this._gcsClient.bucket(this._bucket).file(filePath);
file.delete((err, response) => {
if (err !== null) {
reject(err);
return;
}
resolve(response);
});
});
} 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.
GCSAdapter.prototype.getFileLocation = function (config, filename) {
if (this._directAccess) {
return `https://storage.googleapis.com/${this._bucket}/${this._bucketPrefix + filename}`;
}
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
}
// 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(filename) {
let filePath;
try {
filePath = this.filePath(filename);
}
catch (err) {
return Promise.reject(err);
}
return new Promise((resolve, reject) => {
const file = this._gcsClient.bucket(this._bucket).file(filePath);
// Check for existence, since gcloud-node seemed to be caching the result
file.exists((err, exists) => {
if (exists) {
file.download((downloadError, data) => {
if (downloadError !== null) {
reject(downloadError);
return;
}
resolve(data);
});
}
else {
reject(err || new Error(`File ${filename} does not exist.`));
}
});
});
}
// 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) {
const filePath = this.filePath(filename);
if (this._directAccess) {
return `https://storage.googleapis.com/${this._bucket}/${encodeGCSObjectName(filePath)}`;
}
return `${config.mount}/files/${config.applicationId}/${encodeURIComponent(filename)}`;
}
filePath(filename) {
return validateFilePath(this._bucketPrefix + filename);
}
}

exports.default = GCSAdapter;
module.exports = GCSAdapter;
module.exports.default = GCSAdapter;
Loading