From 8f799d76ae68557c5cbe705eebfe6ba2f06891de Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Fri, 13 Oct 2023 15:03:44 -0500 Subject: [PATCH 1/5] adapt multipart upload feature to 4.18.x --- src/Formio.js | 4 +- src/components/file/File.js | 17 ++ .../file/editForm/File.edit.file.js | 40 +++++ src/providers/storage/s3.js | 166 ++++++++++++++---- src/providers/storage/s3.unit.js | 79 +++++++++ src/providers/storage/util.js | 6 + src/providers/storage/xhr.js | 150 ++++++++-------- 7 files changed, 355 insertions(+), 107 deletions(-) create mode 100644 src/providers/storage/s3.unit.js create mode 100644 src/providers/storage/util.js diff --git a/src/Formio.js b/src/Formio.js index 0803c872e2..34ff6c4f52 100644 --- a/src/Formio.js +++ b/src/Formio.js @@ -585,7 +585,7 @@ class Formio { }); } - uploadFile(storage, file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, uploadStartCallback, abortCallback) { + uploadFile(storage, file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, uploadStartCallback, abortCallback, multipartOptions) { const requestArgs = { provider: storage, method: 'upload', @@ -605,7 +605,7 @@ class Formio { 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'); diff --git a/src/components/file/File.js b/src/components/file/File.js index 1d200cef5e..38bbe8fb5c 100644 --- a/src/components/file/File.js +++ b/src/components/file/File.js @@ -771,6 +771,22 @@ export default class FileComponent extends Field { } } + 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(); @@ -797,6 +813,7 @@ export default class FileComponent extends Field { }, // Abort upload callback (abort) => this.abortUpload = abort, + multipartOptions ).then((fileInfo) => { const index = this.statuses.indexOf(fileUpload); if (index !== -1) { diff --git a/src/components/file/editForm/File.edit.file.js b/src/components/file/editForm/File.edit.file.js index d1aeafd6c3..f27bf36a42 100644 --- a/src/components/file/editForm/File.edit.file.js +++ b/src/components/file/editForm/File.edit.file.js @@ -21,6 +21,46 @@ export default [ } } }, + { + type: 'checkbox', + input: true, + key: 'useMultipartUpload', + label: 'Use the S3 Multipart Upload API', + tooltip: "The S3 Multipart Upload API 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, diff --git a/src/providers/storage/s3.js b/src/providers/storage/s3.js index a89ba0f1bd..05afce2631 100644 --- a/src/providers/storage/s3.js +++ b/src/providers/storage/s3.js @@ -1,27 +1,71 @@ import NativePromise from 'native-promise-only'; 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; - } - else { - const fd = new FormData(); - for (const key in response.data) { - fd.append(key, response.data[key]); +function s3(formio) { + return { + async uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback, multipartOptions) { + const xhrCallback = async(xhr, response, abortCallback) => { + response.data.fileName = fileName; + response.data.key = XHR.path([response.data.key, dir, fileName]); + if (response.signed) { + if (multipartOptions && Array.isArray(response.signed)) { + // patch abort callback + const abortController = new AbortController(); + const abortSignal = abortController.signal; + if (typeof abortCallback === 'function') { + abortCallback(() => abortController.abort()); + } + try { + const parts = await this.uploadParts( + file, + response.signed, + response.data.headers, + response.partSizeActual, + multipartOptions, + abortSignal + ); + await withRetries(this.completeMultipartUpload, [response, parts, multipartOptions], 3); + return; + } + catch (err) { + // abort in-progress fetch requests + abortController.abort(); + // attempt to cancel the multipart upload + this.abortMultipartUpload(response); + throw err; + } + } + else { + xhr.openAndSetHeaders('PUT', response.signed); + xhr.setRequestHeader('Content-Type', file.type); + Object.keys(response.data.headers).forEach((key) => { + xhr.setRequestHeader(key, response.data.headers[key]); + }); + return file; + } } - fd.append('file', file); - xhr.openAndSetHeaders('POST', response.url); - return fd; - } - }, file, fileName, dir, progressCallback, groupPermissions, groupId, abortCallback).then((response) => { + 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; + } + }; + const response = await XHR.upload( + formio, + 's3', + xhrCallback, + file, + fileName, + dir, + progressCallback, + groupPermissions, + groupId, + abortCallback, + multipartOptions + ); return { storage: 's3', name: fileName, @@ -32,17 +76,77 @@ const s3 = (formio) => ({ 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 NativePromise.resolve(file); + }, + async completeMultipartUpload(serverResponse, parts, multipart) { + const { changeMessage } = multipart; + changeMessage('Completing AWS S3 multipart upload...'); + const response = await fetch(`${formio.formUrl}/storage/s3/multipart/complete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ parts, uploadId: serverResponse.uploadId, key: serverResponse.key }) + }); + const message = await response.text(); + if (!response.ok) { + throw new Error(message); + } + // the AWS S3 SDK CompleteMultipartUpload command can return a HTTP 200 status header but still error; + // we need to parse, and according to AWS, to retry + if (message.match(/Error/)) { + throw new Error(message); + } + }, + abortMultipartUpload(serverResponse) { + const { uploadId, key } = serverResponse; + fetch(`${formio.formUrl}/storage/s3/multipart/abort`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ uploadId, key }) + }); + }, + uploadParts(file, urls, headers, partSize, multipart, abortSignal) { + const { changeMessage, progressCallback } = multipart; + changeMessage('Chunking and uploading parts to AWS S3...'); + const promises = []; + for (let i = 0; i < urls.length; i++) { + const start = i * partSize; + const end = (i + 1) * partSize; + const blob = i < urls.length ? file.slice(start, end) : file.slice(start); + const promise = fetch(urls[i], { + method: 'PUT', + headers, + body: blob, + signal: abortSignal, + }).then((res) => { + if (res.ok) { + progressCallback(urls.length); + const eTag = res.headers.get('etag'); + if (!eTag) { + throw new Error('ETag header not found; it must be exposed in S3 bucket CORS settings'); + } + return { ETag: eTag, PartNumber: i + 1 }; + } + else { + throw new Error(`Part no ${i} failed with status ${res.status}`); + } + }); + promises.push(promise); + } + return Promise.all(promises); + }, + 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/s3.unit.js b/src/providers/storage/s3.unit.js new file mode 100644 index 0000000000..a1f21f1561 --- /dev/null +++ b/src/providers/storage/s3.unit.js @@ -0,0 +1,79 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import fetchMock from 'fetch-mock'; + +import S3 from './s3'; +import { withRetries } from './util'; + +describe('S3 Provider', () => { + describe('Function Unit Tests', () => { + it('withRetries should retry a given function three times, then throw the provided error', (done) => { + function sleepAndReject(ms) { + return new Promise((_, reject) => setTimeout(reject, ms)); + } + + const spy = sinon.spy(sleepAndReject); + withRetries(spy, [200], 3, 'Custom error message').catch((err) => { + assert.equal(err.message, 'Custom error message'); + assert.equal(spy.callCount, 3); + done(); + }); + }); + }); + + describe('Provider Integration Tests', () => { + describe('AWS S3 Multipart Uploads', () => { + before('Mocks fetch', () => { + fetchMock + .post('https://fakeproject.form.io/fakeform/storage/s3', { + signed: new Array(5).fill('https://fakebucketurl.aws.com/signed'), + minio: false, + url: 'https://fakebucketurl.aws.com', + bucket: 'fakebucket', + uploadId: 'fakeuploadid', + key: 'test.jpg', + partSizeActual: 1, + data: {} + }) + .put('https://fakebucketurl.aws.com/signed', { status: 200, headers: { 'Etag': 'fakeetag' } }) + .post('https://fakeproject.form.io/fakeform/storage/s3/multipart/complete', 200) + .post('https://fakeproject.form.io/fakeform/storage/s3/multipart/abort', 200); + }); + it('Given an array of signed urls it should upload a file to S3 using multipart upload', (done) => { + const mockFormio = { + formUrl: 'https://fakeproject.form.io/fakeform', + getToken: () => {} + }; + const s3 = new S3(mockFormio); + const uploadSpy = sinon.spy(s3, 'uploadParts'); + const completeSpy = sinon.spy(s3, 'completeMultipartUpload'); + + const mockFile = new File(['test!'], 'test.jpg', { type: 'image/jpeg' }); + s3.uploadFile( + mockFile, + 'test.jpg', + '', + () => {}, + '', + {}, + 'test.jpg', + {}, + '', + () => {}, + { partSize: 1, changeMessage: () => {}, progressCallback: () => {} } + ).then((response) => { + assert.equal(response.storage, 's3'); + assert.equal(response.name, 'test.jpg'); + assert.equal(response.bucket, 'fakebucket'); + assert.equal(response.url, 'https://fakebucketurl.aws.com/test.jpg'); + assert.equal(response.acl, undefined); + assert.equal(response.size, 5); + assert.equal(response.type, 'image/jpeg'); + assert.equal(uploadSpy.callCount, 1); + assert.equal(completeSpy.callCount, 1); + done(); + }); + }); + }); + }); +}); diff --git a/src/providers/storage/util.js b/src/providers/storage/util.js new file mode 100644 index 0000000000..f84101648f --- /dev/null +++ b/src/providers/storage/util.js @@ -0,0 +1,6 @@ +export async function withRetries(fn, args, retries = 3, err = null) { + if (!retries) { + throw new Error(err); + } + return fn(...args).catch(() => withRetries(fn, args, retries - 1, err)); +} diff --git a/src/providers/storage/xhr.js b/src/providers/storage/xhr.js index 7b66d2c971..8a0275e39a 100644 --- a/src/providers/storage/xhr.js +++ b/src/providers/storage/xhr.js @@ -22,87 +22,89 @@ const XHR = { path(items) { return items.filter(item => !!item).map(XHR.trim).join('/'); }, - upload(formio, type, xhrCb, file, fileName, dir, progressCallback, groupPermissions, groupId, abortCallback) { - return new NativePromise(((resolve, reject) => { - // Send the pre response to sign the upload. - const pre = new XMLHttpRequest(); - - // This only fires on a network error. - pre.onerror = (err) => { - err.networkError = true; - reject(err); + async upload(formio, type, xhrCallback, file, fileName, dir, progressCallback, groupPermissions, groupId, abortCallback, multipartOptions) { + // make request to Form.io server + const token = formio.getToken(); + let response; + try { + response = await fetch(`${formio.formUrl}/storage/${type}`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json; charset=UTF-8', + ...(token ? { 'x-jwt-token': token } : {}), + }, + body: JSON.stringify({ + name: XHR.path([dir, fileName]), + size: file.size, + type: file.type, + groupPermissions, + groupId, + multipart: multipartOptions + }) + }); + } + catch (err) { + // only throws on network errors + err.networkError = true; + throw err; + } + if (!response.ok) { + const message = await response.text(); + throw new Error(message || 'Unable to sign file.'); + } + const serverResponse = await response.json(); + return await XHR.makeXhrRequest(formio, xhrCallback, serverResponse, progressCallback, abortCallback); + }, + makeXhrRequest(formio, xhrCallback, serverResponse, progressCallback, abortCallback) { + return new Promise((resolve, reject) => { + // Send the file with data. + const xhr = new XMLHttpRequest(); + xhr.openAndSetHeaders = (...params) => { + xhr.open(...params); + setXhrHeaders(formio, xhr); }; + Promise.resolve(xhrCallback(xhr, serverResponse, abortCallback)).then((payload) => { + // if payload is nullish we can assume the provider took care of the entire upload process + if (!payload) { + return resolve(serverResponse); + } + // Fire on network error. + xhr.onerror = (err) => { + err.networkError = true; + reject(err); + }; - pre.onabort = reject; - pre.onload = () => { - if (pre.status >= 200 && pre.status < 300) { - const response = JSON.parse(pre.response); - - // Send the file with data. - const xhr = new XMLHttpRequest(); - - if (typeof progressCallback === 'function') { - xhr.upload.onprogress = progressCallback; - } - - if (typeof abortCallback === 'function') { - abortCallback(() => xhr.abort()); - } - - xhr.openAndSetHeaders = (...params) => { - xhr.open(...params); - setXhrHeaders(formio, xhr); - }; - - // Fire on network error. - xhr.onerror = (err) => { - err.networkError = true; - reject(err); - }; - - // Fire on network abort. - xhr.onabort = (err) => { - err.networkError = true; - reject(err); - }; - - // Fired when the response has made it back from the server. - xhr.onload = () => { - if (xhr.status >= 200 && xhr.status < 300) { - resolve(response); - } - else { - reject(xhr.response || 'Unable to upload file'); - } - }; + // Fire on network abort. + xhr.onabort = (err) => { + err.networkError = true; + reject(err); + }; - // Set the onabort error callback. - xhr.onabort = reject; + // Set the onabort error callback. + xhr.onabort = reject; - // Get the request and send it to the server. - xhr.send(xhrCb(xhr, response)); - } - else { - reject(pre.response || 'Unable to sign file'); + if (typeof progressCallback === 'function') { + xhr.upload.onprogress = progressCallback; } - }; - pre.open('POST', `${formio.formUrl}/storage/${type}`); - pre.setRequestHeader('Accept', 'application/json'); - pre.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - const token = formio.getToken(); - if (token) { - pre.setRequestHeader('x-jwt-token', token); - } + if (typeof abortCallback === 'function') { + abortCallback(() => xhr.abort()); + } + // Fired when the response has made it back from the server. + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(serverResponse); + } + else { + reject(xhr.response || 'Unable to upload file'); + } + }; - pre.send(JSON.stringify({ - name: XHR.path([dir, fileName]), - size: file.size, - type: file.type, - groupPermissions, - groupId, - })); - })); + // Get the request and send it to the server. + xhr.send(payload); + }).catch(reject); + }); } }; From 8c19a06e3ae22cfbb6a2ebe31529187644b51b69 Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Fri, 13 Oct 2023 17:09:23 -0500 Subject: [PATCH 2/5] send token with requests that need them; error handling --- src/providers/storage/s3.js | 20 +++++++++++++------- src/providers/storage/xhr.js | 7 ++++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/providers/storage/s3.js b/src/providers/storage/s3.js index 05afce2631..5d70a63380 100644 --- a/src/providers/storage/s3.js +++ b/src/providers/storage/s3.js @@ -1,5 +1,7 @@ import NativePromise from 'native-promise-only'; + import XHR from './xhr'; +import { withRetries } from './util'; function s3(formio) { return { async uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback, multipartOptions) { @@ -79,33 +81,37 @@ function s3(formio) { }, async completeMultipartUpload(serverResponse, parts, multipart) { const { changeMessage } = multipart; + const token = formio.getToken(); changeMessage('Completing AWS S3 multipart upload...'); const response = await fetch(`${formio.formUrl}/storage/s3/multipart/complete`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + ...(token ? { 'x-jwt-token': token } : {}), }, body: JSON.stringify({ parts, uploadId: serverResponse.uploadId, key: serverResponse.key }) }); const message = await response.text(); if (!response.ok) { - throw new Error(message); + throw new Error(message || response.statusText); } // the AWS S3 SDK CompleteMultipartUpload command can return a HTTP 200 status header but still error; // we need to parse, and according to AWS, to retry - if (message.match(/Error/)) { + if (message?.match(/Error/)) { throw new Error(message); } }, abortMultipartUpload(serverResponse) { const { uploadId, key } = serverResponse; + const token = formio.getToken(); fetch(`${formio.formUrl}/storage/s3/multipart/abort`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + ...(token ? { 'x-jwt-token': token } : {}), }, body: JSON.stringify({ uploadId, key }) - }); + }).catch((err) => console.error('Error while aborting multipart upload:', err)); }, uploadParts(file, urls, headers, partSize, multipart, abortSignal) { const { changeMessage, progressCallback } = multipart; @@ -135,14 +141,14 @@ function s3(formio) { }); promises.push(promise); } - return Promise.all(promises); + return NativePromise.all(promises); }, 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); + return NativePromise.resolve(file); } } }; diff --git a/src/providers/storage/xhr.js b/src/providers/storage/xhr.js index 8a0275e39a..bc1c31e7d3 100644 --- a/src/providers/storage/xhr.js +++ b/src/providers/storage/xhr.js @@ -57,16 +57,17 @@ const XHR = { return await XHR.makeXhrRequest(formio, xhrCallback, serverResponse, progressCallback, abortCallback); }, makeXhrRequest(formio, xhrCallback, serverResponse, progressCallback, abortCallback) { - return new Promise((resolve, reject) => { + return new NativePromise((resolve, reject) => { // Send the file with data. - const xhr = new XMLHttpRequest(); + let xhr = new XMLHttpRequest(); xhr.openAndSetHeaders = (...params) => { xhr.open(...params); setXhrHeaders(formio, xhr); }; - Promise.resolve(xhrCallback(xhr, serverResponse, abortCallback)).then((payload) => { + NativePromise.resolve(xhrCallback(xhr, serverResponse, abortCallback)).then((payload) => { // if payload is nullish we can assume the provider took care of the entire upload process if (!payload) { + xhr = null; return resolve(serverResponse); } // Fire on network error. From fcf4d1aa49d2bf8434a2af2b1796b9652f949904 Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Fri, 13 Oct 2023 17:21:06 -0500 Subject: [PATCH 3/5] add logging for error during tests --- test/harness.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/harness.js b/test/harness.js index 6d52f19f47..b81a09656d 100644 --- a/test/harness.js +++ b/test/harness.js @@ -12,8 +12,9 @@ Components.setComponents(AllComponents); if (process) { // Do not handle unhandled rejections. - process.on('unhandledRejection', () => { + process.on('unhandledRejection', (err) => { console.warn('Unhandled rejection!'); + console.warn(err); }); } From 3fd404fa2e56a9946931b04a100f322fe1920c0c Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Fri, 13 Oct 2023 17:36:38 -0500 Subject: [PATCH 4/5] add abortcontroller polyfill --- package.json | 3 ++- src/Formio.js | 1 + yarn.lock | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 19dfa748c2..a2820caa89 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@formio/semantic": "2.6.1", "@formio/text-mask-addons": "^3.8.0-formio.2", "@formio/vanilla-text-mask": "^5.1.1-formio.1", + "abortcontroller-polyfill": "^1.7.5", "autocompleter": "^7.0.1", "browser-cookies": "^1.2.0", "browser-md5-file": "^1.1.1", @@ -105,8 +106,8 @@ "@babel/polyfill": "^7.12.1", "@babel/preset-env": "^7.20.2", "@babel/register": "^7.17.7", - "async-limiter": "^2.0.0", "ace-builds": "1.23.4", + "async-limiter": "^2.0.0", "babel-loader": "^9.1.0", "bootstrap": "^4.6.0", "bootswatch": "^4.6.0", diff --git a/src/Formio.js b/src/Formio.js index 34ff6c4f52..d616307f7e 100644 --- a/src/Formio.js +++ b/src/Formio.js @@ -4,6 +4,7 @@ // also duck-punches the global Promise definition. For now, keep native-promise-only. import NativePromise from 'native-promise-only'; import fetchPonyfill from 'fetch-ponyfill'; +import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-patch-fetch'; import EventEmitter from './EventEmitter'; import cookies from 'browser-cookies'; import Providers from './providers'; diff --git a/yarn.lock b/yarn.lock index dfc584b15e..a1100d398f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1548,6 +1548,11 @@ abbrev@^1.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abortcontroller-polyfill@^1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed" + integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ== + accepts@~1.3.4: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" From 4ef9779f8b3e67db0588debf90fa2d43778f02ac Mon Sep 17 00:00:00 2001 From: brendanjbond Date: Sat, 14 Oct 2023 17:27:57 -0500 Subject: [PATCH 5/5] try to lazily require abortcontroller polyfill --- src/Formio.js | 1 - src/providers/storage/s3.js | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Formio.js b/src/Formio.js index d616307f7e..34ff6c4f52 100644 --- a/src/Formio.js +++ b/src/Formio.js @@ -4,7 +4,6 @@ // also duck-punches the global Promise definition. For now, keep native-promise-only. import NativePromise from 'native-promise-only'; import fetchPonyfill from 'fetch-ponyfill'; -import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-patch-fetch'; import EventEmitter from './EventEmitter'; import cookies from 'browser-cookies'; import Providers from './providers'; diff --git a/src/providers/storage/s3.js b/src/providers/storage/s3.js index 5d70a63380..9a8599bbcb 100644 --- a/src/providers/storage/s3.js +++ b/src/providers/storage/s3.js @@ -2,6 +2,8 @@ import NativePromise from 'native-promise-only'; import XHR from './xhr'; import { withRetries } from './util'; + +const AbortController = window.AbortController || require('abortcontroller-polyfill/dist/cjs-ponyfill'); function s3(formio) { return { async uploadFile(file, fileName, dir, progressCallback, url, options, fileKey, groupPermissions, groupId, abortCallback, multipartOptions) {