diff --git a/README.md b/README.md index f426aa6b1b..6352eeeef6 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ $ mongodb-runner start $ parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://localhost/test ``` ***Note:*** *If installation with* `-g` *fails due to permission problems* (`npm ERR! code 'EACCES'`), *please refer to [this link](https://docs.npmjs.com/getting-started/fixing-npm-permissions).* - + ### Inside a Docker container ``` @@ -232,6 +232,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo #### Advanced options * `fileKey` - For migrated apps, this is necessary to provide access to files already hosted on Parse. +* `filesCacheControl` - Set `Cache-Control` header when serving files with builtin files router. Defaults to `public, max-age=86400` (1 day). * `allowClientClassCreation` - Set to false to disable client class creation. Defaults to true. * `enableAnonymousUsers` - Set to false to disable anonymous users. Defaults to true. * `auth` - Used to configure support for [3rd party authentication](http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication). @@ -313,14 +314,14 @@ var server = ParseServer({ }, // optional settings to enforce password policies passwordPolicy: { - // Two optional settings to enforce strong passwords. Either one or both can be specified. + // Two optional settings to enforce strong passwords. Either one or both can be specified. // If both are specified, both checks must pass to accept the password - // 1. a RegExp object or a regex string representing the pattern to enforce + // 1. a RegExp object or a regex string representing the pattern to enforce validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // enforce password with at least 8 char with at least 1 lower case, 1 upper case and 1 digit // 2. a callback function to be invoked to validate the password - validatorCallback: (password) => { return validatePassword(password) }, + validatorCallback: (password) => { return validatePassword(password) }, doNotAllowUsername: true, // optional setting to disallow username in passwords - maxPasswordAge: 90, // optional setting in days for password expiry. Login fails if user does not reset the password within this period after signup/last reset. + maxPasswordAge: 90, // optional setting in days for password expiry. Login fails if user does not reset the password within this period after signup/last reset. maxPasswordHistory: 5, // optional setting to prevent reuse of previous n passwords. Maximum value that can be specified is 20. Not specifying it or specifying 0 will not enforce history. //optional setting to set a validity duration for password reset links (in seconds) resetTokenValidityDuration: 24*60*60, // expire after 24 hours @@ -480,4 +481,3 @@ Become a sponsor and get your logo on our README on Github with a link to your s - diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 9ec2c680f3..205b1cf0be 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -668,6 +668,31 @@ describe('Parse.File testing', () => { }); }); + it("responses with configured Cache-Control header", done => { + var headers = { + 'Content-Type': 'text/html', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/cache_control_1.txt', + body: 'cache_control_1', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + request.get({ url: b.url, headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + } }, (error, response) => { + expect(error).toBe(null); + expect(response.headers['cache-control']).toEqual('public, max-age=86400'); + done(); + }); + }) + }); + describe_only_db('mongo')('Gridstore Range tests', () => { it('supports range requests', done => { var headers = { @@ -850,6 +875,32 @@ describe('Parse.File testing', () => { done(); }); }); + + it('supports responses with Cache-Control header for range requests', done => { + var headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/cache_control_2.txt', + body: 'cache_control_2', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + request.get({ url: b.url, headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'Range': 'bytes=0-5' + } }, (error, response) => { + expect(error).toBe(null); + expect(response.headers['cache-control']).toEqual('public, max-age=86400'); + done(); + }); + }); + }); }); // Because GridStore is not loaded on PG, those are perfect diff --git a/spec/helper.js b/spec/helper.js index 95d52aa732..420e000582 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -92,6 +92,7 @@ var defaultConfiguration = { masterKey: 'test', readOnlyMasterKey: 'read-only-test', fileKey: 'test', + filesCacheControl: 'public, max-age=86400', silent, logLevel, push: { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 70ffdb76a4..c1e39b7b42 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -135,6 +135,11 @@ module.exports.ParseServerOptions = { "env": "PARSE_SERVER_FILE_KEY", "help": "Key for your files" }, + "filesCacheControl": { + "env": "PARSE_SERVER_FILES_CACHE_CONTROL", + "help": "Cache-Control header for files router", + "default": "public, max-age=86400" + }, "userSensitiveFields": { "env": "PARSE_SERVER_USER_SENSITIVE_FIELDS", "help": "Personally identifiable information fields in the user table the should be removed for non-authorized users.", diff --git a/src/Options/index.js b/src/Options/index.js index 5d5f4ecc8b..85c3977d70 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -64,6 +64,8 @@ export interface ParseServerOptions { webhookKey: ?string; /* Key for your files */ fileKey: ?string; + /* Cache-Control header for files router */ + filesCacheControl: ?string; // = public, max-age=86400 /* Personally identifiable information fields in the user table the should be removed for non-authorized users. */ userSensitiveFields: ?string[]; // = ["email"] /* Enable (or disable) anon users, defaults to true diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 8312526f30..49ea14ad55 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -40,7 +40,7 @@ export class FilesRouter { const contentType = mime.getType(filename); if (isFileStreamable(req, filesController)) { filesController.getFileStream(config, filename).then((stream) => { - handleFileStream(stream, req, res, contentType); + handleFileStream(stream, req, res, contentType, config); }).catch(() => { res.status(404); res.set('Content-Type', 'text/plain'); @@ -51,6 +51,9 @@ export class FilesRouter { res.status(200); res.set('Content-Type', contentType); res.set('Content-Length', data.length); + if(config.filesCacheControl) { + res.set('Cache-Control', config.filesCacheControl); + } res.end(data); }).catch(() => { res.status(404); @@ -118,7 +121,7 @@ function getRange(req) { // handleFileStream is licenced under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/). // Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/). -function handleFileStream(stream, req, res, contentType) { +function handleFileStream(stream, req, res, contentType, config) { const buffer_size = 1024 * 1024; //1024Kb // Range request, partiall stream the file let { @@ -144,12 +147,16 @@ function handleFileStream(stream, req, res, contentType) { const contentLength = (end - start) + 1; - res.writeHead(206, { + const headers = { 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, 'Accept-Ranges': 'bytes', 'Content-Length': contentLength, - 'Content-Type': contentType, - }); + 'Content-Type': contentType + }; + if(config.filesCacheControl) { + headers['Cache-Control'] = config.filesCacheControl; + } + res.writeHead(206, headers); stream.seek(start, function () { // get gridFile stream