Line data Source code
1 : import 'package:flutter/foundation.dart';
2 : import '../worker.dart';
3 :
4 : /// Parallel multi-file HTTP upload worker configuration.
5 : ///
6 : /// Uploads each file in [files] as a **separate** concurrent multipart request
7 : /// (one request per file) with a per-host concurrency limit. This differs
8 : /// from [HttpUploadWorker], which bundles all files into a single request.
9 : ///
10 : /// **When to use over [HttpUploadWorker]:**
11 : /// - You need to upload many files independently (each gets its own response).
12 : /// - You want individual retry-per-file semantics (failed files retry without
13 : /// re-sending already-succeeded files).
14 : /// - The server accepts one file per request (most REST APIs).
15 : ///
16 : /// **Per-host concurrency:**
17 : /// [maxConcurrent] caps how many simultaneous uploads run against the same
18 : /// host at once, preventing connection-pool exhaustion and rate-limiting.
19 : ///
20 : /// ## Example
21 : ///
22 : /// ```dart
23 : /// await NativeWorkManager.enqueue(
24 : /// taskId: 'batch-photos',
25 : /// trigger: TaskTrigger.oneTime(),
26 : /// worker: NativeWorker.parallelHttpUpload(
27 : /// url: 'https://api.example.com/photos',
28 : /// files: [
29 : /// UploadFile(filePath: '/data/user/0/.../img1.jpg'),
30 : /// UploadFile(filePath: '/data/user/0/.../img2.jpg', fieldName: 'photo'),
31 : /// ],
32 : /// maxConcurrent: 3,
33 : /// maxRetries: 2,
34 : /// ),
35 : /// constraints: Constraints.networkRequired,
36 : /// );
37 : /// ```
38 : ///
39 : /// ## Progress events
40 : ///
41 : /// One [TaskProgress] event is emitted per uploaded chunk across all files, so
42 : /// [TaskProgress.progress] rises smoothly from 0 to 100. The message field
43 : /// includes per-file counts (`"Uploaded 2/5 files"`).
44 : ///
45 : /// ## Result data
46 : ///
47 : /// The success result (`TaskEvent.resultData`) map contains:
48 : /// - `uploadedCount` — number of files successfully uploaded.
49 : /// - `failedCount` — number of files that ultimately failed.
50 : /// - `totalBytes` — aggregate bytes sent across all files.
51 : /// - `fileResults` — list of per-file result maps.
52 : @immutable
53 : final class ParallelHttpUploadWorker extends Worker {
54 : // NET-028: use RangeError / ArgumentError instead of assert() so validation
55 : // fires in release builds too (assert is stripped when asserts are disabled).
56 2 : ParallelHttpUploadWorker({
57 : required this.url,
58 : required this.files,
59 : this.headers = const {},
60 : this.fields = const {},
61 : this.maxConcurrent = 3,
62 : this.maxRetries = 1,
63 : this.timeout = const Duration(minutes: 5),
64 : this.showNotification = false,
65 : this.notificationTitle,
66 : this.notificationBody,
67 : }) {
68 4 : if (files.isEmpty) {
69 4 : throw ArgumentError.value(files, 'files', 'must not be empty');
70 : }
71 8 : if (maxConcurrent < 1 || maxConcurrent > 16) {
72 4 : throw RangeError.range(maxConcurrent, 1, 16, 'maxConcurrent');
73 : }
74 8 : if (maxRetries < 0 || maxRetries > 5) {
75 4 : throw RangeError.range(maxRetries, 0, 5, 'maxRetries');
76 : }
77 : }
78 :
79 : /// The endpoint URL that receives each file upload.
80 : final String url;
81 :
82 : /// List of files to upload.
83 : final List<UploadFile> files;
84 :
85 : /// HTTP headers added to every upload request.
86 : ///
87 : /// Typical use: `{'Authorization': 'Bearer token'}`.
88 : final Map<String, String> headers;
89 :
90 : /// Additional form fields added to every multipart request alongside the
91 : /// file part (e.g. `{'albumId': '42'}`).
92 : final Map<String, String> fields;
93 :
94 : /// Maximum simultaneous uploads per host (1–16, default 3).
95 : ///
96 : /// Uploads beyond this limit are queued until a slot opens.
97 : final int maxConcurrent;
98 :
99 : /// How many times to retry a failed individual file upload (0–5, default 1).
100 : ///
101 : /// A retry is attempted only when the server returns a 5xx response or when
102 : /// a network error occurs. 4xx responses (e.g. 400, 401) are not retried.
103 : final int maxRetries;
104 :
105 : /// Per-file request timeout (default: 5 minutes).
106 : final Duration timeout;
107 :
108 : /// Show a system notification with aggregate upload progress.
109 : final bool showNotification;
110 :
111 : /// Title for the progress notification.
112 : ///
113 : /// Defaults to `"Uploading N files"` derived from [files].
114 : final String? notificationTitle;
115 :
116 : /// Body text for the progress notification.
117 : final String? notificationBody;
118 :
119 1 : @override
120 : String get workerClassName => 'ParallelHttpUploadWorker';
121 :
122 2 : @override
123 2 : Map<String, dynamic> toMap() => {
124 2 : 'workerType': 'parallelHttpUpload',
125 4 : 'url': url,
126 4 : 'files': files
127 6 : .map((f) => {
128 4 : 'filePath': f.filePath,
129 4 : 'fieldName': f.fieldName,
130 4 : if (f.fileName != null) 'fileName': f.fileName,
131 4 : if (f.mimeType != null) 'mimeType': f.mimeType,
132 : })
133 2 : .toList(),
134 4 : 'headers': headers,
135 4 : 'fields': fields,
136 4 : 'maxConcurrent': maxConcurrent,
137 4 : 'maxRetries': maxRetries,
138 6 : 'timeoutMs': timeout.inMilliseconds,
139 4 : 'showNotification': showNotification,
140 4 : if (notificationTitle != null) 'notificationTitle': notificationTitle,
141 4 : if (notificationBody != null) 'notificationBody': notificationBody,
142 : };
143 : }
144 :
145 : // UploadFile is defined in multi_upload_worker.dart and re-exported via worker.dart.
|