-
-
Save IgorDePaula/1cbb50e88c5b908bee6f3fc5619ecd3a to your computer and use it in GitHub Desktop.
Alek's Multipart Upload
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import apiFetch from 'modules/api-fetch'; | |
import Toast from 'modules/toast/index.native'; | |
import Sentry from 'modules/sentry'; | |
const FILE_CHUNK_SIZE = 5242880; | |
const uploadMultipart = async ( | |
file: string, | |
setIsUploading = (set: boolean) => {}, | |
setUploadProgress = (set: number) => {}, | |
destinationBucket = 'ClientFileBucket', | |
): Promise<string> => { | |
const parts = file.split('.'); | |
const fileExtension = parts[parts.length - 1].toLowerCase(); | |
setIsUploading(true); | |
try { | |
Sentry.addBreadcrumb({ | |
message: 'Beginning multipart upload', | |
category: 'multipartUpload', | |
}); | |
const fetchedFile = await fetch(file); | |
const blob = await fetchedFile.blob(); | |
Sentry.addBreadcrumb({ | |
message: 'Fetched file successfully', | |
category: 'multipartUpload', | |
}); | |
const numParts = Math.ceil(blob.size / FILE_CHUNK_SIZE); | |
const result = await apiFetch({ | |
url: 'Client/GetMultipartUploadUrls', | |
method: 'GET', | |
query: { fileExtension, destinationBucket, parts: numParts }, | |
}); | |
const { | |
uploadUrls, | |
uploadId, | |
fullBucketName, | |
filename, | |
}: { | |
uploadUrls: string[]; | |
uploadId: string; | |
fullBucketName: string; | |
filename: string; | |
} = result as any; | |
Sentry.addBreadcrumb({ | |
message: 'Retrieved multipart upload URLs', | |
category: 'multipartUpload', | |
}); | |
const apiCallsList = uploadUrls.map((url, index) => { | |
const start = index * FILE_CHUNK_SIZE; | |
const end = (index + 1) * FILE_CHUNK_SIZE; | |
const blobPart = | |
index < uploadUrls.length - 1 | |
? blob.slice(start, end) | |
: blob.slice(start); | |
const promise = () => | |
fetch(url, { | |
method: 'PUT', | |
body: blobPart, | |
}); | |
return promise; | |
}); | |
const uploadResults: Response[] = []; | |
let retryCount = 0; | |
const uploadSingleChunk = async (index: number) => { | |
Sentry.addBreadcrumb({ | |
message: `Executing upload single chunk for index #${index}, retry #${retryCount}`, | |
category: 'multipartUpload', | |
}); | |
if (index >= apiCallsList.length) { | |
fireAllPartUploadsCompleted(); | |
return; | |
} | |
setUploadProgress(index / apiCallsList.length); | |
try { | |
const callResult = await apiCallsList[index](); | |
retryCount = 0; | |
uploadResults.push(callResult); | |
await uploadSingleChunk(index + 1); | |
} catch (e) { | |
if (retryCount < 3) { | |
Sentry.addBreadcrumb({ | |
message: `Retrying for index #${index} and retry number ${retryCount}`, | |
category: 'multipartUpload', | |
}); | |
retryCount += 1; | |
const delay = (retryCount + 1) * 2000; | |
console.devLog.verbose(`Delaying: ${delay}`); | |
await new Promise((resolve) => setTimeout(resolve, delay)); | |
await uploadSingleChunk(index); | |
} else { | |
Sentry.addBreadcrumb({ | |
message: `No more retries, index #${index}`, | |
category: 'multipartUpload', | |
}); | |
Sentry.captureException(e); | |
throw new Error(`File upload failed.`); | |
} | |
} | |
}; | |
const fireAllPartUploadsCompleted = async () => { | |
Sentry.addBreadcrumb({ | |
message: 'Firing all parts uploaded', | |
category: 'multipartUpload', | |
}); | |
const resParts = uploadResults.map((part, index) => ({ | |
ETag: (part as any).headers.map.etag, | |
PartNumber: index + 1, | |
})); | |
await apiFetch({ | |
url: 'Client/CompleteMultipartUpload', | |
method: 'POST', | |
query: { | |
uploadId, | |
parts: resParts, | |
bucketName: fullBucketName, | |
guid: filename, | |
}, | |
body: resParts, | |
}); | |
Sentry.addBreadcrumb({ | |
message: 'Multipart upload completed, HUZZAH!', | |
category: 'multipartUpload', | |
}); | |
}; | |
await uploadSingleChunk(0); | |
return filename; | |
} catch (e) { | |
console.devLog.info(e); | |
console.log('Error setting up somewhere in the multipart higher chain'); | |
Toast.error('Error uploading file', { | |
position: 'bottom', | |
duration: 15000, | |
}); | |
Sentry.captureException(e); | |
throw e; | |
} finally { | |
setUploadProgress(0); | |
setIsUploading(false); | |
} | |
}; | |
export { uploadMultipart }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment