@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.
- package/android/src/main/java/ee/forgr/capacitor/uploader/Uploader.java +20 -10
- package/android/src/main/java/ee/forgr/capacitor/uploader/UploaderPlugin.java +51 -4
- package/ios/Sources/UploaderPlugin/Uploader.swift +97 -34
- package/ios/Sources/UploaderPlugin/UploaderPlugin.swift +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
result =
|
|
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
|
|
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(
|
|
147
|
+
String mimeType = call.getString("mimeType", getMimeType(localFilePath));
|
|
139
148
|
|
|
140
149
|
String id = implementation.startUpload(
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
88
|
+
task = self.getUrlSession().uploadTask(with: request, fromFile: tempFile)
|
|
50
89
|
}
|
|
51
90
|
|
|
52
91
|
task.taskDescription = id
|
|
53
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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.
|
|
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] = [
|