diff --git a/.versions b/.versions index b527ccc..ad710d0 100644 --- a/.versions +++ b/.versions @@ -1,51 +1,51 @@ -allow-deny@1.1.0 -babel-compiler@7.6.0 -babel-runtime@1.5.0 +allow-deny@1.1.1 +babel-compiler@7.9.0 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 boilerplate-generator@1.7.1 -callback-hook@1.3.0 +callback-hook@1.4.0 check@1.3.1 ddp@1.4.0 -ddp-client@2.4.0 +ddp-client@2.5.0 ddp-common@1.4.0 -ddp-server@2.3.2 +ddp-server@2.5.0 diff-sequence@1.1.1 -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 -ejson@1.1.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 +ejson@1.1.2 fetch@0.1.1 geojson-utils@1.0.10 -id-map@1.1.0 +id-map@1.1.1 inter-process-messaging@0.1.1 -local-test:ostrio:files@2.0.1 -logging@1.2.0 -meteor@1.9.3 -minimongo@1.6.1 -modern-browsers@0.1.5 -modules@0.16.0 -modules-runtime@0.12.0 -mongo@1.10.1 -mongo-decimal@0.1.2 +local-test:ostrio:files@2.1.0 +logging@1.3.1 +meteor@1.10.0 +minimongo@1.8.0 +modern-browsers@0.1.8 +modules@0.18.0 +modules-runtime@0.13.0 +mongo@1.15.0 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 -mongo-id@1.0.7 -npm-mongo@3.8.1 +mongo-id@1.0.8 +npm-mongo@4.3.1 ordered-dict@1.1.0 -ostrio:cookies@2.7.0 -ostrio:files@2.0.1 -promise@0.11.2 +ostrio:cookies@2.7.2 +ostrio:files@2.1.0 +promise@0.12.0 random@1.2.0 -react-fast-refresh@0.1.0 +react-fast-refresh@0.2.3 reactive-var@1.0.11 reload@1.3.1 retry@1.1.0 -routepolicy@1.1.0 -socket-stream-client@0.3.1 -tinytest@1.1.0 +routepolicy@1.1.1 +socket-stream-client@0.5.0 +tinytest@1.2.1 tracker@1.2.0 underscore@1.0.10 -webapp@1.10.0 +webapp@1.13.1 webapp-hashing@1.1.0 diff --git a/core.js b/core.js index 59f600d..dd803d3 100644 --- a/core.js +++ b/core.js @@ -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: '' }; diff --git a/lib.js b/lib.js index f089f86..3d0431e 100644 --- a/lib.js +++ b/lib.js @@ -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; }, diff --git a/package.js b/package.js index 8e087f8..9a6422d 100755 --- a/package.js +++ b/package.js @@ -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:cookies@2.7.0'], ['client', 'server']); + api.use(['mongo', 'check', 'random', 'ecmascript', 'fetch', 'ostrio:cookies@2.7.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' +}); diff --git a/server.js b/server.js index 4f558b4..7877f43 100644 --- a/server.js +++ b/server.js @@ -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); @@ -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); @@ -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') { @@ -590,7 +591,7 @@ 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: {}}; } @@ -598,6 +599,10 @@ export class FilesCollection extends FilesCollectionCore { 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); @@ -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'); @@ -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'); } @@ -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; } @@ -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)); @@ -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 @@ -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; @@ -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 @@ -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; @@ -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){...} @@ -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 }); @@ -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); } }; @@ -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', () => { @@ -1874,7 +1890,6 @@ export class FilesCollection extends FilesCollectionCore { closeStream(); streamErrorHandler(err); }).on('end', () => { - closeStream(); if (!http.response.finished) { http.response.end(); } diff --git a/upload.js b/upload.js index fdd6b6b..6ecee3d 100644 --- a/upload.js +++ b/upload.js @@ -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); diff --git a/write-stream.js b/write-stream.js index f37db84..f5d5d68 100644 --- a/write-stream.js +++ b/write-stream.js @@ -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;