Merge remote-tracking branch 'github/master'
This commit is contained in:
33
.npmignore
Normal file
33
.npmignore
Normal file
@@ -0,0 +1,33 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
|
||||
# WebStorm/IntelliJ
|
||||
.idea
|
||||
@@ -21,7 +21,6 @@ if (process.env.PARSE_SERVER_OPTIONS) {
|
||||
options.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY;
|
||||
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
|
||||
options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_KEY;
|
||||
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
|
||||
options.masterKey = process.env.PARSE_SERVER_MASTER_KEY;
|
||||
options.fileKey = process.env.PARSE_SERVER_FILE_KEY;
|
||||
// Comma separated list of facebook app ids
|
||||
|
||||
@@ -64,6 +64,22 @@ describe('Parse.User testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("user login with files", (done) => {
|
||||
"use strict";
|
||||
|
||||
let file = new Parse.File("yolo.txt", [1,2,3], "text/plain");
|
||||
file.save().then((file) => {
|
||||
return Parse.User.signUp("asdf", "zxcv", { "file" : file });
|
||||
}).then(() => {
|
||||
return Parse.User.logIn("asdf", "zxcv");
|
||||
}).then((user) => {
|
||||
let fileAgain = user.get('file');
|
||||
ok(fileAgain.name());
|
||||
ok(fileAgain.url());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("become", (done) => {
|
||||
var user = null;
|
||||
var sessionToken = null;
|
||||
|
||||
22
src/Adapters/Files/FilesAdapter.js
Normal file
22
src/Adapters/Files/FilesAdapter.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// Files Adapter
|
||||
//
|
||||
// Allows you to change the file storage mechanism.
|
||||
//
|
||||
// Adapter classes must implement the following functions:
|
||||
// * createFile(config, filename, data)
|
||||
// * getFileData(config, filename)
|
||||
// * getFileLocation(config, request, filename)
|
||||
//
|
||||
// Default is GridStoreAdapter, which requires mongo
|
||||
// and for the API server to be using the ExportAdapter
|
||||
// database adapter.
|
||||
|
||||
export class FilesAdapter {
|
||||
createFile(config, filename, data) { }
|
||||
|
||||
getFileData(config, filename) { }
|
||||
|
||||
getFileLocation(config, filename) { }
|
||||
}
|
||||
|
||||
export default FilesAdapter;
|
||||
39
src/Adapters/Files/GridStoreAdapter.js
Normal file
39
src/Adapters/Files/GridStoreAdapter.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// GridStoreAdapter
|
||||
//
|
||||
// Stores files in Mongo using GridStore
|
||||
// Requires the database adapter to be based on mongoclient
|
||||
|
||||
import { GridStore } from 'mongodb';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
|
||||
export class GridStoreAdapter extends FilesAdapter {
|
||||
// For a given config object, filename, and data, store a file
|
||||
// Returns a promise
|
||||
createFile(config, filename, data) {
|
||||
return config.database.connect().then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.write(data);
|
||||
}).then((gridStore) => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
getFileData(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
return GridStore.exist(config.database.db, filename);
|
||||
}).then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'r');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.read();
|
||||
});
|
||||
}
|
||||
|
||||
getFileLocation(config, filename) {
|
||||
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
|
||||
}
|
||||
}
|
||||
|
||||
export default GridStoreAdapter;
|
||||
83
src/Adapters/Files/S3Adapter.js
Normal file
83
src/Adapters/Files/S3Adapter.js
Normal file
@@ -0,0 +1,83 @@
|
||||
// S3Adapter
|
||||
//
|
||||
// Stores Parse files in AWS S3.
|
||||
|
||||
import * as AWS from 'aws-sdk';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
|
||||
const DEFAULT_S3_REGION = "us-east-1";
|
||||
const DEFAULT_S3_BUCKET = "parse-files";
|
||||
|
||||
export class S3Adapter extends FilesAdapter {
|
||||
// Creates an S3 session.
|
||||
// Providing AWS access and secret keys is mandatory
|
||||
// Region and bucket will use sane defaults if omitted
|
||||
constructor(
|
||||
accessKey,
|
||||
secretKey,
|
||||
{ region = DEFAULT_S3_REGION,
|
||||
bucket = DEFAULT_S3_BUCKET,
|
||||
bucketPrefix = '',
|
||||
directAccess = false } = {}
|
||||
) {
|
||||
super();
|
||||
|
||||
this._region = region;
|
||||
this._bucket = bucket;
|
||||
this._bucketPrefix = bucketPrefix;
|
||||
this._directAccess = directAccess;
|
||||
|
||||
let s3Options = {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretKey,
|
||||
params: { Bucket: this._bucket }
|
||||
};
|
||||
AWS.config._region = this._region;
|
||||
this._s3Client = new AWS.S3(s3Options);
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file in S3
|
||||
// Returns a promise containing the S3 object creation response
|
||||
createFile(config, filename, data) {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename,
|
||||
Body: data
|
||||
};
|
||||
if (this._directAccess) {
|
||||
params.ACL = "public-read"
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.upload(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Search for and return a file if found by filename
|
||||
// Returns a promise that succeeds with the buffer result from S3
|
||||
getFileData(config, filename) {
|
||||
let params = {Key: this._bucketPrefix + filename};
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.getObject(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data.Body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generates and returns the location of a file stored in S3 for the given request and filename
|
||||
// The location is the direct S3 link if the option is set, otherwise we serve the file through parse-server
|
||||
getFileLocation(config, filename) {
|
||||
if (this._directAccess) {
|
||||
return ('https://' + this.bucket + '._s3Client.amazonaws.com' + '/' + this._bucketPrefix + filename);
|
||||
}
|
||||
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
|
||||
}
|
||||
}
|
||||
|
||||
export default S3Adapter;
|
||||
@@ -13,7 +13,6 @@ function Config(applicationId, mount) {
|
||||
|
||||
this.applicationId = applicationId;
|
||||
this.collectionPrefix = cacheInfo.collectionPrefix || '';
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
||||
this.masterKey = cacheInfo.masterKey;
|
||||
this.clientKey = cacheInfo.clientKey;
|
||||
this.javascriptKey = cacheInfo.javascriptKey;
|
||||
@@ -21,6 +20,10 @@ function Config(applicationId, mount) {
|
||||
this.restAPIKey = cacheInfo.restAPIKey;
|
||||
this.fileKey = cacheInfo.fileKey;
|
||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
||||
this.filesController = cacheInfo.filesController;
|
||||
|
||||
this.mount = mount;
|
||||
}
|
||||
|
||||
|
||||
126
src/Controllers/FilesController.js
Normal file
126
src/Controllers/FilesController.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// FilesController.js
|
||||
|
||||
import express from 'express';
|
||||
import mime from 'mime';
|
||||
import { Parse } from 'parse/node';
|
||||
import BodyParser from 'body-parser';
|
||||
import hat from 'hat';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import Config from '../Config';
|
||||
|
||||
const rack = hat.rack();
|
||||
|
||||
export class FilesController {
|
||||
constructor(filesAdapter) {
|
||||
this._filesAdapter = filesAdapter;
|
||||
}
|
||||
|
||||
getHandler() {
|
||||
return (req, res) => {
|
||||
let config = new Config(req.params.appId);
|
||||
let filename = req.params.filename;
|
||||
this._filesAdapter.getFileData(config, filename).then((data) => {
|
||||
res.status(200);
|
||||
var contentType = mime.lookup(filename);
|
||||
res.set('Content-type', contentType);
|
||||
res.end(data);
|
||||
}).catch((error) => {
|
||||
res.status(404);
|
||||
res.set('Content-type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
createHandler() {
|
||||
return (req, res, next) => {
|
||||
if (!req.body || !req.body.length) {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Invalid file upload.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.params.filename.length > 128) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename too long.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename contains invalid characters.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// If a content-type is included, we'll add an extension so we can
|
||||
// return the same content-type.
|
||||
let extension = '';
|
||||
let hasExtension = req.params.filename.indexOf('.') > 0;
|
||||
let contentType = req.get('Content-type');
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
extension = '.' + mime.extension(contentType);
|
||||
}
|
||||
|
||||
let filename = rack() + '_' + req.params.filename + extension;
|
||||
this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
|
||||
res.status(201);
|
||||
var location = this._filesAdapter.getFileLocation(req.config, filename);
|
||||
res.set('Location', location);
|
||||
res.json({ url: location, name: filename });
|
||||
}).catch((error) => {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Could not store file.'));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find file references in REST-format object and adds the url key
|
||||
* with the current mount point and app id.
|
||||
* Object may be a single object or list of REST-format objects.
|
||||
*/
|
||||
expandFilesInObject(config, object) {
|
||||
if (object instanceof Array) {
|
||||
object.map((obj) => this.expandFilesInObject(config, obj));
|
||||
return;
|
||||
}
|
||||
if (typeof object !== 'object') {
|
||||
return;
|
||||
}
|
||||
for (let key in object) {
|
||||
let fileObject = object[key];
|
||||
if (fileObject && fileObject['__type'] === 'File') {
|
||||
if (fileObject['url']) {
|
||||
continue;
|
||||
}
|
||||
let filename = fileObject['name'];
|
||||
if (filename.indexOf('tfss-') === 0) {
|
||||
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
|
||||
} else {
|
||||
fileObject['url'] = this._filesAdapter.getFileLocation(config, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getExpressRouter() {
|
||||
let router = express.Router();
|
||||
router.get('/files/:appId/:filename', this.getHandler());
|
||||
|
||||
router.post('/files', function(req, res, next) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename not provided.'));
|
||||
});
|
||||
|
||||
router.post('/files/:filename',
|
||||
Middlewares.allowCrossDomain,
|
||||
BodyParser.raw({type: '*/*', limit: '20mb'}),
|
||||
Middlewares.handleParseHeaders,
|
||||
this.createHandler()
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
export default FilesController;
|
||||
@@ -10,9 +10,8 @@ var transform = require('./transform');
|
||||
|
||||
// options can contain:
|
||||
// collectionPrefix: the string to put in front of every collection name.
|
||||
function ExportAdapter(mongoURI, options) {
|
||||
function ExportAdapter(mongoURI, options = {}) {
|
||||
this.mongoURI = mongoURI;
|
||||
options = options || {};
|
||||
|
||||
this.collectionPrefix = options.collectionPrefix;
|
||||
|
||||
@@ -67,8 +66,7 @@ function returnsTrue() {
|
||||
// Returns a promise for a schema object.
|
||||
// If we are provided a acceptor, then we run it on the schema.
|
||||
// If the schema isn't accepted, we reload it at most once.
|
||||
ExportAdapter.prototype.loadSchema = function(acceptor) {
|
||||
acceptor = acceptor || returnsTrue;
|
||||
ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
|
||||
if (!this.schemaPromise) {
|
||||
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
|
||||
@@ -281,8 +279,7 @@ ExportAdapter.prototype.removeRelation = function(key, fromClassName,
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
ExportAdapter.prototype.destroy = function(className, query, options) {
|
||||
options = options || {};
|
||||
ExportAdapter.prototype.destroy = function(className, query, options = {}) {
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
|
||||
@@ -350,8 +347,7 @@ ExportAdapter.prototype.create = function(className, object, options) {
|
||||
// This should only be used for testing - use 'find' for normal code
|
||||
// to avoid Mongo-format dependencies.
|
||||
// Returns a promise that resolves to a list of items.
|
||||
ExportAdapter.prototype.mongoFind = function(className, query, options) {
|
||||
options = options || {};
|
||||
ExportAdapter.prototype.mongoFind = function(className, query, options = {}) {
|
||||
return this.collection(className).then((coll) => {
|
||||
return coll.find(query, options).toArray();
|
||||
});
|
||||
@@ -507,8 +503,7 @@ ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
||||
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||
// anything about users, ideally. Then, improve the format of the ACL
|
||||
// arg to work like the others.
|
||||
ExportAdapter.prototype.find = function(className, query, options) {
|
||||
options = options || {};
|
||||
ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
var mongoOptions = {};
|
||||
if (options.skip) {
|
||||
mongoOptions.skip = options.skip;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Files Adapter
|
||||
//
|
||||
// Allows you to change the file storage mechanism.
|
||||
//
|
||||
// Adapter classes must implement the following functions:
|
||||
// * create(config, filename, data)
|
||||
// * get(config, filename)
|
||||
// * location(config, req, filename)
|
||||
//
|
||||
// Default is GridStoreAdapter, which requires mongo
|
||||
// and for the API server to be using the ExportAdapter
|
||||
// database adapter.
|
||||
|
||||
var GridStoreAdapter = require('./GridStoreAdapter');
|
||||
|
||||
var adapter = GridStoreAdapter;
|
||||
|
||||
function setAdapter(filesAdapter) {
|
||||
adapter = filesAdapter;
|
||||
}
|
||||
|
||||
function getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAdapter: getAdapter,
|
||||
setAdapter: setAdapter
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
// GridStoreAdapter
|
||||
//
|
||||
// Stores files in Mongo using GridStore
|
||||
// Requires the database adapter to be based on mongoclient
|
||||
|
||||
var GridStore = require('mongodb').GridStore;
|
||||
var path = require('path');
|
||||
|
||||
// For a given config object, filename, and data, store a file
|
||||
// Returns a promise
|
||||
function create(config, filename, data) {
|
||||
return config.database.connect().then(() => {
|
||||
var gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.write(data);
|
||||
}).then((gridStore) => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Search for and return a file if found by filename
|
||||
// Resolves a promise that succeeds with the buffer result
|
||||
// from GridStore
|
||||
function get(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
return GridStore.exist(config.database.db, filename);
|
||||
}).then(() => {
|
||||
var gridStore = new GridStore(config.database.db, filename, 'r');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.read();
|
||||
});
|
||||
}
|
||||
|
||||
// Generates and returns the location of a file stored in GridStore for the
|
||||
// given request and filename
|
||||
function location(config, req, filename) {
|
||||
return (req.protocol + '://' + req.get('host') +
|
||||
path.dirname(req.originalUrl) + '/' + req.config.applicationId +
|
||||
'/' + encodeURIComponent(filename));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create: create,
|
||||
get: get,
|
||||
location: location
|
||||
};
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import { default as FilesController } from './Controllers/FilesController';
|
||||
|
||||
// restOptions can include:
|
||||
// skip
|
||||
// limit
|
||||
@@ -11,13 +13,12 @@ var Parse = require('parse/node').Parse;
|
||||
// include
|
||||
// keys
|
||||
// redirectClassNameForKey
|
||||
function RestQuery(config, auth, className, restWhere, restOptions) {
|
||||
restOptions = restOptions || {};
|
||||
function RestQuery(config, auth, className, restWhere = {}, restOptions = {}) {
|
||||
|
||||
this.config = config;
|
||||
this.auth = auth;
|
||||
this.className = className;
|
||||
this.restWhere = restWhere || {};
|
||||
this.restWhere = restWhere;
|
||||
this.response = null;
|
||||
|
||||
this.findOptions = {};
|
||||
@@ -317,35 +318,35 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
RestQuery.prototype.runFind = function() {
|
||||
return this.config.database.find(
|
||||
this.className, this.restWhere, this.findOptions).then((results) => {
|
||||
if (this.className == '_User') {
|
||||
for (var result of results) {
|
||||
delete result.password;
|
||||
}
|
||||
if (this.className == '_User') {
|
||||
for (var result of results) {
|
||||
delete result.password;
|
||||
}
|
||||
}
|
||||
|
||||
updateParseFiles(this.config, results);
|
||||
this.config.filesController.expandFilesInObject(this.config, results);
|
||||
|
||||
if (this.keys) {
|
||||
var keySet = this.keys;
|
||||
results = results.map((object) => {
|
||||
var newObject = {};
|
||||
for (var key in object) {
|
||||
if (keySet.has(key)) {
|
||||
newObject[key] = object[key];
|
||||
}
|
||||
if (this.keys) {
|
||||
var keySet = this.keys;
|
||||
results = results.map((object) => {
|
||||
var newObject = {};
|
||||
for (var key in object) {
|
||||
if (keySet.has(key)) {
|
||||
newObject[key] = object[key];
|
||||
}
|
||||
return newObject;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.redirectClassName) {
|
||||
for (var r of results) {
|
||||
r.className = this.redirectClassName;
|
||||
}
|
||||
}
|
||||
return newObject;
|
||||
});
|
||||
}
|
||||
|
||||
this.response = {results: results};
|
||||
});
|
||||
if (this.redirectClassName) {
|
||||
for (var r of results) {
|
||||
r.className = this.redirectClassName;
|
||||
}
|
||||
}
|
||||
|
||||
this.response = {results: results};
|
||||
});
|
||||
};
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
@@ -498,35 +499,6 @@ function replacePointers(object, path, replace) {
|
||||
return answer;
|
||||
}
|
||||
|
||||
// Find file references in REST-format object and adds the url key
|
||||
// with the current mount point and app id
|
||||
// Object may be a single object or list of REST-format objects
|
||||
function updateParseFiles(config, object) {
|
||||
if (object instanceof Array) {
|
||||
object.map((obj) => updateParseFiles(config, obj));
|
||||
return;
|
||||
}
|
||||
if (typeof object !== 'object') {
|
||||
return;
|
||||
}
|
||||
for (var key in object) {
|
||||
if (object[key] && object[key]['__type'] &&
|
||||
object[key]['__type'] == 'File') {
|
||||
var filename = object[key]['name'];
|
||||
var encoded = encodeURIComponent(filename);
|
||||
encoded = encoded.replace('%40', '@');
|
||||
if (filename.indexOf('tfss-') === 0) {
|
||||
object[key]['url'] = 'http://files.parsetfss.com/' +
|
||||
config.fileKey + '/' + encoded;
|
||||
} else {
|
||||
object[key]['url'] = config.mount + '/files/' +
|
||||
config.applicationId + '/' +
|
||||
encoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds a subobject that has the given key, if there is one.
|
||||
// Returns undefined otherwise.
|
||||
function findObjectWithKey(root, key) {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// S3Adapter
|
||||
//
|
||||
// Stores Parse files in AWS S3.
|
||||
|
||||
var AWS = require('aws-sdk');
|
||||
var path = require('path');
|
||||
|
||||
var DEFAULT_REGION = "us-east-1";
|
||||
var DEFAULT_BUCKET = "parse-files";
|
||||
|
||||
// Creates an S3 session.
|
||||
// Providing AWS access and secret keys is mandatory
|
||||
// Region and bucket will use sane defaults if omitted
|
||||
function S3Adapter(accessKey, secretKey, options) {
|
||||
options = options || {};
|
||||
|
||||
this.region = options.region || DEFAULT_REGION;
|
||||
this.bucket = options.bucket || DEFAULT_BUCKET;
|
||||
this.bucketPrefix = options.bucketPrefix || "";
|
||||
this.directAccess = options.directAccess || false;
|
||||
|
||||
s3Options = {
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretKey,
|
||||
params: {Bucket: this.bucket}
|
||||
};
|
||||
AWS.config.region = this.region;
|
||||
this.s3 = new AWS.S3(s3Options);
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file in S3
|
||||
// Returns a promise containing the S3 object creation response
|
||||
S3Adapter.prototype.create = function(config, filename, data) {
|
||||
var params = {
|
||||
Key: this.bucketPrefix + filename,
|
||||
Body: data,
|
||||
};
|
||||
if (this.directAccess) {
|
||||
params.ACL = "public-read"
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.s3.upload(params, (err, data) => {
|
||||
if (err !== null) return reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Search for and return a file if found by filename
|
||||
// Returns a promise that succeeds with the buffer result from S3
|
||||
S3Adapter.prototype.get = function(config, filename) {
|
||||
var params = {Key: this.bucketPrefix + filename};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.s3.getObject(params, (err, data) => {
|
||||
if (err !== null) return reject(err);
|
||||
resolve(data.Body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generates and returns the location of a file stored in S3 for the given request and
|
||||
// filename
|
||||
// The location is the direct S3 link if the option is set, otherwise we serve
|
||||
// the file through parse-server
|
||||
S3Adapter.prototype.location = function(config, req, filename) {
|
||||
if (this.directAccess) {
|
||||
return ('https://' + this.bucket + '.s3.amazonaws.com' + '/' +
|
||||
this.bucketPrefix + filename);
|
||||
}
|
||||
return (req.protocol + '://' + req.get('host') +
|
||||
path.dirname(req.originalUrl) + '/' + req.config.applicationId +
|
||||
'/' + encodeURIComponent(filename));
|
||||
}
|
||||
|
||||
module.exports = S3Adapter;
|
||||
85
src/files.js
85
src/files.js
@@ -1,85 +0,0 @@
|
||||
// files.js
|
||||
|
||||
var bodyParser = require('body-parser'),
|
||||
Config = require('./Config'),
|
||||
express = require('express'),
|
||||
FilesAdapter = require('./FilesAdapter'),
|
||||
middlewares = require('./middlewares.js'),
|
||||
mime = require('mime'),
|
||||
Parse = require('parse/node').Parse,
|
||||
rack = require('hat').rack();
|
||||
|
||||
var router = express.Router();
|
||||
|
||||
var processCreate = function(req, res, next) {
|
||||
if (!req.body || !req.body.length) {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Invalid file upload.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.params.filename.length > 128) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename too long.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename contains invalid characters.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// If a content-type is included, we'll add an extension so we can
|
||||
// return the same content-type.
|
||||
var extension = '';
|
||||
var hasExtension = req.params.filename.indexOf('.') > 0;
|
||||
var contentType = req.get('Content-type');
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
extension = '.' + mime.extension(contentType);
|
||||
}
|
||||
|
||||
var filename = rack() + '_' + req.params.filename + extension;
|
||||
FilesAdapter.getAdapter().create(req.config, filename, req.body)
|
||||
.then(() => {
|
||||
res.status(201);
|
||||
var location = FilesAdapter.getAdapter().location(req.config, req, filename);
|
||||
res.set('Location', location);
|
||||
res.json({ url: location, name: filename });
|
||||
}).catch((error) => {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Could not store file.'));
|
||||
});
|
||||
};
|
||||
|
||||
var processGet = function(req, res) {
|
||||
var config = new Config(req.params.appId);
|
||||
FilesAdapter.getAdapter().get(config, req.params.filename)
|
||||
.then((data) => {
|
||||
res.status(200);
|
||||
var contentType = mime.lookup(req.params.filename);
|
||||
res.set('Content-type', contentType);
|
||||
res.end(data);
|
||||
}).catch((error) => {
|
||||
res.status(404);
|
||||
res.set('Content-type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
});
|
||||
};
|
||||
|
||||
router.get('/files/:appId/:filename', processGet);
|
||||
|
||||
router.post('/files', function(req, res, next) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename not provided.'));
|
||||
});
|
||||
|
||||
router.post('/files/:filename',
|
||||
middlewares.allowCrossDomain,
|
||||
bodyParser.raw({type: '*/*', limit: '20mb'}),
|
||||
middlewares.handleParseHeaders,
|
||||
processCreate);
|
||||
|
||||
module.exports = {
|
||||
router: router
|
||||
};
|
||||
20
src/index.js
20
src/index.js
@@ -5,14 +5,17 @@ var batch = require('./batch'),
|
||||
cache = require('./cache'),
|
||||
DatabaseAdapter = require('./DatabaseAdapter'),
|
||||
express = require('express'),
|
||||
FilesAdapter = require('./FilesAdapter'),
|
||||
S3Adapter = require('./S3Adapter'),
|
||||
middlewares = require('./middlewares'),
|
||||
multer = require('multer'),
|
||||
Parse = require('parse/node').Parse,
|
||||
PromiseRouter = require('./PromiseRouter'),
|
||||
httpRequest = require('./httpRequest');
|
||||
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
|
||||
@@ -46,9 +49,9 @@ function ParseServer(args) {
|
||||
if (args.databaseAdapter) {
|
||||
DatabaseAdapter.setAdapter(args.databaseAdapter);
|
||||
}
|
||||
if (args.filesAdapter) {
|
||||
FilesAdapter.setAdapter(args.filesAdapter);
|
||||
}
|
||||
|
||||
let filesAdapter = args.filesAdapter || new GridStoreAdapter();
|
||||
|
||||
if (args.databaseURI) {
|
||||
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
|
||||
}
|
||||
@@ -64,6 +67,8 @@ function ParseServer(args) {
|
||||
|
||||
}
|
||||
|
||||
let filesController = new FilesController(filesAdapter);
|
||||
|
||||
cache.apps[args.appId] = {
|
||||
masterKey: args.masterKey,
|
||||
collectionPrefix: args.collectionPrefix || '',
|
||||
@@ -72,7 +77,8 @@ function ParseServer(args) {
|
||||
dotNetKey: args.dotNetKey || '',
|
||||
restAPIKey: args.restAPIKey || '',
|
||||
fileKey: args.fileKey || 'invalid-file-key',
|
||||
facebookAppIds: args.facebookAppIds || []
|
||||
facebookAppIds: args.facebookAppIds || [],
|
||||
filesController: filesController
|
||||
};
|
||||
|
||||
// To maintain compatibility. TODO: Remove in v2.1
|
||||
@@ -91,7 +97,7 @@ function ParseServer(args) {
|
||||
var api = express();
|
||||
|
||||
// File handling needs to be before default middlewares are applied
|
||||
api.use('/', require('./files').router);
|
||||
api.use('/', filesController.getExpressRouter());
|
||||
|
||||
// TODO: separate this from the regular ParseServer object
|
||||
if (process.env.TESTING == 1) {
|
||||
|
||||
@@ -58,6 +58,8 @@ function handleLogIn(req) {
|
||||
user.sessionToken = token;
|
||||
delete user.password;
|
||||
|
||||
req.config.filesController.expandFilesInObject(req.config, user);
|
||||
|
||||
var expiresAt = new Date();
|
||||
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user