|
3 | 3 | * SPDX-License-Identifier: AGPL-3.0-or-later |
4 | 4 | */ |
5 | 5 |
|
6 | | -import type { AxiosResponse } from 'axios' |
| 6 | +import type PQueue from 'p-queue' |
7 | 7 |
|
8 | | -import { getMaxChunksSize } from '../utils/config.ts' |
| 8 | +import { TypedEventTarget } from 'typescript-event-target' |
9 | 9 |
|
10 | 10 | export const UploadStatus = Object.freeze({ |
| 11 | + /** The upload was initialized */ |
11 | 12 | INITIALIZED: 0, |
12 | | - UPLOADING: 1, |
13 | | - ASSEMBLING: 2, |
14 | | - FINISHED: 3, |
15 | | - CANCELLED: 4, |
16 | | - FAILED: 5, |
| 13 | + /** The upload was scheduled but is not yet uploading */ |
| 14 | + SCHEDULED: 1, |
| 15 | + /** The upload itself is running */ |
| 16 | + UPLOADING: 2, |
| 17 | + /** Chunks are being assembled */ |
| 18 | + ASSEMBLING: 3, |
| 19 | + /** The upload finished successfully */ |
| 20 | + FINISHED: 4, |
| 21 | + /** The upload was cancelled by the user */ |
| 22 | + CANCELLED: 5, |
| 23 | + /** The upload failed */ |
| 24 | + FAILED: 6, |
17 | 25 | }) |
18 | 26 |
|
19 | | -type TUploadStatus = typeof UploadStatus[keyof typeof UploadStatus] |
| 27 | +export type TUploadStatus = typeof UploadStatus[keyof typeof UploadStatus] |
20 | 28 |
|
21 | | -export class Upload { |
22 | | - private _source: string |
23 | | - private _file: File |
24 | | - private _isChunked: boolean |
25 | | - private _chunks: number |
26 | | - |
27 | | - private _size: number |
28 | | - private _uploaded = 0 |
29 | | - private _startTime = 0 |
30 | | - |
31 | | - private _status: TUploadStatus = UploadStatus.INITIALIZED |
32 | | - private _controller: AbortController |
33 | | - private _response: AxiosResponse | null = null |
34 | | - |
35 | | - constructor(source: string, chunked = false, size: number, file: File) { |
36 | | - const chunks = Math.min(getMaxChunksSize() > 0 ? Math.ceil(size / getMaxChunksSize()) : 1, 10000) |
37 | | - this._source = source |
38 | | - this._isChunked = chunked && getMaxChunksSize() > 0 && chunks > 1 |
39 | | - this._chunks = this._isChunked ? chunks : 1 |
40 | | - this._size = size |
41 | | - this._file = file |
42 | | - this._controller = new AbortController() |
43 | | - } |
44 | | - |
45 | | - get source(): string { |
46 | | - return this._source |
47 | | - } |
48 | | - |
49 | | - get file(): File { |
50 | | - return this._file |
51 | | - } |
52 | | - |
53 | | - get isChunked(): boolean { |
54 | | - return this._isChunked |
55 | | - } |
56 | | - |
57 | | - get chunks(): number { |
58 | | - return this._chunks |
59 | | - } |
60 | | - |
61 | | - get size(): number { |
62 | | - return this._size |
63 | | - } |
64 | | - |
65 | | - get startTime(): number { |
66 | | - return this._startTime |
67 | | - } |
68 | | - |
69 | | - set response(response: AxiosResponse | null) { |
70 | | - this._response = response |
71 | | - } |
72 | | - |
73 | | - get response(): AxiosResponse | null { |
74 | | - return this._response |
75 | | - } |
76 | | - |
77 | | - get uploaded(): number { |
78 | | - return this._uploaded |
79 | | - } |
| 29 | +interface UploadEvents { |
| 30 | + finished: CustomEvent<IUpload> |
| 31 | + progress: CustomEvent<IUpload> |
| 32 | +} |
80 | 33 |
|
| 34 | +export interface IUpload extends TypedEventTarget<UploadEvents> { |
81 | 35 | /** |
82 | | - * Update the uploaded bytes of this upload |
| 36 | + * The source of the upload |
83 | 37 | */ |
84 | | - set uploaded(length: number) { |
85 | | - if (length >= this._size) { |
86 | | - this._status = this._isChunked |
87 | | - ? UploadStatus.ASSEMBLING |
88 | | - : UploadStatus.FINISHED |
89 | | - this._uploaded = this._size |
90 | | - return |
91 | | - } |
92 | | - |
93 | | - this._status = UploadStatus.UPLOADING |
94 | | - this._uploaded = length |
95 | | - |
96 | | - // If first progress, let's log the start time |
97 | | - if (this._startTime === 0) { |
98 | | - this._startTime = new Date().getTime() |
99 | | - } |
100 | | - } |
101 | | - |
102 | | - get status(): TUploadStatus { |
103 | | - return this._status |
104 | | - } |
105 | | - |
| 38 | + readonly source: string |
106 | 39 | /** |
107 | | - * Update this upload status |
| 40 | + * Whether the upload is chunked or not |
108 | 41 | */ |
109 | | - set status(status: TUploadStatus) { |
110 | | - this._status = status |
111 | | - } |
| 42 | + readonly isChunked: boolean |
| 43 | + /** |
| 44 | + * The total size of the upload in bytes |
| 45 | + */ |
| 46 | + readonly totalBytes: number |
| 47 | + /** |
| 48 | + * Timestamp of when the upload started. |
| 49 | + * Will return `undefined` if the upload has not started yet. |
| 50 | + */ |
| 51 | + readonly startTime?: number |
| 52 | + /** |
| 53 | + * The number of bytes that have been uploaded so far |
| 54 | + */ |
| 55 | + readonly uploadedBytes: number |
| 56 | + /** |
| 57 | + * The current status of the upload |
| 58 | + */ |
| 59 | + readonly status: TUploadStatus |
| 60 | + /** |
| 61 | + * The internal abort signal |
| 62 | + */ |
| 63 | + readonly signal: AbortSignal |
112 | 64 |
|
113 | 65 | /** |
114 | | - * Returns the axios cancel token source |
| 66 | + * Cancels the upload |
115 | 67 | */ |
| 68 | + cancel(): void |
| 69 | +} |
| 70 | + |
| 71 | +export abstract class Upload extends TypedEventTarget<UploadEvents> implements Partial<IUpload> { |
| 72 | + #abortController = new AbortController() |
| 73 | + |
116 | 74 | get signal(): AbortSignal { |
117 | | - return this._controller.signal |
| 75 | + return this.#abortController.signal |
118 | 76 | } |
119 | 77 |
|
120 | 78 | /** |
121 | | - * Cancel any ongoing requests linked to this upload |
| 79 | + * Cancels the upload |
122 | 80 | */ |
123 | | - cancel() { |
124 | | - this._controller.abort() |
125 | | - this._status = UploadStatus.CANCELLED |
| 81 | + public cancel(): void { |
| 82 | + this.#abortController.abort() |
126 | 83 | } |
| 84 | + |
| 85 | + /** |
| 86 | + * Start the upload |
| 87 | + * |
| 88 | + * @param queue - The job queue. It is used to limit the number of concurrent upload jobs. |
| 89 | + */ |
| 90 | + public abstract start(queue: PQueue): Promise<void> |
127 | 91 | } |
0 commit comments