@capgo/capacitor-uploader 8.1.10 → 8.1.12

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.
@@ -63,10 +63,14 @@ public class Uploader {
63
63
  request.addFileToUpload(filePath, fileField, getFileNameFromUri(Uri.parse(filePath)), mimeType);
64
64
 
65
65
  for (Map.Entry<String, String> entry : headers.entrySet()) {
66
- request.addHeader(entry.getKey(), entry.getValue());
66
+ if (entry.getKey() != null && entry.getValue() != null) {
67
+ request.addHeader(entry.getKey(), entry.getValue());
68
+ }
67
69
  }
68
70
  for (Map.Entry<String, String> entry : parameters.entrySet()) {
69
- request.addParameter(entry.getKey(), entry.getValue());
71
+ if (entry.getKey() != null && entry.getValue() != null) {
72
+ request.addParameter(entry.getKey(), entry.getValue());
73
+ }
70
74
  }
71
75
 
72
76
  return request.startUpload();
@@ -91,14 +95,20 @@ public class Uploader {
91
95
  .setNotificationConfig((ctx, uploadId) -> notificationConfig)
92
96
  .setMaxRetries(maxRetries);
93
97
 
94
- request.addHeader("Content-Type", mimeType);
98
+ if (mimeType != null && !mimeType.isEmpty()) {
99
+ request.addHeader("Content-Type", mimeType);
100
+ }
95
101
 
96
102
  for (Map.Entry<String, String> entry : headers.entrySet()) {
97
- request.addHeader(entry.getKey(), entry.getValue());
103
+ if (entry.getKey() != null && entry.getValue() != null) {
104
+ request.addHeader(entry.getKey(), entry.getValue());
105
+ }
98
106
  }
99
107
 
100
108
  for (Map.Entry<String, String> entry : parameters.entrySet()) {
101
- request.addParameter(entry.getKey(), entry.getValue());
109
+ if (entry.getKey() != null && entry.getValue() != null) {
110
+ request.addParameter(entry.getKey(), entry.getValue());
111
+ }
102
112
  }
103
113
 
104
114
  return request.startUpload();
@@ -132,7 +142,7 @@ public class Uploader {
132
142
 
133
143
  private String getFileNameFromUri(Uri uri) {
134
144
  String result = null;
135
- if (uri.getScheme().equals("content")) {
145
+ if ("content".equals(uri.getScheme())) {
136
146
  try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) {
137
147
  if (cursor != null && cursor.moveToFirst()) {
138
148
  int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
@@ -145,10 +155,10 @@ public class Uploader {
145
155
  }
146
156
  }
147
157
  if (result == null) {
148
- result = uri.getPath();
149
- int cut = result.lastIndexOf('/');
150
- if (cut != -1) {
151
- result = result.substring(cut + 1);
158
+ String path = uri.getPath();
159
+ if (path != null) {
160
+ int cut = path.lastIndexOf('/');
161
+ result = cut != -1 ? path.substring(cut + 1) : path;
152
162
  }
153
163
  }
154
164
  return result;
@@ -3,6 +3,7 @@ package ee.forgr.capacitor.uploader;
3
3
  import android.app.NotificationChannel;
4
4
  import android.app.NotificationManager;
5
5
  import android.content.Context;
6
+ import android.net.Uri;
6
7
  import android.os.Build;
7
8
  import android.webkit.MimeTypeMap;
8
9
  import com.getcapacitor.JSObject;
@@ -21,7 +22,11 @@ import net.gotev.uploadservice.observer.request.RequestObserverDelegate;
21
22
  @CapacitorPlugin(name = "Uploader")
22
23
  public class UploaderPlugin extends Plugin {
23
24
 
24
- private final String pluginVersion = "8.1.10";
25
+ private static final String CAPACITOR_FILE_PATH_PREFIX = "/_capacitor_file_";
26
+
27
+ private static final String CAPACITOR_CONTENT_PATH_PREFIX = "/_capacitor_content_";
28
+
29
+ private final String pluginVersion = "8.1.12";
25
30
 
26
31
  private Uploader implementation;
27
32
 
@@ -123,6 +128,10 @@ public class UploaderPlugin extends Plugin {
123
128
  return;
124
129
  }
125
130
 
131
+ // Convert Capacitor web-accessible URLs to paths native code can open.
132
+ // Capacitor 8+ removed Bridge.getLocalUrl(String); mirror AndroidProtocolHandler logic.
133
+ String localFilePath = resolveCapacitorPath(filePath);
134
+
126
135
  JSObject headersObj = call.getObject("headers", new JSObject());
127
136
  JSObject parametersObj = call.getObject("parameters", new JSObject());
128
137
  String httpMethod = call.getString("method", "POST");
@@ -135,10 +144,10 @@ public class UploaderPlugin extends Plugin {
135
144
  Map<String, String> parameters = JSObjectToMap(parametersObj);
136
145
 
137
146
  try {
138
- String mimeType = call.getString("mimeType", getMimeType(filePath));
147
+ String mimeType = call.getString("mimeType", getMimeType(localFilePath));
139
148
 
140
149
  String id = implementation.startUpload(
141
- filePath,
150
+ localFilePath,
142
151
  serverUrl,
143
152
  headers,
144
153
  parameters,
@@ -172,12 +181,50 @@ public class UploaderPlugin extends Plugin {
172
181
  }
173
182
  }
174
183
 
184
+ /**
185
+ * Maps WebView URLs (e.g. http(s)://localhost/_capacitor_file_/...) to filesystem or content
186
+ * paths, matching {@link com.getcapacitor.AndroidProtocolHandler}. Plain absolute paths and
187
+ * unrecognized URLs are returned unchanged.
188
+ */
189
+ private static String resolveCapacitorPath(String filePath) {
190
+ if (filePath == null || filePath.isEmpty()) {
191
+ return filePath;
192
+ }
193
+ Uri uri = Uri.parse(filePath);
194
+ String path = uri.getPath();
195
+ if (path != null) {
196
+ if (path.startsWith(CAPACITOR_FILE_PATH_PREFIX)) {
197
+ return path.substring(CAPACITOR_FILE_PATH_PREFIX.length());
198
+ }
199
+ if (path.startsWith(CAPACITOR_CONTENT_PATH_PREFIX)) {
200
+ String scheme = uri.getScheme();
201
+ String host = uri.getHost();
202
+ if (scheme != null && host != null) {
203
+ String baseUrl = scheme + "://" + host;
204
+ if (uri.getPort() != -1) {
205
+ baseUrl += ":" + uri.getPort();
206
+ }
207
+ return filePath.replace(baseUrl + CAPACITOR_CONTENT_PATH_PREFIX, "content:/");
208
+ }
209
+ return filePath.replace(CAPACITOR_CONTENT_PATH_PREFIX, "content:/");
210
+ }
211
+ }
212
+ if ("file".equalsIgnoreCase(uri.getScheme()) && uri.getPath() != null) {
213
+ return uri.getPath();
214
+ }
215
+ return filePath;
216
+ }
217
+
175
218
  private Map<String, String> JSObjectToMap(JSObject object) {
176
219
  Map<String, String> map = new HashMap<>();
177
220
  if (object != null) {
178
221
  for (Iterator<String> it = object.keys(); it.hasNext(); ) {
179
222
  String key = it.next();
180
- map.put(key, object.getString(key));
223
+ String value = object.getString(key);
224
+ // Only add non-null and non-empty values to prevent upload service errors
225
+ if (value != null && !value.isEmpty()) {
226
+ map.put(key, value);
227
+ }
181
228
  }
182
229
  }
183
230
  return map;
@@ -8,6 +8,15 @@ import MobileCoreServices
8
8
  private var responsesData: [Int: Data] = [:]
9
9
  private var tasks: [String: URLSessionTask] = [:]
10
10
  private var retries: [String: Int] = [:]
11
+ private var tempBodyFiles: [String: URL] = [:]
12
+
13
+ private struct UploadConfig {
14
+ let filePath: String
15
+ let serverUrl: String
16
+ let options: [String: Any]
17
+ }
18
+
19
+ private var uploadConfigs: [String: UploadConfig] = [:]
11
20
 
12
21
  var eventHandler: (([String: Any]) -> Void)?
13
22
 
@@ -15,22 +24,46 @@ import MobileCoreServices
15
24
  let id = UUID().uuidString
16
25
  print("startUpload: \(id)")
17
26
 
18
- guard let url = URL(string: serverUrl) else {
27
+ let config = UploadConfig(filePath: filePath, serverUrl: serverUrl, options: options)
28
+ do {
29
+ let task = try createUploadTask(id: id, config: config)
30
+ uploadConfigs[id] = config
31
+ tasks[id] = task
32
+ retries[id] = maxRetries
33
+ task.resume()
34
+ return id
35
+ } catch {
36
+ if let tempUrl = tempBodyFiles.removeValue(forKey: id) {
37
+ try? FileManager.default.removeItem(at: tempUrl)
38
+ }
39
+ throw error
40
+ }
41
+ }
42
+
43
+ private func createUploadTask(id: String, config: UploadConfig) throws -> URLSessionTask {
44
+ guard let url = URL(string: config.serverUrl) else {
19
45
  throw NSError(domain: "UploaderPlugin", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
20
46
  }
21
47
 
22
48
  var request = URLRequest(url: url)
23
- request.httpMethod = (options["method"] as? String)?.uppercased() ?? "POST"
49
+ request.httpMethod = (config.options["method"] as? String)?.uppercased() ?? "POST"
24
50
 
25
- let headers = options["headers"] as? [String: String] ?? [:]
51
+ let headers = config.options["headers"] as? [String: String] ?? [:]
26
52
  for (key, value) in headers {
27
53
  request.setValue(value, forHTTPHeaderField: key)
28
54
  }
29
55
 
30
- guard let fileUrl = URL(string: filePath) else {
56
+ let filePath = config.filePath
57
+ let fileUrl: URL
58
+ if let candidateUrl = URL(string: filePath), candidateUrl.isFileURL {
59
+ fileUrl = candidateUrl
60
+ } else {
61
+ fileUrl = URL(fileURLWithPath: filePath)
62
+ }
63
+ guard fileUrl.isFileURL else {
31
64
  throw NSError(domain: "UploaderPlugin", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid file URL"])
32
65
  }
33
- let mimeType = options["mimeType"] as? String ?? guessMIMEType(from: filePath)
66
+ let mimeType = config.options["mimeType"] as? String ?? guessMIMEType(from: filePath)
34
67
 
35
68
  let task: URLSessionTask
36
69
  if request.httpMethod == "PUT" {
@@ -42,19 +75,21 @@ import MobileCoreServices
42
75
  let boundary = UUID().uuidString
43
76
  request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
44
77
 
45
- let parameters = options["parameters"] as? [String: String] ?? [:]
78
+ let parameters = config.options["parameters"] as? [String: String] ?? [:]
46
79
 
47
- let dataBody = createDataBody(withParameters: parameters, filePath: filePath, mimeType: mimeType, boundary: boundary)
80
+ if let oldTemp = tempBodyFiles[id] {
81
+ try? FileManager.default.removeItem(at: oldTemp)
82
+ }
83
+ let tempDir = FileManager.default.temporaryDirectory
84
+ let tempFile = tempDir.appendingPathComponent("upload-\(id).tmp")
85
+ try writeMultipartBodyToFile(at: tempFile, parameters: parameters, fileUrl: fileUrl, mimeType: mimeType, boundary: boundary)
86
+ tempBodyFiles[id] = tempFile
48
87
 
49
- task = self.getUrlSession().uploadTask(with: request, from: dataBody)
88
+ task = self.getUrlSession().uploadTask(with: request, fromFile: tempFile)
50
89
  }
51
90
 
52
91
  task.taskDescription = id
53
- tasks[id] = task
54
- retries[id] = maxRetries
55
- task.resume()
56
-
57
- return id
92
+ return task
58
93
  }
59
94
 
60
95
  @objc public func removeUpload(_ id: String) async throws {
@@ -62,6 +97,11 @@ import MobileCoreServices
62
97
  if let task = tasks[id] {
63
98
  task.cancel()
64
99
  tasks.removeValue(forKey: id)
100
+ retries.removeValue(forKey: id)
101
+ uploadConfigs.removeValue(forKey: id)
102
+ if let tempUrl = tempBodyFiles.removeValue(forKey: id) {
103
+ try? FileManager.default.removeItem(at: tempUrl)
104
+ }
65
105
  }
66
106
  }
67
107
 
@@ -89,21 +129,33 @@ import MobileCoreServices
89
129
  }
90
130
 
91
131
  if let error = error {
92
- if let retriesLeft = retries[id], retriesLeft > 0 {
93
- retries[id] = retriesLeft - 1
94
- print("Retrying upload (retries left: \(retriesLeft - 1))")
95
- task.resume()
96
- return
132
+ let isCancelled = (error as NSError).code == NSURLErrorCancelled
133
+ if !isCancelled, let retriesLeft = retries[id], retriesLeft > 0, let config = uploadConfigs[id] {
134
+ let newRetries = retriesLeft - 1
135
+ retries[id] = newRetries
136
+ print("Retrying upload (retries left: \(newRetries))")
137
+ do {
138
+ let newTask = try createUploadTask(id: id, config: config)
139
+ tasks[id] = newTask
140
+ newTask.resume()
141
+ return
142
+ } catch {
143
+ payload["error"] = error.localizedDescription
144
+ }
145
+ } else {
146
+ payload["error"] = error.localizedDescription
97
147
  }
98
-
99
- payload["error"] = error.localizedDescription
100
148
  sendEvent(name: "failed", id: id, payload: payload)
101
149
  } else {
102
150
  sendEvent(name: "completed", id: id, payload: payload)
103
151
  }
104
152
 
153
+ if let tempUrl = tempBodyFiles.removeValue(forKey: id) {
154
+ try? FileManager.default.removeItem(at: tempUrl)
155
+ }
105
156
  tasks.removeValue(forKey: id)
106
157
  retries.removeValue(forKey: id)
158
+ uploadConfigs.removeValue(forKey: id)
107
159
  }
108
160
 
109
161
  public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
@@ -126,25 +178,36 @@ import MobileCoreServices
126
178
  eventHandler?(event)
127
179
  }
128
180
 
129
- private func createDataBody(withParameters params: [String: String], filePath: String, mimeType: String, boundary: String) -> Data {
130
- let data = NSMutableData()
181
+ // Writes multipart/form-data body directly to a file, streaming the file content in chunks
182
+ private func writeMultipartBodyToFile(at tempFile: URL, parameters: [String: String], fileUrl: URL, mimeType: String, boundary: String) throws {
183
+ FileManager.default.createFile(atPath: tempFile.path, contents: nil)
184
+ let writeHandle = try FileHandle(forWritingTo: tempFile)
185
+ defer { try? writeHandle.close() }
131
186
 
132
- for (key, value) in params {
133
- data.append("--\(boundary)\r\n".data(using: .utf8)!)
134
- data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
135
- data.append("\(value)\r\n".data(using: .utf8)!)
187
+ func write(_ string: String) throws {
188
+ try writeHandle.write(contentsOf: string.data(using: .utf8)!)
136
189
  }
137
190
 
138
- data.append("--\(boundary)\r\n".data(using: .utf8)!)
139
- data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(URL(fileURLWithPath: filePath).lastPathComponent)\"\r\n".data(using: .utf8)!)
140
- data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
141
- data.append(try! Data(contentsOf: URL(fileURLWithPath: filePath)))
142
- data.append("\r\n".data(using: .utf8)!)
143
- data.append("--\(boundary)--".data(using: .utf8)!)
191
+ for (key, value) in parameters {
192
+ try write("--\(boundary)\r\n")
193
+ try write("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n")
194
+ try write("\(value)\r\n")
195
+ }
144
196
 
145
- return data as Data
146
- }
197
+ try write("--\(boundary)\r\n")
198
+ try write("Content-Disposition: form-data; name=\"file\"; filename=\"\(fileUrl.lastPathComponent)\"\r\n")
199
+ try write("Content-Type: \(mimeType)\r\n\r\n")
200
+
201
+ let readHandle = try FileHandle(forReadingFrom: fileUrl)
202
+ defer { try? readHandle.close() }
203
+ let chunkSize = 64 * 1024
204
+ while true {
205
+ guard let chunk = try readHandle.read(upToCount: chunkSize), !chunk.isEmpty else { break }
206
+ try writeHandle.write(contentsOf: chunk)
207
+ }
147
208
 
209
+ try write("\r\n--\(boundary)--")
210
+ }
148
211
  }
149
212
 
150
213
  extension Uploader: URLSessionDataDelegate {
@@ -3,7 +3,7 @@ import Capacitor
3
3
 
4
4
  @objc(UploaderPlugin)
5
5
  public class UploaderPlugin: CAPPlugin, CAPBridgedPlugin {
6
- private let pluginVersion: String = "8.1.10"
6
+ private let pluginVersion: String = "8.1.12"
7
7
  public let identifier = "UploaderPlugin"
8
8
  public let jsName = "Uploader"
9
9
  public let pluginMethods: [CAPPluginMethod] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-uploader",
3
- "version": "8.1.10",
3
+ "version": "8.1.12",
4
4
  "description": "Upload file natively",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",