@wlindabla/file_uploader 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -28
- package/dist/cjs/cache/index.d.ts +198 -0
- package/dist/cjs/cache/index.js +318 -0
- package/dist/cjs/cache/index.js.map +1 -0
- package/dist/cjs/core/index.d.ts +271 -0
- package/dist/cjs/core/index.js +762 -0
- package/dist/cjs/core/index.js.map +1 -0
- package/dist/cjs/events/chunk/index.d.ts +27 -0
- package/dist/cjs/events/chunk/index.js +70 -0
- package/dist/cjs/events/chunk/index.js.map +1 -0
- package/dist/cjs/events/complete/index.d.ts +64 -0
- package/dist/cjs/events/complete/index.js +152 -0
- package/dist/cjs/events/complete/index.js.map +1 -0
- package/dist/cjs/events/index.d.ts +95 -0
- package/dist/cjs/events/index.js +85 -0
- package/dist/cjs/events/index.js.map +1 -0
- package/dist/cjs/events/initialize/index.d.ts +45 -0
- package/dist/cjs/events/initialize/index.js +105 -0
- package/dist/cjs/events/initialize/index.js.map +1 -0
- package/dist/cjs/events/state/index.d.ts +67 -0
- package/dist/cjs/events/state/index.js +145 -0
- package/dist/cjs/events/state/index.js.map +1 -0
- package/dist/cjs/exceptions/index.d.ts +84 -0
- package/dist/{exceptions → cjs/exceptions}/index.js +38 -18
- package/dist/cjs/exceptions/index.js.map +1 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.js +33 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/subscribers/index.d.ts +34 -0
- package/dist/cjs/subscribers/index.js +187 -0
- package/dist/cjs/subscribers/index.js.map +1 -0
- package/dist/cjs/types/index.d.ts +110 -0
- package/dist/cjs/types/index.js +53 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +72 -0
- package/dist/cjs/utils/index.js +208 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/esm/cache/index.d.mts +198 -0
- package/dist/esm/cache/index.js +4 -0
- package/dist/{index.js.map → esm/cache/index.js.map} +1 -1
- package/dist/{events/initialize/index.js → esm/chunk-3JTTZCSQ.js} +25 -19
- package/dist/esm/chunk-3JTTZCSQ.js.map +1 -0
- package/dist/{events/state/index.js → esm/chunk-6225YMFE.js} +38 -20
- package/dist/esm/chunk-6225YMFE.js.map +1 -0
- package/dist/esm/chunk-7QVYU63E.js +6 -0
- package/dist/esm/chunk-7QVYU63E.js.map +1 -0
- package/dist/{events/complete/index.js → esm/chunk-BNMI7DW3.js} +25 -19
- package/dist/esm/chunk-BNMI7DW3.js.map +1 -0
- package/dist/{subscribers/index.js → esm/chunk-HYNJBWW5.js} +36 -34
- package/dist/esm/chunk-HYNJBWW5.js.map +1 -0
- package/dist/{events/index.js → esm/chunk-JDL3U4OX.js} +7 -37
- package/dist/esm/chunk-JDL3U4OX.js.map +1 -0
- package/dist/{events/chunk/index.js → esm/chunk-LD2DWZRJ.js} +14 -11
- package/dist/esm/chunk-LD2DWZRJ.js.map +1 -0
- package/dist/{utils/index.js → esm/chunk-MFYC4PBP.js} +15 -22
- package/dist/esm/chunk-MFYC4PBP.js.map +1 -0
- package/dist/esm/chunk-NXYS73I4.js +125 -0
- package/dist/esm/chunk-NXYS73I4.js.map +1 -0
- package/dist/{cache/index.js → esm/chunk-PFALORWQ.js} +10 -11
- package/dist/esm/chunk-PFALORWQ.js.map +1 -0
- package/dist/{core/index.js → esm/chunk-TONVXBLH.js} +239 -231
- package/dist/esm/chunk-TONVXBLH.js.map +1 -0
- package/dist/{types/index.js → esm/chunk-X757PBC5.js} +5 -7
- package/dist/esm/chunk-X757PBC5.js.map +1 -0
- package/dist/esm/core/index.d.mts +271 -0
- package/dist/esm/core/index.js +12 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/events/chunk/index.d.mts +27 -0
- package/dist/esm/events/chunk/index.js +4 -0
- package/dist/esm/events/chunk/index.js.map +1 -0
- package/dist/esm/events/complete/index.d.mts +64 -0
- package/dist/esm/events/complete/index.js +4 -0
- package/dist/esm/events/complete/index.js.map +1 -0
- package/dist/esm/events/index.d.mts +95 -0
- package/dist/esm/events/index.js +8 -0
- package/dist/esm/events/index.js.map +1 -0
- package/dist/esm/events/initialize/index.d.mts +45 -0
- package/dist/esm/events/initialize/index.js +4 -0
- package/dist/esm/events/initialize/index.js.map +1 -0
- package/dist/esm/events/state/index.d.mts +67 -0
- package/dist/esm/events/state/index.js +4 -0
- package/dist/esm/events/state/index.js.map +1 -0
- package/dist/esm/exceptions/index.d.mts +84 -0
- package/dist/esm/exceptions/index.js +4 -0
- package/dist/esm/exceptions/index.js.map +1 -0
- package/dist/esm/index.d.mts +14 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/subscribers/index.d.mts +34 -0
- package/dist/esm/subscribers/index.js +10 -0
- package/dist/esm/subscribers/index.js.map +1 -0
- package/dist/esm/types/index.d.mts +110 -0
- package/dist/esm/types/index.js +4 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/utils/index.d.mts +72 -0
- package/dist/esm/utils/index.js +5 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/package.json +165 -14
- package/dist/cache/index.js.map +0 -1
- package/dist/core/index.js.map +0 -1
- package/dist/events/chunk/index.js.map +0 -1
- package/dist/events/complete/index.js.map +0 -1
- package/dist/events/index.js.map +0 -1
- package/dist/events/initialize/index.js.map +0 -1
- package/dist/events/state/index.js.map +0 -1
- package/dist/exceptions/index.js.map +0 -1
- package/dist/index.js +0 -49
- package/dist/subscribers/index.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/index.js.map +0 -1
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
var core_exports = {};
|
|
31
|
+
__export(core_exports, {
|
|
32
|
+
ChunkedFileUploader: () => ChunkedFileUploader
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(core_exports);
|
|
35
|
+
var import_core = require("@wlindabla/http_client/core");
|
|
36
|
+
var import_types = require("../types");
|
|
37
|
+
var import_utils = require("../utils");
|
|
38
|
+
var import_events = require("../events");
|
|
39
|
+
var import_exceptions = require("../exceptions");
|
|
40
|
+
var import_p_limit = __toESM(require("p-limit"));
|
|
41
|
+
/**
|
|
42
|
+
* ChunkedFileUploader
|
|
43
|
+
*
|
|
44
|
+
* A production-ready, event-driven chunked file upload engine for Browser and Node.js.
|
|
45
|
+
*
|
|
46
|
+
* Designed and developed by **AGBOKOUDJO Franck** at
|
|
47
|
+
* **INTERNATIONALES WEB APPS & SERVICES**, this class provides a robust,
|
|
48
|
+
* framework-agnostic solution for uploading large files to a remote server
|
|
49
|
+
* by splitting them into smaller chunks and sending them in parallel with
|
|
50
|
+
* configurable concurrency control.
|
|
51
|
+
*
|
|
52
|
+
* ---
|
|
53
|
+
*
|
|
54
|
+
* ### How It Works
|
|
55
|
+
*
|
|
56
|
+
* The upload process follows a strict three-phase lifecycle:
|
|
57
|
+
*
|
|
58
|
+
* 1. **Initialization** — A session is opened with the server via the `init` endpoint.
|
|
59
|
+
* The file is identified by its name, size, type, and a SHA-256 hash of its
|
|
60
|
+
* first megabyte. The server returns a unique `mediaId` that identifies the session.
|
|
61
|
+
*
|
|
62
|
+
* 2. **Chunk Upload** — The file is sliced into fixed-size chunks and uploaded
|
|
63
|
+
* concurrently using `p-limit`. Each chunk carries its index, the total number
|
|
64
|
+
* of chunks, and the session `mediaId`. Failed chunks are retried automatically
|
|
65
|
+
* with exponential backoff up to `maxRetries` attempts.
|
|
66
|
+
*
|
|
67
|
+
* 3. **Finalization** — Once all chunks are successfully uploaded, the `finalize`
|
|
68
|
+
* endpoint is called to instruct the server to assemble the chunks into the
|
|
69
|
+
* final file.
|
|
70
|
+
*
|
|
71
|
+
* ---
|
|
72
|
+
*
|
|
73
|
+
* ### Event-Driven Architecture
|
|
74
|
+
*
|
|
75
|
+
* This class follows the **Symfony EventDispatcher pattern** via
|
|
76
|
+
* `@wlindabla/event_dispatcher`. Every meaningful moment in the upload
|
|
77
|
+
* lifecycle emits a typed event that your application can listen to:
|
|
78
|
+
*
|
|
79
|
+
* ```
|
|
80
|
+
* IDLE → INITIALIZING → UPLOADING → FINALIZING → COMPLETED
|
|
81
|
+
* ↓ ↓
|
|
82
|
+
* FAILED PAUSED ↔ UPLOADING
|
|
83
|
+
* ↓
|
|
84
|
+
* CANCELLED
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* All event name constants are centralized in {@link HttpFileUploaderEvents}.
|
|
88
|
+
*
|
|
89
|
+
* ---
|
|
90
|
+
*
|
|
91
|
+
* ### Subscriber Registration (Required)
|
|
92
|
+
*
|
|
93
|
+
* Before calling `.upload()`, you **must** register the two built-in subscribers
|
|
94
|
+
* on your dispatcher. They handle the HTTP communication for the init and finalize phases:
|
|
95
|
+
*
|
|
96
|
+
* ```typescript
|
|
97
|
+
* // Browser
|
|
98
|
+
* const dispatcher = new BrowserEventDispatcher(document);
|
|
99
|
+
* dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
100
|
+
* dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
101
|
+
*
|
|
102
|
+
* // Node.js
|
|
103
|
+
* const dispatcher = new NodeEventDispatcher();
|
|
104
|
+
* dispatcher.addSubscriber(new InitializeUploadSubscriber(dispatcher));
|
|
105
|
+
* dispatcher.addSubscriber(new FinalizeUploadSubscriber(dispatcher));
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* ---
|
|
109
|
+
*
|
|
110
|
+
* ### Fluent Builder API
|
|
111
|
+
*
|
|
112
|
+
* The class exposes a fluent API to configure the upload before starting:
|
|
113
|
+
*
|
|
114
|
+
* ```typescript
|
|
115
|
+
* const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
116
|
+
*
|
|
117
|
+
* await uploader
|
|
118
|
+
* .withFile(file)
|
|
119
|
+
* .withEndpoints({
|
|
120
|
+
* init: 'https://api.example.com/upload/init',
|
|
121
|
+
* upload: 'https://api.example.com/upload/chunk',
|
|
122
|
+
* finalize: 'https://api.example.com/upload/finalize'
|
|
123
|
+
* })
|
|
124
|
+
* .upload();
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* ---
|
|
128
|
+
*
|
|
129
|
+
* ### Resumable Uploads
|
|
130
|
+
*
|
|
131
|
+
* When `autoSave: true` is set in options, the upload progress is persisted
|
|
132
|
+
* after each successful chunk via the {@link UploadResumeCacheInterface}.
|
|
133
|
+
* A failed or interrupted upload can be resumed later:
|
|
134
|
+
*
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const resumeData = await uploader.loadResumeData(file.name);
|
|
137
|
+
*
|
|
138
|
+
* if (resumeData) {
|
|
139
|
+
* await uploader
|
|
140
|
+
* .withFile(file)
|
|
141
|
+
* .withEndpoints(endpoints)
|
|
142
|
+
* .resumeUpload(resumeData);
|
|
143
|
+
* }
|
|
144
|
+
* ```
|
|
145
|
+
*
|
|
146
|
+
* ---
|
|
147
|
+
*
|
|
148
|
+
* ### Concurrency
|
|
149
|
+
*
|
|
150
|
+
* Multiple chunks can be uploaded simultaneously. The `concurrency` option
|
|
151
|
+
* controls how many parallel uploads are active at any given time.
|
|
152
|
+
* The default value is `3`, which provides a good balance between speed
|
|
153
|
+
* and server/network load:
|
|
154
|
+
*
|
|
155
|
+
* ```typescript
|
|
156
|
+
* // Upload 5 chunks in parallel
|
|
157
|
+
* new ChunkedFileUploader(dispatcher, cache, { concurrency: 5 });
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* ---
|
|
161
|
+
*
|
|
162
|
+
* ### Pause, Resume and Cancel
|
|
163
|
+
*
|
|
164
|
+
* The upload can be paused, resumed, or cancelled at any time:
|
|
165
|
+
*
|
|
166
|
+
* ```typescript
|
|
167
|
+
* uploader.pause(); // Pauses after the current chunk finishes
|
|
168
|
+
* uploader.resume(); // Resumes from where it was paused
|
|
169
|
+
* uploader.cancel(); // Aborts immediately via AbortController
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* ---
|
|
173
|
+
*
|
|
174
|
+
* ### Cache
|
|
175
|
+
*
|
|
176
|
+
* The library does **not** provide a built-in cache implementation to stay
|
|
177
|
+
* lightweight and environment-agnostic. You must implement
|
|
178
|
+
* {@link UploadResumeCacheInterface} with your preferred storage backend
|
|
179
|
+
* (localStorage, IndexedDB, Redis, filesystem, etc.).
|
|
180
|
+
*
|
|
181
|
+
* ---
|
|
182
|
+
*
|
|
183
|
+
* @author AGBOKOUDJO Franck <internationaleswebservices@gmail.com>
|
|
184
|
+
* @company INTERNATIONALES WEB APPS & SERVICES
|
|
185
|
+
* @phone +229 0167 25 18 86
|
|
186
|
+
* @linkedin https://www.linkedin.com/in/internationales-web-apps-services-120520193/
|
|
187
|
+
* @github https://github.com/Agbokoudjo/file_uploader
|
|
188
|
+
*
|
|
189
|
+
* @version 2.0.1
|
|
190
|
+
* @since 1.0.0
|
|
191
|
+
* @license MIT
|
|
192
|
+
*
|
|
193
|
+
* @see {@link HttpFileUploaderEvents} All event name constants
|
|
194
|
+
* @see {@link UploadResumeCacheInterface} Cache interface to implement
|
|
195
|
+
* @see {@link InitializeUploadSubscriber} Handles the init HTTP phase
|
|
196
|
+
* @see {@link FinalizeUploadSubscriber} Handles the finalize HTTP phase
|
|
197
|
+
* @see {@link UploadOptions} Full configuration reference
|
|
198
|
+
* @see {@link https://github.com/Agbokoudjo/file_uploader | GitHub Repository}
|
|
199
|
+
*/
|
|
200
|
+
class ChunkedFileUploader {
|
|
201
|
+
/**
|
|
202
|
+
* @param _uploadEventDispatcher: new BrowserEventDispatcher(), //or new NodeJSEventDispatcher() if you have an environment NodeJs
|
|
203
|
+
* @param uploadResumeData: UploadResumeCacheInterface
|
|
204
|
+
* @param options: UploadOptions
|
|
205
|
+
*/
|
|
206
|
+
constructor(_uploadEventDispatcher, uploadResumeData, options) {
|
|
207
|
+
this._uploadEventDispatcher = _uploadEventDispatcher;
|
|
208
|
+
this.uploadResumeData = uploadResumeData;
|
|
209
|
+
this.options = options;
|
|
210
|
+
this._file = null;
|
|
211
|
+
this._endpoints = null;
|
|
212
|
+
this.isPaused = false;
|
|
213
|
+
this.startTime = 0;
|
|
214
|
+
this.uploadedBytes = 0;
|
|
215
|
+
this.abortController = new AbortController();
|
|
216
|
+
this.state = import_types.UploadState.IDLE;
|
|
217
|
+
this.totalChunks = 0;
|
|
218
|
+
this.percentage = 0;
|
|
219
|
+
this.uploadedChunks = 0;
|
|
220
|
+
this.lastUploadedChunkIndex = -1;
|
|
221
|
+
}
|
|
222
|
+
_uploadEventDispatcher;
|
|
223
|
+
uploadResumeData;
|
|
224
|
+
options;
|
|
225
|
+
static {
|
|
226
|
+
__name(this, "ChunkedFileUploader");
|
|
227
|
+
}
|
|
228
|
+
_file;
|
|
229
|
+
_endpoints;
|
|
230
|
+
isPaused;
|
|
231
|
+
startTime;
|
|
232
|
+
uploadedBytes;
|
|
233
|
+
abortController;
|
|
234
|
+
state;
|
|
235
|
+
totalChunks;
|
|
236
|
+
percentage;
|
|
237
|
+
uploadedChunks;
|
|
238
|
+
lastUploadedChunkIndex;
|
|
239
|
+
/**
|
|
240
|
+
* Starts the chunked file upload process.
|
|
241
|
+
*
|
|
242
|
+
* @throws {Error} If file or endpoints are not set
|
|
243
|
+
* @throws {InitializeUploadFailureException} If server initialization fails
|
|
244
|
+
* @throws {FileUploadChunkError} If a chunk fails after all retries
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* const uploader = new ChunkedFileUploader(dispatcher, cache, options);
|
|
249
|
+
*
|
|
250
|
+
* uploader
|
|
251
|
+
* .withFile(file)
|
|
252
|
+
* .withEndpoints({ init, upload, finalize });
|
|
253
|
+
*
|
|
254
|
+
* await uploader.upload();
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
async upload() {
|
|
258
|
+
this.setState(import_types.UploadState.INITIALIZING);
|
|
259
|
+
const {
|
|
260
|
+
maxRetries = 3,
|
|
261
|
+
config,
|
|
262
|
+
speedMbps,
|
|
263
|
+
metadata,
|
|
264
|
+
headerInitialzingUpload,
|
|
265
|
+
concurrency = 3
|
|
266
|
+
} = this.options;
|
|
267
|
+
let file;
|
|
268
|
+
try {
|
|
269
|
+
file = this.file;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
const fileHash = await import_utils.FileUtils.generateFileHash(this.file);
|
|
274
|
+
let fileId;
|
|
275
|
+
try {
|
|
276
|
+
const initializationEvent = await this._uploadEventDispatcher.dispatchAsync(
|
|
277
|
+
new import_events.InitializingUploadEvent(
|
|
278
|
+
{
|
|
279
|
+
fileHash,
|
|
280
|
+
fileName: file.name,
|
|
281
|
+
fileSize: file.size,
|
|
282
|
+
fileType: file.type,
|
|
283
|
+
metadata,
|
|
284
|
+
endpointInit: this.endPointOptions.init,
|
|
285
|
+
headers: headerInitialzingUpload
|
|
286
|
+
}
|
|
287
|
+
),
|
|
288
|
+
import_events.HttpFileUploaderEvents.INITIALIZE_UPLOAD
|
|
289
|
+
);
|
|
290
|
+
fileId = initializationEvent.mediaId;
|
|
291
|
+
} catch (error) {
|
|
292
|
+
this.setState(import_types.UploadState.FAILED);
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
this.setState(import_types.UploadState.UPLOADING);
|
|
296
|
+
this.startTime = Date.now();
|
|
297
|
+
this.uploadedBytes = 0;
|
|
298
|
+
this.uploadedChunks = 0;
|
|
299
|
+
this.lastUploadedChunkIndex = -1;
|
|
300
|
+
const chunkSize = this.options.chunkSize || import_utils.FileUtils.calculateChunkSize(file.size, speedMbps, config);
|
|
301
|
+
this.totalChunks = Math.ceil(file.size / chunkSize);
|
|
302
|
+
try {
|
|
303
|
+
await this.uploadChunksWithConcurrency(
|
|
304
|
+
file,
|
|
305
|
+
chunkSize,
|
|
306
|
+
fileId,
|
|
307
|
+
fileHash,
|
|
308
|
+
maxRetries,
|
|
309
|
+
concurrency,
|
|
310
|
+
0
|
|
311
|
+
);
|
|
312
|
+
await this.finalizeUpload(fileId, fileHash);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
this.handleUploadFailure(error);
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
withFile(file) {
|
|
319
|
+
if (!file) {
|
|
320
|
+
throw new Error("File is required");
|
|
321
|
+
}
|
|
322
|
+
if (file.size === 0) {
|
|
323
|
+
throw new Error("Cannot upload empty file");
|
|
324
|
+
}
|
|
325
|
+
if (!(file instanceof File) || !(file instanceof Blob)) {
|
|
326
|
+
throw new TypeError("Expected File or Blob instance");
|
|
327
|
+
}
|
|
328
|
+
this._file = file;
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
withEndpoints(endpoints) {
|
|
332
|
+
if (!endpoints.init || !endpoints.upload || !endpoints.finalize) {
|
|
333
|
+
throw new Error("All endpoints (init, upload, finalize) are required");
|
|
334
|
+
}
|
|
335
|
+
this._endpoints = endpoints;
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
get endPointOptions() {
|
|
339
|
+
if (!this._endpoints) {
|
|
340
|
+
throw new Error("Endpoint URL is required");
|
|
341
|
+
}
|
|
342
|
+
return this._endpoints;
|
|
343
|
+
}
|
|
344
|
+
get file() {
|
|
345
|
+
if (!this._file) {
|
|
346
|
+
throw new Error(`
|
|
347
|
+
This operation requires a mandatory file.Did you forget to upload the file?
|
|
348
|
+
Use the withFile(file: File|Blob) function of the ChunkedFileUploader ${this} class that you instantiated.
|
|
349
|
+
`);
|
|
350
|
+
}
|
|
351
|
+
return this._file;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Upload all chunks with concurrency control using p-limit
|
|
355
|
+
*
|
|
356
|
+
* @param file - File to upload
|
|
357
|
+
* @param chunkSize - Size of each chunk
|
|
358
|
+
* @param fileId - Server file ID
|
|
359
|
+
* @param fileHash - File hash
|
|
360
|
+
* @param maxRetries - Max retry attempts per chunk
|
|
361
|
+
* @param concurrency - Number of concurrent uploads (default: 3)
|
|
362
|
+
*/
|
|
363
|
+
async uploadChunksWithConcurrency(file, chunkSize, fileId, fileHash, maxRetries, concurrency, startIndex = 0) {
|
|
364
|
+
try {
|
|
365
|
+
const limit = (0, import_p_limit.default)(concurrency);
|
|
366
|
+
const uploadPromises = [];
|
|
367
|
+
for (let chunkIndex = startIndex; chunkIndex < this.totalChunks; chunkIndex++) {
|
|
368
|
+
const limitedUpload = limit(
|
|
369
|
+
() => this.processChunk(
|
|
370
|
+
file,
|
|
371
|
+
chunkIndex,
|
|
372
|
+
chunkSize,
|
|
373
|
+
fileId,
|
|
374
|
+
fileHash,
|
|
375
|
+
maxRetries
|
|
376
|
+
)
|
|
377
|
+
);
|
|
378
|
+
uploadPromises.push(limitedUpload);
|
|
379
|
+
}
|
|
380
|
+
await Promise.all(uploadPromises);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Process a single chunk: slice, upload with retry, and save progress.
|
|
387
|
+
*
|
|
388
|
+
* @param file - The file being uploaded
|
|
389
|
+
* @param currentChunkIndex - Index of the current chunk (0-based)
|
|
390
|
+
* @param chunkSize - Size of each chunk in bytes
|
|
391
|
+
* @param fileId - Server-provided file/session ID
|
|
392
|
+
* @param fileHash - SHA-256 hash of the file
|
|
393
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
394
|
+
*
|
|
395
|
+
* @throws {UploadCancelledException} If upload is cancelled
|
|
396
|
+
* @throws {FileUploadChunkError} If chunk upload fails after all retries
|
|
397
|
+
*/
|
|
398
|
+
async processChunk(file, currentChunkIndex, chunkSize, fileId, fileHash, maxRetries) {
|
|
399
|
+
try {
|
|
400
|
+
if (this.abortController.signal.aborted) {
|
|
401
|
+
this._uploadEventDispatcher.dispatch(
|
|
402
|
+
new import_events.UploadCancelledEvent(
|
|
403
|
+
file.name,
|
|
404
|
+
this.totalChunks,
|
|
405
|
+
this.uploadedBytes,
|
|
406
|
+
this.percentage,
|
|
407
|
+
currentChunkIndex,
|
|
408
|
+
"Upload cancelled by user",
|
|
409
|
+
Date.now()
|
|
410
|
+
),
|
|
411
|
+
import_events.HttpFileUploaderEvents.UPLOAD_CANCELLED
|
|
412
|
+
);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
while (this.isPaused) {
|
|
416
|
+
await this.sleep(100);
|
|
417
|
+
}
|
|
418
|
+
const start = currentChunkIndex * chunkSize;
|
|
419
|
+
const end = Math.min(file.size, start + chunkSize);
|
|
420
|
+
const chunk = file.slice(start, end);
|
|
421
|
+
const chunkInfo = {
|
|
422
|
+
index: currentChunkIndex,
|
|
423
|
+
start,
|
|
424
|
+
end,
|
|
425
|
+
size: chunk.size,
|
|
426
|
+
attempt: 0,
|
|
427
|
+
status: "pending"
|
|
428
|
+
};
|
|
429
|
+
this._uploadEventDispatcher.dispatch(
|
|
430
|
+
new import_events.UploadChunkStartedEvent(chunkInfo),
|
|
431
|
+
import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_STARTED
|
|
432
|
+
);
|
|
433
|
+
await this.uploadChunkWithRetry(
|
|
434
|
+
chunk,
|
|
435
|
+
chunkInfo,
|
|
436
|
+
fileId,
|
|
437
|
+
fileHash,
|
|
438
|
+
this.totalChunks,
|
|
439
|
+
maxRetries
|
|
440
|
+
);
|
|
441
|
+
if (this.options.autoSave) {
|
|
442
|
+
await this.saveResumeData(fileId, chunkSize);
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async uploadChunkWithRetry(chunk, chunkInfo, fileId, fileHash, totalChunks, maxRetries) {
|
|
449
|
+
let success = false;
|
|
450
|
+
let lastError = null;
|
|
451
|
+
for (let attempt = 0; attempt < maxRetries && !success; attempt++) {
|
|
452
|
+
chunkInfo.attempt = attempt + 1;
|
|
453
|
+
chunkInfo.status = "uploading";
|
|
454
|
+
try {
|
|
455
|
+
const response = await this.uploadChunk(chunk, chunkInfo, fileId, fileHash, totalChunks);
|
|
456
|
+
success = true;
|
|
457
|
+
chunkInfo.status = "success";
|
|
458
|
+
this.updateProgress(chunk.size, chunkInfo.index);
|
|
459
|
+
this.notifyProgress(
|
|
460
|
+
this.uploadedChunks,
|
|
461
|
+
totalChunks,
|
|
462
|
+
this.file.size,
|
|
463
|
+
response
|
|
464
|
+
);
|
|
465
|
+
return;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof import_exceptions.ChunkUploadHttpErrorException) {
|
|
468
|
+
this._uploadEventDispatcher.dispatch(
|
|
469
|
+
new import_events.ChunkUploadHttpErrorResponseEvent(
|
|
470
|
+
error.errorPayload,
|
|
471
|
+
error.statusResponse,
|
|
472
|
+
this.endPointOptions.upload,
|
|
473
|
+
chunkInfo
|
|
474
|
+
),
|
|
475
|
+
import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_HTTP_ERROR_RESPONSE
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
lastError = error;
|
|
479
|
+
chunkInfo.status = "error";
|
|
480
|
+
const chunkError = {
|
|
481
|
+
chunk: chunkInfo,
|
|
482
|
+
error: lastError,
|
|
483
|
+
attempt: attempt + 1,
|
|
484
|
+
willRetry: attempt < maxRetries - 1
|
|
485
|
+
};
|
|
486
|
+
if (error instanceof import_core.HttpFetchError) {
|
|
487
|
+
this._uploadEventDispatcher.dispatch(chunkError, import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_FAILED);
|
|
488
|
+
}
|
|
489
|
+
if (attempt < maxRetries - 1) {
|
|
490
|
+
const delay = Math.pow(2, attempt) * 1e3;
|
|
491
|
+
await this.sleep(delay);
|
|
492
|
+
console.info(`Retry #${attempt + 2} in ${delay / 1e3}s...`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (!success) {
|
|
497
|
+
const fileUploadChunkError = new import_exceptions.FileUploadChunkError(
|
|
498
|
+
`Failed to upload chunk ${chunkInfo.index} after ${maxRetries} attempts`,
|
|
499
|
+
{
|
|
500
|
+
chunk: chunkInfo,
|
|
501
|
+
error: lastError,
|
|
502
|
+
attempt: maxRetries,
|
|
503
|
+
willRetry: false
|
|
504
|
+
}
|
|
505
|
+
);
|
|
506
|
+
this._uploadEventDispatcher.dispatch(
|
|
507
|
+
fileUploadChunkError,
|
|
508
|
+
import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_MAXRETRY_EXPIRE
|
|
509
|
+
);
|
|
510
|
+
throw fileUploadChunkError;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async uploadChunk(chunk, chunkInfo, mediaIdFromServer, fileHash, totalChunks) {
|
|
514
|
+
const media = this.file;
|
|
515
|
+
const chunkFormData = (0, import_utils.createChunkFormData)(
|
|
516
|
+
chunk,
|
|
517
|
+
{
|
|
518
|
+
chunkIndex: chunkInfo.index,
|
|
519
|
+
mediaId: mediaIdFromServer,
|
|
520
|
+
fileSize: media.size,
|
|
521
|
+
fileName: media.name,
|
|
522
|
+
fileHash,
|
|
523
|
+
totalChunks
|
|
524
|
+
}
|
|
525
|
+
);
|
|
526
|
+
try {
|
|
527
|
+
const response_of_server = await (0, import_core.safeFetch)({
|
|
528
|
+
url: this.endPointOptions.upload,
|
|
529
|
+
headers: this.options.headers,
|
|
530
|
+
data: chunkFormData,
|
|
531
|
+
methodSend: "POST",
|
|
532
|
+
responseType: "json",
|
|
533
|
+
timeout: this.options.timeout ?? 6e4,
|
|
534
|
+
retryCount: 2,
|
|
535
|
+
retryOnStatusCode: false,
|
|
536
|
+
signal: this.createChunkAbortSignal()
|
|
537
|
+
});
|
|
538
|
+
const statusResponse = response_of_server.status;
|
|
539
|
+
if (response_of_server.failed) {
|
|
540
|
+
throw new import_exceptions.ChunkUploadHttpErrorException(response_of_server.data, statusResponse);
|
|
541
|
+
}
|
|
542
|
+
return response_of_server;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
createChunkAbortSignal() {
|
|
548
|
+
const chunkController = new AbortController();
|
|
549
|
+
this.abortController.signal.addEventListener("abort", () => {
|
|
550
|
+
chunkController.abort();
|
|
551
|
+
});
|
|
552
|
+
return chunkController.signal;
|
|
553
|
+
}
|
|
554
|
+
notifyProgress(uploadedChunks, totalChunks, totalBytes, httpResponse) {
|
|
555
|
+
const elapsed = Math.max((Date.now() - this.startTime) / 1e3, 0.1);
|
|
556
|
+
const speed = this.uploadedBytes / elapsed;
|
|
557
|
+
const remainingBytes = totalBytes - this.uploadedBytes;
|
|
558
|
+
const estimatedTimeRemaining = uploadedChunks < 2 ? null : remainingBytes / speed;
|
|
559
|
+
this.percentage = Math.round(this.uploadedBytes / totalBytes * 100);
|
|
560
|
+
const progress = {
|
|
561
|
+
uploadedChunks,
|
|
562
|
+
totalChunks,
|
|
563
|
+
uploadedBytes: this.uploadedBytes,
|
|
564
|
+
totalBytes,
|
|
565
|
+
percentage: this.percentage,
|
|
566
|
+
currentChunk: uploadedChunks,
|
|
567
|
+
speed,
|
|
568
|
+
// bytes/seconde
|
|
569
|
+
estimatedTimeRemaining,
|
|
570
|
+
// secondes (ou null)
|
|
571
|
+
elapsed
|
|
572
|
+
// secondes écoulées
|
|
573
|
+
};
|
|
574
|
+
this._uploadEventDispatcher.dispatch(
|
|
575
|
+
new import_events.UploadProgressEvent(
|
|
576
|
+
progress,
|
|
577
|
+
httpResponse.data,
|
|
578
|
+
httpResponse.status
|
|
579
|
+
)
|
|
580
|
+
);
|
|
581
|
+
console.info(
|
|
582
|
+
`Progress: ${this.percentage}% | Speed: ${import_utils.FileUtils.formatBytes(speed)}/s | ETA: ${estimatedTimeRemaining ? import_utils.FileUtils.formatDuration(estimatedTimeRemaining) : "Calculating..."}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
sleep(ms) {
|
|
586
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Save current upload progress to cache for resume capability
|
|
590
|
+
*
|
|
591
|
+
* @param fileId - Server-provided file/session ID
|
|
592
|
+
* @param chunkSize - Size of each chunk in bytes
|
|
593
|
+
* @returns Saved resume data
|
|
594
|
+
*/
|
|
595
|
+
async saveResumeData(fileId, chunkSize) {
|
|
596
|
+
const data = {
|
|
597
|
+
fileId,
|
|
598
|
+
fileName: this.file.name,
|
|
599
|
+
fileSize: this.file.size,
|
|
600
|
+
uploadedChunks: this.uploadedChunks,
|
|
601
|
+
lastChunkIndex: this.lastUploadedChunkIndex,
|
|
602
|
+
lastBytePosition: this.uploadedBytes,
|
|
603
|
+
chunkSize,
|
|
604
|
+
concurrency: this.options.concurrency || 3
|
|
605
|
+
};
|
|
606
|
+
await this.uploadResumeData.setItem(`upload_${this.file.name}`, data);
|
|
607
|
+
console.info(
|
|
608
|
+
`Resume data saved: ${this.uploadedChunks}/${Math.ceil(this.file.size / chunkSize)} chunks, last chunk index: ${this.lastUploadedChunkIndex}`
|
|
609
|
+
);
|
|
610
|
+
return data;
|
|
611
|
+
}
|
|
612
|
+
setState(newState) {
|
|
613
|
+
const oldState = this.state;
|
|
614
|
+
this.state = newState;
|
|
615
|
+
this._uploadEventDispatcher.dispatch(
|
|
616
|
+
new import_events.UploadStateChangedEvent(
|
|
617
|
+
oldState,
|
|
618
|
+
newState,
|
|
619
|
+
Date.now()
|
|
620
|
+
),
|
|
621
|
+
import_events.HttpFileUploaderEvents.UPLOAD_STATE_CHANGED
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
getState() {
|
|
625
|
+
return this.state;
|
|
626
|
+
}
|
|
627
|
+
handleUploadFailure(error) {
|
|
628
|
+
this.setState(import_types.UploadState.FAILED);
|
|
629
|
+
this._uploadEventDispatcher.dispatch(error, import_events.HttpFileUploaderEvents.DOWNLOAD_MEDIA_FAILURE);
|
|
630
|
+
console.error("Upload failed:", error);
|
|
631
|
+
}
|
|
632
|
+
cancel() {
|
|
633
|
+
this.abortController.abort();
|
|
634
|
+
this.setState(import_types.UploadState.CANCELLED);
|
|
635
|
+
}
|
|
636
|
+
pause() {
|
|
637
|
+
this.isPaused = true;
|
|
638
|
+
this.setState(import_types.UploadState.PAUSED);
|
|
639
|
+
this._uploadEventDispatcher.dispatch(
|
|
640
|
+
new import_events.UploadPausedEvent(
|
|
641
|
+
this.file.name,
|
|
642
|
+
this.totalChunks,
|
|
643
|
+
this.uploadedBytes,
|
|
644
|
+
this.percentage,
|
|
645
|
+
Date.now()
|
|
646
|
+
),
|
|
647
|
+
import_events.HttpFileUploaderEvents.UPLOAD_PAUSED
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
resume() {
|
|
651
|
+
this.isPaused = false;
|
|
652
|
+
this.setState(import_types.UploadState.UPLOADING);
|
|
653
|
+
this._uploadEventDispatcher.dispatch(
|
|
654
|
+
new import_events.UploadResumedEvent(
|
|
655
|
+
this.file.name,
|
|
656
|
+
this.totalChunks,
|
|
657
|
+
this.uploadedBytes,
|
|
658
|
+
this.percentage
|
|
659
|
+
),
|
|
660
|
+
import_events.HttpFileUploaderEvents.UPLOAD_RESUMED
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Load previously saved resume data
|
|
665
|
+
*
|
|
666
|
+
* @param fileName - Name of the file to resume
|
|
667
|
+
* @returns Resume data or null if not found
|
|
668
|
+
*/
|
|
669
|
+
async loadResumeData(fileName) {
|
|
670
|
+
try {
|
|
671
|
+
const data = await this.uploadResumeData.getItem(`upload_${fileName}`);
|
|
672
|
+
if (data) {
|
|
673
|
+
console.info(
|
|
674
|
+
`Resume data loaded: ${data.uploadedChunks} chunks uploaded, last index: ${data.lastChunkIndex}`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return data;
|
|
678
|
+
} catch (error) {
|
|
679
|
+
console.warn(`No resume data found for ${fileName}`);
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Resume upload from saved state
|
|
685
|
+
*
|
|
686
|
+
* @param resumeData - Previously saved resume data
|
|
687
|
+
* @returns Upload result
|
|
688
|
+
*/
|
|
689
|
+
async resumeUpload(resumeData) {
|
|
690
|
+
const __message = `Resuming upload from chunk ${resumeData.lastChunkIndex + 1} (${resumeData.uploadedChunks} chunks already uploaded)`;
|
|
691
|
+
this.uploadedBytes = resumeData.lastBytePosition;
|
|
692
|
+
this.uploadedChunks = resumeData.uploadedChunks;
|
|
693
|
+
this.lastUploadedChunkIndex = resumeData.lastChunkIndex;
|
|
694
|
+
const assumedSpeed = 5e5;
|
|
695
|
+
const timeAlreadySpent = resumeData.lastBytePosition / assumedSpeed;
|
|
696
|
+
this.startTime = Date.now() - timeAlreadySpent * 1e3;
|
|
697
|
+
const { maxRetries = 3 } = this.options;
|
|
698
|
+
const fileHash = await import_utils.FileUtils.generateFileHash(this.file);
|
|
699
|
+
const chunkSize = resumeData.chunkSize;
|
|
700
|
+
this.totalChunks = Math.ceil(this.file.size / chunkSize);
|
|
701
|
+
try {
|
|
702
|
+
this._uploadEventDispatcher.dispatch(
|
|
703
|
+
new import_events.ResumeUploadEvent(resumeData, __message),
|
|
704
|
+
import_events.HttpFileUploaderEvents.MEDIA_CHUNK_UPLOAD_RESUME
|
|
705
|
+
);
|
|
706
|
+
await this.uploadChunksWithConcurrency(
|
|
707
|
+
this.file,
|
|
708
|
+
chunkSize,
|
|
709
|
+
resumeData.fileId,
|
|
710
|
+
fileHash,
|
|
711
|
+
maxRetries,
|
|
712
|
+
resumeData.concurrency,
|
|
713
|
+
resumeData.lastChunkIndex + 1
|
|
714
|
+
);
|
|
715
|
+
await this.finalizeUpload(resumeData.fileId, fileHash);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
this.handleUploadFailure(error);
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
updateProgress(chunkSize, chunkIndex) {
|
|
722
|
+
this.uploadedBytes += chunkSize;
|
|
723
|
+
this.uploadedChunks++;
|
|
724
|
+
this.lastUploadedChunkIndex = Math.max(this.lastUploadedChunkIndex, chunkIndex);
|
|
725
|
+
}
|
|
726
|
+
async finalizeUpload(mediaId, fileHash) {
|
|
727
|
+
try {
|
|
728
|
+
const finalizeUploadEvent = await this._uploadEventDispatcher.dispatchAsync(
|
|
729
|
+
new import_events.FinalizeUploadEvent(
|
|
730
|
+
this.endPointOptions.finalize,
|
|
731
|
+
mediaId,
|
|
732
|
+
fileHash,
|
|
733
|
+
this.options.headerFinalezingUpload
|
|
734
|
+
),
|
|
735
|
+
import_events.HttpFileUploaderEvents.FINALIZE_UPLOAD
|
|
736
|
+
);
|
|
737
|
+
this.setState(import_types.UploadState.FINALIZING);
|
|
738
|
+
const duration = (Date.now() - this.startTime) / 1e3;
|
|
739
|
+
const uploadResult = {
|
|
740
|
+
success: true,
|
|
741
|
+
finalizeUploadResponse: finalizeUploadEvent.hasResponse() ? finalizeUploadEvent.getResponse() : null,
|
|
742
|
+
totalChunks: this.totalChunks,
|
|
743
|
+
totalBytes: this.file.size,
|
|
744
|
+
duration,
|
|
745
|
+
averageSpeed: this.file.size / duration,
|
|
746
|
+
fileId: mediaId
|
|
747
|
+
};
|
|
748
|
+
this.setState(import_types.UploadState.COMPLETED);
|
|
749
|
+
this._uploadEventDispatcher.dispatch(
|
|
750
|
+
new import_events.UploadMediaCompleteEvent(uploadResult),
|
|
751
|
+
import_events.HttpFileUploaderEvents.DOWNLOAD_MEDIA_COMPLETE
|
|
752
|
+
);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
throw error;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
759
|
+
0 && (module.exports = {
|
|
760
|
+
ChunkedFileUploader
|
|
761
|
+
});
|
|
762
|
+
//# sourceMappingURL=index.js.map
|