├── .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 |
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 |
--------------------------------------------------------------------------------