Refactors FilesController in FilesRouter and FilesController

This commit is contained in:
Florent Vilmart
2016-02-20 12:25:43 -05:00
parent b490688652
commit 305879a251
6 changed files with 181 additions and 154 deletions

View File

@@ -0,0 +1,27 @@
var FilesController = require('../src/Controllers/FilesController').FilesController;
var Config = require("../src/Config");
// Small additional tests to improve overall coverage
describe("FilesController",()=>{
it("should properly expand objects", (done) => {
var config = new Config(Parse.applicationId);
var filesController = new FilesController();
var result = filesController.expandFilesInObject(config, function(){});
expect(result).toBeUndefined();
var fullFile = {
type: '__type',
url: "http://an.url"
}
var anObject = {
aFile: fullFile
}
filesController.expandFilesInObject(config, anObject);
expect(anObject.aFile.url).toEqual("http://an.url");
done();
})
})

View File

@@ -1,34 +1,37 @@
// A Config object provides information about how a specific app is // A Config object provides information about how a specific app is
// configured. // configured.
// mount is the URL for the root of the API; includes http, domain, etc. // mount is the URL for the root of the API; includes http, domain, etc.
function Config(applicationId, mount) { export class Config {
var cache = require('./cache');
var DatabaseAdapter = require('./DatabaseAdapter');
var cacheInfo = cache.apps[applicationId]; constructor(applicationId, mount) {
this.valid = !!cacheInfo; var cache = require('./cache');
if (!this.valid) { var DatabaseAdapter = require('./DatabaseAdapter');
return;
var cacheInfo = cache.apps[applicationId];
this.valid = !!cacheInfo;
if (!this.valid) {
return;
}
this.applicationId = applicationId;
this.collectionPrefix = cacheInfo.collectionPrefix || '';
this.masterKey = cacheInfo.masterKey;
this.clientKey = cacheInfo.clientKey;
this.javascriptKey = cacheInfo.javascriptKey;
this.dotNetKey = cacheInfo.dotNetKey;
this.restAPIKey = cacheInfo.restAPIKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
this.filesController = cacheInfo.filesController;
this.pushController = cacheInfo.pushController;
this.oauth = cacheInfo.oauth;
this.mount = mount;
} }
};
this.applicationId = applicationId; export default Config;
this.collectionPrefix = cacheInfo.collectionPrefix || '';
this.masterKey = cacheInfo.masterKey;
this.clientKey = cacheInfo.clientKey;
this.javascriptKey = cacheInfo.javascriptKey;
this.dotNetKey = cacheInfo.dotNetKey;
this.restAPIKey = cacheInfo.restAPIKey;
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
this.filesController = cacheInfo.filesController;
this.pushController = cacheInfo.pushController;
this.oauth = cacheInfo.oauth;
this.mount = mount;
}
module.exports = Config; module.exports = Config;

View File

