Support resume-able upload via tus
## Issue 38 Handle resumable file upload ## Notes Since we can't serialize a File object, we'll need to the user to re-select the file to resume.
This commit is contained in:
parent
fa48b4a99b
commit
bef7ff4a2d
14 changed files with 448 additions and 111 deletions
|
@ -27,7 +27,7 @@ export {
|
||||||
doResetSync,
|
doResetSync,
|
||||||
doSyncEncryptAndDecrypt,
|
doSyncEncryptAndDecrypt,
|
||||||
} from 'redux/actions/sync';
|
} from 'redux/actions/sync';
|
||||||
export { doUpdateUploadProgress } from './redux/actions/web';
|
export { doUpdateUploadAdd, doUpdateUploadProgress, doUpdateUploadRemove } from './redux/actions/web';
|
||||||
|
|
||||||
// reducers
|
// reducers
|
||||||
export { authReducer } from './redux/reducers/auth';
|
export { authReducer } from './redux/reducers/auth';
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
|
||||||
export const doUpdateUploadProgress = (
|
export function doUpdateUploadAdd(file: File, params: { [key: string]: any }, tusUploader: any) {
|
||||||
progress: string,
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_UPLOAD_ADD,
|
||||||
|
data: { file, params, tusUploader },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const doUpdateUploadProgress = (props: {
|
||||||
params: { [key: string]: any },
|
params: { [key: string]: any },
|
||||||
xhr: any
|
progress?: string,
|
||||||
) => (dispatch: Dispatch) =>
|
status?: string,
|
||||||
|
}) => (dispatch: Dispatch) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.UPDATE_UPLOAD_PROGRESS,
|
type: ACTIONS.UPDATE_UPLOAD_PROGRESS,
|
||||||
data: { progress, params, xhr },
|
data: props,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function doUpdateUploadRemove(params: { [key: string]: any }) {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.UPDATE_UPLOAD_REMOVE,
|
||||||
|
data: { params },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,34 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { serializeFileObj } from 'util/file';
|
||||||
|
|
||||||
/*
|
const CURRENT_UPLOADS = 'current_uploads';
|
||||||
test mock:
|
const getKeyFromParam = (params) => `${params.name}#${params.channel || 'anonymous'}`;
|
||||||
currentUploads: {
|
|
||||||
'test#upload': {
|
|
||||||
progress: 50,
|
|
||||||
params: {
|
|
||||||
name: 'steve',
|
|
||||||
thumbnail_url: 'https://dev2.spee.ch/4/KMNtoSZ009fawGz59VG8PrID.jpeg',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type Params = {
|
|
||||||
channel?: string,
|
|
||||||
name: string,
|
|
||||||
thumbnail_url: ?string,
|
|
||||||
title: ?string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UploadItem = {
|
|
||||||
progess: string,
|
|
||||||
params: Params,
|
|
||||||
xhr?: any,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TvState = {
|
export type TvState = {
|
||||||
currentUploads: { [key: string]: UploadItem },
|
currentUploads: { [key: string]: FileUploadItem },
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
|
@ -37,21 +15,62 @@ const defaultState: TvState = {
|
||||||
currentUploads: {},
|
currentUploads: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.UPDATE_UPLOAD_PROGRESS] = (state: TvState, action) => {
|
try {
|
||||||
const { progress, params, xhr } = action.data;
|
const uploads = JSON.parse(window.localStorage.getItem(CURRENT_UPLOADS));
|
||||||
const key = params.channel ? `${params.name}#${params.channel}` : `${params.name}#anonymous`;
|
if (uploads) {
|
||||||
let currentUploads;
|
defaultState.currentUploads = uploads;
|
||||||
if (!progress) {
|
Object.keys(defaultState.currentUploads).forEach((key) => {
|
||||||
currentUploads = Object.assign({}, state.currentUploads);
|
delete defaultState.currentUploads[key].tusUploader;
|
||||||
Object.keys(currentUploads).forEach(k => {
|
|
||||||
if (k === key) {
|
|
||||||
delete currentUploads[key];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
currentUploads = Object.assign({}, state.currentUploads);
|
|
||||||
currentUploads[key] = { progress, params, xhr };
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
reducers[ACTIONS.UPDATE_UPLOAD_ADD] = (state: TvState, action) => {
|
||||||
|
const { file, params, tusUploader } = action.data;
|
||||||
|
const key = getKeyFromParam(params);
|
||||||
|
const currentUploads = Object.assign({}, state.currentUploads);
|
||||||
|
|
||||||
|
currentUploads[key] = {
|
||||||
|
file,
|
||||||
|
fileFingerprint: serializeFileObj(file), // TODO: get hash instead?
|
||||||
|
progress: '0',
|
||||||
|
params,
|
||||||
|
tusUploader,
|
||||||
|
};
|
||||||
|
|
||||||
|
window.localStorage.setItem(CURRENT_UPLOADS, JSON.stringify(currentUploads));
|
||||||
|
return { ...state, currentUploads };
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.UPDATE_UPLOAD_PROGRESS] = (state: TvState, action) => {
|
||||||
|
const { params, progress, status } = action.data;
|
||||||
|
const key = getKeyFromParam(params);
|
||||||
|
const currentUploads = Object.assign({}, state.currentUploads);
|
||||||
|
|
||||||
|
if (progress) {
|
||||||
|
currentUploads[key].progress = progress;
|
||||||
|
delete currentUploads[key].status;
|
||||||
|
} else if (status) {
|
||||||
|
currentUploads[key].status = status;
|
||||||
|
if (status === 'error') {
|
||||||
|
delete currentUploads[key].tusUploader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(CURRENT_UPLOADS, JSON.stringify(currentUploads));
|
||||||
|
return { ...state, currentUploads };
|
||||||
|
};
|
||||||
|
|
||||||
|
reducers[ACTIONS.UPDATE_UPLOAD_REMOVE] = (state: TvState, action) => {
|
||||||
|
const { params } = action.data;
|
||||||
|
const key = getKeyFromParam(params);
|
||||||
|
const currentUploads = Object.assign({}, state.currentUploads);
|
||||||
|
|
||||||
|
delete currentUploads[key];
|
||||||
|
|
||||||
|
window.localStorage.setItem(CURRENT_UPLOADS, JSON.stringify(currentUploads));
|
||||||
return { ...state, currentUploads };
|
return { ...state, currentUploads };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
9
flow-typed/File.js
vendored
9
flow-typed/File.js
vendored
|
@ -76,3 +76,12 @@ declare type DeletePurchasedUri = {
|
||||||
uri: string,
|
uri: string,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare type FileUploadItem = {
|
||||||
|
params: UpdatePublishFormData,
|
||||||
|
file: File,
|
||||||
|
fileFingerprint: string,
|
||||||
|
progress: string,
|
||||||
|
status?: string,
|
||||||
|
tusUploader?: any,
|
||||||
|
};
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
"rss": "^1.2.2",
|
"rss": "^1.2.2",
|
||||||
"source-map-explorer": "^2.5.2",
|
"source-map-explorer": "^2.5.2",
|
||||||
"tempy": "^0.6.0",
|
"tempy": "^0.6.0",
|
||||||
|
"tus-js-client": "^2.3.0",
|
||||||
"videojs-contrib-ads": "^6.9.0",
|
"videojs-contrib-ads": "^6.9.0",
|
||||||
"videojs-ima": "^1.11.0",
|
"videojs-ima": "^1.11.0",
|
||||||
"videojs-ima-player": "^0.5.6",
|
"videojs-ima-player": "^0.5.6",
|
||||||
|
|
|
@ -1291,6 +1291,16 @@
|
||||||
"Select a file to upload": "Select a file to upload",
|
"Select a file to upload": "Select a file to upload",
|
||||||
"Select file to upload": "Select file to upload",
|
"Select file to upload": "Select file to upload",
|
||||||
"Url copied.": "Url copied.",
|
"Url copied.": "Url copied.",
|
||||||
|
"Failed to initiate upload (%err%)": "Failed to initiate upload (%err%)",
|
||||||
|
"Invalid file": "Invalid file",
|
||||||
|
"It appears to be a different or modified file.": "It appears to be a different or modified file.",
|
||||||
|
"Please select the same file from the initial upload.": "Please select the same file from the initial upload.",
|
||||||
|
"Cancel upload": "Cancel upload",
|
||||||
|
"Cancel and remove the selected upload?": "Cancel and remove the selected upload?",
|
||||||
|
"Select the file to resume upload...": "Select the file to resume upload...",
|
||||||
|
"Stopped.": "Stopped.",
|
||||||
|
"Resume": "Resume",
|
||||||
|
"Retrying...": "Retrying...",
|
||||||
"Uploading...": "Uploading...",
|
"Uploading...": "Uploading...",
|
||||||
"Creating...": "Creating...",
|
"Creating...": "Creating...",
|
||||||
"Use a URL": "Use a URL",
|
"Use a URL": "Use a URL",
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectCurrentUploads, selectUploadCount } from 'lbryinc';
|
import { selectCurrentUploads, selectUploadCount, doUpdateUploadRemove } from 'lbryinc';
|
||||||
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import { doPublishResume } from 'redux/actions/publish';
|
||||||
import WebUploadList from './view';
|
import WebUploadList from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = (state) => ({
|
||||||
currentUploads: selectCurrentUploads(state),
|
currentUploads: selectCurrentUploads(state),
|
||||||
uploadCount: selectUploadCount(state),
|
uploadCount: selectUploadCount(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
const perform = {
|
||||||
select,
|
doPublishResume,
|
||||||
null
|
doUpdateUploadRemove,
|
||||||
)(WebUploadList);
|
doOpenModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(select, perform)(WebUploadList);
|
||||||
|
|
|
@ -1,40 +1,138 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import FileSelector from 'component/common/file-selector';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import FileThumbnail from 'component/fileThumbnail';
|
import FileThumbnail from 'component/fileThumbnail';
|
||||||
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
import { serializeFileObj } from 'util/file';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: UpdatePublishFormData,
|
uploadItem: FileUploadItem,
|
||||||
progress: string,
|
doPublishResume: (any) => void,
|
||||||
xhr?: () => void,
|
doUpdateUploadRemove: (any) => void,
|
||||||
|
doOpenModal: (string, {}) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebUploadItem(props: Props) {
|
export default function WebUploadItem(props: Props) {
|
||||||
const { params, progress, xhr } = props;
|
const { uploadItem, doPublishResume, doUpdateUploadRemove, doOpenModal } = props;
|
||||||
|
const { params, file, fileFingerprint, progress, status, tusUploader } = uploadItem;
|
||||||
|
|
||||||
|
const [showFileSelector, setShowFileSelector] = useState(false);
|
||||||
|
|
||||||
|
function handleFileChange(newFile: WebFile, clearName = true) {
|
||||||
|
if (serializeFileObj(newFile) === fileFingerprint) {
|
||||||
|
setShowFileSelector(false);
|
||||||
|
doPublishResume({ ...params, file_path: newFile });
|
||||||
|
} else {
|
||||||
|
doOpenModal(MODALS.CONFIRM, {
|
||||||
|
title: __('Invalid file'),
|
||||||
|
subtitle: __('It appears to be a different or modified file.'),
|
||||||
|
body: <p className="help--warning">{__('Please select the same file from the initial upload.')}</p>,
|
||||||
|
onConfirm: (closeModal) => closeModal(),
|
||||||
|
hideCancel: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel() {
|
||||||
|
doOpenModal(MODALS.CONFIRM, {
|
||||||
|
title: __('Cancel upload'),
|
||||||
|
subtitle: __('Cancel and remove the selected upload?'),
|
||||||
|
body: params.name ? <p className="empty">{`lbry://${params.name}`}</p> : undefined,
|
||||||
|
onConfirm: (closeModal) => {
|
||||||
|
if (tusUploader) {
|
||||||
|
tusUploader.abort(true);
|
||||||
|
}
|
||||||
|
doUpdateUploadRemove(params);
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProgressStr() {
|
||||||
|
if (!tusUploader) {
|
||||||
|
return __('Stopped.');
|
||||||
|
} else if (status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'retry':
|
||||||
|
return __('Retrying...');
|
||||||
|
case 'error':
|
||||||
|
return __('Failed.');
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const progressInt = parseInt(progress);
|
||||||
|
return progressInt === 100 ? __('Processing...') : __('Uploading...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRetryButton() {
|
||||||
|
if (!tusUploader) {
|
||||||
|
const isFileActive = file instanceof File;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
label={isFileActive ? __('Resume') : __('Retry')}
|
||||||
|
button="link"
|
||||||
|
onClick={() => {
|
||||||
|
if (isFileActive) {
|
||||||
|
doPublishResume({ ...params, file_path: file });
|
||||||
|
} else {
|
||||||
|
setShowFileSelector(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={showFileSelector}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCancelButton() {
|
||||||
|
return <Button label={__('Cancel')} button="link" onClick={handleCancel} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileSelector() {
|
||||||
|
return (
|
||||||
|
<div className="claim-preview--padded">
|
||||||
|
<FileSelector
|
||||||
|
label={__('File')}
|
||||||
|
onFileChosen={handleFileChange}
|
||||||
|
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
|
||||||
|
accept={'video/mp4,video/x-m4v,video/*,audio/*'}
|
||||||
|
placeholder={__('Select the file to resume upload...')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProgressBar() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="claim-upload__progress--label">lbry://{params.name}</div>
|
||||||
|
<div className={'claim-upload__progress--outer card--inline'}>
|
||||||
|
<div className={'claim-upload__progress--inner'} style={{ width: `${progress}%` }}>
|
||||||
|
{resolveProgressStr()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={'claim-preview claim-preview--padded claim-preview--inactive card--inline'}>
|
<li className={'web-upload-item claim-preview claim-preview--padded claim-preview--inactive card--inline'}>
|
||||||
<FileThumbnail thumbnail={params.thumbnail_url} />
|
<FileThumbnail thumbnail={params.thumbnail_url} />
|
||||||
<div className={'claim-preview-metadata'}>
|
<div className={'claim-preview-metadata'}>
|
||||||
<div className="claim-preview-info">
|
<div className="claim-preview-info">
|
||||||
<div className="claim-preview__title">{params.title}</div>
|
<div className="claim-preview__title">{params.title}</div>
|
||||||
{xhr && (
|
<div className="card__actions--inline">
|
||||||
<div className="card__actions--inline">
|
{getRetryButton()}
|
||||||
<Button
|
{getCancelButton()}
|
||||||
button="link"
|
|
||||||
onClick={() => {
|
|
||||||
xhr.abort();
|
|
||||||
}}
|
|
||||||
label={__('Cancel')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h2>lbry://{params.name}</h2>
|
|
||||||
<div className={'claim-upload__progress--outer card--inline'}>
|
|
||||||
<div className={'claim-upload__progress--inner'} style={{ width: `${progress}%` }}>
|
|
||||||
Uploading...
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showFileSelector && getFileSelector()}
|
||||||
|
{!showFileSelector && getProgressBar()}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,19 +3,16 @@ import * as React from 'react';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import WebUploadItem from './internal/web-upload-item';
|
import WebUploadItem from './internal/web-upload-item';
|
||||||
|
|
||||||
export type UploadItem = {
|
|
||||||
progess: string,
|
|
||||||
params: UpdatePublishFormData,
|
|
||||||
xhr?: { abort: () => void },
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentUploads: { [key: string]: UploadItem },
|
currentUploads: { [key: string]: FileUploadItem },
|
||||||
uploadCount: number,
|
uploadCount: number,
|
||||||
|
doPublishResume: (any) => void,
|
||||||
|
doUpdateUploadRemove: (any) => void,
|
||||||
|
doOpenModal: (string, {}) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebUploadList(props: Props) {
|
export default function WebUploadList(props: Props) {
|
||||||
const { currentUploads, uploadCount } = props;
|
const { currentUploads, uploadCount, doPublishResume, doUpdateUploadRemove, doOpenModal } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!!uploadCount && (
|
!!uploadCount && (
|
||||||
|
@ -25,8 +22,16 @@ export default function WebUploadList(props: Props) {
|
||||||
body={
|
body={
|
||||||
<section>
|
<section>
|
||||||
{/* $FlowFixMe */}
|
{/* $FlowFixMe */}
|
||||||
{Object.values(currentUploads).map(({ progress, params, xhr }) => (
|
{Object.values(currentUploads).map((uploadItem) => (
|
||||||
<WebUploadItem key={`upload${params.name}`} progress={progress} params={params} xhr={xhr} />
|
<WebUploadItem
|
||||||
|
// $FlowFixMe
|
||||||
|
key={`upload${uploadItem.params.name}`}
|
||||||
|
// $FlowFixMe
|
||||||
|
uploadItem={uploadItem}
|
||||||
|
doPublishResume={doPublishResume}
|
||||||
|
doUpdateUploadRemove={doUpdateUploadRemove}
|
||||||
|
doOpenModal={doOpenModal}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
|
@ -492,7 +492,9 @@ export const FETCH_SUB_COUNT_FAILED = 'FETCH_SUB_COUNT_FAILED';
|
||||||
export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED';
|
export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED';
|
||||||
|
|
||||||
// Lbry.tv
|
// Lbry.tv
|
||||||
|
export const UPDATE_UPLOAD_ADD = 'UPDATE_UPLOAD_ADD';
|
||||||
export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS';
|
export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS';
|
||||||
|
export const UPDATE_UPLOAD_REMOVE = 'UPDATE_UPLOAD_REMOVE';
|
||||||
|
|
||||||
// User
|
// User
|
||||||
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
|
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
|
||||||
|
|
|
@ -363,6 +363,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.claim-upload__progress--label {
|
||||||
|
font-size: var(--font-small);
|
||||||
|
color: var(--color-text-subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
.claim-upload__progress--outer {
|
.claim-upload__progress--outer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -857,3 +862,17 @@
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.web-upload-item.claim-preview {
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.media__thumb {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-preview-metadata {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
8
ui/util/file.js
Normal file
8
ui/util/file.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export function serializeFileObj(file: File) {
|
||||||
|
// $FlowFixMe - these are non standard, but try to include anyway.
|
||||||
|
const aux = `${String(file.lastModifiedDate)}#${String(file.webkitRelativePath)}`;
|
||||||
|
|
||||||
|
return `${file.name}#${file.type}#${file.size}#${file.lastModified}#${aux}`;
|
||||||
|
}
|
|
@ -4,9 +4,14 @@
|
||||||
of a multipart/form-data POST request with:
|
of a multipart/form-data POST request with:
|
||||||
- 'file' binary
|
- 'file' binary
|
||||||
- 'json_payload' collection of publish params to be passed to the server's sdk.
|
- 'json_payload' collection of publish params to be passed to the server's sdk.
|
||||||
|
|
||||||
|
v2 no longer uses 'multipart/form-data'. It uses TUS to support resummable upload.
|
||||||
*/
|
*/
|
||||||
|
import * as tus from 'tus-js-client';
|
||||||
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
|
||||||
import { doUpdateUploadProgress } from 'lbryinc';
|
import { doUpdateUploadAdd, doUpdateUploadProgress, doUpdateUploadRemove } from 'lbryinc';
|
||||||
|
|
||||||
|
const UPLOAD_CHUNK_SIZE_BYTE = 100000000;
|
||||||
|
|
||||||
// A modified version of Lbry.apiCall that allows
|
// A modified version of Lbry.apiCall that allows
|
||||||
// to perform calling methods at arbitrary urls
|
// to perform calling methods at arbitrary urls
|
||||||
|
@ -26,6 +31,11 @@ export default function apiPublishCallViaWeb(
|
||||||
return apiCall(method, params, resolve, reject);
|
return apiCall(method, params, resolve, reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!tus.isSupported) {
|
||||||
|
reject(new Error(__('Uploading is not supported with this browser.')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const counter = new Date().getTime();
|
const counter = new Date().getTime();
|
||||||
let fileField = filePath;
|
let fileField = filePath;
|
||||||
|
|
||||||
|
@ -38,13 +48,10 @@ export default function apiPublishCallViaWeb(
|
||||||
// Putting a dummy value here, the server is going to process the POSTed file
|
// Putting a dummy value here, the server is going to process the POSTed file
|
||||||
// and set the file_path itself
|
// and set the file_path itself
|
||||||
|
|
||||||
const body = new FormData();
|
|
||||||
if (fileField) {
|
if (fileField) {
|
||||||
body.append('file', fileField);
|
|
||||||
params.file_path = '__POST_FILE__';
|
params.file_path = '__POST_FILE__';
|
||||||
delete params['remote_url'];
|
delete params['remote_url'];
|
||||||
} else if (remoteUrl) {
|
} else if (remoteUrl) {
|
||||||
body.append('remote_url', remoteUrl);
|
|
||||||
delete params['remote_url'];
|
delete params['remote_url'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,36 +61,78 @@ export default function apiPublishCallViaWeb(
|
||||||
params,
|
params,
|
||||||
id: counter,
|
id: counter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// no fileData? do the livestream remote publish
|
// no fileData? do the livestream remote publish
|
||||||
body.append('json_payload', jsonPayload);
|
|
||||||
|
|
||||||
function makeRequest(connectionString, method, token, body, params) {
|
function makeRequest(connectionString, token, params, file: File | string) {
|
||||||
|
const metadata = {
|
||||||
|
filename: file instanceof File ? file.name : file,
|
||||||
|
filetype: file instanceof File ? file.type : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let xhr = new XMLHttpRequest();
|
const uploader = new tus.Upload(fileField, {
|
||||||
xhr.open(method, connectionString);
|
endpoint: connectionString,
|
||||||
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
|
chunkSize: UPLOAD_CHUNK_SIZE_BYTE,
|
||||||
xhr.responseType = 'json';
|
retryDelays: [0, 3000, 3000],
|
||||||
xhr.upload.onprogress = (e) => {
|
parallelUploads: 1,
|
||||||
let percentComplete = Math.ceil((e.loaded / e.total) * 100);
|
removeFingerprintOnSuccess: true,
|
||||||
window.store.dispatch(doUpdateUploadProgress(percentComplete, params, xhr));
|
headers: { [X_LBRY_AUTH_TOKEN]: token },
|
||||||
};
|
metadata: metadata,
|
||||||
xhr.onload = () => {
|
onShouldRetry: (err, retryAttempt, options) => {
|
||||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
window.store.dispatch(doUpdateUploadProgress({ params, status: 'retry' }));
|
||||||
resolve(xhr);
|
const FORBIDDEN_ERROR = 403;
|
||||||
};
|
const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
|
||||||
xhr.onerror = () => {
|
return status !== FORBIDDEN_ERROR;
|
||||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
},
|
||||||
reject(new Error(__('There was a problem with your upload. Please try again.')));
|
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: () => {
|
||||||
|
// Notify lbrynet server
|
||||||
|
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 = () => {
|
||||||
|
reject(new Error(__('There was a problem with your upload. Please try again.')));
|
||||||
|
};
|
||||||
|
xhr.onabort = () => {
|
||||||
|
window.store.dispatch(doUpdateUploadRemove(params));
|
||||||
|
};
|
||||||
|
|
||||||
xhr.onabort = () => {
|
xhr.send(jsonPayload);
|
||||||
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
},
|
||||||
};
|
});
|
||||||
xhr.send(body);
|
|
||||||
|
uploader
|
||||||
|
.findPreviousUploads()
|
||||||
|
.then((previousUploads) => {
|
||||||
|
if (previousUploads.length > 0) {
|
||||||
|
uploader.resumeFromPreviousUpload(previousUploads[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.store.dispatch(doUpdateUploadAdd(fileField, params, uploader));
|
||||||
|
uploader.start();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(new Error(__('Failed to initiate upload (%err%)', { err })));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeRequest(connectionString, 'POST', token, body, params)
|
return makeRequest(connectionString, token, params, fileField)
|
||||||
.then((xhr) => {
|
.then((xhr) => {
|
||||||
let error;
|
let error;
|
||||||
if (xhr && xhr.response) {
|
if (xhr && xhr.response) {
|
||||||
|
|
96
yarn.lock
96
yarn.lock
|
@ -3869,6 +3869,11 @@ buffer-fill@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
|
||||||
|
|
||||||
|
buffer-from@^0.1.1:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0"
|
||||||
|
integrity sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==
|
||||||
|
|
||||||
buffer-from@^1.0.0:
|
buffer-from@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||||
|
@ -4634,6 +4639,14 @@ colors@~1.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
||||||
integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
|
integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM=
|
||||||
|
|
||||||
|
combine-errors@^3.0.3:
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/combine-errors/-/combine-errors-3.0.3.tgz#f4df6740083e5703a3181110c2b10551f003da86"
|
||||||
|
integrity sha1-9N9nQAg+VwOjGBEQwrEFUfAD2oY=
|
||||||
|
dependencies:
|
||||||
|
custom-error-instance "2.1.1"
|
||||||
|
lodash.uniqby "4.5.0"
|
||||||
|
|
||||||
combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
|
combined-stream@^1.0.6, combined-stream@~1.0.5, combined-stream@~1.0.6:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
|
@ -5337,6 +5350,11 @@ currently-unhandled@^0.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-find-index "^1.0.1"
|
array-find-index "^1.0.1"
|
||||||
|
|
||||||
|
custom-error-instance@2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz#3cf6391487a6629a6247eb0ca0ce00081b7e361a"
|
||||||
|
integrity sha1-PPY5FIemYppiR+sMoM4ACBt+Nho=
|
||||||
|
|
||||||
cyclist@^1.0.1:
|
cyclist@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||||
|
@ -9824,7 +9842,7 @@ jest@20.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
jest-cli "^20.0.4"
|
jest-cli "^20.0.4"
|
||||||
|
|
||||||
js-base64@^2.1.9:
|
js-base64@^2.1.9, js-base64@^2.6.1:
|
||||||
version "2.6.4"
|
version "2.6.4"
|
||||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
|
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4"
|
||||||
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
|
integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==
|
||||||
|
@ -10375,11 +10393,48 @@ lodash-es@^4.17.14, lodash-es@^4.2.1:
|
||||||
version "4.17.15"
|
version "4.17.15"
|
||||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||||
|
|
||||||
|
lodash._baseiteratee@~4.7.0:
|
||||||
|
version "4.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz#34a9b5543572727c3db2e78edae3c0e9e66bd102"
|
||||||
|
integrity sha1-NKm1VDVycnw9sueO2uPA6eZr0QI=
|
||||||
|
dependencies:
|
||||||
|
lodash._stringtopath "~4.8.0"
|
||||||
|
|
||||||
|
lodash._basetostring@~4.12.0:
|
||||||
|
version "4.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz#9327c9dc5158866b7fa4b9d42f4638e5766dd9df"
|
||||||
|
integrity sha1-kyfJ3FFYhmt/pLnUL0Y45XZt2d8=
|
||||||
|
|
||||||
|
lodash._baseuniq@~4.6.0:
|
||||||
|
version "4.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
|
||||||
|
integrity sha1-DrtE5FaBSveQXGIS+iybLVG4Qeg=
|
||||||
|
dependencies:
|
||||||
|
lodash._createset "~4.0.0"
|
||||||
|
lodash._root "~3.0.0"
|
||||||
|
|
||||||
|
lodash._createset@~4.0.0:
|
||||||
|
version "4.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
|
||||||
|
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
|
||||||
|
|
||||||
lodash._reinterpolate@^3.0.0:
|
lodash._reinterpolate@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||||
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
||||||
|
|
||||||
|
lodash._root@~3.0.0:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
|
||||||
|
integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=
|
||||||
|
|
||||||
|
lodash._stringtopath@~4.8.0:
|
||||||
|
version "4.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz#941bcf0e64266e5fc1d66fed0a6959544c576824"
|
||||||
|
integrity sha1-lBvPDmQmbl/B1m/tCmlZVExXaCQ=
|
||||||
|
dependencies:
|
||||||
|
lodash._basetostring "~4.12.0"
|
||||||
|
|
||||||
lodash.camelcase@^4.3.0:
|
lodash.camelcase@^4.3.0:
|
||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||||
|
@ -10449,6 +10504,11 @@ lodash.templatesettings@^4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash._reinterpolate "^3.0.0"
|
lodash._reinterpolate "^3.0.0"
|
||||||
|
|
||||||
|
lodash.throttle@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||||
|
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
|
||||||
|
|
||||||
lodash.toarray@^4.4.0:
|
lodash.toarray@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
|
resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
|
||||||
|
@ -10457,6 +10517,14 @@ lodash.uniq@^4.5.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
|
|
||||||
|
lodash.uniqby@4.5.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz#a3a17bbf62eeb6240f491846e97c1c4e2a5e1e21"
|
||||||
|
integrity sha1-o6F7v2LutiQPSRhG6XwcTipeHiE=
|
||||||
|
dependencies:
|
||||||
|
lodash._baseiteratee "~4.7.0"
|
||||||
|
lodash._baseuniq "~4.6.0"
|
||||||
|
|
||||||
lodash.unset@^4.5.2:
|
lodash.unset@^4.5.2:
|
||||||
version "4.5.2"
|
version "4.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed"
|
resolved "https://registry.yarnpkg.com/lodash.unset/-/lodash.unset-4.5.2.tgz#370d1d3e85b72a7e1b0cdf2d272121306f23e4ed"
|
||||||
|
@ -12980,6 +13048,14 @@ prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0,
|
||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.8.1"
|
react-is "^16.8.1"
|
||||||
|
|
||||||
|
proper-lockfile@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-2.0.1.tgz#159fb06193d32003f4b3691dd2ec1a634aa80d1d"
|
||||||
|
integrity sha1-FZ+wYZPTIAP0s2kd0uwaY0qoDR0=
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.2"
|
||||||
|
retry "^0.10.0"
|
||||||
|
|
||||||
property-information@^5.3.0:
|
property-information@^5.3.0:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
|
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
|
||||||
|
@ -14245,6 +14321,11 @@ ret@~0.1.10:
|
||||||
version "0.1.15"
|
version "0.1.15"
|
||||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
|
||||||
|
|
||||||
|
retry@^0.10.0:
|
||||||
|
version "0.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
||||||
|
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
|
||||||
|
|
||||||
retry@^0.12.0:
|
retry@^0.12.0:
|
||||||
version "0.12.0"
|
version "0.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
||||||
|
@ -15896,6 +15977,19 @@ tunnel@^0.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
|
||||||
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
|
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
|
||||||
|
|
||||||
|
tus-js-client@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tus-js-client/-/tus-js-client-2.3.0.tgz#5d76145476cea46a4e7c045a0054637cddf8dc39"
|
||||||
|
integrity sha512-I4cSwm6N5qxqCmBqenvutwSHe9ntf81lLrtf6BmLpG2v4wTl89atCQKqGgqvkodE6Lx+iKIjMbaXmfvStTg01g==
|
||||||
|
dependencies:
|
||||||
|
buffer-from "^0.1.1"
|
||||||
|
combine-errors "^3.0.3"
|
||||||
|
is-stream "^2.0.0"
|
||||||
|
js-base64 "^2.6.1"
|
||||||
|
lodash.throttle "^4.1.1"
|
||||||
|
proper-lockfile "^2.0.1"
|
||||||
|
url-parse "^1.4.3"
|
||||||
|
|
||||||
tween-functions@^1.2.0:
|
tween-functions@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff"
|
||||||
|
|
Loading…
Reference in a new issue