From 5d305e68b8026f729cb3a29d29f386fd882b4b5d Mon Sep 17 00:00:00 2001 From: Aleksandr Grenishin Date: Tue, 16 Mar 2021 19:38:30 +0300 Subject: [PATCH] fix(react-native): prevent app crashes while multipart uploading (#308) --- package.json | 3 +- src/uploadFile/prepareChunks.node.ts | 10 ++++++ src/uploadFile/prepareChunks.react-native.ts | 24 +++++++++++++ src/uploadFile/sliceChunk.ts | 11 ++++++ src/uploadFile/uploadMultipart.ts | 38 +++++++------------- 5 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 src/uploadFile/prepareChunks.node.ts create mode 100644 src/uploadFile/prepareChunks.react-native.ts create mode 100644 src/uploadFile/sliceChunk.ts diff --git a/package.json b/package.json index 31af35996..870d00a89 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "react-native": { "./lib/request/request.node.js": "./lib/request/request.browser.js", "./lib/tools/getFormData.node.js": "./lib/tools/getFormData.react-native.js", - "./lib/tools/sockets.node.js": "./lib/tools/sockets.browser.js" + "./lib/tools/sockets.node.js": "./lib/tools/sockets.browser.js", + "./lib/uploadFile/prepareChunks.node.js": "./lib/uploadFile/prepareChunks.react-native.js" }, "scripts": { "check-env-vars": "node ./checkvars.js", diff --git a/src/uploadFile/prepareChunks.node.ts b/src/uploadFile/prepareChunks.node.ts new file mode 100644 index 000000000..d309bfe55 --- /dev/null +++ b/src/uploadFile/prepareChunks.node.ts @@ -0,0 +1,10 @@ +import { sliceChunk } from './sliceChunk' + +export function prepareChunks( + file: Buffer | Blob, + fileSize: number, + chunkSize: number +): (index: number) => Buffer | Blob { + return (index: number): Buffer | Blob => + sliceChunk(file, index, fileSize, chunkSize) +} diff --git a/src/uploadFile/prepareChunks.react-native.ts b/src/uploadFile/prepareChunks.react-native.ts new file mode 100644 index 000000000..8a993207b --- /dev/null +++ b/src/uploadFile/prepareChunks.react-native.ts @@ -0,0 +1,24 @@ +import { sliceChunk } from './sliceChunk' + +/** + * React-native hack for blob slicing + * + * We need to store references to sliced blobs to prevent source blob from + * being deallocated until uploading complete. Access to deallocated blob + * causes app crash. + * + * See https://github.com/uploadcare/uploadcare-upload-client/issues/306 + * and https://github.com/facebook/react-native/issues/27543 + */ +export function prepareChunks( + file: Buffer | Blob, + fileSize: number, + chunkSize: number +): (index: number) => Buffer | Blob { + const chunks: (Buffer | Blob)[] = [] + return (index: number): Buffer | Blob => { + const chunk = sliceChunk(file, index, fileSize, chunkSize) + chunks.push(chunk) + return chunk + } +} diff --git a/src/uploadFile/sliceChunk.ts b/src/uploadFile/sliceChunk.ts new file mode 100644 index 000000000..214c0bc76 --- /dev/null +++ b/src/uploadFile/sliceChunk.ts @@ -0,0 +1,11 @@ +export const sliceChunk = ( + file: Buffer | Blob, + index: number, + fileSize: number, + chunkSize: number +): Buffer | Blob => { + const start = chunkSize * index + const end = Math.min(start + chunkSize, fileSize) + + return file.slice(start, end) +} diff --git a/src/uploadFile/uploadMultipart.ts b/src/uploadFile/uploadMultipart.ts index 73e70c3ef..e886727c2 100644 --- a/src/uploadFile/uploadMultipart.ts +++ b/src/uploadFile/uploadMultipart.ts @@ -1,4 +1,5 @@ import defaultSettings from '../defaultSettings' +import { prepareChunks } from './prepareChunks.node' import multipartStart from '../api/multipartStart' import multipartUpload from '../api/multipartUpload' import multipartComplete from '../api/multipartComplete' @@ -38,18 +39,6 @@ export type MultipartOptions = { baseCDN?: string } -const getChunk = ( - file: Buffer | Blob, - index: number, - fileSize: number, - chunkSize: number -): Buffer | Blob => { - const start = chunkSize * index - const end = Math.min(start + chunkSize, fileSize) - - return file.slice(start, end) -} - const uploadPartWithRetry = ( chunk: Buffer | Blob, url: string, @@ -138,27 +127,24 @@ const uploadMultipart = ( integration, retryThrottledRequestMaxTimes }) - .then(({ uuid, parts }) => - Promise.all([ + .then(({ uuid, parts }) => { + const getChunk = prepareChunks(file, size, multipartChunkSize) + return Promise.all([ uuid, runWithConcurrency( maxConcurrentRequests, parts.map((url, index) => (): Promise => - uploadPartWithRetry( - getChunk(file, index, size, multipartChunkSize), - url, - { - publicKey, - onProgress: createProgressHandler(parts.length, index), - cancel, - integration, - multipartMaxAttempts - } - ) + uploadPartWithRetry(getChunk(index), url, { + publicKey, + onProgress: createProgressHandler(parts.length, index), + cancel, + integration, + multipartMaxAttempts + }) ) ) ]) - ) + }) .then(([uuid]) => multipartComplete(uuid, { publicKey,