diff --git a/package.json b/package.json index 703fb401ed..3676b2ee87 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "apn": "^1.7.5", "aws-sdk": "~2.2.33", "babel-polyfill": "^6.5.0", + "azure-storage": "^0.8.0", "babel-runtime": "^6.5.0", "bcrypt-nodejs": "0.0.3", "body-parser": "^1.14.2", diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 3b2108e71e..a64113f4f9 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -2,6 +2,7 @@ var FilesController = require('../src/Controllers/FilesController').FilesControl var GridStoreAdapter = require("../src/Adapters/Files/GridStoreAdapter").GridStoreAdapter; var S3Adapter = require("../src/Adapters/Files/S3Adapter").S3Adapter; var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").GCSAdapter; +var AzureBlobStorageAdapter = require("../src/Adapters/Files/AzureBlobStorageAdapter").AzureBlobStorageAdapter; var Config = require("../src/Config"); var FCTestFactory = require("./FilesControllerTestFactory"); @@ -49,4 +50,19 @@ describe("FilesController",()=>{ } else if (!process.env.TRAVIS) { console.log("set GCP_PROJECT_ID, GCP_KEYFILE_PATH, and GCS_BUCKET to test GCSAdapter") } + + if (process.env.AZURE_STORAGE_ACCOUNT_NAME && process.env.AZURE_STORAGE_ACCOUNT_KEY) { + // Test the Azure Blob Storage Adapter + var azureBlobStorageAdapter = new AzureBlobStorageAdapter(process.env.AZURE_STORAGE_ACCOUNT_NAME, 'parseservertests', { storageAccessKey: process.env.AZURE_STORAGE_ACCOUNT_KEY }); + + FCTestFactory.testAdapter("AzureBlobStorageAdapter",azureBlobStorageAdapter); + + // Test Azure Blob Storage with direct access + var azureBlobStorageDirectAccessAdapter = new AzureBlobStorageAdapter(process.env.AZURE_STORAGE_ACCOUNT_NAME, 'parseservertests', { storageAccessKey: process.env.AZURE_STORAGE_ACCOUNT_KEY, directAccess: true }); + + FCTestFactory.testAdapter("AzureBlobStorageAdapterDirect", azureBlobStorageDirectAccessAdapter); + + } else if (!process.env.TRAVIS) { + console.log("set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_ACCOUNT_KEY to test AzureBlobStorageAdapter") + } }); diff --git a/spec/FilesControllerTestFactory.js b/spec/FilesControllerTestFactory.js index b467d031f5..721de0f064 100644 --- a/spec/FilesControllerTestFactory.js +++ b/spec/FilesControllerTestFactory.js @@ -4,7 +4,7 @@ var Config = require("../src/Config"); var testAdapter = function(name, adapter) { // Small additional tests to improve overall coverage - var config = new Config(Parse.applicationId); + var config = new Config(Parse.applicationId, 'testmount'); var filesController = new FilesController(adapter); describe("FilesController with "+name,()=>{ diff --git a/src/Adapters/Files/AzureBlobStorageAdapter.js b/src/Adapters/Files/AzureBlobStorageAdapter.js new file mode 100644 index 0000000000..98cdbc28ce --- /dev/null +++ b/src/Adapters/Files/AzureBlobStorageAdapter.js @@ -0,0 +1,122 @@ +// AzureBlobStorageAdapter +// +// Stores Parse files in Azure Blob Storage. + +import * as azure from 'azure-storage'; +import { FilesAdapter } from './FilesAdapter'; + +export class AzureBlobStorageAdapter extends FilesAdapter { + // Creates an Azure Storage client. + // Provide storage account name or storage account connection string as first parameter + // Provide container name as second parameter + // If you had provided storage account name, then also provide storage access key + // Host is optional, Azure will default to the default host + // directAccess defaults to false. If set to true, the file URL will be the actual blob URL + constructor( + storageAccountOrConnectionString, + container, { + storageAccessKey = '', + host = '', + directAccess = false + } = {} + ) { + super(); + + this._storageAccountOrConnectionString = storageAccountOrConnectionString; + this._storageAccessKey = storageAccessKey; + this._host = host; + this._container = container; + this._directAccess = directAccess; + if (this._storageAccountOrConnectionString.indexOf(';') != -1) { + // Connection string was passed + // Extract storage account name + // Storage account name is needed in getFileLocation + this._storageAccountName = this._storageAccountOrConnectionString.substring( + this._storageAccountOrConnectionString.indexOf('AccountName') + 12, + this._storageAccountOrConnectionString.indexOf(';', this._storageAccountOrConnectionString.indexOf('AccountName') + 12) + ); + } else { + // Storage account name was passed + this._storageAccountName = this._storageAccountOrConnectionString; + } + // Init client + this._azureBlobStorageClient = azure.createBlobService(this._storageAccountOrConnectionString, this._storageAccessKey, this._host); + } + + // For a given config object, filename, and data, store a file in Azure Blob Storage + // Returns a promise containing the Azure Blob Storage blob creation response + createFile(config, filename, data) { + let containerParams = {}; + if (this._directAccess) { + containerParams.publicAccessLevel = 'blob'; + } + + return new Promise((resolve, reject) => { + this._azureBlobStorageClient.createContainerIfNotExists( + this._container, + containerParams, + (cerror, cresult, cresponse) => { + if (cerror) { + return reject(cerror); + } + this._azureBlobStorageClient.createBlockBlobFromText( + this._container, + filename, + data, + (error, result, response) => { + if (error) { + return reject(error); + } + resolve(result); + }); + }); + }); + } + + deleteFile(config, filename) { + return new Promise((resolve, reject) => { + this._azureBlobStorageClient.deleteBlob( + this._container, + filename, + (error, response) => { + if (error) { + return reject(error); + } + resolve(response); + }); + }); + } + + // Search for and return a file if found by filename + // Returns a promise that succeeds with the buffer result from Azure Blob Storage + getFileData(config, filename) { + return new Promise((resolve, reject) => { + this._azureBlobStorageClient.getBlobToText( + this._container, + filename, + (error, text, blob, response) => { + if (error) { + return reject(error); + } + if(Buffer.isBuffer(text)) { + resolve(text); + } + else { + resolve(new Buffer(text, 'utf-8')); + } + + }); + }); + } + + // Generates and returns the location of a file stored in Azure Blob Storage for the given request and filename + // The location is the direct Azure Blob Storage link if the option is set, otherwise we serve the file through parse-server + getFileLocation(config, filename) { + if (this._directAccess) { + return `http://${this._storageAccountName}.blob.core.windows.net/${this._container}/${filename}`; + } + return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename)); + } +} + +export default AzureBlobStorageAdapter; diff --git a/src/index.js b/src/index.js index 87ab0331eb..214b49b94e 100644 --- a/src/index.js +++ b/src/index.js @@ -41,6 +41,7 @@ import { PushRouter } from './Routers/PushRouter'; import { randomString } from './cryptoUtils'; import { RolesRouter } from './Routers/RolesRouter'; import { S3Adapter } from './Adapters/Files/S3Adapter'; +import { AzureBlobStorageAdapter } from './Adapters/Files/AzureBlobStorageAdapter'; import { SchemasRouter } from './Routers/SchemasRouter'; import { SessionsRouter } from './Routers/SessionsRouter'; import { setFeature } from './features'; @@ -265,5 +266,6 @@ function addParseCloud() { module.exports = { ParseServer: ParseServer, S3Adapter: S3Adapter, - GCSAdapter: GCSAdapter + GCSAdapter: GCSAdapter, + AzureBlobStorageAdapter: AzureBlobStorageAdapter };