From 022a3d2a306eb5cbb2f4b21741a3e8cd00a8bed6 Mon Sep 17 00:00:00 2001 From: Roman Letsuk Date: Tue, 10 Oct 2023 17:47:31 +0300 Subject: [PATCH] fixed issues and tests --- src/components/file/File.js | 227 ++++++++++++++----------- src/components/file/File.unit.js | 32 +++- src/providers/storage/azure.js | 51 +++--- src/providers/storage/base64.js | 56 ++++--- src/providers/storage/googleDrive.js | 106 ++++++------ src/providers/storage/indexeddb.js | 230 +++++++++++++------------- src/providers/storage/s3.js | 90 +++++----- src/providers/storage/url.js | 4 +- src/templates/bootstrap/file/form.ejs | 177 -------------------- 9 files changed, 430 insertions(+), 543 deletions(-) delete mode 100644 src/templates/bootstrap/file/form.ejs diff --git a/src/components/file/File.js b/src/components/file/File.js index f13d88d27e..ac81adbdb1 100644 --- a/src/components/file/File.js +++ b/src/components/file/File.js @@ -37,6 +37,8 @@ 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({ @@ -362,6 +364,12 @@ export default class FileComponent extends Field { return options; } + get actions() { + return { + abort: this.abortRequest.bind(this), + }; + } + attach(element) { this.loadRefs(element, { fileDrop: 'single', @@ -379,7 +387,7 @@ export default class FileComponent extends Field { fileProcessingLoader: 'single', syncNow: 'single', restoreFile: 'multiple', - abortRequest: '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 = []; @@ -389,7 +397,7 @@ export default class FileComponent extends Field { // if (!this.statuses.length) { // this.refs.fileDrop.removeAttribute('hidden'); // } - const element = this; + const _this = this; this.addEventListener(this.refs.fileDrop, 'dragover', function(event) { this.className = 'fileSelector fileDragOver'; event.preventDefault(); @@ -401,10 +409,14 @@ export default class FileComponent extends Field { this.addEventListener(this.refs.fileDrop, 'drop', function(event) { this.className = 'fileSelector'; event.preventDefault(); - element.handleFilesToUpload(event.dataTransfer.files); + _this.handleFilesToUpload(event.dataTransfer.files); }); } + this.addEventListener(element, 'click', (event) => { + this.handleAction(event); + }); + if (this.refs.fileBrowse) { this.addEventListener(this.refs.fileBrowse, 'click', (event) => { event.preventDefault(); @@ -438,18 +450,6 @@ export default class FileComponent extends Field { }); }); - this.refs.abortRequest.forEach((abort, index) => { - this.addEventListener(abort, 'click', (event) => { - event.preventDefault(); - const fileInfo = this.filesToSync.filesToUpload[index]; - const abortUpload = this.abortUploads.find(abortUpload => abortUpload.name === fileInfo.name); - if (abortUpload) { - abortUpload.abort(); - } - this.redraw(); - }); - }); - this.refs.restoreFile.forEach((fileToRestore, index) => { this.addEventListener(fileToRestore, 'click', (event) => { event.preventDefault(); @@ -676,6 +676,26 @@ 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()); } @@ -683,6 +703,7 @@ export default class FileComponent extends Field { 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), @@ -893,16 +914,13 @@ export default class FileComponent extends Field { if (this.component.storage && files && files.length) { this.fileDropHidden = true; - return NativePromise.all([...files].map((file) => { - return new NativePromise(async(resolve) => { - await this.prepareFileToUpload(file); - this.redraw(); - resolve(); - }); + return Promise.all([...files].map(async(file) => { + await this.prepareFileToUpload(file); + this.redraw(); })); } else { - return NativePromise.resolve(); + return Promise.resolve(); } } @@ -937,7 +955,7 @@ export default class FileComponent extends Field { async deleteFile(fileInfo) { const { options = {} } = this.component; - if (fileInfo && (['url', 'indexeddb', 's3'].includes(this.component.storage))) { + 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); @@ -954,47 +972,54 @@ export default class FileComponent extends Field { async delete() { if (!this.filesToSync.filesToDelete.length) { - return NativePromise.resolve(); + return Promise.resolve(); } - return await Promise.all(this.filesToSync.filesToDelete.map((fileToSync, i) => { - return new NativePromise(async(resolve) => { - try { - if (fileToSync.isValidationError) { - return; - } - - 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(); - resolve({ - fileToSync, - }); + return await Promise.all(this.filesToSync.filesToDelete.map(async(fileToSync) => { + try { + if (fileToSync.isValidationError) { + return { fileToSync }; } - }); + + 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 }; })); } - async uploadFile(fileToSync, abortIndex) { + 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(); + } + } + + async uploadFile(fileToSync) { return await this.fileService.uploadFile( fileToSync.storage, fileToSync.file, fileToSync.name, fileToSync.dir, // Progress callback - (evt) => { - fileToSync.status = 'progress'; - fileToSync.progress = parseInt(100.0 * evt.loaded / evt.total); - delete fileToSync.message; - this.redraw(); - }, + this.updateProgress.bind(this, fileToSync), fileToSync.url, fileToSync.options, fileToSync.fileKey, @@ -1003,7 +1028,7 @@ export default class FileComponent extends Field { () => {}, // Abort upload callback (abort) => this.abortUploads.push({ - name: fileToSync.name, + id: fileToSync.id, abort, }), ); @@ -1011,46 +1036,53 @@ export default class FileComponent extends Field { async upload() { if (!this.filesToSync.filesToUpload.length) { - return NativePromise.resolve(); + return Promise.resolve(); } - return await NativePromise.all(this.filesToSync.filesToUpload.map((fileToSync, i) => { - return new NativePromise(async(resolve) => { - let fileInfo = null; - try { - if (fileToSync.isValidationError) { - return; - } - - fileInfo = await this.uploadFile(fileToSync); - fileToSync.status = 'success'; - fileToSync.message = this.t('Succefully uploaded'); - - fileInfo.originalName = fileToSync.originalName; - fileInfo.hash = fileToSync.hash; - } - catch (response) { - fileToSync.status = 'error'; - fileToSync.message = typeof response === 'string' ? response : response.toString(); - delete fileToSync.progress; - } - finally { - delete fileToSync.progress; - this.redraw(); - resolve({ + return await Promise.all(this.filesToSync.filesToUpload.map(async(fileToSync) => { + let fileInfo = null; + try { + if (fileToSync.isValidationError) { + return { fileToSync, fileInfo, - }); + }; } - }); + + fileInfo = await this.uploadFile(fileToSync); + fileToSync.status = 'success'; + fileToSync.message = this.t('Succefully uploaded'); + + 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(); + } + finally { + delete fileToSync.progress; + this.redraw(); + } + + return { + fileToSync, + fileInfo, + }; })); } async syncFiles() { this.isSyncing = true; + this.fileDropHidden = true; this.redraw(); try { - const [filesToDelete = [], filesToUpload = []] = await NativePromise.all([this.delete(), this.upload()]); + const [filesToDelete = [], filesToUpload = []] = await Promise.all([this.delete(), this.upload()]); this.filesToSync.filesToDelete = filesToDelete .filter(file => file.fileToSync?.status === 'error') .map(file => file.fileToSync); @@ -1067,13 +1099,14 @@ export default class FileComponent extends Field { .map(file => file.fileInfo); this.dataValue.push(...data); this.triggerChange(); - return NativePromise.resolve(); + return Promise.resolve(); } catch (err) { - return NativePromise.reject(); + return Promise.reject(); } finally { this.isSyncing = false; + this.fileDropHidden = false; this.abortUploads = []; this.redraw(); } @@ -1115,22 +1148,20 @@ export default class FileComponent extends Field { } } - beforeSubmit() { - return new NativePromise(async(resolve, reject) => { - try { - if (!this.autoSync) { - return resolve(); - } - - await this.syncFiles(); - this.shouldSyncFiles - ? reject('Synchronization is failed') - : resolve(); - } - catch (error) { - reject(error.message); + 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) { diff --git a/src/components/file/File.unit.js b/src/components/file/File.unit.js index 4d4359ea0d..0a06811fc4 100644 --- a/src/components/file/File.unit.js +++ b/src/components/file/File.unit.js @@ -206,10 +206,13 @@ describe('File Component', () => { const options = { fileService: { uploadFile: function(storage, file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, uploadStartCallback, abortCallbackSetter) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { // complete upload after 1s. - const timeout = setTimeout(function() { + setTimeout(() => { progressCallback({ loaded: 1, total: 1 }); + }, 10); + + const timeout = setTimeout(() => { const uploadResponse = { name: fileName, size: file.size, @@ -222,6 +225,9 @@ describe('File Component', () => { abortCallbackSetter(function() { abortedFiles.push(file.name); clearTimeout(timeout); + reject({ + type: 'abort', + }); }); }); } @@ -238,23 +244,31 @@ describe('File Component', () => { const content = [1]; const files = [new File(content, 'file.0'), new File([content], 'file.1'), new File([content], 'file.2')]; - component.upload(files); + component.handleFilesToUpload(files); setTimeout(function() { - Harness.testElements(component, 'div.file .fileName', 3); + // 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); - component.element.querySelectorAll('i[ref="fileStatusRemove"]')[1].click(); + const abortIcon = component.element.querySelectorAll(`#abort-${component.filesToSync.filesToUpload[1].id}`)[0]; + assert.notEqual(abortIcon, null); + abortIcon.click(); setTimeout(() => { - assert(component !== null); + assert.notEqual(component !== null); assert(abortedFiles[0] === 'file.1' && abortedFiles.length === 1); - assert(component.filesUploading.join(',') === 'file.0,file.2'); + assert.equal(component.filesToSync.filesToUpload[1].status, 'error'); + assert.equal(component.filesToSync.filesToUpload[1].message, 'Request was aborted'); - Harness.testElements(component, 'div.file .fileName', 2); + Harness.testElements(component, '.list-group-item', 4); component.root = null; done(); }, 20); - }, 50); + }, 100); }); }); }); diff --git a/src/providers/storage/azure.js b/src/providers/storage/azure.js index 9eb301f0ac..46fd5fbfb7 100644 --- a/src/providers/storage/azure.js +++ b/src/providers/storage/azure.js @@ -1,26 +1,33 @@ import XHR from './xhr'; -const azure = (formio) => ({ - uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback) { - return XHR.upload(formio, 'azure', (xhr, response) => { - xhr.openAndSetHeaders('PUT', response.url); - xhr.setRequestHeader('Content-Type', file.type); - xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob'); - return file; - }, file, fileName, dir, progressCallback, groupPermissions, groupId, abortCallback).then(() => { - return { - storage: 'azure', - name: XHR.path([dir, fileName]), - size: file.size, - type: file.type, - groupPermissions, - groupId, - }; - }); - }, - downloadFile(file) { - return formio.makeRequest('file', `${formio.formUrl}/storage/azure?name=${XHR.trim(file.name)}`, 'GET'); - } -}); +function azure(formio) { + return { + uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback) { + return XHR.upload(formio, 'azure', (xhr, response) => { + xhr.openAndSetHeaders('PUT', response.url); + xhr.setRequestHeader('Content-Type', file.type); + xhr.setRequestHeader('x-ms-blob-type', 'BlockBlob'); + return file; + }, file, fileName, dir, progressCallback, groupPermissions, groupId, abortCallback).then((response) => { + return { + storage: 'azure', + name: XHR.path([dir, fileName]), + size: file.size, + type: file.type, + groupPermissions, + groupId, + key: response.key, + }; + }); + }, + 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'); + } + }; +} azure.title = 'Azure File Services'; export default azure; diff --git a/src/providers/storage/base64.js b/src/providers/storage/base64.js index 9e8a080295..c5fc7ab950 100644 --- a/src/providers/storage/base64.js +++ b/src/providers/storage/base64.js @@ -1,33 +1,35 @@ -const base64 = () => ({ - title: 'Base64', - name: 'base64', - uploadFile(file, fileName) { - const reader = new FileReader(); +function base64() { + return { + title: 'Base64', + name: '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 3540669ffe..655d01b5cd 100644 --- a/src/providers/storage/googleDrive.js +++ b/src/providers/storage/googleDrive.js @@ -1,64 +1,70 @@ import { setXhrHeaders } from './xhr'; -const googledrive = (formio) => ({ - uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback) { - return new Promise(((resolve, reject) => { - // Send the file with data. - const xhr = new XMLHttpRequest(); +function googledrive(formio) { + return { + uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback) { + return new Promise(((resolve, reject) => { + // 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(); - 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); - } -}); + 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'); + }, + }; +} googledrive.title = 'Google Drive'; export default googledrive; diff --git a/src/providers/storage/indexeddb.js b/src/providers/storage/indexeddb.js index a24f7f4b8f..0fbf206799 100644 --- a/src/providers/storage/indexeddb.js +++ b/src/providers/storage/indexeddb.js @@ -1,135 +1,137 @@ import { v4 as uuidv4 } from 'uuid'; -const indexeddb = () => ({ - title: 'indexedDB', - name: 'indexeddb', - uploadFile(file, fileName, dir, progressCallback, url, options) { - if (!('indexedDB' in window)) { - console.log('This browser doesn\'t support IndexedDB'); - return; - } - - return new Promise((resolve) => { - const request = indexedDB.open(options.indexeddb); - request.onsuccess = function(event) { - const db = event.target.result; - resolve(db); - }; - request.onupgradeneeded = function(e) { - const db = e.target.result; - db.createObjectStore(options.indexeddbTable); - }; - }).then((db) => { - const reader = new FileReader(); - - return new Promise((resolve, reject) => { - reader.onload = () => { - const blobObject = new Blob([file], { type: file.type }); - - const id = uuidv4(blobObject); - - const data = { - id, - data: blobObject, - name: file.name, - size: file.size, - type: file.type, - url, - }; +function indexeddb() { + return { + title: 'indexedDB', + name: 'indexeddb', + uploadFile(file, fileName, dir, progressCallback, url, options) { + if (!('indexedDB' in window)) { + console.log('This browser doesn\'t support IndexedDB'); + return; + } + + return new Promise((resolve) => { + const request = indexedDB.open(options.indexeddb); + request.onsuccess = function(event) { + const db = event.target.result; + resolve(db); + }; + request.onupgradeneeded = function(e) { + const db = e.target.result; + db.createObjectStore(options.indexeddbTable); + }; + }).then((db) => { + const reader = new FileReader(); - const trans = db.transaction([options.indexeddbTable], 'readwrite'); - const addReq = trans.objectStore(options.indexeddbTable).put(data, id); + return new Promise((resolve, reject) => { + reader.onload = () => { + const blobObject = new Blob([file], { type: file.type }); - addReq.onerror = function(e) { - console.log('error storing data'); - console.error(e); - }; + const id = uuidv4(blobObject); - trans.oncomplete = function() { - resolve({ - storage: 'indexeddb', + const data = { + id, + data: blobObject, name: file.name, size: file.size, type: file.type, - url: url, - id, - }); - }; - }; + url, + }; - reader.onerror = () => { - return reject(this); - }; + const trans = db.transaction([options.indexeddbTable], 'readwrite'); + const addReq = trans.objectStore(options.indexeddbTable).put(data, id); - 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); + addReq.onerror = function(e) { + console.log('error storing data'); + console.error(e); }; - reader.onerror = () => { - return reject(this); + trans.oncomplete = function() { + resolve({ + storage: 'indexeddb', + name: file.name, + size: file.size, + type: file.type, + url: url, + id, + }); }; + }; - reader.readAsDataURL(dbFile); + reader.onerror = () => { + return reject(this); }; - }; - store.onerror = () => { - return reject(this); - }; + + reader.readAsDataURL(file); + }); }); - }); - }, - 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); - }; + }, + downloadFile(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], '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); }; + }).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 a2fd698c1c..61fb0a293a 100644 --- a/src/providers/storage/s3.js +++ b/src/providers/storage/s3.js @@ -1,51 +1,53 @@ import XHR from './xhr'; -const s3 = (formio) => ({ - uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback) { - return XHR.upload(formio, 's3', (xhr, response) => { - response.data.fileName = fileName; - response.data.key = XHR.path([response.data.key, dir, fileName]); - if (response.signed) { - xhr.openAndSetHeaders('PUT', response.signed); - Object.keys(response.data.headers).forEach(key => { - xhr.setRequestHeader(key, response.data.headers[key]); - }); - return file; +function s3(formio) { + return { + uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback) { + return XHR.upload(formio, 's3', (xhr, response) => { + response.data.fileName = fileName; + response.data.key = XHR.path([response.data.key, dir, fileName]); + if (response.signed) { + xhr.openAndSetHeaders('PUT', response.signed); + Object.keys(response.data.headers || {}).forEach(key => { + xhr.setRequestHeader(key, response.data.headers[key]); + }); + return file; + } + else { + 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; + } + }, 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 { - 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; + return Promise.resolve(file); } - }, 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); - } - }, - deleteFile(fileInfo) { - const url = `${formio.formUrl}/file/${XHR.trim(fileInfo.name)}?bucket=${XHR.trim(fileInfo.bucket)}&key=${XHR.trim(fileInfo.key)}`; - return formio.makeRequest('', url, 'delete'); - }, -}); + }, + deleteFile(fileInfo) { + const url = `${formio.formUrl}/storage/s3?bucket=${XHR.trim(fileInfo.bucket)}&key=${XHR.trim(fileInfo.key)}`; + return formio.makeRequest('', url, 'delete'); + }, + }; +} s3.title = 'S3'; export default s3; diff --git a/src/providers/storage/url.js b/src/providers/storage/url.js index c58b383752..8acd63162f 100644 --- a/src/providers/storage/url.js +++ b/src/providers/storage/url.js @@ -1,4 +1,4 @@ -const url = (formio) => { +function url(formio) { const xhrRequest = (url, name, query, data, options, progressCallback, abortCallback) => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); @@ -148,7 +148,7 @@ const url = (formio) => { return Promise.resolve(file); } }; -}; +} url.title = 'Url'; export default url; diff --git a/src/templates/bootstrap/file/form.ejs b/src/templates/bootstrap/file/form.ejs deleted file mode 100644 index 0cf414df4c..0000000000 --- a/src/templates/bootstrap/file/form.ejs +++ /dev/null @@ -1,177 +0,0 @@ -{% if (ctx.options.vpat) { %} - -{% } %} -{% if (!ctx.self.imageUpload) { %} - {% if (ctx.options.vpat) { %} -
{{(!ctx.component.filePattern || ctx.component.filePattern === '*') ? 'Any file types are allowed' : ctx.t('Allowed file types: ') + ctx.component.filePattern}}
- {% } %} - -{% } else { %} -
- {% ctx.files.forEach(function(file) { %} -
- - {{file.originalName || file.name}} - {% if (!ctx.disabled) { %} - - {% } %} - -
- {% }) %} -
-{% } %} -{% if (!ctx.disabled && (ctx.component.multiple || !ctx.files.length)) { %} - {% if (ctx.self.useWebViewCamera) { %} -
- - -
- {% } else if (!ctx.self.cameraMode) { %} -
- {{ctx.t('Drop files to attach,')}} - {% if (ctx.self.imageUpload && ctx.component.webcam) { %} - {{ctx.t('use camera')}} - {% } %} - {{ctx.t('or')}} - - {{ctx.t('browse')}} - - {{ctx.t('Browse to attach file for ' + ctx.component.label + '. ' + - (ctx.component.description ? ctx.component.description + '. ' : '') + - ((!ctx.component.filePattern || ctx.component.filePattern === '*') ? 'Any file types are allowed' : ctx.t('Allowed file types: ') + ctx.component.filePattern))}} - - -
-
-
-
- {% } else { %} -
- -
- - - {% } %} -{% } %} -{% if (!ctx.component.storage || ctx.support.hasWarning) { %} -
- {% if (!ctx.component.storage) { %} -

{{ctx.t('No storage has been set for this field. File uploads are disabled until storage is set up.')}}

- {% } %} - {% if (!ctx.support.filereader) { %} -

{{ctx.t('File API & FileReader API not supported.')}}

- {% } %} - {% if (!ctx.support.formdata) { %} -

{{ctx.t("XHR2's FormData is not supported.")}}

- {% } %} - {% if (!ctx.support.progress) { %} -

{{ctx.t("XHR2's upload progress isn't supported.")}}

- {% } %} -
-{% } %}