Skip to content

Commit

Permalink
FIO 7239: support for AWS S3 Multipart Upload (#5356)
Browse files Browse the repository at this point in the history
* add multipart checkbox to file component editForm

* refactor xhr in storage and add support for s3
multipart uploads

* refactor to async/await

* add support for midstream cancel

* minor

* refactor withRetries into util

* begin tests

* minor
  • Loading branch information
brendanbond authored Oct 12, 2023
1 parent a381c68 commit 19b1e4f
Show file tree
Hide file tree
Showing 13 changed files with 634 additions and 373 deletions.
4 changes: 2 additions & 2 deletions src/Formio.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Formio.Providers = Providers;
Formio.version = 'FORMIO_VERSION';

const isNil = (val) => val === null || val === undefined;
Formio.prototype.uploadFile = function(storage, file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, uploadStartCallback, abortCallback) {
Formio.prototype.uploadFile = function(storage, file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, uploadStartCallback, abortCallback, multipartOptions) {
const requestArgs = {
provider: storage,
method: 'upload',
Expand All @@ -26,7 +26,7 @@ Formio.prototype.uploadFile = function(storage, file, fileName, dir, progressCal
if (uploadStartCallback) {
uploadStartCallback();
}
return provider.uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback);
return provider.uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback, multipartOptions);
}
else {
throw ('Storage provider not found');
Expand Down
18 changes: 18 additions & 0 deletions src/components/file/File.js
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,23 @@ export default class FileComponent extends Field {
}
}

// Prep for a potential multipart upload
let count = 0;
const multipartOptions = this.component.useMultipartUpload && this.component.multipart ? {
...this.component.multipart,
progressCallback: (total) => {
count++;
fileUpload.status = 'progress';
fileUpload.progress = parseInt(100 * count / total);
delete fileUpload.message;
this.redraw();
},
changeMessage: (message) => {
fileUpload.message = message;
this.redraw();
},
} : false;

fileUpload.message = this.t('Starting upload...');
this.redraw();

Expand All @@ -814,6 +831,7 @@ export default class FileComponent extends Field {
this.emit('fileUploadingStart', filePromise);
},
(abort) => fileUpload.abort = abort,
multipartOptions
).then((fileInfo) => {
const index = this.statuses.indexOf(fileUpload);
if (index !== -1) {
Expand Down
40 changes: 40 additions & 0 deletions src/components/file/editForm/File.edit.file.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,46 @@ export default [
}
}
},
{
type: 'checkbox',
input: true,
key: 'useMultipartUpload',
label: 'Use the S3 Multipart Upload API',
tooltip: "The <a href='https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html'>S3 Multipart Upload API</a> is designed to improve the upload experience for larger objects (> 5GB).",
conditional: {
json: { '===': [{ var: 'data.storage' }, 's3'] }
},
},
{
label: 'Multipart Upload',
tableView: false,
key: 'multipart',
type: 'container',
input: true,
components: [
{
label: 'Part Size (MB)',
applyMaskOn: 'change',
mask: false,
tableView: false,
delimiter: false,
requireDecimal: false,
inputFormat: 'plain',
truncateMultipleSpaces: false,
validate: {
min: 5,
max: 5000,
},
key: 'partSize',
type: 'number',
input: true,
defaultValue: 500,
},
],
conditional: {
json: { '===': [{ var: 'data.useMultipartUpload' }, true] }
},
},
{
type: 'textfield',
input: true,
Expand Down
46 changes: 24 additions & 22 deletions src/providers/storage/azure.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
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(() => {
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');
}
};
}

azure.title = 'Azure File Services';
export default azure;
56 changes: 29 additions & 27 deletions src/providers/storage/base64.js
Original file line number Diff line number Diff line change
@@ -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;
104 changes: 53 additions & 51 deletions src/providers/storage/dropbox.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,67 @@
import { setXhrHeaders } from './xhr';
const dropbox = (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 dropbox(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 = 'dropbox';
response.size = file.size;
response.type = file.type;
response.groupId = groupId;
response.groupPermissions = groupPermissions;
response.url = response.path_lower;
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 = 'dropbox';
response.size = file.size;
response.type = file.type;
response.groupId = groupId;
response.groupPermissions = groupPermissions;
response.url = response.path_lower;
resolve(response);
}
else {
reject(xhr.response || 'Unable to upload file');
}
};

xhr.onabort = reject;
xhr.onabort = reject;

xhr.open('POST', `${formio.formUrl}/storage/dropbox`);
xhr.open('POST', `${formio.formUrl}/storage/dropbox`);

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/dropbox?path_lower=${file.path_lower}${token ? `&x-jwt-token=${token}` : ''}`;
return Promise.resolve(file);
}
});
file.url =
`${formio.formUrl}/storage/dropbox?path_lower=${file.path_lower}${token ? `&x-jwt-token=${token}` : ''}`;
return Promise.resolve(file);
}
};
}

dropbox.title = 'Dropbox';
export default dropbox;
Loading

0 comments on commit 19b1e4f

Please sign in to comment.