Skip to content

Commit

Permalink
📦 v2.1.0
Browse files Browse the repository at this point in the history
__Major changes:__

- ⚠️ `FSName` now sanitized and limited to 28 symbols, no white-spaces allowed
- ⚠️ `fileId` now sanitized and limited to 20 symbols, no white-spaces allowed
- ⚠️ File extension now sanitized and limited to 20 symbols, no white-spaces allowed
- 📦 Decouple `fs-extra`, use native node.js `fs` module
- 👨‍💻 Add missing `isData` helper via #795, by @harryadel

__Changes:__

- 👷‍♂️ Fix: #827, thanks to @aertms
- 👷‍♂️ Fix: #832, thanks to @polygonwood
- 👷‍♂️ Fix: #813, thanks to @Prinzhorn
- 👨‍💻 Fix: #834, thanks to @bladerunner2020
- 📔 Fix: #803, thanks to @bartenra
- 🏗 Sanitize inputs (`FSName`, `fileId`, `extension`) to avoid command injection
- 📔 Improved documentation, special thanks to @Prinzhorn and @make-github-pseudonymous-again
- 📔 GridFS documentation fix: #818
- 👷‍♂️ Utilize `Meteor._debug` where possible instead of `console.log`

__Other:__

- 🤝 Compatibility with `[email protected]`
  • Loading branch information
