From fcb79866857073e8cd9db7742683703cd836ef94 Mon Sep 17 00:00:00 2001 From: lane-formio Date: Tue, 20 Aug 2024 15:54:33 -0500 Subject: [PATCH 1/3] Revert "FIO-3820 File Synchronization Feature" This reverts commit c54e953b56742e5a6e1c82b7b427187152042f74. This reverts commit 022a3d2a306eb5cbb2f4b21741a3e8cd00a8bed6. This reverts commit 9f4b7d0ba7b7a99653b57a89de9abc68d51cde15. This reverts commit 44ef260113b74e81f6772aff32cd7b171a3bdf61. This reverts commit a2ec08f0d1811856f4d2636c68da31b163b3d252. --- src/components/file/File.js | 765 ++++++------------ src/components/file/File.unit.js | 32 +- .../file/editForm/File.edit.display.js | 17 - src/providers/storage/azure.js | 4 - src/providers/storage/base64.js | 45 +- src/providers/storage/googleDrive.js | 95 ++- src/providers/storage/indexeddb.js | 172 ++-- src/providers/storage/s3.js | 37 +- src/providers/storage/url.js | 2 +- src/sass/formio.form.scss | 19 - .../component-bootstrap-file-html-value0.html | 3 +- .../component-bootstrap-file-html-value1.html | 3 +- .../component-bootstrap-file-multiple.html | 3 +- .../component-bootstrap-file-required.html | 3 +- test/renders/component-bootstrap-file.html | 3 +- test/renders/form-bootstrap-premium.html | 3 +- 16 files changed, 421 insertions(+), 785 deletions(-) diff --git a/src/components/file/File.js b/src/components/file/File.js index ed01cb9f6f..a29bcd15fb 100644 --- a/src/components/file/File.js +++ b/src/components/file/File.js @@ -37,8 +37,6 @@ if (htmlCanvasElement && !htmlCanvasElement.prototype.toBlob) { }); } -const createRandomString = () => Math.random().toString(36).substring(2, 15); - export default class FileComponent extends Field { static schema(...extend) { return Field.schema({ @@ -97,13 +95,8 @@ export default class FileComponent extends Field { progress: progressSupported, }; this.cameraMode = false; + this.statuses = []; this.fileDropHidden = false; - this.filesToSync = { - filesToUpload: [], - filesToDelete: [], - }; - this.isSyncing = false; - this.abortUploads = []; } get dataReady() { @@ -160,41 +153,14 @@ export default class FileComponent extends Field { this._fileBrowseHidden = value; } - get shouldSyncFiles() { - return Boolean(this.filesToSync.filesToDelete.length || this.filesToSync.filesToUpload.length); - } - - get autoSync() { - return _.get(this, 'component.autoSync', false); - } - - get columnsSize() { - const actionsColumn = this.disabled ? 0 : this.autoSync ? 2 : 1; - const typeColumn = this.hasTypes ? 2 : 0; - const sizeColumn = 2; - const nameColumn = 12 - actionsColumn - typeColumn - sizeColumn; - - return { - name: nameColumn, - size: sizeColumn, - type: typeColumn, - actions: actionsColumn, - }; - } - render() { - const { filesToDelete, filesToUpload } = this.filesToSync; return super.render(this.renderTemplate('file', { fileSize: this.fileSize, files: this.dataValue || [], - filesToDelete, - filesToUpload, + statuses: this.statuses, disabled: this.disabled, support: this.support, - fileDropHidden: this.fileDropHidden, - showSyncButton: this.autoSync && (filesToDelete.length || filesToUpload.length), - isSyncing: this.isSyncing, - columns: this.columnsSize, + fileDropHidden: this.fileDropHidden })); } @@ -269,7 +235,7 @@ export default class FileComponent extends Field { this.getFrame(videoPlayer) .then((frame) => { frame.name = `photo-${Date.now()}.png`; - this.handleFilesToUpload([frame]); + this.upload([frame]); this.cameraMode = false; this.redraw(); }); @@ -364,10 +330,22 @@ export default class FileComponent extends Field { return options; } - get actions() { - return { - abort: this.abortRequest.bind(this), - }; + deleteFile(fileInfo) { + const { options = {} } = this.component; + + if (fileInfo && (['url', 'indexeddb'].includes(this.component.storage))) { + const { fileService } = this; + if (fileService && typeof fileService.deleteFile === 'function') { + fileService.deleteFile(fileInfo, options); + } + else { + const formio = this.options.formio || (this.root && this.root.formio); + + if (formio) { + formio.makeRequest('', fileInfo.url, 'delete'); + } + } + } } attach(element) { @@ -381,23 +359,20 @@ export default class FileComponent extends Field { videoPlayer: 'single', fileLink: 'multiple', removeLink: 'multiple', - fileToSyncRemove: 'multiple', + fileStatusRemove: 'multiple', fileImage: 'multiple', fileType: 'multiple', fileProcessingLoader: 'single', - syncNow: 'single', - restoreFile: 'multiple', - progress: 'multiple', }); // Ensure we have an empty input refs. We need this for the setValue method to redraw the control when it is set. this.refs.input = []; const superAttach = super.attach(element); if (this.refs.fileDrop) { - // if (!this.statuses.length) { - // this.refs.fileDrop.removeAttribute('hidden'); - // } - const _this = this; + if (!this.statuses.length) { + this.refs.fileDrop.removeAttribute('hidden'); + } + const element = this; this.addEventListener(this.refs.fileDrop, 'dragover', function(event) { this.className = 'fileSelector fileDragOver'; event.preventDefault(); @@ -409,20 +384,16 @@ export default class FileComponent extends Field { this.addEventListener(this.refs.fileDrop, 'drop', function(event) { this.className = 'fileSelector'; event.preventDefault(); - _this.handleFilesToUpload(event.dataTransfer.files); + element.upload(event.dataTransfer.files); }); } - this.addEventListener(element, 'click', (event) => { - this.handleAction(event); - }); - if (this.refs.fileBrowse) { this.addEventListener(this.refs.fileBrowse, 'click', (event) => { event.preventDefault(); this.browseFiles(this.browseOptions) .then((files) => { - this.handleFilesToUpload(files); + this.upload(files); }); }); } @@ -436,29 +407,27 @@ export default class FileComponent extends Field { this.refs.removeLink.forEach((removeLink, index) => { this.addEventListener(removeLink, 'click', (event) => { - event.preventDefault(); const fileInfo = this.dataValue[index]; - this.handleFileToRemove(fileInfo); - }); - }); - this.refs.fileToSyncRemove.forEach((fileToSyncRemove, index) => { - this.addEventListener(fileToSyncRemove, 'click', (event) => { + this.deleteFile(fileInfo); event.preventDefault(); - this.filesToSync.filesToUpload.splice(index, 1); + this.splice(index); this.redraw(); }); }); - this.refs.restoreFile.forEach((fileToRestore, index) => { - this.addEventListener(fileToRestore, 'click', (event) => { + this.refs.fileStatusRemove.forEach((fileStatusRemove, index) => { + this.addEventListener(fileStatusRemove, 'click', (event) => { event.preventDefault(); - const fileInfo = this.filesToSync.filesToDelete[index]; - delete fileInfo.status; - delete fileInfo.message; - this.filesToSync.filesToDelete.splice(index, 1); - this.dataValue.push(fileInfo); - this.triggerChange(); + + const fileUpload = this.statuses[index]; + _.pull(this.filesUploading, fileUpload.originalName); + + if (fileUpload.abort) { + fileUpload.abort(); + } + + this.statuses.splice(index, 1); this.redraw(); }); }); @@ -473,7 +442,7 @@ export default class FileComponent extends Field { reader.onloadend = (evt) => { const blob = new Blob([new Uint8Array(evt.target.result)], { type: file.type }); blob.name = file.name; - this.handleFilesToUpload([blob]); + this.upload([blob]); }; reader.readAsArrayBuffer(file); }); @@ -497,7 +466,7 @@ export default class FileComponent extends Field { reader.onloadend = (evt) => { const blob = new Blob([new Uint8Array(evt.target.result)], { type: file.type }); blob.name = file.name; - this.handleFilesToUpload([blob]); + this.upload([blob]); }; reader.readAsArrayBuffer(file); }); @@ -531,10 +500,6 @@ export default class FileComponent extends Field { } this.refs.fileType.forEach((fileType, index) => { - if (!this.dataValue[index]) { - return; - } - this.dataValue[index].fileType = this.dataValue[index].fileType || this.component.fileTypes[0].label; this.addEventListener(fileType, 'change', (event) => { @@ -546,11 +511,6 @@ export default class FileComponent extends Field { }); }); - this.addEventListener(this.refs.syncNow, 'click', (event) => { - event.preventDefault(); - this.syncFiles(); - }); - const fileService = this.fileService; if (fileService) { const loadingImages = []; @@ -676,462 +636,215 @@ export default class FileComponent extends Field { return file.size - 0.1 <= this.translateScalars(val); } - abortRequest(id) { - const abortUpload = this.abortUploads.find(abortUpload => abortUpload.id === id); - if (abortUpload) { - abortUpload.abort(); - } - } - - handleAction(event) { - const target = event.target; - if (!target.id) { - return; - } - const [action, id] = target.id.split('-'); - if (!action || !id || !this.actions[action]) { - return; - } - - this.actions[action](id); - } - - getFileName(file) { - return uniqueName(file.name, this.component.fileNameTemplate, this.evalContext()); - } - - getInitFileToSync(file) { - const escapedFileName = file.name ? file.name.replaceAll('<', '<').replaceAll('>', '>') : file.name; - return { - id: createRandomString(), - // Get a unique name for this file to keep file collisions from occurring. - dir: this.interpolate(this.component.dir || ''), - name: this.getFileName(file), - originalName: escapedFileName, - fileKey: this.component.fileKey || 'file', - storage: this.component.storage, - options: this.component.options, - file, - size: file.size, - status: 'info', - message: this.t('Processing file. Please wait...'), - hash: '', - }; - } - - async handleSubmissionRevisions(file) { - if (this.root.form.submissionRevisions !== 'true') { - return ''; - } - - const bmf = new BMF(); - const hash = await new Promise((resolve, reject) => { - this.emit('fileUploadingStart'); - bmf.md5(file, (err, md5)=>{ - if (err) { - return reject(err); - } - return resolve(md5); - }); - }); - this.emit('fileUploadingEnd'); - - return hash; - } - - validateFileName(file) { - // Check if file with the same name is being uploaded - const fileWithSameNameUploading = this.filesToSync.filesToUpload - .some(fileToSync => fileToSync.file?.name === file.name); - - const fileWithSameNameUploaded = _.some(this.dataValue, fileStatus => fileStatus.originalName === file.name); - - return fileWithSameNameUploaded || fileWithSameNameUploading - ? { - status: 'error', - message: this.t(`File with the same name is already ${fileWithSameNameUploading ? 'being ' : ''}uploaded`), - } - : {}; - } - - validateFileSettings(file) { - // Check file pattern - if (this.component.filePattern && !this.validatePattern(file, this.component.filePattern)) { - return { - status: 'error', - message: this.t('File is the wrong type; it must be {{ pattern }}', { - pattern: this.component.filePattern, - }), - }; - } - - // Check file minimum size - if (this.component.fileMinSize && !this.validateMinSize(file, this.component.fileMinSize)) { - return { - status: 'error', - message: this.t('File is too small; it must be at least {{ size }}', { - size: this.component.fileMinSize, - }), - }; - } - - // Check file maximum size - if (this.component.fileMaxSize && !this.validateMaxSize(file, this.component.fileMaxSize)) { - return { - status: 'error', - message: this.t('File is too big; it must be at most {{ size }}', { - size: this.component.fileMaxSize, - }), - }; - } - - return {}; - } - - validateFileService() { - const { fileService } = this; - return !fileService - ? { - status: 'error', - message: this.t('File Service not provided.'), - } - : {}; - } - - validateFile(file) { - const fileServiceValidation = this.validateFileService(); - if (fileServiceValidation.status === 'error') { - return fileServiceValidation; - } - - const fileNameValidation = this.validateFileName(file); - if (fileNameValidation.status === 'error') { - return fileNameValidation; - } - - return this.validateFileSettings(file); - } - - getGroupPermissions() { - let groupKey = null; - let groupPermissions = null; - - //Iterate through form components to find group resource if one exists - this.root.everyComponent((element) => { - if (element.component?.submissionAccess || element.component?.defaultPermission) { - groupPermissions = !element.component.submissionAccess ? [ - { - type: element.component.defaultPermission, - roles: [], - }, - ] : element.component.submissionAccess; - - groupPermissions.forEach((permission) => { - groupKey = ['admin', 'write', 'create'].includes(permission.type) ? element.component.key : null; - }); - } - }); - - return { groupKey, groupPermissions }; - } - - async triggerFileProcessor(file) { - let processedFile = null; - - if (this.root.options.fileProcessor) { - try { - if (this.refs.fileProcessingLoader) { - this.refs.fileProcessingLoader.style.display = 'block'; - } - const fileProcessorHandler = fileProcessor(this.fileService, this.root.options.fileProcessor); - processedFile = await fileProcessorHandler(file, this.component.properties); - } - catch (err) { - this.fileDropHidden = false; - return { - status: 'error', - message: this.t('File processing has been failed.'), - }; - } - finally { - if (this.refs.fileProcessingLoader) { - this.refs.fileProcessingLoader.style.display = 'none'; - } - } - } - - return { - file: processedFile, - }; - } - - async prepareFileToUpload(file) { - const fileToSync = this.getInitFileToSync(file); - fileToSync.hash = await this.handleSubmissionRevisions(file); - - const { status, message } = this.validateFile(file); - if (status === 'error') { - fileToSync.isValidationError = true; - fileToSync.status = status; - fileToSync.message = message; - return this.filesToSync.filesToUpload.push(fileToSync); - } - - if (this.component.privateDownload) { - file.private = true; - } - - const { groupKey, groupPermissions } = this.getGroupPermissions(); - - const processedFile = await this.triggerFileProcessor(file); - if (processedFile.status === 'error') { - fileToSync.status === 'error'; - fileToSync.message = processedFile.message; - return this.filesToSync.filesToUpload.push(fileToSync); - } - - if (this.autoSync) { - fileToSync.message = this.t('Ready to be uploaded into storage'); - } - - this.filesToSync.filesToUpload.push({ - ...fileToSync, - message: fileToSync.message, - file: processedFile.file || file, - url: this.interpolate(this.component.url, { file: fileToSync }), - groupPermissions, - groupResourceId: groupKey ? this.currentForm.submission.data[groupKey]._id : null, - }); - } - - async prepareFilesToUpload(files) { + upload(files) { // Only allow one upload if not multiple. if (!this.component.multiple) { + if (this.statuses.length) { + this.statuses = []; + } files = Array.prototype.slice.call(files, 0, 1); } if (this.component.storage && files && files.length) { this.fileDropHidden = true; - return Promise.all([...files].map(async(file) => { - await this.prepareFileToUpload(file); - this.redraw(); - })); - } - else { - return Promise.resolve(); - } - } - - async handleFilesToUpload(files) { - await this.prepareFilesToUpload(files); - if (!this.autoSync) { - await this.syncFiles(); - } - } - - prepareFileToDelete(fileInfo) { - this.filesToSync.filesToDelete.push({ - ...fileInfo, - status: 'info', - message: this.autoSync - ? this.t('Ready to be removed from storage') - : this.t('Preparing file to remove'), - }); - - const index = this.dataValue.findIndex(file => file.name === fileInfo.name); - this.splice(index); - this.redraw(); - } - - handleFileToRemove(fileInfo) { - this.prepareFileToDelete(fileInfo); - if (!this.autoSync) { - this.syncFiles(); - } - } - - async deleteFile(fileInfo) { - const { options = {} } = this.component; - - if (fileInfo && (['url', 'indexeddb', 's3', 'azure', 'googledrive'].includes(this.component.storage))) { - const { fileService } = this; - if (fileService && typeof fileService.deleteFile === 'function') { - return await fileService.deleteFile(fileInfo, options); - } - else { - const formio = this.options.formio || (this.root && this.root.formio); + // files is not really an array and does not have a forEach method, so fake it. + /* eslint-disable max-statements */ + Array.prototype.forEach.call(files, async(file) => { + const fileName = uniqueName(file.name, this.component.fileNameTemplate, this.evalContext()); + const escapedFileName = file.name ? file.name.replaceAll('<', '<').replaceAll('>', '>') : file.name; + const fileUpload = { + abort: () => null, + originalName: escapedFileName, + name: fileName, + size: file.size, + status: 'info', + message: this.t('Processing file. Please wait...'), + hash: '', + }; - if (formio) { - return await formio.makeRequest('', fileInfo.url, 'delete'); + if (this.root.form.submissionRevisions === 'true') { + this.statuses.push(fileUpload); + this.redraw(); + const bmf = new BMF(); + const hash = await new Promise((resolve, reject) => { + this.emit('fileUploadingStart'); + bmf.md5(file, (err, md5)=>{ + if (err) { + return reject(err); + } + return resolve(md5); + }); + }); + this.emit('fileUploadingEnd'); + fileUpload.hash = hash; } - } - } - } - - async delete() { - if (!this.filesToSync.filesToDelete.length) { - return Promise.resolve(); - } - return await Promise.all(this.filesToSync.filesToDelete.map(async(fileToSync) => { - try { - if (fileToSync.isValidationError) { - return { fileToSync }; + // Check if file with the same name is being uploaded + if (!this.filesUploading) { + this.filesUploading = []; + } + const fileWithSameNameUploading = this.filesUploading.some(fileUploading => fileUploading === file.name); + this.filesUploading.push(file.name); + + const fileWithSameNameUploaded = this.dataValue.some(fileStatus => fileStatus.originalName === file.name); + const fileWithSameNameUploadedWithError = this.statuses.findIndex(fileStatus => + fileStatus.originalName === file.name + && fileStatus.status === 'error' + ); + + if (fileWithSameNameUploaded || fileWithSameNameUploading) { + fileUpload.status = 'error'; + fileUpload.message = this.t(`File with the same name is already ${fileWithSameNameUploading ? 'being ' : ''}uploaded`); } - await this.deleteFile(fileToSync); - fileToSync.status = 'success'; - fileToSync.message = this.t('Succefully removed'); - } - catch (response) { - fileToSync.status = 'error'; - fileToSync.message = typeof response === 'string' ? response : response.toString(); - } - finally { - this.redraw(); - } - - return { fileToSync }; - })); - } - - updateProgress(fileInfo, progressEvent) { - fileInfo.progress = parseInt(100.0 * progressEvent.loaded / progressEvent.total); - if (fileInfo.status !== 'progress') { - fileInfo.status = 'progress'; - delete fileInfo.message; - this.redraw(); - } - else { - const progress = Array.prototype.find.call(this.refs.progress, progressElement => progressElement.id === fileInfo.id); - progress.innerHTML = `${fileInfo.progress}% ${this.t('Complete')}`; - progress.style.width = `${fileInfo.progress}%`; - progress.ariaValueNow = fileInfo.progress.toString(); - } - } - - getMultipartOptions(fileToSync) { - let count = 0; - return this.component.useMultipartUpload && this.component.multipart ? { - ...this.component.multipart, - progressCallback: (total) => { - count++; - fileToSync.status = 'progress'; - fileToSync.progress = parseInt(100 * count / total); - delete fileToSync.message; - this.redraw(); - }, - changeMessage: (message) => { - fileToSync.message = message; - this.redraw(); - }, - } : false; - } - - async uploadFile(fileToSync) { - return await this.fileService.uploadFile( - fileToSync.storage, - fileToSync.file, - fileToSync.name, - fileToSync.dir, - // Progress callback - this.updateProgress.bind(this, fileToSync), - fileToSync.url, - fileToSync.options, - fileToSync.fileKey, - fileToSync.groupPermissions, - fileToSync.groupResourceId, - () => {}, - // Abort upload callback - (abort) => this.abortUploads.push({ - id: fileToSync.id, - abort, - }), - this.getMultipartOptions(fileToSync), - ); - } + if (fileWithSameNameUploadedWithError !== -1) { + this.statuses.splice(fileWithSameNameUploadedWithError, 1); + this.redraw(); + } - async upload() { - if (!this.filesToSync.filesToUpload.length) { - return Promise.resolve(); - } + // Check file pattern + if (this.component.filePattern && !this.validatePattern(file, this.component.filePattern)) { + fileUpload.status = 'error'; + fileUpload.message = this.t('File is the wrong type; it must be {{ pattern }}', { + pattern: this.component.filePattern, + }); + } + // Check file minimum size + if (this.component.fileMinSize && !this.validateMinSize(file, this.component.fileMinSize)) { + fileUpload.status = 'error'; + fileUpload.message = this.t('File is too small; it must be at least {{ size }}', { + size: this.component.fileMinSize, + }); + } - return await Promise.all(this.filesToSync.filesToUpload.map(async(fileToSync) => { - let fileInfo = null; - try { - if (fileToSync.isValidationError) { - return { - fileToSync, - fileInfo, - }; + // Check file maximum size + if (this.component.fileMaxSize && !this.validateMaxSize(file, this.component.fileMaxSize)) { + fileUpload.status = 'error'; + fileUpload.message = this.t('File is too big; it must be at most {{ size }}', { + size: this.component.fileMaxSize, + }); } - fileInfo = await this.uploadFile(fileToSync); - fileToSync.status = 'success'; - fileToSync.message = this.t('Succefully uploaded'); + // Get a unique name for this file to keep file collisions from occurring. + const dir = this.interpolate(this.component.dir || ''); + const { fileService } = this; + if (!fileService) { + fileUpload.status = 'error'; + fileUpload.message = this.t('File Service not provided.'); + } - fileInfo.originalName = fileToSync.originalName; - fileInfo.hash = fileToSync.hash; - } - catch (response) { - fileToSync.status = 'error'; - delete fileToSync.progress; - fileToSync.message = typeof response === 'string' - ? response - : response.type === 'abort' - ? this.t('Request was aborted') - : response.toString(); - - this.emit('fileUploadError', { - fileToSync, - response, - }); - } - finally { - delete fileToSync.progress; - this.redraw(); - } + if (this.root.form.submissionRevisions !== 'true') { + this.statuses.push(fileUpload); + this.redraw(); + } - return { - fileToSync, - fileInfo, - }; - })); - } + if (fileUpload.status !== 'error') { + if (this.component.privateDownload) { + file.private = true; + } + const { storage, options = {} } = this.component; + const url = this.interpolate(this.component.url, { file: fileUpload }); + let groupKey = null; + let groupPermissions = null; + + //Iterate through form components to find group resource if one exists + this.root.everyComponent((element) => { + if (element.component?.submissionAccess || element.component?.defaultPermission) { + groupPermissions = !element.component.submissionAccess ? [ + { + type: element.component.defaultPermission, + roles: [], + }, + ] : element.component.submissionAccess; + + groupPermissions.forEach((permission) => { + groupKey = ['admin', 'write', 'create'].includes(permission.type) ? element.component.key : null; + }); + } + }); + const fileKey = this.component.fileKey || 'file'; + const groupResourceId = groupKey ? this.currentForm.submission.data[groupKey]._id : null; + let processedFile = null; + + if (this.root.options.fileProcessor) { + try { + if (this.refs.fileProcessingLoader) { + this.refs.fileProcessingLoader.style.display = 'block'; + } + const fileProcessorHandler = fileProcessor(this.fileService, this.root.options.fileProcessor); + processedFile = await fileProcessorHandler(file, this.component.properties); + } + catch (err) { + fileUpload.status = 'error'; + fileUpload.message = this.t('File processing has been failed.'); + this.fileDropHidden = false; + this.redraw(); + return; + } + finally { + if (this.refs.fileProcessingLoader) { + this.refs.fileProcessingLoader.style.display = 'none'; + } + } + } - async syncFiles() { - this.isSyncing = true; - this.fileDropHidden = true; - this.redraw(); - try { - const [filesToDelete = [], filesToUpload = []] = await Promise.all([this.delete(), this.upload()]); - this.filesToSync.filesToDelete = filesToDelete - .filter(file => file.fileToSync?.status === 'error') - .map(file => file.fileToSync); - this.filesToSync.filesToUpload = filesToUpload - .filter(file => file.fileToSync?.status === 'error') - .map(file => file.fileToSync); - - if (!this.hasValue()) { - this.dataValue =[]; - } + fileUpload.message = this.t('Starting upload...'); + this.redraw(); - const data = filesToUpload - .filter(file => file.fileToSync?.status === 'success') - .map(file => file.fileInfo); - this.dataValue.push(...data); - this.triggerChange(); - return Promise.resolve(); - } - catch (err) { - return Promise.reject(); - } - finally { - this.isSyncing = false; - this.fileDropHidden = false; - this.abortUploads = []; - this.redraw(); + const filePromise = fileService.uploadFile( + storage, + processedFile || file, + fileName, + dir, + // Progress callback + (evt) => { + fileUpload.status = 'progress'; + fileUpload.progress = parseInt(100.0 * evt.loaded / evt.total); + delete fileUpload.message; + this.redraw(); + }, + url, + options, + fileKey, + groupPermissions, + groupResourceId, + // Upload start callback + () => { + this.emit('fileUploadingStart', filePromise); + }, + (abort) => fileUpload.abort = abort, + ).then((fileInfo) => { + const index = this.statuses.indexOf(fileUpload); + if (index !== -1) { + this.statuses.splice(index, 1); + } + fileInfo.originalName = escapedFileName; + fileInfo.hash = fileUpload.hash; + if (!this.hasValue()) { + this.dataValue = []; + } + this.dataValue.push(fileInfo); + _.pull(this.filesUploading, fileInfo.originalName); + this.fileDropHidden = false; + this.redraw(); + this.triggerChange(); + this.emit('fileUploadingEnd', filePromise); + }) + .catch((response) => { + fileUpload.status = 'error'; + fileUpload.message = typeof response === 'string' ? response : response.toString(); + delete fileUpload.progress; + this.fileDropHidden = false; + _.pull(this.filesUploading, file.name); + this.redraw(); + this.emit('fileUploadingEnd', filePromise); + }); + } + else { + this.filesUploading.splice(this.filesUploading.indexOf(file.name),1); + } + }); } } @@ -1171,23 +884,7 @@ export default class FileComponent extends Field { } } - async beforeSubmit() { - try { - if (!this.autoSync) { - return Promise.resolve(); - } - - await this.syncFiles(); - return this.shouldSyncFiles - ? Promise.reject('Synchronization is failed') - : Promise.resolve(); - } - catch (error) { - return Promise.reject(error.message); - } - } - - destroy(all) { + destroy(all = false) { this.stopVideo(); super.destroy(all); } diff --git a/src/components/file/File.unit.js b/src/components/file/File.unit.js index 0affe77b13..ccef6ff316 100644 --- a/src/components/file/File.unit.js +++ b/src/components/file/File.unit.js @@ -206,13 +206,10 @@ describe('File Component', () => { const options = { fileService: { uploadFile: function(storage, file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, uploadStartCallback, abortCallbackSetter) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { // complete upload after 1s. - setTimeout(() => { + const timeout = setTimeout(function() { progressCallback({ loaded: 1, total: 1 }); - }, 10); - - const timeout = setTimeout(() => { const uploadResponse = { name: fileName, size: file.size, @@ -225,9 +222,6 @@ describe('File Component', () => { abortCallbackSetter(function() { abortedFiles.push(file.name); clearTimeout(timeout); - reject({ - type: 'abort', - }); }); }); } @@ -244,31 +238,23 @@ describe('File Component', () => { const content = [1]; const files = [new File(content, 'file.0'), new File([content], 'file.1'), new File([content], 'file.2')]; - component.handleFilesToUpload(files); + component.upload(files); setTimeout(function() { - // Table header and 3 rows for files - Harness.testElements(component, '.list-group-item', 4); - assert.equal(component.dataValue.length, 0); - assert.equal(component.filesToSync.filesToUpload.length, 3); - assert.equal(component.filesToSync.filesToUpload[1].status, 'progress'); - assert.equal(component.filesToSync.filesToDelete.length, 0); + Harness.testElements(component, 'div.file .fileName', 3); - const abortIcon = component.element.querySelectorAll(`#abort-${component.filesToSync.filesToUpload[1].id}`)[0]; - assert.notEqual(abortIcon, null); - abortIcon.click(); + component.element.querySelectorAll('i[ref="fileStatusRemove"]')[1].click(); setTimeout(() => { - assert.notEqual(component !== null); + assert(component !== null); assert(abortedFiles[0] === 'file.1' && abortedFiles.length === 1); - assert.equal(component.filesToSync.filesToUpload[1].status, 'error'); - assert.equal(component.filesToSync.filesToUpload[1].message, 'Request was aborted'); + assert(component.filesUploading.join(',') === 'file.0,file.2'); - Harness.testElements(component, '.list-group-item', 4); + Harness.testElements(component, 'div.file .fileName', 2); component.root = null; done(); }, 20); - }, 100); + }, 50); }); }); it('should not error on upload when noDefaults is set to true', () => { diff --git a/src/components/file/editForm/File.edit.display.js b/src/components/file/editForm/File.edit.display.js index 63f0c68770..2e7810dd7b 100644 --- a/src/components/file/editForm/File.edit.display.js +++ b/src/components/file/editForm/File.edit.display.js @@ -3,21 +3,4 @@ export default [ key: 'placeholder', ignore: true }, - { - type: 'checkbox', - label: 'Files Synchronization feature', - tooltip: 'Enable ability to control files synchronization. Files will be auto synced before submit.', - key: 'autoSync', - input: true, - conditional: { - json: { - in: [ - { - var: 'data.storage' - }, - ['s3', 'azure', 'googledrive'] - ], - } - } - }, ]; diff --git a/src/providers/storage/azure.js b/src/providers/storage/azure.js index 5476fb18a6..ae1b7fb1e0 100644 --- a/src/providers/storage/azure.js +++ b/src/providers/storage/azure.js @@ -29,10 +29,6 @@ function azure(formio) { }, downloadFile(file) { return formio.makeRequest('file', `${formio.formUrl}/storage/azure?name=${XHR.trim(file.name)}`, 'GET'); - }, - deleteFile: function deleteFile(fileInfo) { - var url = `${formio.formUrl}/storage/azure?name=${XHR.trim(fileInfo.name)}&key=${XHR.trim(fileInfo.key)}`; - return formio.makeRequest('', url, 'delete'); } }; } diff --git a/src/providers/storage/base64.js b/src/providers/storage/base64.js index 8935173e87..67f1ac34b1 100644 --- a/src/providers/storage/base64.js +++ b/src/providers/storage/base64.js @@ -9,31 +9,30 @@ function base64() { uploadFile(file, fileName) { const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onload = (event) => { - const url = event.target.result; - resolve({ - storage: 'base64', - name: fileName, - url: url, - size: file.size, - type: file.type, - }); - }; + return new Promise((resolve, reject) => { + reader.onload = (event) => { + const url = event.target.result; + resolve({ + storage: 'base64', + name: fileName, + url: url, + size: file.size, + type: file.type, + }); + }; - reader.onerror = () => { - return reject(this); - }; + reader.onerror = () => { + return reject(this); + }; - reader.readAsDataURL(file); - }); - }, - downloadFile(file) { - // Return the original as there is nothing to do. - return Promise.resolve(file); - }, - }; -} + reader.readAsDataURL(file); + }); + }, + downloadFile(file) { + // Return the original as there is nothing to do. + return Promise.resolve(file); + } +}); base64.title = 'Base64'; export default base64; diff --git a/src/providers/storage/googleDrive.js b/src/providers/storage/googleDrive.js index b97a46a8c8..cad8790216 100644 --- a/src/providers/storage/googleDrive.js +++ b/src/providers/storage/googleDrive.js @@ -13,65 +13,60 @@ function googledrive(formio) { // Send the file with data. const xhr = new XMLHttpRequest(); - if (typeof progressCallback === 'function') { - xhr.upload.onprogress = progressCallback; - } + if (typeof progressCallback === 'function') { + xhr.upload.onprogress = progressCallback; + } - if (typeof abortCallback === 'function') { - abortCallback(() => xhr.abort()); - } + if (typeof abortCallback === 'function') { + abortCallback(() => xhr.abort()); + } - const fd = new FormData(); - fd.append('name', fileName); - fd.append('dir', dir); - fd.append('file', file); + const fd = new FormData(); + fd.append('name', fileName); + fd.append('dir', dir); + fd.append('file', file); - // Fire on network error. - xhr.onerror = (err) => { - err.networkError = true; - reject(err); - }; + // Fire on network error. + xhr.onerror = (err) => { + err.networkError = true; + reject(err); + }; - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - const response = JSON.parse(xhr.response); - response.storage = 'googledrive'; - response.size = file.size; - response.type = file.type; - response.groupId = groupId; - response.groupPermissions = groupPermissions; - resolve(response); - } - else { - reject(xhr.response || 'Unable to upload file'); - } - }; + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + const response = JSON.parse(xhr.response); + response.storage = 'googledrive'; + response.size = file.size; + response.type = file.type; + response.groupId = groupId; + response.groupPermissions = groupPermissions; + resolve(response); + } + else { + reject(xhr.response || 'Unable to upload file'); + } + }; - xhr.onabort = reject; + xhr.onabort = reject; - xhr.open('POST', `${formio.formUrl}/storage/gdrive`); + xhr.open('POST', `${formio.formUrl}/storage/gdrive`); - setXhrHeaders(formio, xhr); + setXhrHeaders(formio, xhr); - const token = formio.getToken(); - if (token) { - xhr.setRequestHeader('x-jwt-token', token); - } - xhr.send(fd); - })); - }, - downloadFile(file) { const token = formio.getToken(); - file.url = - `${formio.formUrl}/storage/gdrive?fileId=${file.id}&fileName=${file.originalName}${token ? `&x-jwt-token=${token}` : ''}`; - return Promise.resolve(file); - }, - deleteFile: function deleteFile(fileInfo) { - var url = ''.concat(formio.formUrl, `/storage/gdrive?id=${fileInfo.id}&name=${fileInfo.originalName}`); - return formio.makeRequest('', url, 'delete'); - }, - }; -} + if (token) { + xhr.setRequestHeader('x-jwt-token', token); + } + xhr.send(fd); + })); + }, + downloadFile(file) { + const token = formio.getToken(); + file.url = + `${formio.formUrl}/storage/gdrive?fileId=${file.id}&fileName=${file.originalName}${token ? `&x-jwt-token=${token}` : ''}`; + return Promise.resolve(file); + } +}); googledrive.title = 'Google Drive'; export default googledrive; diff --git a/src/providers/storage/indexeddb.js b/src/providers/storage/indexeddb.js index 21c0fc6aa0..196203bef4 100644 --- a/src/providers/storage/indexeddb.js +++ b/src/providers/storage/indexeddb.js @@ -26,116 +26,100 @@ function indexeddb() { }).then((db) => { const reader = new FileReader(); - return new Promise((resolve, reject) => { - reader.onload = () => { - const blobObject = new Blob([file], { type: file.type }); + const trans = db.transaction([options.indexeddbTable], 'readwrite'); + const addReq = trans.objectStore(options.indexeddbTable).put(data, id); - const id = uuidv4(blobObject); + addReq.onerror = function(e) { + console.log('error storing data'); + console.error(e); + }; - const data = { - id, - data: blobObject, + trans.oncomplete = function() { + resolve({ + storage: 'indexeddb', name: file.name, size: file.size, type: file.type, - url, - }; + url: url, + id, + }); + }; + }; - const trans = db.transaction([options.indexeddbTable], 'readwrite'); - const addReq = trans.objectStore(options.indexeddbTable).put(data, id); + reader.onerror = () => { + return reject(this); + }; - addReq.onerror = function(e) { - console.log('error storing data'); - console.error(e); + reader.readAsDataURL(file); + }); + }); + }, + downloadFile(file, options) { + return new Promise((resolve) => { + const request = indexedDB.open(options.indexeddb); + + request.onsuccess = function(event) { + const db = event.target.result; + resolve(db); + }; + }).then((db) => { + return new Promise((resolve, reject) => { + const trans = db.transaction([options.indexeddbTable], 'readonly'); + const store = trans.objectStore(options.indexeddbTable).get(file.id); + store.onsuccess = () => { + trans.oncomplete = () => { + const result = store.result; + const dbFile = new File([store.result.data], file.name, { + type: store.result.type, + }); + + const reader = new FileReader(); + + reader.onload = (event) => { + result.url = event.target.result; + result.storage = file.storage; + resolve(result); }; - trans.oncomplete = function() { - resolve({ - storage: 'indexeddb', - name: file.name, - size: file.size, - type: file.type, - url: url, - id, - }); + reader.onerror = () => { + return reject(this); }; - }; - reader.onerror = () => { - return reject(this); + reader.readAsDataURL(dbFile); }; - - reader.readAsDataURL(file); - }); - }); - }, - downloadFile(file, options) { - return new Promise((resolve) => { - const request = indexedDB.open(options.indexeddb); - - request.onsuccess = function(event) { - const db = event.target.result; - resolve(db); }; - }).then((db) => { - return new Promise((resolve, reject) => { - const trans = db.transaction([options.indexeddbTable], 'readonly'); - const store = trans.objectStore(options.indexeddbTable).get(file.id); - store.onsuccess = () => { - trans.oncomplete = () => { - const result = store.result; - const dbFile = new File([store.result.data], file.name, { - type: store.result.type, - }); - - const reader = new FileReader(); - - reader.onload = (event) => { - result.url = event.target.result; - result.storage = file.storage; - resolve(result); - }; - - reader.onerror = () => { - return reject(this); - }; - - reader.readAsDataURL(dbFile); - }; - }; - store.onerror = () => { - return reject(this); - }; - }); - }); - }, - deleteFile(file, options) { - return new Promise((resolve) => { - const request = indexedDB.open(options.indexeddb); - - request.onsuccess = function(event) { - const db = event.target.result; - resolve(db); + store.onerror = () => { + return reject(this); }; - }).then((db) => { - return new Promise((resolve, reject) => { - const trans = db.transaction([options.indexeddbTable], 'readwrite'); - const store = trans.objectStore(options.indexeddbTable).delete(file.id); - store.onsuccess = () => { - trans.oncomplete = () => { - const result = store.result; - - resolve(result); - }; - }; - store.onerror = () => { - return reject(this); + }); + }); + }, + deleteFile(file, options) { + return new Promise((resolve) => { + const request = indexedDB.open(options.indexeddb); + + request.onsuccess = function(event) { + const db = event.target.result; + resolve(db); + }; + }).then((db) => { + return new Promise((resolve, reject) => { + const trans = db.transaction([options.indexeddbTable], 'readwrite'); + const store = trans.objectStore(options.indexeddbTable).delete(file.id); + store.onsuccess = () => { + trans.oncomplete = () => { + const result = store.result; + + resolve(result); }; - }); + }; + store.onerror = () => { + return reject(this); + }; }); - }, - }; -} + }); + } +}); indexeddb.title = 'IndexedDB'; export default indexeddb; diff --git a/src/providers/storage/s3.js b/src/providers/storage/s3.js index c50e62c9a1..9db8ae0752 100644 --- a/src/providers/storage/s3.js +++ b/src/providers/storage/s3.js @@ -161,15 +161,36 @@ function s3(formio) { return formio.makeRequest('file', `${formio.formUrl}/storage/s3?bucket=${XHR.trim(file.bucket)}&key=${XHR.trim(file.key)}`, 'GET'); } else { - return Promise.resolve(file); + const fd = new FormData(); + for (const key in response.data) { + fd.append(key, response.data[key]); + } + fd.append('file', file); + xhr.openAndSetHeaders('POST', response.url); + return fd; } - }, - deleteFile(fileInfo) { - const url = `${formio.formUrl}/storage/s3?bucket=${XHR.trim(fileInfo.bucket)}&key=${XHR.trim(fileInfo.key)}`; - return formio.makeRequest('', url, 'delete'); - }, - }; -} + }, file, fileName, dir, progressCallback, groupPermissions, groupId, abortCallback).then((response) => { + return { + storage: 's3', + name: fileName, + bucket: response.bucket, + key: response.data.key, + url: XHR.path([response.url, response.data.key]), + acl: response.data.acl, + size: file.size, + type: file.type + }; + }); + }, + downloadFile(file) { + if (file.acl !== 'public-read') { + return formio.makeRequest('file', `${formio.formUrl}/storage/s3?bucket=${XHR.trim(file.bucket)}&key=${XHR.trim(file.key)}`, 'GET'); + } + else { + return Promise.resolve(file); + } + } + }); s3.title = 'S3'; export default s3; diff --git a/src/providers/storage/url.js b/src/providers/storage/url.js index c65803e90c..e963d09ef7 100644 --- a/src/providers/storage/url.js +++ b/src/providers/storage/url.js @@ -165,7 +165,7 @@ function url(formio) { return Promise.resolve(file); } }; -} +}; url.title = 'Url'; export default url; diff --git a/src/sass/formio.form.scss b/src/sass/formio.form.scss index 4d0767d9d9..cb8f17da6c 100644 --- a/src/sass/formio.form.scss +++ b/src/sass/formio.form.scss @@ -378,15 +378,6 @@ td > .formio-form-group { } } -.formio-component-file .status { - margin-top: 4px; - font-size: 0.9rem; -} - -.formio-component-file .list-group-item .fa { - cursor: pointer; -} - .formio-component-file .fileSelector.fileDragOver { border-color: #127abe; } @@ -1747,13 +1738,3 @@ span[role="link"] { .hidden { display: none !important; } - -.align-center { - display: flex; - align-items: center; -} - -.justify-center { - display: flex; - justify-content: center; -} diff --git a/test/renders/component-bootstrap-file-html-value0.html b/test/renders/component-bootstrap-file-html-value0.html index ee416ffefd..5838be1431 100644 --- a/test/renders/component-bootstrap-file-html-value0.html +++ b/test/renders/component-bootstrap-file-html-value0.html @@ -5,10 +5,9 @@ diff --git a/test/renders/component-bootstrap-file-html-value1.html b/test/renders/component-bootstrap-file-html-value1.html index 1d5927e824..7b6f45afbd 100644 --- a/test/renders/component-bootstrap-file-html-value1.html +++ b/test/renders/component-bootstrap-file-html-value1.html @@ -5,10 +5,9 @@