lbry-desktop/web/setup/publish-v2.js
infinite-persistence dfe30b6d78
TUS: fix parallel uploads of the same file
## Issue
If you make 2 claims from the same source file, the second upload thinks it's trying to resume from the first one. They should be unique uploads.

## Approach
Stash the upload url for comparison when looking up existing uploads to resume.

Stash that in `params` to minimize code changes. We'll just need to ensure it is cleared before we generate the SDK payload.
2021-11-12 14:32:40 +08:00

113 lines
3.8 KiB
JavaScript

// @flow
import * as tus from 'tus-js-client';
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
import { doUpdateUploadAdd, doUpdateUploadProgress, doUpdateUploadRemove } from '../../ui/redux/actions/publish';
import { LBRY_WEB_PUBLISH_API_V2 } from 'config';
const RESUMABLE_ENDPOINT = LBRY_WEB_PUBLISH_API_V2;
const RESUMABLE_ENDPOINT_METHOD = 'publish';
const UPLOAD_CHUNK_SIZE_BYTE = 100000000;
export function makeResumableUploadRequest(
token: string,
params: FileUploadSdkParams,
file: File | string,
isPreview?: boolean
) {
return new Promise<any>((resolve, reject) => {
if (!RESUMABLE_ENDPOINT) {
reject(new Error('Publish: endpoint undefined'));
}
if (params.remote_url) {
reject(new Error('Publish: v2 does not support remote_url'));
}
const payloadParams = Object.assign({}, params);
delete payloadParams.uploadUrl; // cleanup
const jsonPayload = JSON.stringify({
jsonrpc: '2.0',
method: RESUMABLE_ENDPOINT_METHOD,
params: payloadParams,
id: new Date().getTime(),
});
const uploader = new tus.Upload(file, {
endpoint: RESUMABLE_ENDPOINT,
chunkSize: UPLOAD_CHUNK_SIZE_BYTE,
retryDelays: [0, 5000, 10000, 15000],
parallelUploads: 1,
removeFingerprintOnSuccess: true,
headers: { [X_LBRY_AUTH_TOKEN]: token },
metadata: {
filename: file instanceof File ? file.name : file,
filetype: file instanceof File ? file.type : undefined,
},
onShouldRetry: (err, retryAttempt, options) => {
window.store.dispatch(doUpdateUploadProgress({ params, status: 'retry' }));
const FORBIDDEN_ERROR = 403;
const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
return status !== FORBIDDEN_ERROR;
},
onError: (error) => {
window.store.dispatch(doUpdateUploadProgress({ params, status: 'error' }));
reject(new Error(error));
},
onProgress: (bytesUploaded, bytesTotal) => {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
window.store.dispatch(doUpdateUploadProgress({ params, progress: percentage }));
},
onSuccess: () => {
let retries = 1;
function makeNotifyRequest() {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${uploader.url}/notify`);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Tus-Resumable', '1.0.0');
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
xhr.responseType = 'json';
xhr.onload = () => {
window.store.dispatch(doUpdateUploadRemove(params));
resolve(xhr);
};
xhr.onerror = () => {
if (retries > 0 && xhr.status === 0) {
--retries;
setTimeout(() => makeNotifyRequest(), 10000); // Auto-retry after 10s delay.
} else {
window.store.dispatch(doUpdateUploadProgress({ params, status: 'error' }));
reject(new Error(`There was a problem in the processing. Please retry. (${xhr.status})`));
}
};
xhr.onabort = () => {
window.store.dispatch(doUpdateUploadRemove(params));
};
xhr.send(jsonPayload);
}
makeNotifyRequest();
},
});
uploader
.findPreviousUploads()
.then((previousUploads) => {
const index = previousUploads.findIndex((prev) => prev.uploadUrl === params.uploadUrl);
if (index !== -1) {
uploader.resumeFromPreviousUpload(previousUploads[index]);
}
if (!isPreview) {
window.store.dispatch(doUpdateUploadAdd(file, params, uploader));
}
uploader.start();
})
.catch((err) => {
reject(new Error(__('Failed to initiate upload (%err%)', { err })));
});
});
}