Resumable v2 uploads

This commit is contained in:
Fedor Indutny 2024-07-16 19:25:07 -07:00 committed by GitHub
parent 72c6fa8884
commit 57f7dc1b16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 78 additions and 18 deletions

View file

@ -1305,7 +1305,8 @@ export type WebAPIType = {
elements: VerifyServiceIdRequestType elements: VerifyServiceIdRequestType
) => Promise<VerifyServiceIdResponseType>; ) => Promise<VerifyServiceIdResponseType>;
putEncryptedAttachment: ( putEncryptedAttachment: (
encryptedBin: Uint8Array | (() => Readable), encryptedBin: (start: number, end?: number) => Readable,
encryptedSize: number,
uploadForm: AttachmentUploadFormResponseType uploadForm: AttachmentUploadFormResponseType
) => Promise<void>; ) => Promise<void>;
putProfile: ( putProfile: (
@ -3532,7 +3533,8 @@ export function initialize({
} }
async function putEncryptedAttachment( async function putEncryptedAttachment(
encryptedBin: Uint8Array | (() => Readable), encryptedBin: (start: number, end?: number) => Readable,
encryptedSize: number,
uploadForm: AttachmentUploadFormResponseType uploadForm: AttachmentUploadFormResponseType
) { ) {
const { signedUploadLocation, headers } = uploadForm; const { signedUploadLocation, headers } = uploadForm;
@ -3563,21 +3565,78 @@ export function initialize({
'attachment upload form header has no location' 'attachment upload form header has no location'
); );
// This is going to the CDN, not the service, so we use _outerAjax const redactUrl = () => {
await _outerAjax(uploadLocation, { const tmp = new URL(uploadLocation);
certificateAuthority, tmp.search = '';
proxyUrl, tmp.pathname = '';
timeout: 0, return `${tmp}[REDACTED]`;
type: 'PUT', };
version,
data: encryptedBin, const MAX_RETRIES = 10;
redactUrl: () => { for (
const tmp = new URL(uploadLocation); let start = 0, retries = 0;
tmp.search = ''; start < encryptedSize && retries < MAX_RETRIES;
tmp.pathname = ''; retries += 1
return `${tmp}[REDACTED]`; ) {
}, const logId = `putEncryptedAttachment(attempt=${retries})`;
});
if (retries !== 0) {
log.warn(`${logId}: resuming from ${start}`);
}
try {
// This is going to the CDN, not the service, so we use _outerAjax
// eslint-disable-next-line no-await-in-loop
await _outerAjax(uploadLocation, {
disableRetries: true,
certificateAuthority,
proxyUrl,
timeout: 0,
type: 'PUT',
version,
headers: {
'Content-Range': `bytes ${start}-*/${encryptedSize}`,
},
data: () => encryptedBin(start),
redactUrl,
});
if (retries !== 0) {
log.warn(`${logId}: Attachment upload succeeded`);
}
return;
} catch (error) {
log.warn(
`${logId}: Failed to upload attachment chunk: ${toLogFormat(error)}`
);
}
// eslint-disable-next-line no-await-in-loop
const result: BytesWithDetailsType = await _outerAjax(uploadLocation, {
certificateAuthority,
proxyUrl,
type: 'PUT',
version,
headers: {
'Content-Range': `bytes */${encryptedSize}`,
},
data: new Uint8Array(0),
redactUrl,
responseType: 'byteswithdetails',
});
const { response } = result;
strictAssert(response.status === 308, 'Invalid server response');
const range = response.headers.get('range');
if (range != null) {
const match = range.match(/^bytes=0-(\d+)$/);
strictAssert(match != null, `Invalid range header: ${range}`);
start = parseInt(match[1], 10);
} else {
log.warn(`${logId}: No range header`);
}
}
throw new Error('Upload failed');
} }
function getHeaderPadding() { function getHeaderPadding() {

View file

@ -148,7 +148,8 @@ export async function uploadFile({
}); });
} else { } else {
await server.putEncryptedAttachment( await server.putEncryptedAttachment(
() => createReadStream(absoluteCiphertextPath), (start, end) => createReadStream(absoluteCiphertextPath, { start, end }),
ciphertextFileSize,
uploadForm uploadForm
); );
} }