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