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.restAPIKey = process.env.PARSE_SERVER_REST_API_KEY;
|
||||||
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
|
options.dotNetKey = process.env.PARSE_SERVER_DOTNET_KEY;
|
||||||
options.javascriptKey = process.env.PARSE_SERVER_JAVASCRIPT_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.masterKey = process.env.PARSE_SERVER_MASTER_KEY;
|
||||||
options.fileKey = process.env.PARSE_SERVER_FILE_KEY;
|
options.fileKey = process.env.PARSE_SERVER_FILE_KEY;
|
||||||
// Comma separated list of facebook app ids
|
// 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) => {
|
it("become", (done) => {
|
||||||
var user = null;
|
var user = null;
|
||||||
var sessionToken = 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.applicationId = applicationId;
|
||||||
this.collectionPrefix = cacheInfo.collectionPrefix || '';
|
this.collectionPrefix = cacheInfo.collectionPrefix || '';
|
||||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
|
||||||
this.masterKey = cacheInfo.masterKey;
|
this.masterKey = cacheInfo.masterKey;
|
||||||
this.clientKey = cacheInfo.clientKey;
|
this.clientKey = cacheInfo.clientKey;
|
||||||
this.javascriptKey = cacheInfo.javascriptKey;
|
this.javascriptKey = cacheInfo.javascriptKey;
|
||||||
@@ -21,6 +20,10 @@ function Config(applicationId, mount) {
|
|||||||
this.restAPIKey = cacheInfo.restAPIKey;
|
this.restAPIKey = cacheInfo.restAPIKey;
|
||||||
this.fileKey = cacheInfo.fileKey;
|
this.fileKey = cacheInfo.fileKey;
|
||||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||||
|
|
||||||
|
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
||||||
|
this.filesController = cacheInfo.filesController;
|
||||||
|
|
||||||
this.mount = mount;
|
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:
|
// options can contain:
|
||||||
// collectionPrefix: the string to put in front of every collection name.
|
// collectionPrefix: the string to put in front of every collection name.
|
||||||
function ExportAdapter(mongoURI, options) {
|
function ExportAdapter(mongoURI, options = {}) {
|
||||||
this.mongoURI = mongoURI;
|
this.mongoURI = mongoURI;
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
this.collectionPrefix = options.collectionPrefix;
|
this.collectionPrefix = options.collectionPrefix;
|
||||||
|
|
||||||
@@ -67,8 +66,7 @@ function returnsTrue() {
|
|||||||
// Returns a promise for a schema object.
|
// Returns a promise for a schema object.
|
||||||
// If we are provided a acceptor, then we run it on the schema.
|
// 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.
|
// If the schema isn't accepted, we reload it at most once.
|
||||||
ExportAdapter.prototype.loadSchema = function(acceptor) {
|
ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||||
acceptor = acceptor || returnsTrue;
|
|
||||||
|
|
||||||
if (!this.schemaPromise) {
|
if (!this.schemaPromise) {
|
||||||
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
|
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,
|
// acl: a list of strings. If the object to be updated has an ACL,
|
||||||
// one of the provided strings must provide the caller with
|
// one of the provided strings must provide the caller with
|
||||||
// write permissions.
|
// write permissions.
|
||||||
ExportAdapter.prototype.destroy = function(className, query, options) {
|
ExportAdapter.prototype.destroy = function(className, query, options = {}) {
|
||||||
options = options || {};
|
|
||||||
var isMaster = !('acl' in options);
|
var isMaster = !('acl' in options);
|
||||||
var aclGroup = options.acl || [];
|
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
|
// This should only be used for testing - use 'find' for normal code
|
||||||
// to avoid Mongo-format dependencies.
|
// to avoid Mongo-format dependencies.
|
||||||
// Returns a promise that resolves to a list of items.
|
// Returns a promise that resolves to a list of items.
|
||||||
ExportAdapter.prototype.mongoFind = function(className, query, options) {
|
ExportAdapter.prototype.mongoFind = function(className, query, options = {}) {
|
||||||
options = options || {};
|
|
||||||
return this.collection(className).then((coll) => {
|
return this.collection(className).then((coll) => {
|
||||||
return coll.find(query, options).toArray();
|
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
|
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||||
// anything about users, ideally. Then, improve the format of the ACL
|
// anything about users, ideally. Then, improve the format of the ACL
|
||||||
// arg to work like the others.
|
// arg to work like the others.
|
||||||
ExportAdapter.prototype.find = function(className, query, options) {
|
ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||||
options = options || {};
|
|
||||||
var mongoOptions = {};
|
var mongoOptions = {};
|
||||||
if (options.skip) {
|
if (options.skip) {
|
||||||
mongoOptions.skip = 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;
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
import { default as FilesController } from './Controllers/FilesController';
|
||||||
|
|
||||||
// restOptions can include:
|
// restOptions can include:
|
||||||
// skip
|
// skip
|
||||||
// limit
|
// limit
|
||||||
@@ -11,13 +13,12 @@ var Parse = require('parse/node').Parse;
|
|||||||
// include
|
// include
|
||||||
// keys
|
// keys
|
||||||
// redirectClassNameForKey
|
// redirectClassNameForKey
|
||||||
function RestQuery(config, auth, className, restWhere, restOptions) {
|
function RestQuery(config, auth, className, restWhere = {}, restOptions = {}) {
|
||||||
restOptions = restOptions || {};
|
|
||||||
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
this.className = className;
|
this.className = className;
|
||||||
this.restWhere = restWhere || {};
|
this.restWhere = restWhere;
|
||||||
this.response = null;
|
this.response = null;
|
||||||
|
|
||||||
this.findOptions = {};
|
this.findOptions = {};
|
||||||
@@ -317,35 +318,35 @@ RestQuery.prototype.replaceDontSelect = function() {
|
|||||||
RestQuery.prototype.runFind = function() {
|
RestQuery.prototype.runFind = function() {
|
||||||
return this.config.database.find(
|
return this.config.database.find(
|
||||||
this.className, this.restWhere, this.findOptions).then((results) => {
|
this.className, this.restWhere, this.findOptions).then((results) => {
|
||||||
if (this.className == '_User') {
|
if (this.className == '_User') {
|
||||||
for (var result of results) {
|
for (var result of results) {
|
||||||
delete result.password;
|
delete result.password;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateParseFiles(this.config, results);
|
this.config.filesController.expandFilesInObject(this.config, results);
|
||||||
|
|
||||||
if (this.keys) {
|
if (this.keys) {
|
||||||
var keySet = this.keys;
|
var keySet = this.keys;
|
||||||
results = results.map((object) => {
|
results = results.map((object) => {
|
||||||
var newObject = {};
|
var newObject = {};
|
||||||
for (var key in object) {
|
for (var key in object) {
|
||||||
if (keySet.has(key)) {
|
if (keySet.has(key)) {
|
||||||
newObject[key] = object[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.
|
// Returns a promise for whether it was successful.
|
||||||
@@ -498,35 +499,6 @@ function replacePointers(object, path, replace) {
|
|||||||
return answer;
|
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.
|
// Finds a subobject that has the given key, if there is one.
|
||||||
// Returns undefined otherwise.
|
// Returns undefined otherwise.
|
||||||
function findObjectWithKey(root, key) {
|
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'),
|
cache = require('./cache'),
|
||||||
DatabaseAdapter = require('./DatabaseAdapter'),
|
DatabaseAdapter = require('./DatabaseAdapter'),
|
||||||
express = require('express'),
|
express = require('express'),
|
||||||
FilesAdapter = require('./FilesAdapter'),
|
|
||||||
S3Adapter = require('./S3Adapter'),
|
|
||||||
middlewares = require('./middlewares'),
|
middlewares = require('./middlewares'),
|
||||||
multer = require('multer'),
|
multer = require('multer'),
|
||||||
Parse = require('parse/node').Parse,
|
Parse = require('parse/node').Parse,
|
||||||
PromiseRouter = require('./PromiseRouter'),
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
httpRequest = require('./httpRequest');
|
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
|
// Mutate the Parse object to add the Cloud Code handlers
|
||||||
addParseCloud();
|
addParseCloud();
|
||||||
|
|
||||||
@@ -46,9 +49,9 @@ function ParseServer(args) {
|
|||||||
if (args.databaseAdapter) {
|
if (args.databaseAdapter) {
|
||||||
DatabaseAdapter.setAdapter(args.databaseAdapter);
|
DatabaseAdapter.setAdapter(args.databaseAdapter);
|
||||||
}
|
}
|
||||||
if (args.filesAdapter) {
|
|
||||||
FilesAdapter.setAdapter(args.filesAdapter);
|
let filesAdapter = args.filesAdapter || new GridStoreAdapter();
|
||||||
}
|
|
||||||
if (args.databaseURI) {
|
if (args.databaseURI) {
|
||||||
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
|
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
|
||||||
}
|
}
|
||||||
@@ -64,6 +67,8 @@ function ParseServer(args) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let filesController = new FilesController(filesAdapter);
|
||||||
|
|
||||||
cache.apps[args.appId] = {
|
cache.apps[args.appId] = {
|
||||||
masterKey: args.masterKey,
|
masterKey: args.masterKey,
|
||||||
collectionPrefix: args.collectionPrefix || '',
|
collectionPrefix: args.collectionPrefix || '',
|
||||||
@@ -72,7 +77,8 @@ function ParseServer(args) {
|
|||||||
dotNetKey: args.dotNetKey || '',
|
dotNetKey: args.dotNetKey || '',
|
||||||
restAPIKey: args.restAPIKey || '',
|
restAPIKey: args.restAPIKey || '',
|
||||||
fileKey: args.fileKey || 'invalid-file-key',
|
fileKey: args.fileKey || 'invalid-file-key',
|
||||||
facebookAppIds: args.facebookAppIds || []
|
facebookAppIds: args.facebookAppIds || [],
|
||||||
|
filesController: filesController
|
||||||
};
|
};
|
||||||
|
|
||||||
// To maintain compatibility. TODO: Remove in v2.1
|
// To maintain compatibility. TODO: Remove in v2.1
|
||||||
@@ -91,7 +97,7 @@ function ParseServer(args) {
|
|||||||
var api = express();
|
var api = express();
|
||||||
|
|
||||||
// File handling needs to be before default middlewares are applied
|
// 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
|
// TODO: separate this from the regular ParseServer object
|
||||||
if (process.env.TESTING == 1) {
|
if (process.env.TESTING == 1) {
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ function handleLogIn(req) {
|
|||||||
user.sessionToken = token;
|
user.sessionToken = token;
|
||||||
delete user.password;
|
delete user.password;
|
||||||
|
|
||||||
|
req.config.filesController.expandFilesInObject(req.config, user);
|
||||||
|
|
||||||
var expiresAt = new Date();
|
var expiresAt = new Date();
|
||||||
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
|
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user