Skip to content

FilesAdapter for Azure Blob Storage #545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions spec/FilesController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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")
}
});
2 changes: 1 addition & 1 deletion spec/FilesControllerTestFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,()=>{
Expand Down
122 changes: 122 additions & 0 deletions src/Adapters/Files/AzureBlobStorageAdapter.js
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be more consistent with other adapters to only have one parameter options

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;
4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -265,5 +266,6 @@ function addParseCloud() {
module.exports = {
ParseServer: ParseServer,
S3Adapter: S3Adapter,
GCSAdapter: GCSAdapter
GCSAdapter: GCSAdapter,
AzureBlobStorageAdapter: AzureBlobStorageAdapter
};