cdcf7e7772
## Steps
When it upload reaches 100%, click Cancel (not refresh).
## Issue
There was an old hack in b0509bc9
where we decided to wait a while before sending `notify` as the server was not responsive. Since the task was dispatched before the Cancel action, the server cleared the upload first and later received the `notify`.
## Change
Instead of trying to cancel the timer, I think the hack is no longer needed given the throughput and lock fixes. With things running back in sequential mode, the Cancel button will now just show the "upload already completed" modal.
157 lines
5.7 KiB
JavaScript
157 lines
5.7 KiB
JavaScript
// @flow
|
|
import * as tus from 'tus-js-client';
|
|
import NoopUrlStorage from 'tus-js-client/lib/noopUrlStorage';
|
|
import analytics from '../../ui/analytics';
|
|
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 = 25 * 1024 * 1024;
|
|
|
|
const STATUS_CONFLICT = 409;
|
|
const STATUS_LOCKED = 423;
|
|
|
|
/**
|
|
* Checks whether a given status is in the range of the expected category.
|
|
*
|
|
* @param status
|
|
* @param category
|
|
* @returns {boolean}
|
|
*/
|
|
function inStatusCategory(status, category) {
|
|
return status >= category && status < category + 100;
|
|
}
|
|
|
|
function getTusErrorType(errMsg: string) {
|
|
if (errMsg.startsWith('tus: failed to upload chunk at offset')) {
|
|
// This is the only message that contains dynamic value prior to the first comma.
|
|
return 'tus: failed to upload chunk at offset';
|
|
} else {
|
|
return errMsg.startsWith('tus:') ? errMsg.substring(0, errMsg.indexOf(',')) : errMsg;
|
|
}
|
|
}
|
|
|
|
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 { uploadUrl, guid, ...sdkParams } = params;
|
|
|
|
const jsonPayload = JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
method: RESUMABLE_ENDPOINT_METHOD,
|
|
params: sdkParams,
|
|
id: new Date().getTime(),
|
|
});
|
|
|
|
const urlOptions = {};
|
|
if (params.uploadUrl) {
|
|
// Resuming from previous upload. TUS clears the resume fingerprint on any
|
|
// 4xx error, so we need to use the fixed URL mode instead.
|
|
urlOptions.uploadUrl = params.uploadUrl;
|
|
} else {
|
|
// New upload, so use `endpoint`.
|
|
urlOptions.endpoint = RESUMABLE_ENDPOINT;
|
|
}
|
|
|
|
const uploader = new tus.Upload(file, {
|
|
...urlOptions,
|
|
chunkSize: UPLOAD_CHUNK_SIZE_BYTE,
|
|
retryDelays: [8000, 10000, 15000, 20000, 30000],
|
|
parallelUploads: 1,
|
|
storeFingerprintForResuming: false,
|
|
urlStorage: new NoopUrlStorage(),
|
|
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({ guid, status: 'retry' }));
|
|
const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
|
|
return !inStatusCategory(status, 400) || status === STATUS_CONFLICT || status === STATUS_LOCKED;
|
|
},
|
|
onError: (err) => {
|
|
const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
|
|
const errMsg = typeof err === 'string' ? err : err.message;
|
|
|
|
let customErr;
|
|
if (status === STATUS_LOCKED || errMsg === 'file currently locked') {
|
|
customErr = 'File is locked. Try resuming after waiting a few minutes';
|
|
}
|
|
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'error' }));
|
|
analytics.sentryError(getTusErrorType(errMsg), { onError: err, tusUpload: uploader });
|
|
|
|
reject(
|
|
// $FlowFixMe - flow's constructor for Error is incorrect.
|
|
new Error(customErr || err, {
|
|
cause: {
|
|
// ...(uploader._fingerprint ? { fingerprint: uploader._fingerprint } : {}),
|
|
// ...(uploader._retryAttempt ? { retryAttempt: uploader._retryAttempt } : {}),
|
|
// ...(uploader._offsetBeforeRetry ? { offsetBeforeRetry: uploader._offsetBeforeRetry } : {}),
|
|
...(customErr ? { original: errMsg } : {}),
|
|
},
|
|
})
|
|
);
|
|
},
|
|
onProgress: (bytesUploaded, bytesTotal) => {
|
|
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, 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.onloadstart = () => {
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'notify' }));
|
|
};
|
|
xhr.onload = () => {
|
|
window.store.dispatch(doUpdateUploadRemove(guid));
|
|
resolve(xhr);
|
|
};
|
|
xhr.onerror = () => {
|
|
if (retries > 0 && xhr.status === 0) {
|
|
--retries;
|
|
analytics.error('notify: first attempt failed (status=0). Retrying after 10s...');
|
|
setTimeout(() => makeNotifyRequest(), 10000); // Auto-retry after 10s delay.
|
|
} else {
|
|
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'error' }));
|
|
reject(new Error(`There was a problem in the processing. Please retry. (${xhr.status})`));
|
|
}
|
|
};
|
|
xhr.onabort = () => {
|
|
window.store.dispatch(doUpdateUploadRemove(guid));
|
|
};
|
|
|
|
xhr.send(jsonPayload);
|
|
}
|
|
|
|
makeNotifyRequest();
|
|
},
|
|
});
|
|
|
|
window.store.dispatch(doUpdateUploadAdd(file, params, uploader));
|
|
uploader.start();
|
|
});
|
|
}
|