@capgo/capacitor-uploader 0.0.1
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/CapgoCapacitorUploader.podspec +17 -0
- package/Package.swift +28 -0
- package/README.md +228 -0
- package/android/build.gradle +59 -0
- package/android/src/main/AndroidManifest.xml +33 -0
- package/android/src/main/java/ee/forgr/capacitor/uploader/Uploader.java +123 -0
- package/android/src/main/java/ee/forgr/capacitor/uploader/UploaderPlugin.java +116 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +310 -0
- package/dist/esm/definitions.d.ts +92 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +15 -0
- package/dist/esm/web.js +18 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +34 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +37 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/UploaderPlugin/Uploader.swift +162 -0
- package/ios/Sources/UploaderPlugin/UploaderPlugin.swift +55 -0
- package/ios/Tests/UploaderPluginTests/UploaderPluginTests.swift +15 -0
- package/package.json +81 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
import UniformTypeIdentifiers
|
|
4
|
+
import MobileCoreServices
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@objc public class Uploader: NSObject, URLSessionTaskDelegate {
|
|
8
|
+
private var urlSession: URLSession?
|
|
9
|
+
private var responsesData: [Int: Data] = [:]
|
|
10
|
+
private var tasks: [String: URLSessionTask] = [:]
|
|
11
|
+
private var retries: [String: Int] = [:]
|
|
12
|
+
|
|
13
|
+
var eventHandler: (([String: Any]) -> Void)?
|
|
14
|
+
|
|
15
|
+
@objc public func startUpload(_ filePath: String, _ serverUrl: String, _ options: [String: Any], maxRetries: Int = 3) async throws -> String {
|
|
16
|
+
let id = UUID().uuidString
|
|
17
|
+
print("startUpload: \(id)")
|
|
18
|
+
|
|
19
|
+
guard let url = URL(string: serverUrl) else {
|
|
20
|
+
throw NSError(domain: "UploaderPlugin", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var request = URLRequest(url: url)
|
|
24
|
+
request.httpMethod = (options["method"] as? String)?.uppercased() ?? "POST"
|
|
25
|
+
|
|
26
|
+
let headers = options["headers"] as? [String: String] ?? [:]
|
|
27
|
+
for (key, value) in headers {
|
|
28
|
+
request.setValue(value, forHTTPHeaderField: key)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let fileUrl = URL(fileURLWithPath: filePath)
|
|
32
|
+
let mimeType = options["mimeType"] as? String ?? guessMIMEType(from: filePath)
|
|
33
|
+
|
|
34
|
+
let task: URLSessionTask
|
|
35
|
+
if request.httpMethod == "PUT" {
|
|
36
|
+
// For S3 presigned URL uploads
|
|
37
|
+
request.setValue(mimeType, forHTTPHeaderField: "Content-Type")
|
|
38
|
+
task = self.getUrlSession().uploadTask(with: request, fromFile: fileUrl)
|
|
39
|
+
} else {
|
|
40
|
+
// For POST uploads
|
|
41
|
+
let boundary = UUID().uuidString
|
|
42
|
+
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
|
43
|
+
|
|
44
|
+
let parameters = options["parameters"] as? [String: String] ?? [:]
|
|
45
|
+
|
|
46
|
+
let dataBody = createDataBody(withParameters: parameters, filePath: filePath, mimeType: mimeType, boundary: boundary)
|
|
47
|
+
|
|
48
|
+
task = self.getUrlSession().uploadTask(with: request, from: dataBody)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
task.taskDescription = id
|
|
52
|
+
tasks[id] = task
|
|
53
|
+
retries[id] = maxRetries
|
|
54
|
+
task.resume()
|
|
55
|
+
|
|
56
|
+
return id
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@objc public func removeUpload(_ id: String) async throws {
|
|
60
|
+
print("removeUpload: \(id)")
|
|
61
|
+
if let task = tasks[id] {
|
|
62
|
+
task.cancel()
|
|
63
|
+
tasks.removeValue(forKey: id)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private func getUrlSession() -> URLSession {
|
|
68
|
+
if urlSession == nil {
|
|
69
|
+
let config = URLSessionConfiguration.background(withIdentifier: "CapacitorUploaderBackgroundSession")
|
|
70
|
+
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
|
71
|
+
}
|
|
72
|
+
return urlSession!
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func guessMIMEType(from filePath: String) -> String {
|
|
76
|
+
let url = URL(fileURLWithPath: filePath)
|
|
77
|
+
if #available(iOS 14.0, *) {
|
|
78
|
+
if let mimeType = UTType(filenameExtension: url.pathExtension)?.preferredMIMEType {
|
|
79
|
+
return mimeType
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, url.pathExtension as CFString, nil)?.takeRetainedValue(),
|
|
83
|
+
let mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() as String? {
|
|
84
|
+
return mimeType
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return "application/octet-stream"
|
|
88
|
+
}
|
|
89
|
+
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
90
|
+
guard let id = task.taskDescription else { return }
|
|
91
|
+
|
|
92
|
+
var payload: [String: Any] = [:]
|
|
93
|
+
if let response = task.response as? HTTPURLResponse {
|
|
94
|
+
payload["statusCode"] = response.statusCode
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if let error = error {
|
|
98
|
+
if let retriesLeft = retries[id], retriesLeft > 0 {
|
|
99
|
+
retries[id] = retriesLeft - 1
|
|
100
|
+
print("Retrying upload (retries left: \(retriesLeft - 1))")
|
|
101
|
+
task.resume()
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
payload["error"] = error.localizedDescription
|
|
106
|
+
sendEvent(name: "failed", id: id, payload: payload)
|
|
107
|
+
} else {
|
|
108
|
+
sendEvent(name: "completed", id: id, payload: payload)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
tasks.removeValue(forKey: id)
|
|
112
|
+
retries.removeValue(forKey: id)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
|
116
|
+
guard let id = task.taskDescription else { return }
|
|
117
|
+
|
|
118
|
+
var percent: Float = -1
|
|
119
|
+
if totalBytesExpectedToSend > 0 {
|
|
120
|
+
percent = Float(totalBytesSent) / Float(totalBytesExpectedToSend) * 100
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
sendEvent(name: "uploading", id: id, payload: ["percent": percent])
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func sendEvent(name: String, id: String, payload: [String: Any]) {
|
|
127
|
+
var event: [String: Any] = [
|
|
128
|
+
"name": name,
|
|
129
|
+
"id": id,
|
|
130
|
+
"payload": payload
|
|
131
|
+
]
|
|
132
|
+
eventHandler?(event)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private func createDataBody(withParameters params: [String: String], filePath: String, mimeType: String, boundary: String) -> Data {
|
|
136
|
+
let data = NSMutableData()
|
|
137
|
+
|
|
138
|
+
for (key, value) in params {
|
|
139
|
+
data.append("--\(boundary)\r\n".data(using: .utf8)!)
|
|
140
|
+
data.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
|
|
141
|
+
data.append("\(value)\r\n".data(using: .utf8)!)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
data.append("--\(boundary)\r\n".data(using: .utf8)!)
|
|
145
|
+
data.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(URL(fileURLWithPath: filePath).lastPathComponent)\"\r\n".data(using: .utf8)!)
|
|
146
|
+
data.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!)
|
|
147
|
+
data.append(try! Data(contentsOf: URL(fileURLWithPath: filePath)))
|
|
148
|
+
data.append("\r\n".data(using: .utf8)!)
|
|
149
|
+
data.append("--\(boundary)--".data(using: .utf8)!)
|
|
150
|
+
|
|
151
|
+
return data as Data
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
extension Uploader: URLSessionDataDelegate {
|
|
157
|
+
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
|
158
|
+
var responseData = responsesData[dataTask.taskIdentifier] ?? Data()
|
|
159
|
+
responseData.append(data)
|
|
160
|
+
responsesData[dataTask.taskIdentifier] = responseData
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc(UploaderPlugin)
|
|
5
|
+
public class UploaderPlugin: CAPPlugin {
|
|
6
|
+
private let implementation = Uploader()
|
|
7
|
+
|
|
8
|
+
override public func load() {
|
|
9
|
+
implementation.eventHandler = { [weak self] event in
|
|
10
|
+
self?.notifyListeners("events", data: event)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@objc func startUpload(_ call: CAPPluginCall) {
|
|
15
|
+
guard let filePath = call.getString("filePath"),
|
|
16
|
+
let serverUrl = call.getString("serverUrl") else {
|
|
17
|
+
call.reject("Missing required parameters")
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let options: [String: Any] = [
|
|
22
|
+
"headers": call.getObject("headers") as Any,
|
|
23
|
+
"method": call.getString("method") as Any,
|
|
24
|
+
"mimeType": call.getString("mimeType") as Any,
|
|
25
|
+
"parameters": call.getObject("parameters") as Any
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
let maxRetries = call.getInt("maxRetries") ?? 3
|
|
29
|
+
|
|
30
|
+
Task {
|
|
31
|
+
do {
|
|
32
|
+
let id = try await implementation.startUpload(filePath, serverUrl, options, maxRetries: maxRetries)
|
|
33
|
+
call.resolve(["id": id])
|
|
34
|
+
} catch {
|
|
35
|
+
call.reject("Failed to start upload: \(error.localizedDescription)")
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@objc func removeUpload(_ call: CAPPluginCall) {
|
|
41
|
+
guard let id = call.getString("id") else {
|
|
42
|
+
call.reject("Missing required parameter: id")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Task {
|
|
47
|
+
do {
|
|
48
|
+
try await implementation.removeUpload(id)
|
|
49
|
+
call.resolve()
|
|
50
|
+
} catch {
|
|
51
|
+
call.reject("Failed to remove upload: \(error.localizedDescription)")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import UploaderPlugin
|
|
3
|
+
|
|
4
|
+
class UploaderTests: XCTestCase {
|
|
5
|
+
func testEcho() {
|
|
6
|
+
// This is an example of a functional test case for a plugin.
|
|
7
|
+
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
8
|
+
|
|
9
|
+
let implementation = Uploader()
|
|
10
|
+
let value = "Hello, World!"
|
|
11
|
+
let result = implementation.echo(value)
|
|
12
|
+
|
|
13
|
+
XCTAssertEqual(value, result)
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capgo/capacitor-uploader",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Upload file natively",
|
|
5
|
+
"main": "dist/plugin.cjs.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/esm/index.d.ts",
|
|
8
|
+
"unpkg": "dist/plugin.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"android/src/main/",
|
|
11
|
+
"android/build.gradle",
|
|
12
|
+
"dist/",
|
|
13
|
+
"ios/Sources",
|
|
14
|
+
"ios/Tests",
|
|
15
|
+
"Package.swift",
|
|
16
|
+
"CapgoCapacitorUploader.podspec"
|
|
17
|
+
],
|
|
18
|
+
"author": "Martin Donadieu <martin@capgo.app>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Cap-go/capacitor-uploader.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Cap-go/capacitor-uploader/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"capacitor",
|
|
29
|
+
"plugin",
|
|
30
|
+
"native"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
34
|
+
"verify:ios": "xcodebuild -scheme CapgoCapacitorUploader -destination generic/platform=iOS",
|
|
35
|
+
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
36
|
+
"verify:web": "npm run build",
|
|
37
|
+
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
38
|
+
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
39
|
+
"eslint": "eslint . --ext ts",
|
|
40
|
+
"prettier": "prettier \"**/*.{css,html,ts,js,java}\"",
|
|
41
|
+
"swiftlint": "node-swiftlint",
|
|
42
|
+
"docgen": "docgen --api UploaderPlugin --output-readme README.md --output-json dist/docs.json",
|
|
43
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.js",
|
|
44
|
+
"clean": "rimraf ./dist",
|
|
45
|
+
"watch": "tsc --watch",
|
|
46
|
+
"prepublishOnly": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@capacitor/android": "^6.0.0",
|
|
50
|
+
"@capacitor/core": "^6.0.0",
|
|
51
|
+
"@capacitor/docgen": "^0.2.2",
|
|
52
|
+
"@capacitor/ios": "^6.0.0",
|
|
53
|
+
"@ionic/eslint-config": "^0.4.0",
|
|
54
|
+
"@ionic/prettier-config": "^1.0.1",
|
|
55
|
+
"@ionic/swiftlint-config": "^1.1.2",
|
|
56
|
+
"eslint": "^8.57.0",
|
|
57
|
+
"prettier": "~2.3.0",
|
|
58
|
+
"prettier-plugin-java": "~1.0.2",
|
|
59
|
+
"rimraf": "^3.0.2",
|
|
60
|
+
"rollup": "^2.32.0",
|
|
61
|
+
"swiftlint": "^1.0.1",
|
|
62
|
+
"typescript": "~4.1.5"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@capacitor/core": "^6.0.0"
|
|
66
|
+
},
|
|
67
|
+
"prettier": "@ionic/prettier-config",
|
|
68
|
+
"swiftlint": "@ionic/swiftlint-config",
|
|
69
|
+
"eslintConfig": {
|
|
70
|
+
"extends": "@ionic/eslint-config/recommended"
|
|
71
|
+
},
|
|
72
|
+
"capacitor": {
|
|
73
|
+
"ios": {
|
|
74
|
+
"src": "ios"
|
|
75
|
+
},
|
|
76
|
+
"android": {
|
|
77
|
+
"src": "android"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"packageManager": "pnpm@9.9.0+sha512.60c18acd138bff695d339be6ad13f7e936eea6745660d4cc4a776d5247c540d0edee1a563695c183a66eb917ef88f2b4feb1fc25f32a7adcadc7aaf3438e99c1"
|
|
81
|
+
}
|