Skip to content

Commit 1ec0b10

Browse files
author
Mike Patnode
committed
Allow filename validation and key generation to be methods
1 parent aa6f631 commit 1ec0b10

File tree

6 files changed

+125
-88
lines changed

6 files changed

+125
-88
lines changed

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ The preferred method is to use the default AWS credentials pattern. If no AWS c
6767
"signatureVersion": 'v4', // default value
6868
"globalCacheControl": null, // default value. Or 'public, max-age=86400' for 24 hrs Cache-Control
6969
"ServerSideEncryption": 'AES256|aws:kms', //AES256 or aws:kms, or if you do not pass this, encryption won't be done
70-
"fileNameCheck": 'strict', // safe, strict or loose.
71-
"preserveFileName": 'always' // never, haspath, always (default always)
70+
"validateFilename": null, // Default to parse-server FilesAdapter::validateFilename.
71+
"generateKey": null // Will default to Parse.FilesController.preserveFileName
7272
}
7373
}
7474
}
@@ -113,8 +113,15 @@ var s3Adapter = new S3Adapter('accessKey',
113113
baseUrl: 'http://images.example.com',
114114
signatureVersion: 'v4',
115115
globalCacheControl: 'public, max-age=86400', // 24 hrs Cache-Control.
116-
fileNameCheck: 'safe' // allow "directory" creation
117-
preserveFileName: 'haspath' // keep the filename if there's a / (directory) in the name
116+
validateFilename: (filename) => {
117+
if (filename.length > 1024) {
118+
return 'Filename too long.';
119+
}
120+
return null; // Return null on success
121+
},
122+
generateKey: (filename) => {
123+
return `${Date.now()}_${filename}`; // unique prefix for every filename
124+
}
118125
});
119126
120127
var api = new ParseServer({
@@ -151,8 +158,8 @@ var s3Options = {
151158
"baseUrl": null // default value
152159
"signatureVersion": 'v4', // default value
153160
"globalCacheControl": null, // default value. Or 'public, max-age=86400' for 24 hrs Cache-Control
154-
"fileNameCheck": 'loose' // anything goes
155-
"preserveFileName": 'never' // Ensure Parse.FileController.preserveFileName is true!
161+
"validateFilename": () => null, // Anything goes!
162+
"generateKey": (filename) => filename, // Ensure Parse.FilesController.preserveFileName is true!
156163
}
157164
158165
var s3Adapter = new S3Adapter(s3Options);
@@ -176,8 +183,6 @@ var s3Options = {
176183
baseUrl: process.env.SPACES_BASE_URL,
177184
region: process.env.SPACES_REGION,
178185
directAccess: true,
179-
preserveFilename: "always",
180-
fileNameCheck: "safe",
181186
globalCacheControl: "public, max-age=31536000",
182187
bucketPrefix: process.env.SPACES_BUCKET_PREFIX,
183188
s3overrides: {

index.js

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ class S3Adapter {
2626
this._signatureVersion = options.signatureVersion;
2727
this._globalCacheControl = options.globalCacheControl;
2828
this._encryption = options.ServerSideEncryption;
29-
this._fileNameCheck = options.fileNameCheck;
30-
this._preserveFileName = options.preserveFileName;
29+
this._generateKey = options.generateKey;
30+
// Optional FilesAdaptor method
31+
this.validateFilename = options.validateFilename;
3132

3233
const s3Options = {
3334
params: { Bucket: this._bucket },
@@ -67,22 +68,12 @@ class S3Adapter {
6768
// Returns a promise containing the S3 object creation response
6869
createFile(filename, data, contentType) {
6970
const params = {
70-
Key: this._bucketPrefix,
71+
Key: this._bucketPrefix + filename,
7172
Body: data,
7273
};
7374

74-
let prefix = '';
75-
const lastSlash = filename.lastIndexOf('/');
76-
if (this._preserveFileName === 'never' || (this._preserveFileName === 'haspath' && lastSlash === -1)) {
77-
prefix = `${Date.now()}_`;
78-
}
79-
80-
if (lastSlash > 0) {
81-
// put the prefix before the last component of the filename
82-
params.Key += filename.substring(0, lastSlash + 1) + prefix
83-
+ filename.substring(lastSlash + 1);
84-
} else {
85-
params.Key += prefix + filename;
75+
if (this._generateKey instanceof Function) {
76+
params.Key = this._bucketPrefix + this._generateKey(filename);
8677
}
8778

8879
if (this._directAccess) {
@@ -180,36 +171,6 @@ class S3Adapter {
180171
});
181172
}));
182173
}
183-
184-
validateFilename(filename) {
185-
let regex;
186-
187-
if (filename.length > 1024) {
188-
return 'Filename too long.';
189-
}
190-
191-
// From https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#object-keys
192-
switch (this._fileNameCheck) {
193-
case 'loose':
194-
// Just don't start with a /
195-
regex = /[^/].*$/;
196-
break;
197-
case 'safe':
198-
// eslint-disable-next-line no-control-regex
199-
regex = /^[_a-zA-Z0-9][a-zA-Z0-9@. ~_\-/$&\x00-\x1F\x7F=;:+,?%]*$/;
200-
break;
201-
case 'strict':
202-
default:
203-
regex = /^[_a-zA-Z0-9][a-zA-Z0-9@. ~_-]*$/;
204-
break;
205-
}
206-
207-
if (!filename.match(regex)) {
208-
return 'Filename contains invalid characters.';
209-
}
210-
211-
return null;
212-
}
213174
}
214175

215176
module.exports = S3Adapter;

lib/optionsFromArguments.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ const optionsFromArguments = function optionsFromArguments(args) {
6464
options.signatureVersion = otherOptions.signatureVersion;
6565
options.globalCacheControl = otherOptions.globalCacheControl;
6666
options.ServerSideEncryption = otherOptions.ServerSideEncryption;
67-
options.fileNameCheck = otherOptions.fileNameCheck;
68-
options.preserveFileName = otherOptions.preserveFileName;
67+
options.generateKey = otherOptions.generateKey;
68+
options.validateFilename = otherOptions.validateFilename;
6969
s3overrides = otherOptions.s3overrides;
7070
}
7171
} else if (args.length === 1) {
@@ -92,8 +92,8 @@ const optionsFromArguments = function optionsFromArguments(args) {
9292
options = fromEnvironmentOrDefault(options, 'baseUrlDirect', 'S3_BASE_URL_DIRECT', false);
9393
options = fromEnvironmentOrDefault(options, 'signatureVersion', 'S3_SIGNATURE_VERSION', 'v4');
9494
options = fromEnvironmentOrDefault(options, 'globalCacheControl', 'S3_GLOBAL_CACHE_CONTROL', null);
95-
options = fromEnvironmentOrDefault(options, 'fileNameCheck', 'S3_FILENAME_CHECK', 'strict');
96-
options = fromEnvironmentOrDefault(options, 'preserveFileName', 'S3_PRESERVE_FILENAME', 'always');
95+
options = fromOptionsDictionaryOrDefault(options, 'generateKey', null);
96+
options = fromOptionsDictionaryOrDefault(options, 'validateFilename', null);
9797

9898
return options;
9999
};

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
},
2626
"homepage": "https://github.com/parse-community/parse-server-s3-adapter#readme",
2727
"dependencies": {
28-
"aws-sdk": "^2.59.0"
28+
"aws-sdk": "^2.59.0",
29+
"parse": "^2.9.1"
2930
},
3031
"devDependencies": {
3132
"codecov": "^3.0.0",

spec/test.spec.js

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const AWS = require('aws-sdk');
22
const config = require('config');
33
const filesAdapterTests = require('parse-server-conformance-tests').files;
4+
const Parse = require('parse').Parse;
45
const S3Adapter = require('../index.js');
56
const optionsFromArguments = require('../lib/optionsFromArguments');
67

@@ -304,32 +305,27 @@ describe('S3Adapter tests', () => {
304305

305306
beforeEach(() => {
306307
options = {
307-
fileNameCheck: 'strict',
308+
validateFilename: null,
308309
};
309310
});
310311

311-
it('should not allow directories when strict', () => {
312-
options.fileNameCheck = 'strict';
312+
it('should be null by default', () => {
313313
const s3 = new S3Adapter('accessKey', 'secretKey', 'myBucket', options);
314-
expect(s3.validateFilename('foo/bar')).toBe('Filename contains invalid characters.');
314+
expect(s3.validateFilename === null).toBe(true);
315315
});
316316

317-
it('should allow directories when safe', () => {
318-
options.fileNameCheck = 'safe';
319-
const s3 = new S3Adapter('accessKey', 'secretKey', 'myBucket', options);
320-
expect(s3.validateFilename('foo/bar')).toBe(null);
321-
});
322-
323-
it('should allow not allow emojis when safe', () => {
324-
options.fileNameCheck = 'safe';
325-
const s3 = new S3Adapter('accessKey', 'secretKey', 'myBucket', options);
326-
expect(s3.validateFilename('foo🛒/bar')).toBe('Filename contains invalid characters.');
327-
});
328-
329-
it('should allow allow emojis when loose', () => {
330-
options.fileNameCheck = 'loose';
317+
it('should not allow directories when overridden', () => {
318+
options.validateFilename = (filename) => {
319+
if (filename.indexOf('/') !== -1) {
320+
return new Parse.Error(
321+
Parse.Error.INVALID_FILE_NAME,
322+
'Filename contains invalid characters.',
323+
);
324+
}
325+
return null;
326+
};
331327
const s3 = new S3Adapter('accessKey', 'secretKey', 'myBucket', options);
332-
expect(s3.validateFilename('foo🛒/bar')).toBe(null);
328+
expect(s3.validateFilename('foo/bar') instanceof Parse.Error).toBe(true);
333329
});
334330
});
335331

@@ -387,19 +383,30 @@ describe('S3Adapter tests', () => {
387383
return s3;
388384
}
389385

390-
describe('preserveFilename', () => {
386+
describe('generateKey', () => {
391387
let options;
392388
const promises = [];
393389

394390
beforeEach(() => {
395391
options = {
396-
fileNameCheck: 'loose',
397-
preserveFileName: 'never',
392+
bucketPrefix: 'test/',
393+
generateKey: (filename) => {
394+
let key = '';
395+
const lastSlash = filename.lastIndexOf('/');
396+
const prefix = `${Date.now()}_`;
397+
if (lastSlash > 0) {
398+
// put the prefix before the last component of the filename
399+
key += filename.substring(0, lastSlash + 1) + prefix
400+
+ filename.substring(lastSlash + 1);
401+
} else {
402+
key += prefix + filename;
403+
}
404+
return key;
405+
},
398406
};
399407
});
400408

401-
it('should add a unique timestamp to the file name when the preserveFileName option is never', () => {
402-
options.preserveFileName = 'never';
409+
it('should return a file with a date stamp inserted in the path', () => {
403410
const s3 = makeS3Adapter(options);
404411
const fileName = 'randomFileName.txt';
405412
const response = s3.createFile(fileName, 'hello world', 'text/utf8').then((value) => {
@@ -409,24 +416,23 @@ describe('S3Adapter tests', () => {
409416
promises.push(response);
410417
});
411418

412-
it('should not add unique timestamp to the file name when the preserveFileName option is hasPath and there is a path', () => {
413-
options.preserveFileName = 'hasPath';
419+
it('should do nothing when null', () => {
420+
options.generateKey = null;
414421
const s3 = makeS3Adapter(options);
415422
const fileName = 'foo/randomFileName.txt';
416423
const response = s3.createFile(fileName, 'hello world', 'text/utf8').then((value) => {
417424
const url = new URL(value.Location);
418-
expect(url.pathname.substring(1)).toEqual(fileName);
425+
expect(url.pathname.substring(1)).toEqual(options.bucketPrefix + fileName);
419426
});
420427
promises.push(response);
421428
});
422429

423-
it('should add unique timestamp to the file name after the last directory when the preserveFileName option is never and there is a path', () => {
424-
options.preserveFileName = 'never';
430+
it('should add unique timestamp to the file name after the last directory when there is a path', () => {
425431
const s3 = makeS3Adapter(options);
426432
const fileName = 'foo/randomFileName.txt';
427433
const response = s3.createFile(fileName, 'hello world', 'text/utf8').then((value) => {
428434
const url = new URL(value.Location);
429-
expect(url.pathname.indexOf('foo/')).toEqual(1);
435+
expect(url.pathname.indexOf('foo/')).toEqual(6);
430436
expect(url.pathname.indexOf('random') > 13).toBe(true);
431437
});
432438
promises.push(response);

0 commit comments

Comments
 (0)