├── .babelrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── AzureStorageAdapter.js ├── FilesAdapter.js └── RequiredParameter.js ├── package.json ├── spec ├── support │ └── jasmine.json └── test.spec.js └── src ├── AzureStorageAdapter.js └── RequiredParameter.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.3" 4 | after_success: ./node_modules/.bin/codecov 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Felix Rieseberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Storage Files Adapter for Parse Server 2 | npm version dependencies Coverage via Codecov 3 | This module allows you to use Azure Blob Storage with the open source Parse Server, brought to you by your friends in Microsoft's Open Source Engineering team. 4 | 5 | ## Usage 6 | First, ensure that you have an Azure Blob Storage account, with a container setup. Then, install the adapter: 7 | 8 | ``` 9 | npm install parse-server-azure-storage 10 | ``` 11 | 12 | If you're using `parse-server` at version 2.2 (or below), please install with: 13 | 14 | ``` 15 | npm install parse-server-azure-storage@0.3.0 16 | ``` 17 | 18 | #### Direct Access 19 | By default, Parse will proxy all files - meaning that your end user accesses the files via your open source Parse-Server, not directly by going to Azure Blob storage. This is useful if you want files to only be accessible for logged in users or have otherwise security considerations. 20 | 21 | If your files can be public, you'll win performance by accessing files directly on Azure Blob Storage. To enable, ensure that your container's security policy is set to `blob`. Then, in your `AzureStorageAdapter` options, set `directAccess: true`. 22 | 23 | ``` 24 | var ParseServer = require('parse-server').ParseServer; 25 | var AzureStorageAdapter = require('parse-server-azure-storage').AzureStorageAdapter; 26 | 27 | var account = 'YOUR_AZURE_STORAGE_ACCOUNT_NAME'; 28 | var container = 'YOUR_AZURE_STORAGE_CONTAINER_NAME'; 29 | var options = { 30 | accessKey: 'YOUR_ACCESS_KEY', 31 | directAccess: false // If set to true, files will be served by Azure Blob Storage directly 32 | } 33 | 34 | var api = new ParseServer({ 35 | appId: process.env.APP_ID || 'myAppId', 36 | masterKey: process.env.MASTER_KEY || '', //Add your master key here. Keep it secret! 37 | serverURL: process.env.SERVER_URL || 'http://localhost:1337' 38 | (...) 39 | filesAdapter: new AzureStorageAdapter(account, container, options); 40 | }); 41 | ``` 42 | 43 | ## License 44 | The MIT License (MIT); Copyright (c) 2016 Felix Rieseberg and Microsoft Corporation. Please see `LICENSE` for details. 45 | -------------------------------------------------------------------------------- /lib/AzureStorageAdapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.AzureStorageAdapter = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // AzureStorageAdapter 9 | // 10 | // Stores Parse files in Azure Blob Storage. 11 | 12 | var _azureStorage = require('azure-storage'); 13 | 14 | var Azure = _interopRequireWildcard(_azureStorage); 15 | 16 | var _RequiredParameter = require('./RequiredParameter'); 17 | 18 | var _RequiredParameter2 = _interopRequireDefault(_RequiredParameter); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 23 | 24 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 25 | 26 | var AzureStorageAdapter = exports.AzureStorageAdapter = function () { 27 | // Creates an Azure Storage Client. 28 | 29 | function AzureStorageAdapter() { 30 | var accountName = arguments.length <= 0 || arguments[0] === undefined ? (0, _RequiredParameter2.default)('AzureStorageAdapter requires an account name') : arguments[0]; 31 | var container = arguments.length <= 1 || arguments[1] === undefined ? (0, _RequiredParameter2.default)('AzureStorageAdapter requires a container') : arguments[1]; 32 | 33 | var _ref = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; 34 | 35 | var _ref$accessKey = _ref.accessKey; 36 | var accessKey = _ref$accessKey === undefined ? '' : _ref$accessKey; 37 | var _ref$directAccess = _ref.directAccess; 38 | var directAccess = _ref$directAccess === undefined ? false : _ref$directAccess; 39 | 40 | _classCallCheck(this, AzureStorageAdapter); 41 | 42 | this._accountName = accountName; 43 | this._accessKey = accessKey; 44 | this._container = container; 45 | this._directAccess = directAccess; 46 | 47 | // Init client 48 | this._client = Azure.createBlobService(this._accountName, this._accessKey); 49 | } 50 | 51 | /** 52 | * For a given config object, filename, and data, store a file in Azure Blob Storage 53 | * @param {object} config 54 | * @param {string} filename 55 | * @param {string} data 56 | * @return {Promise} Promise containing the Azure Blob Storage blob creation response 57 | */ 58 | 59 | 60 | _createClass(AzureStorageAdapter, [{ 61 | key: 'createFile', 62 | value: function createFile(filename, data) { 63 | var _this = this; 64 | 65 | var containerParams = { 66 | publicAccessLevel: this._directAccess ? 'blob' : undefined 67 | }; 68 | 69 | return new Promise(function (resolve, reject) { 70 | _this._client.createContainerIfNotExists(_this._container, containerParams, function (cerr, cresult, cresponse) { 71 | if (cerr) { 72 | return reject(cerr); 73 | } 74 | 75 | _this._client.createBlockBlobFromText(_this._container, filename, data, function (err, result) { 76 | if (err) { 77 | return reject(err); 78 | } 79 | 80 | resolve(result); 81 | }); 82 | }); 83 | }); 84 | } 85 | 86 | /** 87 | * Delete a file if found by filename 88 | * @param {object} config 89 | * @param {string} filename 90 | * @return {Promise} Promise that succeeds with the result from Azure Storage 91 | */ 92 | 93 | }, { 94 | key: 'deleteFile', 95 | value: function deleteFile(filename) { 96 | var _this2 = this; 97 | 98 | return new Promise(function (resolve, reject) { 99 | _this2._client.deleteBlob(_this2._container, filename, function (err, res) { 100 | if (err) { 101 | return reject(err); 102 | } 103 | 104 | resolve(res); 105 | }); 106 | }); 107 | } 108 | 109 | /** 110 | * Search for and return a file if found by filename 111 | * @param {object} config 112 | * @param {string} filename 113 | * @return {Promise} Promise that succeeds with the result from Azure Storage 114 | */ 115 | 116 | }, { 117 | key: 'getFileData', 118 | value: function getFileData(filename) { 119 | var _this3 = this; 120 | 121 | return new Promise(function (resolve, reject) { 122 | _this3._client.getBlobToText(_this3._container, filename, function (err, text, blob, res) { 123 | if (err) { 124 | return reject(err); 125 | } 126 | 127 | resolve(new Buffer(text)); 128 | }); 129 | }); 130 | } 131 | 132 | /** 133 | * Generates and returns the location of a file stored in Azure Blob Storage for the given request and filename 134 | * The location is the direct Azure Blob Storage link if the option is set, otherwise we serve the file through parse-server 135 | * @param {object} config 136 | * @param {string} filename 137 | * @return {string} file's url 138 | */ 139 | 140 | }, { 141 | key: 'getFileLocation', 142 | value: function getFileLocation(config, filename) { 143 | if (this._directAccess) { 144 | return 'https://' + this._accountName + '.blob.core.windows.net/' + this._container + '/' + filename; 145 | } 146 | return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename); 147 | } 148 | }]); 149 | 150 | return AzureStorageAdapter; 151 | }(); 152 | 153 | exports.default = AzureStorageAdapter; -------------------------------------------------------------------------------- /lib/FilesAdapter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | // Files Adapter 12 | // 13 | // Allows you to change the file storage mechanism. 14 | // 15 | // Adapter classes must implement the following functions: 16 | // * createFile(config, filename, data) 17 | // * getFileData(config, filename) 18 | // * getFileLocation(config, request, filename) 19 | 20 | var FilesAdapter = exports.FilesAdapter = function () { 21 | function FilesAdapter() { 22 | _classCallCheck(this, FilesAdapter); 23 | } 24 | 25 | _createClass(FilesAdapter, [{ 26 | key: "createFile", 27 | value: function createFile(config, filename, data) {} 28 | }, { 29 | key: "deleteFile", 30 | value: function deleteFile(config, filename) {} 31 | }, { 32 | key: "getFileData", 33 | value: function getFileData(config, filename) {} 34 | }, { 35 | key: "getFileLocation", 36 | value: function getFileLocation(config, filename) {} 37 | }]); 38 | 39 | return FilesAdapter; 40 | }(); 41 | 42 | exports.default = FilesAdapter; -------------------------------------------------------------------------------- /lib/RequiredParameter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (errorMessage) { 8 | throw errorMessage; 9 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-server-azure-storage", 3 | "version": "1.1.0", 4 | "description": "Use Azure Blob Storage with Parse Server", 5 | "main": "lib/AzureStorageAdapter.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/felixrieseberg/parse-server-azure-storage" 9 | }, 10 | "files": [ 11 | "lib/", 12 | "LICENSE", 13 | "README.md" 14 | ], 15 | "license": "MIT", 16 | "dependencies": { 17 | "azure-storage": "^1.0.1", 18 | "babel-polyfill": "^6.5.0", 19 | "babel-runtime": "^6.5.0" 20 | }, 21 | "devDependencies": { 22 | "babel-cli": "^6.5.1", 23 | "babel-core": "^6.5.1", 24 | "babel-istanbul": "^0.7.0", 25 | "babel-preset-es2015": "^6.5.0", 26 | "babel-preset-stage-0": "^6.5.0", 27 | "babel-register": "^6.5.1", 28 | "codecov": "^1.0.1", 29 | "jasmine": "^2.4.1", 30 | "parse-server-conformance-tests": "^1.0.0" 31 | }, 32 | "scripts": { 33 | "build": "./node_modules/.bin/babel src/ -d lib/", 34 | "test": "./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js" 35 | }, 36 | "engines": { 37 | "node": ">=4.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "test.spec.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /spec/test.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let filesAdapterTests = require('parse-server-conformance-tests').files; 3 | 4 | let AzureStorageAdapter = require('../src/AzureStorageAdapter.js').default; 5 | 6 | describe('Azure tests', () => { 7 | 8 | it('should throw when not initialized properly', () => { 9 | expect(() => { 10 | new AzureStorageAdapter(); 11 | }).toThrow('AzureStorageAdapter requires an account name'); 12 | 13 | expect(() => { 14 | new AzureStorageAdapter('accountName'); 15 | }).toThrow('AzureStorageAdapter requires a container'); 16 | }); 17 | 18 | it('should not throw when initialized properly', () => { 19 | expect(() => { 20 | new AzureStorageAdapter('accountName', 'container', {'accessKey': new Buffer('accessKey').toString('base64') }); 21 | }).not.toThrow(); 22 | }); 23 | 24 | if (process.env.AZURE_ACCOUNT_NAME && process.env.AZURE_CONTAINER && process.env.AZURE_ACCESS_KEY) { 25 | // Should be initialized from the env 26 | let adapter = new AzureStorageAdapter(process.env.AZURE_ACCOUNT_NAME, process.env.AZURE_CONTAINER, { 27 | accessKey: process.env.AZURE_ACCESS_KEY 28 | }); 29 | filesAdapterTests.testAdapter("AzureAdapter", adapter); 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /src/AzureStorageAdapter.js: -------------------------------------------------------------------------------- 1 | // AzureStorageAdapter 2 | // 3 | // Stores Parse files in Azure Blob Storage. 4 | 5 | import * as Azure from 'azure-storage'; 6 | import requiredParameter from './RequiredParameter'; 7 | 8 | export class AzureStorageAdapter { 9 | // Creates an Azure Storage Client. 10 | constructor( 11 | accountName = requiredParameter('AzureStorageAdapter requires an account name'), 12 | container = requiredParameter('AzureStorageAdapter requires a container'), 13 | { accessKey = '', 14 | directAccess = false } = {} 15 | ) { 16 | this._accountName = accountName; 17 | this._accessKey = accessKey; 18 | this._container = container; 19 | this._directAccess = directAccess; 20 | 21 | // Init client 22 | this._client = Azure.createBlobService(this._accountName, this._accessKey); 23 | } 24 | 25 | /** 26 | * For a given config object, filename, and data, store a file in Azure Blob Storage 27 | * @param {object} config 28 | * @param {string} filename 29 | * @param {string} data 30 | * @return {Promise} Promise containing the Azure Blob Storage blob creation response 31 | */ 32 | createFile(filename, data) { 33 | let containerParams = { 34 | publicAccessLevel: (this._directAccess) ? 'blob' : undefined 35 | }; 36 | 37 | return new Promise((resolve, reject) => { 38 | this._client.createContainerIfNotExists(this._container, containerParams, (cerr, cresult, cresponse) => { 39 | if (cerr) { 40 | return reject(cerr); 41 | } 42 | 43 | this._client.createBlockBlobFromText(this._container, filename, data, (err, result) => { 44 | if (err) { 45 | return reject(err); 46 | } 47 | 48 | resolve(result); 49 | }); 50 | }); 51 | }); 52 | } 53 | 54 | /** 55 | * Delete a file if found by filename 56 | * @param {object} config 57 | * @param {string} filename 58 | * @return {Promise} Promise that succeeds with the result from Azure Storage 59 | */ 60 | deleteFile(filename) { 61 | return new Promise((resolve, reject) => { 62 | this._client.deleteBlob(this._container, filename, (err, res) => { 63 | if (err) { 64 | return reject(err); 65 | } 66 | 67 | resolve(res); 68 | }); 69 | }); 70 | } 71 | 72 | /** 73 | * Search for and return a file if found by filename 74 | * @param {object} config 75 | * @param {string} filename 76 | * @return {Promise} Promise that succeeds with the result from Azure Storage 77 | */ 78 | getFileData(filename) { 79 | return new Promise((resolve, reject) => { 80 | this._client.getBlobToText(this._container, filename, (err, text, blob, res) => { 81 | if (err) { 82 | return reject(err); 83 | } 84 | 85 | resolve(new Buffer(text)); 86 | }); 87 | }); 88 | } 89 | 90 | /** 91 | * Generates and returns the location of a file stored in Azure Blob Storage for the given request and filename 92 | * The location is the direct Azure Blob Storage link if the option is set, otherwise we serve the file through parse-server 93 | * @param {object} config 94 | * @param {string} filename 95 | * @return {string} file's url 96 | */ 97 | getFileLocation(config, filename) { 98 | if (this._directAccess) { 99 | return `https://${this._accountName}.blob.core.windows.net/${this._container}/${filename}`; 100 | } 101 | return (`${config.mount}/files/${config.applicationId}/${encodeURIComponent(filename)}`); 102 | } 103 | } 104 | 105 | export default AzureStorageAdapter; 106 | -------------------------------------------------------------------------------- /src/RequiredParameter.js: -------------------------------------------------------------------------------- 1 | export default (errorMessage) => {throw errorMessage} 2 | --------------------------------------------------------------------------------