lbry-desktop/ui/redux/reducers/publish.js
infinite-persistence 1cc2132a28 Uploads: prevent perpetual locked upload
## Issue
- Closes 592 Force clear stuck upload
- It was possible for an upload to stay "locked" e.g. when browser is killed.

## Change
When refreshing or opening a new tab, always clear the locks. The on-going sessions will re-lock them immediately.
2022-01-03 12:10:55 -05:00

260 lines
7.3 KiB
JavaScript

// @flow
import { handleActions } from 'util/redux-utils';
import { buildURI } from 'util/lbryURI';
import { serializeFileObj } from 'util/file';
import {
tusLockAndNotify,
tusUnlockAndNotify,
tusRemoveAndNotify,
tusClearRemovedUploads,
tusClearLockedUploads,
} from 'util/tus';
import * as ACTIONS from 'constants/action_types';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import { CHANNEL_ANONYMOUS } from 'constants/claim';
// This is the old key formula. Retain it for now to allow users to delete
// any pending uploads. Can be removed from January 2022 onwards.
const getOldKeyFromParam = (params) => `${params.name}#${params.channel || 'anonymous'}`;
type PublishState = {
editingURI: ?string,
fileText: ?string,
filePath: ?string,
remoteFileUrl: ?string,
contentIsFree: boolean,
fileDur: number,
fileSize: number,
fileVid: boolean,
fee: {
amount: number,
currency: string,
},
title: string,
thumbnail_url: string,
thumbnailPath: string,
uploadThumbnailStatus: string,
thumbnailError: ?boolean,
description: string,
language: string,
releaseTime: ?number,
releaseTimeEdited: ?number,
releaseAnytime: boolean,
channel: string,
channelId: ?string,
name: string,
nameError: ?string,
bid: number,
bidError: ?string,
otherLicenseDescription: string,
licenseUrl: string,
tags: Array<string>,
optimize: boolean,
useLBRYUploader: boolean,
currentUploads: { [key: string]: FileUploadItem },
isMarkdownPost: boolean,
isLivestreamPublish: boolean,
};
const defaultState: PublishState = {
editingURI: undefined,
fileText: '',
filePath: undefined,
fileDur: 0,
fileSize: 0,
fileVid: false,
remoteFileUrl: undefined,
contentIsFree: true,
fee: {
amount: 1,
currency: 'LBC',
},
title: '',
thumbnail_url: '',
thumbnailPath: '',
uploadThumbnailStatus: THUMBNAIL_STATUSES.API_DOWN,
thumbnailError: undefined,
description: '',
language: '',
releaseTime: undefined,
releaseTimeEdited: undefined,
releaseAnytime: false,
nsfw: false,
channel: CHANNEL_ANONYMOUS,
channelId: '',
name: '',
nameError: undefined,
bid: 0.01,
bidError: undefined,
licenseType: 'None',
otherLicenseDescription: 'All rights reserved',
licenseUrl: '',
tags: [],
publishing: false,
publishSuccess: false,
publishError: undefined,
optimize: false,
useLBRYUploader: false,
currentUploads: {},
isMarkdownPost: false,
isLivestreamPublish: false,
};
export const publishReducer = handleActions(
{
[ACTIONS.UPDATE_PUBLISH_FORM]: (state, action): PublishState => {
const { data } = action;
return {
...state,
...data,
};
},
[ACTIONS.CLEAR_PUBLISH]: (state: PublishState): PublishState => ({
...defaultState,
uri: undefined,
channel: state.channel,
bid: state.bid,
optimize: state.optimize,
language: state.language,
currentUploads: state.currentUploads,
}),
[ACTIONS.PUBLISH_START]: (state: PublishState): PublishState => ({
...state,
publishing: true,
publishSuccess: false,
}),
[ACTIONS.PUBLISH_FAIL]: (state: PublishState): PublishState => ({
...state,
publishing: false,
}),
[ACTIONS.PUBLISH_SUCCESS]: (state: PublishState): PublishState => ({
...state,
publishing: false,
publishSuccess: true,
}),
[ACTIONS.DO_PREPARE_EDIT]: (state: PublishState, action) => {
const { ...publishData } = action.data;
const { channel, name, uri } = publishData;
// The short uri is what is presented to the user
// The editingUri is the full uri with claim id
const shortUri = buildURI({
channelName: channel,
streamName: name,
});
return {
...defaultState,
...publishData,
editingURI: uri,
uri: shortUri,
currentUploads: state.currentUploads,
};
},
[ACTIONS.UPDATE_UPLOAD_ADD]: (state: PublishState, action) => {
const { file, params, uploader } = action.data;
const currentUploads = Object.assign({}, state.currentUploads);
currentUploads[params.guid] = {
file,
fileFingerprint: file ? serializeFileObj(file) : undefined, // TODO: get hash instead?
progress: '0',
params,
uploader,
resumable: !(uploader instanceof XMLHttpRequest),
};
return { ...state, currentUploads };
},
[ACTIONS.UPDATE_UPLOAD_PROGRESS]: (state: PublishState, action) => {
const { guid, progress, status } = action.data;
const key = guid;
const currentUploads = Object.assign({}, state.currentUploads);
if (guid === 'force--update') {
return { ...state, currentUploads };
} else if (guid === 'refresh--lock') {
// Re-lock all uploads that are in progress under our tab.
const uploadKeys = Object.keys(currentUploads);
uploadKeys.forEach((k) => {
if (currentUploads[k].uploader) {
tusLockAndNotify(k);
}
});
}
if (!currentUploads[key]) {
return state;
}
if (progress) {
currentUploads[key].progress = progress;
delete currentUploads[key].status;
if (currentUploads[key].uploader.url) {
// TUS has finally obtained an upload url from the server...
if (!currentUploads[key].params.uploadUrl) {
// ... Stash that to check later when resuming.
// Ignoring immutable-update requirement (probably doesn't matter to the GUI).
currentUploads[key].params.uploadUrl = currentUploads[key].uploader.url;
}
// ... lock this tab as the active uploader.
tusLockAndNotify(key);
}
} else if (status) {
currentUploads[key].status = status;
if (status === 'error' || status === 'conflict') {
delete currentUploads[key].uploader;
}
}
return { ...state, currentUploads };
},
[ACTIONS.UPDATE_UPLOAD_REMOVE]: (state: PublishState, action) => {
const { guid, params } = action.data;
const key = guid || getOldKeyFromParam(params);
if (state.currentUploads[key]) {
const currentUploads = Object.assign({}, state.currentUploads);
delete currentUploads[key];
tusUnlockAndNotify(key);
tusRemoveAndNotify(key);
return { ...state, currentUploads };
}
return state;
},
[ACTIONS.REHYDRATE]: (state: PublishState, action) => {
if (action && action.payload && action.payload.publish) {
const newPublish = { ...action.payload.publish };
// Cleanup for 'publish::currentUploads'
if (newPublish.currentUploads) {
const uploadKeys = Object.keys(newPublish.currentUploads);
if (uploadKeys.length > 0) {
// Clear uploader and corrupted params
uploadKeys.forEach((key) => {
const params = newPublish.currentUploads[key].params;
if (!params || Object.keys(params).length === 0) {
delete newPublish.currentUploads[key];
} else {
delete newPublish.currentUploads[key].uploader;
}
});
} else {
tusClearRemovedUploads();
}
tusClearLockedUploads();
}
return newPublish;
}
return state;
},
},
defaultState
);