@ammarahmed/react-native-upload 6.33.0 → 7.0.0-alpha.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ammarahmed/react-native-upload",
3
- "version": "6.33.0",
3
+ "version": "7.0.0-alpha.0",
4
4
  "description": "Cross platform http post file uploader with Android and iOS background support.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -40,12 +40,12 @@
40
40
  "@semantic-release/github": "^8.0.0",
41
41
  "@semantic-release/npm": "^9.0.0",
42
42
  "@semantic-release/release-notes-generator": "^10.0.0",
43
- "react-native": "0.82.1",
44
43
  "eslint": "^6.8.0",
45
44
  "husky": "^4.2.3",
46
45
  "lint-staged": "^10.0.8",
46
+ "react-native": "0.82.1",
47
47
  "semantic-release": "^19.0.2",
48
- "typescript": "5.9.3"
48
+ "typescript": "^5.9.3"
49
49
  },
50
50
  "release": {
51
51
  "plugins": [
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  * Handles HTTP background file uploads from an iOS or Android device.
3
3
  */
4
4
  import {
5
+ AppRegistry,
5
6
  NativeModules,
6
7
  DeviceEventEmitter,
7
8
  Platform,
@@ -33,9 +34,21 @@ export type NotificationOptions = {
33
34
  */
34
35
  onProgressTitle: string;
35
36
  /**
36
- * Sets notification progress message { onProgressMessage: "Uploading new video" }
37
+ * Sets notification progress message. For multipart uploads {progress} is replaced with the percentage { onProgressMessage: "Uploading new video" }
37
38
  */
38
39
  onProgressMessage: string;
40
+ /**
41
+ * Sets multipart upload notification start message { onStartMessage: "Starting upload..." }
42
+ */
43
+ onStartMessage: string;
44
+ /**
45
+ * Sets multipart upload notification message when resuming { onResumeMessage: "Resuming upload..." }
46
+ */
47
+ onResumeMessage: string;
48
+ /**
49
+ * Sets the Android notification channel name shown in system settings { notificationChannelName: "Uploads" }
50
+ */
51
+ notificationChannelName: string;
39
52
  /**
40
53
  * Sets notification complete title { onCompleteTitle: "Upload finished" }
41
54
  */
@@ -126,6 +139,95 @@ export type UploadEventData =
126
139
  | CancelledData
127
140
  | CompletedData;
128
141
 
142
+ export type MultipartEventType =
143
+ | "progress"
144
+ | "error"
145
+ | "completed"
146
+ | "cancelled";
147
+
148
+ export interface MultipartS3Options {
149
+ /**
150
+ * Unique upload ID to track the upload.
151
+ */
152
+ uploadId: string;
153
+ /**
154
+ * Path to the local file to upload (e.g. /path/to/file or file:///path/to/file).
155
+ */
156
+ path: string;
157
+ /**
158
+ * Array of presigned URLs for each chunk in sequence.
159
+ */
160
+ parts: string[];
161
+ /**
162
+ * Maximum parallel chunk uploads (default: 3).
163
+ */
164
+ maxParallel?: number;
165
+ /**
166
+ * Notification options for background service (Android).
167
+ */
168
+ notification?: Partial<NotificationOptions>;
169
+ /**
170
+ * AppGroup defined in Xcode for extensions (iOS).
171
+ */
172
+ appGroup?: string;
173
+ /**
174
+ * Fixed-length array of previously uploaded chunk ETags (matching parts array length)
175
+ * for resuming an existing upload session. Completed parts carry their ETag string,
176
+ * while uncompleted parts carry empty string "".
177
+ * Already uploaded chunks will be skipped and pre-filled with these ETags.
178
+ */
179
+ existingEtags?: string[];
180
+ /**
181
+ * Whether the upload can survive the app process being killed (default: true).
182
+ * Resumable uploads are persisted natively, resumed after a restart and can
183
+ * be finished later through getMultipartS3Uploads. When false, the upload
184
+ * still keeps running while the app is in the background but nothing is
185
+ * persisted, so it is gone if the app process dies.
186
+ */
187
+ resumable?: boolean;
188
+ }
189
+
190
+ export interface MultipartProgressEvent {
191
+ type: string;
192
+ uploadId: string;
193
+ progress?: number;
194
+ bytesUploaded?: number;
195
+ totalBytes?: number;
196
+ chunkIndex?: number;
197
+ etag?: string;
198
+ etags?: string[];
199
+ errorMessage?: string;
200
+ }
201
+
202
+ export type MultipartUploadStatus = "running" | "paused" | "completed" | "failed";
203
+
204
+ export interface MultipartPendingUpload {
205
+ uploadId: string;
206
+ status: MultipartUploadStatus;
207
+ progress: number;
208
+ bytesUploaded: number;
209
+ totalBytes: number;
210
+ etags: string[];
211
+ error: string | null;
212
+ }
213
+
214
+ export interface MultipartCompletionEvent {
215
+ uploadId: string;
216
+ status: "completed" | "failed";
217
+ etags: string[];
218
+ error: string | null;
219
+ }
220
+
221
+ export type MultipartCompletionHandler = (
222
+ event: MultipartCompletionEvent,
223
+ ) => Promise<void> | void;
224
+
225
+ export interface MultipartS3Result {
226
+ uploadId: string;
227
+ success: boolean;
228
+ etags: string[];
229
+ }
230
+
129
231
  const NativeModule = NativeModules.RNFileUploader;
130
232
  const eventPrefix = 'RNFileUploader-';
131
233
 
@@ -135,6 +237,68 @@ if (NativeModules.RNFileUploader && NativeModule.addListener) {
135
237
  NativeModule.addListener(eventPrefix + 'error');
136
238
  NativeModule.addListener(eventPrefix + 'cancelled');
137
239
  NativeModule.addListener(eventPrefix + 'completed');
240
+ NativeModule.addListener(eventPrefix + 'multipart-progress');
241
+ NativeModule.addListener(eventPrefix + 'multipart-error');
242
+ NativeModule.addListener(eventPrefix + 'multipart-cancelled');
243
+ NativeModule.addListener(eventPrefix + 'multipart-completed');
244
+ }
245
+
246
+ let multipartCompletionHandler: MultipartCompletionHandler | null = null;
247
+ const runningCompletions = new Map<string, Promise<void>>();
248
+
249
+ const runMultipartCompletion = (event: MultipartCompletionEvent): Promise<void> => {
250
+ const running = runningCompletions.get(event.uploadId);
251
+ if (running) {
252
+ return running;
253
+ }
254
+ const handler = multipartCompletionHandler;
255
+ if (!handler) {
256
+ return Promise.resolve();
257
+ }
258
+ const run = (async () => {
259
+ try {
260
+ await handler(event);
261
+ await NativeModule.acknowledgeMultipartUpload(event.uploadId);
262
+ } catch (error) {
263
+ console.warn(
264
+ `Multipart completion handler failed for ${event.uploadId}`,
265
+ error,
266
+ );
267
+ } finally {
268
+ runningCompletions.delete(event.uploadId);
269
+ }
270
+ })();
271
+ runningCompletions.set(event.uploadId, run);
272
+ return run;
273
+ };
274
+
275
+ DeviceEventEmitter.addListener(eventPrefix + 'multipart-completed', (data: any) => {
276
+ runMultipartCompletion({
277
+ uploadId: data.uploadId,
278
+ status: 'completed',
279
+ etags: data.etags || [],
280
+ error: null,
281
+ });
282
+ });
283
+
284
+ DeviceEventEmitter.addListener(eventPrefix + 'multipart-error', (data: any) => {
285
+ runMultipartCompletion({
286
+ uploadId: data.uploadId,
287
+ status: 'failed',
288
+ etags: data.etags || [],
289
+ error: data.error || null,
290
+ });
291
+ });
292
+
293
+ if (Platform.OS === 'android') {
294
+ AppRegistry.registerHeadlessTask(
295
+ 'RNMultipartUploadKeepAlive',
296
+ () => () => new Promise<void>(() => {}),
297
+ );
298
+ AppRegistry.registerHeadlessTask(
299
+ 'RNMultipartUploadCompletion',
300
+ () => (data: MultipartCompletionEvent) => runMultipartCompletion(data),
301
+ );
138
302
  }
139
303
 
140
304
  export const UploadState = {
@@ -275,6 +439,90 @@ class Upload {
275
439
  /**
276
440
  * Get all currently tracked uploads
277
441
  */
442
+
443
+ /**
444
+ * Create an S3 / Cloud multipart chunked upload instance
445
+ */
446
+ static createMultipartS3(options: MultipartS3Options): MultipartS3Upload {
447
+ return MultipartS3Upload.create(options);
448
+ }
449
+
450
+ /**
451
+ * Start an S3 / Cloud multipart chunked upload directly
452
+ */
453
+ static startMultipartS3(options: MultipartS3Options): Promise<MultipartS3Result> {
454
+ const upload = MultipartS3Upload.create(options);
455
+ return upload.start();
456
+ }
457
+
458
+ /**
459
+ * Cancel an ongoing S3 multipart upload
460
+ */
461
+ static cancelMultipartS3(uploadId: string): Promise<boolean> {
462
+ return MultipartS3Upload.cancel(uploadId);
463
+ }
464
+
465
+ /**
466
+ * Pause an ongoing S3 multipart upload
467
+ */
468
+ static pauseMultipartS3(uploadId: string): Promise<boolean> {
469
+ return MultipartS3Upload.pause(uploadId);
470
+ }
471
+
472
+ /**
473
+ * Resume a paused S3 multipart upload
474
+ */
475
+ static resumeMultipartS3(uploadId: string): Promise<boolean> {
476
+ return MultipartS3Upload.resume(uploadId);
477
+ }
478
+
479
+ /**
480
+ * Registers the function that finishes a multipart upload (for example by
481
+ * sending CompleteMultipartUpload) once every part has been uploaded. It is
482
+ * called when the app is running and, on Android, from a headless task when
483
+ * the upload finished while the app was not running. Register it at the top
484
+ * level of your entry file so it exists when the app is started in the
485
+ * background. The upload is acknowledged when the handler resolves.
486
+ */
487
+ static setMultipartCompletionHandler(
488
+ handler: MultipartCompletionHandler | null,
489
+ ): void {
490
+ multipartCompletionHandler = handler;
491
+ }
492
+
493
+ /**
494
+ * Lists multipart uploads known to the native side, including ones that
495
+ * finished while the app was suspended or relaunched in the background.
496
+ */
497
+ static getMultipartS3Uploads(): Promise<MultipartPendingUpload[]> {
498
+ return NativeModule.getMultipartUploads();
499
+ }
500
+
501
+ /**
502
+ * Removes the stored state of a completed or failed multipart upload.
503
+ */
504
+ static acknowledgeMultipartS3(uploadId: string): Promise<boolean> {
505
+ return NativeModule.acknowledgeMultipartUpload(uploadId);
506
+ }
507
+
508
+ /**
509
+ * Add listener for multipart events
510
+ */
511
+ static addMultipartListener(
512
+ eventType: MultipartEventType,
513
+ uploadId: string,
514
+ listener: (data: any) => void,
515
+ ): EmitterSubscription {
516
+ return DeviceEventEmitter.addListener(
517
+ eventPrefix + "multipart-" + eventType,
518
+ (data: any) => {
519
+ if (!uploadId || !data || data.uploadId === uploadId) {
520
+ listener(data);
521
+ }
522
+ },
523
+ );
524
+ }
525
+
278
526
  static getAll(): Upload[] {
279
527
  const uploads: Upload[] = [];
280
528
  UploadRegistry.uploads.forEach(upload => uploads.push(upload));
@@ -512,6 +760,160 @@ class Upload {
512
760
  }
513
761
  }
514
762
 
763
+
764
+ class MultipartS3Upload {
765
+ private options: MultipartS3Options;
766
+ private subscriptions: EmitterSubscription[] = [];
767
+ private startPromise: Promise<MultipartS3Result> | null = null;
768
+ private progressCallbacks: ((event: MultipartProgressEvent) => void)[] = [];
769
+ private static activeUploads: Map<string, MultipartS3Upload> = new Map();
770
+
771
+ constructor(options: MultipartS3Options) {
772
+ this.options = {
773
+ ...options,
774
+ maxParallel: options.maxParallel ?? 3,
775
+ };
776
+ }
777
+
778
+ static create(options: MultipartS3Options): MultipartS3Upload {
779
+ const existing = MultipartS3Upload.activeUploads.get(options.uploadId);
780
+ if (existing) {
781
+ return existing;
782
+ }
783
+ const upload = new MultipartS3Upload(options);
784
+ MultipartS3Upload.activeUploads.set(options.uploadId, upload);
785
+ return upload;
786
+ }
787
+
788
+ static getById(uploadId: string): MultipartS3Upload | undefined {
789
+ return MultipartS3Upload.activeUploads.get(uploadId);
790
+ }
791
+
792
+ static async cancel(uploadId: string): Promise<boolean> {
793
+ const instance = MultipartS3Upload.activeUploads.get(uploadId);
794
+ if (instance) {
795
+ return instance.cancel();
796
+ }
797
+ return NativeModule.cancelMultipartUpload(uploadId);
798
+ }
799
+
800
+ static async pause(uploadId: string): Promise<boolean> {
801
+ const instance = MultipartS3Upload.activeUploads.get(uploadId);
802
+ if (instance) {
803
+ return instance.pause();
804
+ }
805
+ return NativeModule.pauseMultipartUpload(uploadId);
806
+ }
807
+
808
+ static async resume(uploadId: string): Promise<boolean> {
809
+ const instance = MultipartS3Upload.activeUploads.get(uploadId);
810
+ if (instance) {
811
+ return instance.resume();
812
+ }
813
+ return NativeModule.resumeMultipartUpload(uploadId);
814
+ }
815
+
816
+ onProgress(callback: (event: MultipartProgressEvent) => void): this {
817
+ if (!this.progressCallbacks.includes(callback)) {
818
+ this.progressCallbacks.push(callback);
819
+ }
820
+ return this;
821
+ }
822
+
823
+ getId(): string {
824
+ return this.options.uploadId;
825
+ }
826
+
827
+ getPath(): string {
828
+ return this.options.path;
829
+ }
830
+
831
+ async start(): Promise<MultipartS3Result> {
832
+ if (this.startPromise) {
833
+ return this.startPromise;
834
+ }
835
+
836
+ this.setupEventListeners();
837
+
838
+ this.startPromise = (async () => {
839
+ try {
840
+ const nativeResult = await NativeModule.startMultipartUpload(this.options);
841
+ return nativeResult;
842
+ } finally {
843
+ this.cleanup();
844
+ }
845
+ })();
846
+
847
+ return this.startPromise;
848
+ }
849
+
850
+ private setupEventListeners(): void {
851
+ const progressSub = DeviceEventEmitter.addListener(
852
+ eventPrefix + "multipart-progress",
853
+ (data: MultipartProgressEvent) => {
854
+ if (data.uploadId === this.options.uploadId) {
855
+ this.progressCallbacks.forEach(cb => cb(data));
856
+ }
857
+ },
858
+ );
859
+ this.subscriptions.push(progressSub);
860
+ }
861
+
862
+ async cancel(): Promise<boolean> {
863
+ try {
864
+ const res = await NativeModule.cancelMultipartUpload(this.options.uploadId);
865
+ this.cleanup();
866
+ return res;
867
+ } catch (err) {
868
+ this.cleanup();
869
+ throw err;
870
+ }
871
+ }
872
+
873
+ async pause(): Promise<boolean> {
874
+ return NativeModule.pauseMultipartUpload(this.options.uploadId);
875
+ }
876
+
877
+ async resume(): Promise<boolean> {
878
+ return NativeModule.resumeMultipartUpload(this.options.uploadId);
879
+ }
880
+
881
+ private cleanup(): void {
882
+ this.subscriptions.forEach(sub => sub.remove());
883
+ this.subscriptions = [];
884
+ MultipartS3Upload.activeUploads.delete(this.options.uploadId);
885
+ }
886
+ }
887
+
888
+ export const startMultipartS3 = (options: MultipartS3Options): Promise<MultipartS3Result> =>
889
+ Upload.startMultipartS3(options);
890
+
891
+ export const cancelMultipartS3 = (uploadId: string): Promise<boolean> =>
892
+ Upload.cancelMultipartS3(uploadId);
893
+
894
+ export const pauseMultipartS3 = (uploadId: string): Promise<boolean> =>
895
+ Upload.pauseMultipartS3(uploadId);
896
+
897
+ export const resumeMultipartS3 = (uploadId: string): Promise<boolean> =>
898
+ Upload.resumeMultipartS3(uploadId);
899
+
900
+ export const setMultipartCompletionHandler = (
901
+ handler: MultipartCompletionHandler | null,
902
+ ): void => Upload.setMultipartCompletionHandler(handler);
903
+
904
+ export const getMultipartS3Uploads = (): Promise<MultipartPendingUpload[]> =>
905
+ Upload.getMultipartS3Uploads();
906
+
907
+ export const acknowledgeMultipartS3 = (uploadId: string): Promise<boolean> =>
908
+ Upload.acknowledgeMultipartS3(uploadId);
909
+
910
+ export const addMultipartListener = (
911
+ eventType: MultipartEventType,
912
+ uploadId: string,
913
+ listener: (data: any) => void,
914
+ ): EmitterSubscription =>
915
+ Upload.addMultipartListener(eventType, uploadId, listener);
916
+
515
917
  // Legacy API exports for backward compatibility
516
918
  export const getFileInfo = (path: string): Promise<FileInfo> => {
517
919
  return NativeModule.getFileInfo(path).then((data: FileInfo) => {
@@ -562,6 +964,6 @@ export const getAllUploads = async (): Promise<NativeUploadInfo[]> => {
562
964
  return allUploads;
563
965
  };
564
966
 
565
- export { Upload };
967
+ export { Upload, MultipartS3Upload };
566
968
 
567
969
  export default Upload;