diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index 85924d77d27..948307394aa 100644 --- a/app/api/server/v1/rooms.js +++ b/app/api/server/v1/rooms.js @@ -59,38 +59,48 @@ API.v1.addRoute('rooms.get', { authRequired: true }, { }, }); -API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { - post() { - const room = Meteor.call('canAccessRoom', this.urlParams.rid, this.userId); +const getFiles = Meteor.wrapAsync(({ request }, callback) => { + const busboy = new Busboy({ headers: request.headers }); + const files = []; - if (!room) { - return API.v1.unauthorized(); + const fields = {}; + + + busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + if (fieldname !== 'file') { + return callback(new Meteor.Error('invalid-field')); } - const busboy = new Busboy({ headers: this.request.headers }); - const files = []; - const fields = {}; + const fileDate = []; + file.on('data', (data) => fileDate.push(data)); + + file.on('end', () => { + files.push({ fieldname, file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileDate) }); + }); + }); + + busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); + + busboy.on('finish', Meteor.bindEnvironment(() => callback(null, { files, fields }))); - Meteor.wrapAsync((callback) => { - busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { - if (fieldname !== 'file') { - return callback(new Meteor.Error('invalid-field')); - } + request.pipe(busboy); +}); - const fileDate = []; - file.on('data', (data) => fileDate.push(data)); +const fileStore = FileUpload.getStore('Uploads'); +const fileStoreInsert = Meteor.wrapAsync(fileStore.insert.bind(fileStore)); - file.on('end', () => { - files.push({ fieldname, file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileDate) }); - }); - }); +API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { + post() { + const room = Meteor.call('canAccessRoom', this.urlParams.rid, this.userId); - busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); + if (!room) { + return API.v1.unauthorized(); + } - busboy.on('finish', Meteor.bindEnvironment(() => callback())); - this.request.pipe(busboy); - })(); + const { files, fields } = getFiles({ + request: this.request, + }); if (files.length === 0) { return API.v1.failure('File required'); @@ -102,8 +112,6 @@ API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { const file = files[0]; - const fileStore = FileUpload.getStore('Uploads'); - const details = { name: file.filename, size: file.fileBuffer.length, @@ -112,18 +120,16 @@ API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { userId: this.userId, }; - let fileData = {}; - - Meteor.runAsUser(this.userId, () => { - const uploadedFile = Meteor.wrapAsync(fileStore.insert.bind(fileStore))(details, file.fileBuffer); + const fileData = Meteor.runAsUser(this.userId, () => { + const uploadedFile = fileStoreInsert(details, file.fileBuffer); uploadedFile.description = fields.description; delete fields.description; - API.v1.success(Meteor.call('sendFileMessage', this.urlParams.rid, null, uploadedFile, fields)); + Meteor.call('sendFileMessage', this.urlParams.rid, null, uploadedFile, fields); - fileData = uploadedFile; + return uploadedFile; }); return API.v1.success({ message: Messages.getMessageByFileIdAndUsername(fileData._id, this.userId) }); diff --git a/app/apps/client/admin/appInstall.js b/app/apps/client/admin/appInstall.js index 1db80c13b11..031042ff2f0 100644 --- a/app/apps/client/admin/appInstall.js +++ b/app/apps/client/admin/appInstall.js @@ -148,9 +148,9 @@ Template.appInstall.events({ let result; if (isUpdating) { - result = await APIClient.upload(`apps/${ t.isUpdatingId.get() }`, data); + result = await APIClient.upload(`apps/${ t.isUpdatingId.get() }`, data).promise; } else { - result = await APIClient.upload('apps', data); + result = await APIClient.upload('apps', data).promise; } FlowRouter.go(`/admin/apps/${ result.app.id }?version=${ result.app.version }`); diff --git a/app/ui/client/lib/fileUpload.js b/app/ui/client/lib/fileUpload.js index 42253997a60..e41ea15116d 100644 --- a/app/ui/client/lib/fileUpload.js +++ b/app/ui/client/lib/fileUpload.js @@ -1,12 +1,11 @@ -import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import { Session } from 'meteor/session'; import s from 'underscore.string'; import { Handlebars } from 'meteor/ui'; +import { Random } from 'meteor/random'; -import { fileUploadHandler } from '../../../file-upload'; import { settings } from '../../../settings/client'; -import { t, fileUploadIsValidContentType } from '../../../utils'; +import { t, fileUploadIsValidContentType, APIClient } from '../../../utils'; import { modal, prependReplies } from '../../../ui-utils'; @@ -193,7 +192,7 @@ export const fileUpload = async (files, input, { rid, tmid }) => { cancelButtonText: t('Cancel'), html: true, onRendered: () => $('#file-name').focus(), - }, (isConfirm) => { + }, async (isConfirm) => { if (!isConfirm) { return; } @@ -206,52 +205,48 @@ export const fileUpload = async (files, input, { rid, tmid }) => { description: document.getElementById('file-description').value, }; - const upload = fileUploadHandler('Uploads', record, file.file); + const fileName = document.getElementById('file-name').value || file.name || file.file.name; + + const data = new FormData(); + record.description && data.append('description', record.description); + msg && data.append('msg', msg); + tmid && data.append('tmid', tmid); + data.append('file', file.file, fileName); - uploadNextFile(); const uploads = Session.get('uploading') || []; - uploads.push({ - id: upload.id, - name: upload.getFileName(), + + const upload = { + id: Random.id(), + name: fileName, percentage: 0, - }); + }; + + uploads.push(upload); Session.set('uploading', uploads); - upload.onProgress = (progress) => { - const uploads = Session.get('uploading') || []; - uploads.filter((u) => u.id === upload.id).forEach((u) => { - u.percentage = Math.round(progress * 100) || 0; - }); - Session.set('uploading', uploads); - }; + uploadNextFile(); - upload.start((error, file, storage) => { - if (error) { + const { xhr, promise } = APIClient.upload(`v1/rooms.upload/${ rid }`, {}, data, { + progress(progress) { + const uploads = Session.get('uploading') || []; + + if (progress === 100) { + return; + } + uploads.filter((u) => u.id === upload.id).forEach((u) => { + u.percentage = Math.round(progress) || 0; + }); + Session.set('uploading', uploads); + }, + error(error) { const uploads = Session.get('uploading') || []; uploads.filter((u) => u.id === upload.id).forEach((u) => { u.error = error.message; u.percentage = 0; }); Session.set('uploading', uploads); - - return; - } - - if (!file) { - return; - } - - Meteor.call('sendFileMessage', rid, storage, file, { msg, tmid }, () => { - $(input) - .removeData('reply') - .trigger('dataChange'); - - setTimeout(() => { - const uploads = Session.get('uploading') || []; - Session.set('uploading', uploads.filter((u) => u.id !== upload.id)); - }, 2000); - }); + }, }); Tracker.autorun((computation) => { @@ -259,13 +254,27 @@ export const fileUpload = async (files, input, { rid, tmid }) => { if (!isCanceling) { return; } - computation.stop(); - upload.stop(); + Session.delete(`uploading-cancel-${ upload.id }`); + + xhr.abort(); const uploads = Session.get('uploading') || {}; Session.set('uploading', uploads.filter((u) => u.id !== upload.id)); }); + + try { + await promise; + const uploads = Session.get('uploading') || []; + return Session.set('uploading', uploads.filter((u) => u.id !== upload.id)); + } catch (error) { + const uploads = Session.get('uploading') || []; + uploads.filter((u) => u.id === upload.id).forEach((u) => { + u.error = error.message; + u.percentage = 0; + }); + Session.set('uploading', uploads); + } })); }; diff --git a/app/utils/client/lib/RestApiClient.js b/app/utils/client/lib/RestApiClient.js index 594927ebca1..a323bfeb5e0 100644 --- a/app/utils/client/lib/RestApiClient.js +++ b/app/utils/client/lib/RestApiClient.js @@ -21,13 +21,13 @@ export const APIClient = { return APIClient._jqueryCall('POST', endpoint, params, body); }, - upload(endpoint, params, formData) { + upload(endpoint, params, formData, xhrOptions) { if (!formData) { formData = params; params = {}; } - return APIClient._jqueryFormDataCall(endpoint, params, formData); + return APIClient._jqueryFormDataCall(endpoint, params, formData, xhrOptions); }, _generateQueryFromParams(params) { @@ -68,15 +68,32 @@ export const APIClient = { }); }, - _jqueryFormDataCall(endpoint, params, formData) { + _jqueryFormDataCall(endpoint, params, formData, { progress = () => {}, error = () => {} } = {}, abort = () => {}) { + const ret = { }; + const query = APIClient._generateQueryFromParams(params); if (!(formData instanceof FormData)) { throw new Error('The formData parameter MUST be an instance of the FormData class.'); } - return new Promise(function _jqueryFormDataPromise(resolve, reject) { - jQuery.ajax({ + ret.promise = new Promise(function _jqueryFormDataPromise(resolve, reject) { + ret.xhr = jQuery.ajax({ + xhr() { + const xhr = new window.XMLHttpRequest(); + + xhr.upload.addEventListener('progress', function(evt) { + if (evt.lengthComputable) { + const percentComplete = evt.loaded / evt.total; + progress(percentComplete * 100); + } + }, false); + + xhr.upload.addEventListener('error', error, false); + xhr.upload.addEventListener('abort', abort, false); + + return xhr; + }, url: `${ baseURI }api/${ endpoint }${ query }`, headers: { 'X-User-Id': Meteor._localStorage.getItem(Accounts.USER_ID_KEY), @@ -96,6 +113,8 @@ export const APIClient = { }, }); }); + + return ret; }, v1: {