@wlindabla/file_uploader 1.0.0

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