@@ -1,11 +1,5 @@
// FilesController.js // FilesController.js
import express from 'express';
import mime from 'mime';
import { Parse } from 'parse/node'; import { Parse } from 'parse/node';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import Config from '../Config';
import { randomHexString } from '../cryptoUtils'; import { randomHexString } from '../cryptoUtils';
export class FilesController { export class FilesController {
@@ -13,98 +7,23 @@ export class FilesController {
this._filesAdapter = filesAdapter; this._filesAdapter = filesAdapter;
} }
static getHandler() { getFileData(config, filename) {
return (req, res) => { return this._filesAdapter.getFileData(config, filename);
let config = new Config(req.params.appId);
return config.filesController.getHandler()(req, res);
}
} }
getHandler() { createFile(config, filename, data) {
return (req, res) => { filename = randomHexString(32) + '_' + filename;
let config = new Config(req.params.appId); var location = this._filesAdapter.getFileLocation(config, filename);
let filename = req.params.filename; return this._filesAdapter.createFile(config, filename, data).then(() => {
this._filesAdapter.getFileData(config, filename).then((data) => { return Promise.resolve({
res.status(200); url: location,
var contentType = mime.lookup(filename); name: 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.');
}); });
}; });
} }
static createHandler() { deleteFile(config, filename) {
return (req, res, next) => { return this._filesAdapter.deleteFile(config, filename);
let config = req.config;
return config.filesController.createHandler()(req, res, next);
}
}
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;
}
const filesController = req.config.filesController;
// 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 = randomHexString(32) + '_' + req.params.filename + extension;
filesController._filesAdapter.createFile(req.config, filename, req.body).then(() => {
res.status(201);
var location = filesController._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.'));
});
};
}
static deleteHandler() {
return (req, res, next) => {
let config = req.config;
return config.filesController.deleteHandler()(req, res, next);
}
}
deleteHandler() {
return (req, res, next) => {
this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => {
res.status(200);
// TODO: return useful JSON here?
res.end();
}).catch((error) => {
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR,
'Could not delete file.'));
});
};
} }
/** /**
@@ -135,32 +54,6 @@ export class FilesController {
} }
} }
} }
static getExpressRouter() {
let router = express.Router();
router.get('/files/:appId/:filename', FilesController.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,
FilesController.createHandler()
);
router.delete('/files/:filename',
Middlewares.allowCrossDomain,
Middlewares.handleParseHeaders,
Middlewares.enforceMasterKeyAccess,
FilesController.deleteHandler()
);
return router;
}
} }
export default FilesController; export default FilesController;

View File

@@ -1,7 +1,4 @@
// AnalyticsRouter.js // AnalyticsRouter.js
var Parse = require('parse/node').Parse;
import PromiseRouter from '../PromiseRouter'; import PromiseRouter from '../PromiseRouter';
// Returns a promise that resolves to an empty object response // Returns a promise that resolves to an empty object response

104
src/Routers/FilesRouter.js Normal file
View File

@@ -0,0 +1,104 @@
import PromiseRouter from '../PromiseRouter';
import express from 'express';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import { randomHexString } from '../cryptoUtils';
import mime from 'mime';
import Config from '../Config';
export class FilesRouter {
getExpressRouter() {
var 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
);
router.delete('/files/:filename',
Middlewares.allowCrossDomain,
Middlewares.handleParseHeaders,
Middlewares.enforceMasterKeyAccess,
this.deleteHandler
);
return router;
}
getHandler(req, res, next) {
const config = new Config(req.params.appId);
const filesController = config.filesController;
const filename = req.params.filename;
filesController.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(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;
}
let extension = '';
// Not very safe there.
const hasExtension = req.params.filename.indexOf('.') > 0;
const contentType = req.get('Content-type');
if (!hasExtension && contentType && mime.extension(contentType)) {
extension = '.' + mime.extension(contentType);
}
const filename = req.params.filename + extension;
const config = req.config;
const filesController = config.filesController;
filesController.createFile(config, filename, req.body).then((result) => {
res.status(201);
res.set('Location', result.url);
res.json(result);
}).catch((err) => {
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
'Could not store file.'));
});
}
deleteHandler(req, res, next) {
const filesController = req.config.filesController;
filesController.deleteFile(req.config, req.params.filename).then(() => {
res.status(200);
// TODO: return useful JSON here?
res.end();
}).catch((error) => {
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR,
'Could not delete file.'));
});
}
}

View File

@@ -29,6 +29,7 @@ import { FunctionsRouter } from './Routers/FunctionsRouter';
import { SchemasRouter } from './Routers/SchemasRouter'; import { SchemasRouter } from './Routers/SchemasRouter';
import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { IAPValidationRouter } from './Routers/IAPValidationRouter';
import { PushRouter } from './Routers/PushRouter'; import { PushRouter } from './Routers/PushRouter';
import { FilesRouter } from './Routers/FilesRouter';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { LoggerController } from './Controllers/LoggerController'; import { LoggerController } from './Controllers/LoggerController';
@@ -111,8 +112,9 @@ function ParseServer({
} }
} }
let filesController = new FilesController(filesAdapter); const filesController = new FilesController(filesAdapter);
let pushController = new PushController(pushAdapter); const pushController = new PushController(pushAdapter);
const loggerController = new LoggerController(loggerAdapter);
cache.apps[appId] = { cache.apps[appId] = {
masterKey: masterKey, masterKey: masterKey,
@@ -125,6 +127,7 @@ function ParseServer({
facebookAppIds: facebookAppIds, facebookAppIds: facebookAppIds,
filesController: filesController, filesController: filesController,
pushController: pushController, pushController: pushController,
loggerController: loggerController,
enableAnonymousUsers: enableAnonymousUsers, enableAnonymousUsers: enableAnonymousUsers,
oauth: oauth, oauth: oauth,
}; };
@@ -143,7 +146,7 @@ function ParseServer({
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('/', FilesController.getExpressRouter()); api.use('/', new FilesRouter().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) {