@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.
@@ -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
+ }