From 0bae4ef6ca607155639b0c362a420cfb5d1a9110 Mon Sep 17 00:00:00 2001 From: wangmengyan95 Date: Mon, 8 Feb 2016 23:24:34 -0800 Subject: [PATCH 1/8] Add npmignore --- .npmignore | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .npmignore diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..5e3ded83 --- /dev/null +++ b/.npmignore @@ -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 From 53fdc9bdebe56576910c6f2ec12228318d736b88 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 8 Feb 2016 22:51:58 -0800 Subject: [PATCH 2/8] Refactor FilesAdapter to ES6 style. --- src/FilesAdapter.js | 25 ++++++++------- src/GridStoreAdapter.js | 69 +++++++++++++++++++---------------------- src/files.js | 12 +++---- src/index.js | 8 +++-- 4 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/FilesAdapter.js b/src/FilesAdapter.js index 427e20d9..94fd2bb8 100644 --- a/src/FilesAdapter.js +++ b/src/FilesAdapter.js @@ -3,27 +3,28 @@ // 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) +// * createFileAsync(config, filename, data) +// * getFileDataAsync(config, filename) +// * getFileLocation(config, request, filename) // // Default is GridStoreAdapter, which requires mongo // and for the API server to be using the ExportAdapter // database adapter. -var GridStoreAdapter = require('./GridStoreAdapter'); +let adapter = null; -var adapter = GridStoreAdapter; - -function setAdapter(filesAdapter) { +export function setAdapter(filesAdapter) { adapter = filesAdapter; } -function getAdapter() { +export function getAdapter() { return adapter; } -module.exports = { - getAdapter: getAdapter, - setAdapter: setAdapter -}; +export class FilesAdapter { + createFileAsync(config, filename, data) { } + + getFileDataAsync(config, filename) { } + + getFileLocation(config, request, filename) { } +} diff --git a/src/GridStoreAdapter.js b/src/GridStoreAdapter.js index 0d1e8965..161515c6 100644 --- a/src/GridStoreAdapter.js +++ b/src/GridStoreAdapter.js @@ -3,46 +3,41 @@ // Stores files in Mongo using GridStore // Requires the database adapter to be based on mongoclient -var GridStore = require('mongodb').GridStore; -var path = require('path'); +import { GridStore } from 'mongodb'; -// 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(); - }); -} +import * as Path from 'path'; +import { FilesAdapter } from './FilesAdapter'; -// 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(); - }); -} +class GridStoreAdapter extends FilesAdapter { + // For a given config object, filename, and data, store a file + // Returns a promise + createFileAsync(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(); + }); + } -// 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 + + getFileDataAsync(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, request, filename) { + return (request.protocol + '://' + request.get('host') + + Path.dirname(request.originalUrl) + '/' + config.applicationId + '/' + encodeURIComponent(filename)); + } } -module.exports = { - create: create, - get: get, - location: location -}; +export default GridStoreAdapter; diff --git a/src/files.js b/src/files.js index a840e098..86cdbfbe 100644 --- a/src/files.js +++ b/src/files.js @@ -3,12 +3,13 @@ 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(); +import { getAdapter as getFilesAdapter } from './FilesAdapter'; + var router = express.Router(); var processCreate = function(req, res, next) { @@ -40,13 +41,13 @@ var processCreate = function(req, res, next) { } var filename = rack() + '_' + req.params.filename + extension; - FilesAdapter.getAdapter().create(req.config, filename, req.body) - .then(() => { + getFilesAdapter().createFileAsync(req.config, filename, req.body).then(() => { res.status(201); - var location = FilesAdapter.getAdapter().location(req.config, req, filename); + var location = getFilesAdapter().getFileLocation(req.config, req, filename); res.set('Location', location); res.json({ url: location, name: filename }); }).catch((error) => { + console.log(error); next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.')); }); @@ -54,8 +55,7 @@ var processCreate = function(req, res, next) { var processGet = function(req, res) { var config = new Config(req.params.appId); - FilesAdapter.getAdapter().get(config, req.params.filename) - .then((data) => { + getFilesAdapter().getFileDataAsync(config, req.params.filename).then((data) => { res.status(200); var contentType = mime.lookup(req.params.filename); res.set('Content-type', contentType); diff --git a/src/index.js b/src/index.js index 37a88b89..48d3e8c9 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,6 @@ 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'), @@ -13,6 +12,9 @@ var batch = require('./batch'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); +import { setAdapter as setFilesAdapter } from './FilesAdapter'; +import { default as GridStoreAdapter } from './GridStoreAdapter'; + // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -47,7 +49,9 @@ function ParseServer(args) { DatabaseAdapter.setAdapter(args.databaseAdapter); } if (args.filesAdapter) { - FilesAdapter.setAdapter(args.filesAdapter); + setFilesAdapter(args.filesAdapter); + } else { + setFilesAdapter(new GridStoreAdapter()); } if (args.databaseURI) { DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI); From 25b13ff632e07433ef6ac4c339623f684f4acf58 Mon Sep 17 00:00:00 2001 From: David Keegan Date: Tue, 9 Feb 2016 00:56:00 -0800 Subject: [PATCH 3/8] Remove duplicate dotNetKey --- bin/parse-server | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/parse-server b/bin/parse-server index 902e43b2..7ec17927 100755 --- a/bin/parse-server +++ b/bin/parse-server @@ -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 From 4f128d761ea158bc684ccace237f085d56930c28 Mon Sep 17 00:00:00 2001 From: ksaldana1 Date: Tue, 9 Feb 2016 15:47:13 -0600 Subject: [PATCH 4/8] Implemented ES6 default parameters where valid in ExportAdapter and RestQuery --- src/ExportAdapter.js | 15 +++++---------- src/RestQuery.js | 5 ++--- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/ExportAdapter.js b/src/ExportAdapter.js index c21042fb..1676ccfb 100644 --- a/src/ExportAdapter.js +++ b/src/ExportAdapter.js @@ -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; @@ -63,8 +62,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) => { @@ -277,8 +275,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 || []; @@ -346,8 +343,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(); }); @@ -503,8 +499,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; diff --git a/src/RestQuery.js b/src/RestQuery.js index 8c9bf712..d677e2dd 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -11,13 +11,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 = {}; From 53b2d4e1761cf5abbbbd2b3d465200263712281b Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Tue, 9 Feb 2016 12:53:02 -0800 Subject: [PATCH 5/8] Refactor files.js into FilesController. --- src/Controllers/FilesController.js | 97 ++++++++++++++++++++++++++++++ src/FilesAdapter.js | 12 +--- src/files.js | 85 -------------------------- src/index.js | 13 ++-- 4 files changed, 105 insertions(+), 102 deletions(-) create mode 100644 src/Controllers/FilesController.js delete mode 100644 src/files.js diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js new file mode 100644 index 00000000..4a929a6e --- /dev/null +++ b/src/Controllers/FilesController.js @@ -0,0 +1,97 @@ +// 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); + this._filesAdapter.getFileDataAsync(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.'); + }); + }; + } + + 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.createFileAsync(req.config, filename, req.body).then(() => { + res.status(201); + var location = this._filesAdapter.getFileLocation(req.config, req, filename); + res.set('Location', location); + res.json({ url: location, name: filename }); + }).catch((error) => { + console.log(error); + next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, + 'Could not store file.')); + }); + }; + } + + 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; diff --git a/src/FilesAdapter.js b/src/FilesAdapter.js index 94fd2bb8..62fe0701 100644 --- a/src/FilesAdapter.js +++ b/src/FilesAdapter.js @@ -11,16 +11,6 @@ // and for the API server to be using the ExportAdapter // database adapter. -let adapter = null; - -export function setAdapter(filesAdapter) { - adapter = filesAdapter; -} - -export function getAdapter() { - return adapter; -} - export class FilesAdapter { createFileAsync(config, filename, data) { } @@ -28,3 +18,5 @@ export class FilesAdapter { getFileLocation(config, request, filename) { } } + +export default FilesAdapter; diff --git a/src/files.js b/src/files.js deleted file mode 100644 index 86cdbfbe..00000000 --- a/src/files.js +++ /dev/null @@ -1,85 +0,0 @@ -// files.js - -var bodyParser = require('body-parser'), - Config = require('./Config'), - express = require('express'), - middlewares = require('./middlewares.js'), - mime = require('mime'), - Parse = require('parse/node').Parse, - rack = require('hat').rack(); - -import { getAdapter as getFilesAdapter } from './FilesAdapter'; - -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; - getFilesAdapter().createFileAsync(req.config, filename, req.body).then(() => { - res.status(201); - var location = getFilesAdapter().getFileLocation(req.config, req, filename); - res.set('Location', location); - res.json({ url: location, name: filename }); - }).catch((error) => { - console.log(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); - getFilesAdapter().getFileDataAsync(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 -}; diff --git a/src/index.js b/src/index.js index 48d3e8c9..93d363f7 100644 --- a/src/index.js +++ b/src/index.js @@ -12,8 +12,8 @@ var batch = require('./batch'), PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); -import { setAdapter as setFilesAdapter } from './FilesAdapter'; import { default as GridStoreAdapter } from './GridStoreAdapter'; +import { default as FilesController } from './Controllers/FilesController'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud(); @@ -48,11 +48,9 @@ function ParseServer(args) { if (args.databaseAdapter) { DatabaseAdapter.setAdapter(args.databaseAdapter); } - if (args.filesAdapter) { - setFilesAdapter(args.filesAdapter); - } else { - setFilesAdapter(new GridStoreAdapter()); - } + + let filesAdapter = args.filesAdapter || new GridStoreAdapter(); + if (args.databaseURI) { DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI); } @@ -95,7 +93,8 @@ function ParseServer(args) { var api = express(); // File handling needs to be before default middlewares are applied - api.use('/', require('./files').router); + let filesController = new FilesController(filesAdapter); + api.use('/', filesController.getExpressRouter()); // TODO: separate this from the regular ParseServer object if (process.env.TESTING == 1) { From 8ca25cbabe1de7665b7d1cc3f1be2cde19ef2b21 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Tue, 9 Feb 2016 19:31:23 -0800 Subject: [PATCH 6/8] Moved getting the url for every file from RestQuery into FilesController. --- src/Config.js | 5 +- src/Controllers/FilesController.js | 37 ++++++++++++-- src/FilesAdapter.js | 2 +- src/GridStoreAdapter.js | 8 +--- src/RestQuery.js | 77 ++++++++++-------------------- src/index.js | 6 ++- 6 files changed, 69 insertions(+), 66 deletions(-) diff --git a/src/Config.js b/src/Config.js index df44f8b1..06d7af94 100644 --- a/src/Config.js +++ b/src/Config.js @@ -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; } diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 4a929a6e..a22e2572 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -18,9 +18,10 @@ export class FilesController { getHandler() { return (req, res) => { let config = new Config(req.params.appId); - this._filesAdapter.getFileDataAsync(config, req.params.filename).then((data) => { + let filename = req.params.filename; + this._filesAdapter.getFileDataAsync(config, filename).then((data) => { res.status(200); - var contentType = mime.lookup(req.params.filename); + var contentType = mime.lookup(filename); res.set('Content-type', contentType); res.end(data); }).catch((error) => { @@ -63,17 +64,45 @@ export class FilesController { let filename = rack() + '_' + req.params.filename + extension; this._filesAdapter.createFileAsync(req.config, filename, req.body).then(() => { res.status(201); - var location = this._filesAdapter.getFileLocation(req.config, req, filename); + var location = this._filesAdapter.getFileLocation(req.config, filename); res.set('Location', location); res.json({ url: location, name: filename }); }).catch((error) => { - console.log(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()); diff --git a/src/FilesAdapter.js b/src/FilesAdapter.js index 62fe0701..dbfc923c 100644 --- a/src/FilesAdapter.js +++ b/src/FilesAdapter.js @@ -16,7 +16,7 @@ export class FilesAdapter { getFileDataAsync(config, filename) { } - getFileLocation(config, request, filename) { } + getFileLocation(config, filename) { } } export default FilesAdapter; diff --git a/src/GridStoreAdapter.js b/src/GridStoreAdapter.js index 161515c6..d221b021 100644 --- a/src/GridStoreAdapter.js +++ b/src/GridStoreAdapter.js @@ -4,8 +4,6 @@ // Requires the database adapter to be based on mongoclient import { GridStore } from 'mongodb'; - -import * as Path from 'path'; import { FilesAdapter } from './FilesAdapter'; class GridStoreAdapter extends FilesAdapter { @@ -33,10 +31,8 @@ class GridStoreAdapter extends FilesAdapter { }); } - getFileLocation(config, request, filename) { - return (request.protocol + '://' + request.get('host') + - Path.dirname(request.originalUrl) + '/' + config.applicationId + - '/' + encodeURIComponent(filename)); + getFileLocation(config, filename) { + return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename)); } } diff --git a/src/RestQuery.js b/src/RestQuery.js index d677e2dd..91ebe536 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -3,6 +3,8 @@ var Parse = require('parse/node').Parse; +import { default as FilesController } from './Controllers/FilesController'; + // restOptions can include: // skip // limit @@ -316,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. @@ -497,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) { diff --git a/src/index.js b/src/index.js index 93d363f7..d685e79f 100644 --- a/src/index.js +++ b/src/index.js @@ -66,6 +66,8 @@ function ParseServer(args) { } + let filesController = new FilesController(filesAdapter); + cache.apps[args.appId] = { masterKey: args.masterKey, collectionPrefix: args.collectionPrefix || '', @@ -74,7 +76,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 @@ -93,7 +96,6 @@ function ParseServer(args) { var api = express(); // File handling needs to be before default middlewares are applied - let filesController = new FilesController(filesAdapter); api.use('/', filesController.getExpressRouter()); // TODO: separate this from the regular ParseServer object From 053ac990e69406f5291e8b2328e3205327380cd4 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Tue, 9 Feb 2016 19:31:50 -0800 Subject: [PATCH 7/8] Fixed missing url for files on user login. --- spec/ParseUser.spec.js | 16 ++++++++++++++++ src/users.js | 2 ++ 2 files changed, 18 insertions(+) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index c9f25bd8..6c7ec26b 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -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; diff --git a/src/users.js b/src/users.js index d769b9c5..5f0e01e7 100644 --- a/src/users.js +++ b/src/users.js @@ -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); From 07c9c1d648df650f9fb1a79d4756e265ed1c2b24 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Tue, 9 Feb 2016 19:52:25 -0800 Subject: [PATCH 8/8] Cleanup and modernize S3Adapter to ES6 syntax. --- src/{ => Adapters/Files}/FilesAdapter.js | 8 +- src/{ => Adapters/Files}/GridStoreAdapter.js | 6 +- src/Adapters/Files/S3Adapter.js | 83 ++++++++++++++++++++ src/Controllers/FilesController.js | 4 +- src/S3Adapter.js | 77 ------------------ src/index.js | 7 +- 6 files changed, 96 insertions(+), 89 deletions(-) rename src/{ => Adapters/Files}/FilesAdapter.js (70%) rename src/{ => Adapters/Files}/GridStoreAdapter.js (89%) create mode 100644 src/Adapters/Files/S3Adapter.js delete mode 100644 src/S3Adapter.js diff --git a/src/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js similarity index 70% rename from src/FilesAdapter.js rename to src/Adapters/Files/FilesAdapter.js index dbfc923c..9daed517 100644 --- a/src/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -3,8 +3,8 @@ // Allows you to change the file storage mechanism. // // Adapter classes must implement the following functions: -// * createFileAsync(config, filename, data) -// * getFileDataAsync(config, filename) +// * createFile(config, filename, data) +// * getFileData(config, filename) // * getFileLocation(config, request, filename) // // Default is GridStoreAdapter, which requires mongo @@ -12,9 +12,9 @@ // database adapter. export class FilesAdapter { - createFileAsync(config, filename, data) { } + createFile(config, filename, data) { } - getFileDataAsync(config, filename) { } + getFileData(config, filename) { } getFileLocation(config, filename) { } } diff --git a/src/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js similarity index 89% rename from src/GridStoreAdapter.js rename to src/Adapters/Files/GridStoreAdapter.js index d221b021..8c95319d 100644 --- a/src/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -6,10 +6,10 @@ import { GridStore } from 'mongodb'; import { FilesAdapter } from './FilesAdapter'; -class GridStoreAdapter extends FilesAdapter { +export class GridStoreAdapter extends FilesAdapter { // For a given config object, filename, and data, store a file // Returns a promise - createFileAsync(config, filename, data) { + createFile(config, filename, data) { return config.database.connect().then(() => { let gridStore = new GridStore(config.database.db, filename, 'w'); return gridStore.open(); @@ -20,7 +20,7 @@ class GridStoreAdapter extends FilesAdapter { }); } - getFileDataAsync(config, filename) { + getFileData(config, filename) { return config.database.connect().then(() => { return GridStore.exist(config.database.db, filename); }).then(() => { diff --git a/src/Adapters/Files/S3Adapter.js b/src/Adapters/Files/S3Adapter.js new file mode 100644 index 00000000..2c892246 --- /dev/null +++ b/src/Adapters/Files/S3Adapter.js @@ -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; diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index a22e2572..47454f07 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -19,7 +19,7 @@ export class FilesController { return (req, res) => { let config = new Config(req.params.appId); let filename = req.params.filename; - this._filesAdapter.getFileDataAsync(config, filename).then((data) => { + this._filesAdapter.getFileData(config, filename).then((data) => { res.status(200); var contentType = mime.lookup(filename); res.set('Content-type', contentType); @@ -62,7 +62,7 @@ export class FilesController { } let filename = rack() + '_' + req.params.filename + extension; - this._filesAdapter.createFileAsync(req.config, filename, req.body).then(() => { + this._filesAdapter.createFile(req.config, filename, req.body).then(() => { res.status(201); var location = this._filesAdapter.getFileLocation(req.config, filename); res.set('Location', location); diff --git a/src/S3Adapter.js b/src/S3Adapter.js deleted file mode 100644 index 736ebf8b..00000000 --- a/src/S3Adapter.js +++ /dev/null @@ -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; diff --git a/src/index.js b/src/index.js index d685e79f..73d48298 100644 --- a/src/index.js +++ b/src/index.js @@ -5,15 +5,16 @@ var batch = require('./batch'), cache = require('./cache'), DatabaseAdapter = require('./DatabaseAdapter'), express = require('express'), - S3Adapter = require('./S3Adapter'), middlewares = require('./middlewares'), multer = require('multer'), Parse = require('parse/node').Parse, PromiseRouter = require('./PromiseRouter'), httpRequest = require('./httpRequest'); -import { default as GridStoreAdapter } from './GridStoreAdapter'; -import { default as FilesController } from './Controllers/FilesController'; +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();