dr-dimitru committed Jun 9, 2022
1 parent 0199a2f commit e3a347c
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 65 deletions.
64 changes: 32 additions & 32 deletions .versions
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
[email protected].0
babel-compiler@7.6.0
[email protected].0
[email protected].1
babel-compiler@7.9.0
[email protected].1
[email protected]
[email protected]
[email protected]
callback-hook@1.3.0
callback-hook@1.4.0
[email protected]
[email protected]
ddp-client@2.4.0
ddp-client@2.5.0
[email protected]
ddp-server@2.3.2
ddp-server@2.5.0
[email protected]
dynamic-import@0.6.0
ecmascript@0.15.0
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.11.0
ecmascript-runtime-server@0.10.0
[email protected].1
dynamic-import@0.7.2
ecmascript@0.16.2
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
[email protected].2
[email protected]
[email protected]
[email protected].0
[email protected].1
[email protected]
local-test:ostrio:files@2.0.1
logging@1.2.0
meteor@1.9.3
minimongo@1.6.1
[email protected].5
modules@0.16.0
modules-runtime@0.12.0
mongo@1.10.1
[email protected].2
local-test:ostrio:files@2.1.0
logging@1.3.1
meteor@1.10.0
minimongo@1.8.0
[email protected].8
modules@0.18.0
modules-runtime@0.13.0
mongo@1.15.0
[email protected].3
[email protected]
[email protected].7
npm-mongo@3.8.1
[email protected].8
npm-mongo@4.3.1
[email protected]
ostrio:[email protected].0
ostrio:files@2.0.1
promise@0.11.2
ostrio:[email protected].2
ostrio:files@2.1.0
promise@0.12.0
[email protected]
react-fast-refresh@0.1.0
react-fast-refresh@0.2.3
[email protected]
[email protected]
[email protected]
[email protected].0
socket-stream-client@0.3.1
tinytest@1.1.0
[email protected].1
socket-stream-client@0.5.0
tinytest@1.2.1
[email protected]
[email protected]
webapp@1.10.0
webapp@1.13.1
[email protected]
2 changes: 1 addition & 1 deletion core.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export default class FilesCollectionCore extends EventEmitter {
*/
_getExt(fileName) {
if (fileName.includes('.')) {
const extension = (fileName.split('.').pop().split('?')[0] || '').toLowerCase();
const extension = (fileName.split('.').pop().split('?')[0] || '').toLowerCase().replace(/([^a-z0-9\-\_\.]+)/gi, '').substring(0, 20);
return { ext: extension, extension, extensionWithDot: `.${extension}` };
}
return { ext: '', extension: '', extensionWithDot: '' };
Expand Down
3 changes: 3 additions & 0 deletions lib.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { check } from 'meteor/check';

const helpers = {
sanitize(str = '', max = 28, replacement = '-') {
return str.replace(/([^a-z0-9\-\_]+)/gi, replacement).substring(0, max);
},
isUndefined(obj) {
return obj === void 0;
},
Expand Down
21 changes: 10 additions & 11 deletions package.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
Package.describe({
name: 'ostrio:files',
version: '2.0.1',
version: '2.1.0',
summary: 'Upload files to Meteor application, with 3rd party storage support: AWS:S3, GridFS and other',
git: 'https://github.com/VeliovGroup/Meteor-Files',
git: 'https://github.com/veliovgroup/Meteor-Files',
documentation: 'README.md'
});

Npm.depends({
'fs-extra': '9.1.0',
eventemitter3: '4.0.7',
'abort-controller': '3.0.0'
});

Package.onUse(function(api) {
Package.onUse((api) => {
api.versionsFrom('1.9');
api.use('webapp', 'server');
api.use(['reactive-var', 'tracker', 'ddp-client'], 'client');
api.use(['mongo', 'check', 'random', 'ecmascript', 'fetch', 'ostrio:[email protected].0'], ['client', 'server']);
api.use(['mongo', 'check', 'random', 'ecmascript', 'fetch', 'ostrio:[email protected].2'], ['client', 'server']);
api.addAssets('worker.min.js', 'client');
api.mainModule('server.js', 'server');
api.mainModule('client.js', 'client');
api.export('FilesCollection');
});

Package.onTest(function(api) {
Package.onTest((api) => {
api.use('tinytest');
api.use(['ecmascript', 'ostrio:files'], ['client', 'server']);
api.addFiles('tests/helpers.js', ['client', 'server']);
});

Npm.depends({
eventemitter3: '4.0.7',
'abort-controller': '3.0.0'
});
53 changes: 34 additions & 19 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,16 @@ export class FilesCollection extends FilesCollectionCore {

this._debug('[FilesCollection.storagePath] Set to:', this.storagePath({}));

fs.mkdir(this.storagePath({}), {
mode: this.parentDirPermissions,
recursive: true
}, (error) => {
try {
fs.mkdirSync(this.storagePath({}), {
mode: this.parentDirPermissions,
recursive: true
});
} catch (error) {
if (error) {
throw new Meteor.Error(401, `[FilesCollection.${self.collectionName}] Path "${this.storagePath({})}" is not writable!`, error);
}
});
}

check(this.strict, Boolean);
check(this.permissions, Number);
Expand Down Expand Up @@ -493,8 +495,7 @@ export class FilesCollection extends FilesCollectionCore {

const handleError = (_error) => {
let error = _error;
console.warn('[FilesCollection] [Upload] [HTTP] Exception:', error);
console.trace();
Meteor._debug('[FilesCollection] [Upload] [HTTP] Exception:', error);

if (!httpResp.headersSent) {
httpResp.writeHead(500);
Expand Down Expand Up @@ -523,7 +524,7 @@ export class FilesCollection extends FilesCollectionCore {
if (httpReq.headers['x-start'] !== '1') {
// CHUNK UPLOAD SCENARIO:
opts = {
fileId: httpReq.headers['x-fileid']
fileId: helpers.sanitize(httpReq.headers['x-fileid'], 20, 'a')
};

if (httpReq.headers['x-eof'] === '1') {
Expand Down Expand Up @@ -590,14 +591,18 @@ export class FilesCollection extends FilesCollectionCore {
try {
opts = JSON.parse(body);
} catch (jsonErr) {
console.error('Can\'t parse incoming JSON from Client on [.insert() | upload], something went wrong!', jsonErr);
Meteor._debug('Can\'t parse incoming JSON from Client on [.insert() | upload], something went wrong!', jsonErr);
opts = {file: {}};
}

if (!helpers.isObject(opts.file)) {
opts.file = {};
}

if (opts.fileId) {
opts.fileId = helpers.sanitize(opts.fileId, 20, 'a');
}

this._debug(`[FilesCollection] [File Start HTTP] ${opts.file.name || '[no-name]'} - ${opts.fileId}`);
if (helpers.isObject(opts.file) && opts.file.meta) {
opts.file.meta = fixJSONParse(opts.file.meta);
Expand Down Expand Up @@ -790,6 +795,12 @@ export class FilesCollection extends FilesCollectionCore {

check(returnMeta, Match.Optional(Boolean));

opts.fileId = helpers.sanitize(opts.fileId, 20, 'a');

if (opts.FSName) {
opts.FSName = helpers.sanitize(opts.FSName);
}

self._debug(`[FilesCollection] [File Start Method] ${opts.file.name} - ${opts.fileId}`);
opts.___s = true;
const { result } = self._prepareUpload(helpers.clone(opts), this.userId, 'DDP Start Method');
Expand Down Expand Up @@ -832,6 +843,8 @@ export class FilesCollection extends FilesCollectionCore {
chunkId: Match.Optional(Number)
});

opts.fileId = helpers.sanitize(opts.fileId, 20, 'a');

if (opts.binData) {
opts.binData = Buffer.from(opts.binData, 'base64');
}
Expand Down Expand Up @@ -908,6 +921,10 @@ export class FilesCollection extends FilesCollectionCore {
opts.chunkId = -1;
}

if (opts.fileId) {
opts.fileId = helpers.sanitize(opts.fileId, 20, 'a');
}

if (!helpers.isString(opts.FSName)) {
opts.FSName = opts.fileId;
}
Expand All @@ -928,7 +945,7 @@ export class FilesCollection extends FilesCollectionCore {
result.ext = extension;
result._id = opts.fileId;
result.userId = userId || null;
opts.FSName = opts.FSName.replace(/([^a-z0-9\-\_]+)/gi, '-');
opts.FSName = helpers.sanitize(opts.FSName);
result.path = `${this.storagePath(result)}${nodePath.sep}${opts.FSName}${extensionWithDot}`;
result = Object.assign(result, this._dataToSchema(result));

Expand Down Expand Up @@ -1138,7 +1155,7 @@ export class FilesCollection extends FilesCollectionCore {
* @param {String} opts.type - File mime-type
* @param {Object} opts.meta - File additional meta-data
* @param {String} opts.userId - UserId, default *null*
* @param {String} opts.fileId - _id, default *null*
* @param {String} opts.fileId - _id, sanitized, max-length: 20; default *null*
* @param {Function} callback - function(error, fileObj){...}
* @param {Boolean} proceedAfterUpload - Proceed onAfterUpload hook
* @summary Write buffer to FS and add to FilesCollection Collection
Expand All @@ -1164,6 +1181,7 @@ export class FilesCollection extends FilesCollectionCore {
check(callback, Match.Optional(Function));
check(proceedAfterUpload, Match.Optional(Boolean));

opts.fileId = opts.fileId && helpers.sanitize(opts.fileId, 20, 'a');
const fileId = opts.fileId || Random.id();
const FSName = this.namingFunction ? this.namingFunction(opts) : fileId;
const fileName = (opts.name || opts.fileName) ? (opts.name || opts.fileName) : FSName;
Expand Down Expand Up @@ -1237,7 +1255,7 @@ export class FilesCollection extends FilesCollectionCore {
* @param {String} opts.type - File mime-type
* @param {Object} opts.meta - File additional meta-data
* @param {String} opts.userId - UserId, default *null*
* @param {String} opts.fileId - _id, default *null*
* @param {String} opts.fileId - _id, sanitized, max-length: 20; default *null*
* @param {Number} opts.timeout - Timeout in milliseconds, default: 360000 (6 mins)
* @param {Function} callback - function(error, fileObj){...}
* @param {Boolean} [proceedAfterUpload] - Proceed onAfterUpload hook
Expand Down Expand Up @@ -1275,7 +1293,7 @@ export class FilesCollection extends FilesCollectionCore {
opts.timeout = 360000;
}

const fileId = opts.fileId || Random.id();
const fileId = (opts.fileId && helpers.sanitize(opts.fileId, 20, 'a')) || Random.id();
const FSName = this.namingFunction ? this.namingFunction(opts) : fileId;
const pathParts = url.split('/');
const fileName = (opts.name || opts.fileName) ? (opts.name || opts.fileName) : pathParts[pathParts.length - 1].split('?')[0] || FSName;
Expand Down Expand Up @@ -1418,7 +1436,7 @@ export class FilesCollection extends FilesCollectionCore {
* @param {String} opts - [Optional] Object with file-data
* @param {String} opts.type - [Optional] File mime-type
* @param {Object} opts.meta - [Optional] File additional meta-data
* @param {String} opts.fileId - _id, default *null*
* @param {String} opts.fileId - _id, sanitized, max-length: 20 symbols default *null*
* @param {Object} opts.fileName - [Optional] File name, if not specified file name and extension will be taken from path
* @param {String} opts.userId - [Optional] UserId, default *null*
* @param {Function} callback - [Optional] function(error, fileObj){...}
Expand Down Expand Up @@ -1488,7 +1506,7 @@ export class FilesCollection extends FilesCollectionCore {
userId: opts.userId,
extension,
_storagePath: path.replace(`${nodePath.sep}${opts.fileName}`, ''),
fileId: opts.fileId || null
fileId: (opts.fileId && helpers.sanitize(opts.fileId, 20, 'a')) || null
});


Expand Down Expand Up @@ -1827,7 +1845,7 @@ export class FilesCollection extends FilesCollectionCore {
if (!closeError) {
stream._isEnded = true;
} else {
this._debug(`[FilesCollection] [serve(${vRef.path}, ${version})] [respond] [closeStreamCb] Error:`, closeError);
this._debug(`[FilesCollection] [serve(${vRef.path}, ${version})] [respond] [closeStreamCb] (this is error on the stream we wish to forcefully close after it isn't needed anymore. It's okay that it throws errors. Consider this as purely informational message)`, closeError);
}
};

Expand All @@ -1852,10 +1870,8 @@ export class FilesCollection extends FilesCollectionCore {
http.response.writeHead(code);
}

http.response.on('close', closeStream);
http.request.on('aborted', () => {
http.request.aborted = true;
closeStream();
});

stream.on('open', () => {
Expand All @@ -1874,7 +1890,6 @@ export class FilesCollection extends FilesCollectionCore {
closeStream();
streamErrorHandler(err);
}).on('end', () => {
closeStream();
if (!http.response.finished) {
http.response.end();
}
Expand Down
2 changes: 1 addition & 1 deletion upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ export class UploadInstance extends EventEmitter {
if (!Meteor.status().connected || `${error}` === 'Error: network' || `${error}` === 'Error: Connection lost') {
this.result.pause();
} else if (this.result.state.get() !== 'aborted') {
console.warn('Something went wrong! [sendEOF] method doesn\'t returned JSON! Looks like you\'re on Cordova app or behind proxy, switching to DDP transport is recommended.');
Meteor._debug('Something went wrong! [sendEOF] method doesn\'t returned JSON! Looks like you\'re on Cordova app or behind proxy, switching to DDP transport is recommended.');
this.emit('end', error);
}
}, 512);
Expand Down
2 changes: 1 addition & 1 deletion write-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default class WriteStream {
bound(() => {
callback && callback(error, written, buffer);
if (error) {
console.warn('[FilesCollection] [writeStream] [write] [Error:]', error);
Meteor._debug('[FilesCollection] [writeStream] [write] [Error:]', error);
this.abort();
} else {
++this.writtenChunks;
Expand Down

0 comments on commit e3a347c

Please sign in to comment.