uploados 0.1.0

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.
Files changed (66) hide show
  1. package/android/build.gradle +25 -0
  2. package/android/src/main/AndroidManifest.xml +15 -0
  3. package/android/src/main/java/expo/modules/uploados/UploadosModule.kt +121 -0
  4. package/android/src/main/java/expo/modules/uploados/upload/CompressionPipeline.kt +192 -0
  5. package/android/src/main/java/expo/modules/uploados/upload/FileStager.kt +125 -0
  6. package/android/src/main/java/expo/modules/uploados/upload/ProgressRequestBody.kt +36 -0
  7. package/android/src/main/java/expo/modules/uploados/upload/UploadManager.kt +857 -0
  8. package/android/src/main/java/expo/modules/uploados/upload/UploadModels.kt +209 -0
  9. package/android/src/main/java/expo/modules/uploados/upload/UploadNotificationHelper.kt +93 -0
  10. package/android/src/main/java/expo/modules/uploados/upload/UploadTaskStore.kt +224 -0
  11. package/android/src/main/java/expo/modules/uploados/upload/UploadWorker.kt +31 -0
  12. package/build/Uploados.types.d.ts +226 -0
  13. package/build/Uploados.types.d.ts.map +1 -0
  14. package/build/Uploados.types.js +2 -0
  15. package/build/Uploados.types.js.map +1 -0
  16. package/build/UploadosModule.d.ts +13 -0
  17. package/build/UploadosModule.d.ts.map +1 -0
  18. package/build/UploadosModule.js +3 -0
  19. package/build/UploadosModule.js.map +1 -0
  20. package/build/UploadosModule.web.d.ts +13 -0
  21. package/build/UploadosModule.web.d.ts.map +1 -0
  22. package/build/UploadosModule.web.js +33 -0
  23. package/build/UploadosModule.web.js.map +1 -0
  24. package/build/createUploader.d.ts +3 -0
  25. package/build/createUploader.d.ts.map +1 -0
  26. package/build/createUploader.js +108 -0
  27. package/build/createUploader.js.map +1 -0
  28. package/build/index.d.ts +5 -0
  29. package/build/index.d.ts.map +1 -0
  30. package/build/index.js +5 -0
  31. package/build/index.js.map +1 -0
  32. package/build/normalizeUploadOptions.d.ts +9 -0
  33. package/build/normalizeUploadOptions.d.ts.map +1 -0
  34. package/build/normalizeUploadOptions.js +81 -0
  35. package/build/normalizeUploadOptions.js.map +1 -0
  36. package/build/providers/defineUploadProvider.d.ts +26 -0
  37. package/build/providers/defineUploadProvider.d.ts.map +1 -0
  38. package/build/providers/defineUploadProvider.js +39 -0
  39. package/build/providers/defineUploadProvider.js.map +1 -0
  40. package/build/providers/multipartPlan.d.ts +10 -0
  41. package/build/providers/multipartPlan.d.ts.map +1 -0
  42. package/build/providers/multipartPlan.js +28 -0
  43. package/build/providers/multipartPlan.js.map +1 -0
  44. package/eslint.config.cjs +5 -0
  45. package/expo-module.config.json +10 -0
  46. package/ios/Upload/CompressionPipeline.swift +183 -0
  47. package/ios/Upload/FileStager.swift +67 -0
  48. package/ios/Upload/UploadManager.swift +813 -0
  49. package/ios/Upload/UploadModels.swift +305 -0
  50. package/ios/Upload/UploadSessionDelegate.swift +82 -0
  51. package/ios/Upload/UploadTaskStore.swift +92 -0
  52. package/ios/Upload/UploadosAppDelegate.swift +14 -0
  53. package/ios/Uploados.podspec +23 -0
  54. package/ios/UploadosModule.swift +87 -0
  55. package/jest.config.js +15 -0
  56. package/package.json +54 -0
  57. package/readme.md +169 -0
  58. package/src/Uploados.types.ts +260 -0
  59. package/src/UploadosModule.ts +18 -0
  60. package/src/UploadosModule.web.ts +49 -0
  61. package/src/createUploader.ts +146 -0
  62. package/src/index.ts +4 -0
  63. package/src/normalizeUploadOptions.ts +132 -0
  64. package/src/providers/defineUploadProvider.ts +75 -0
  65. package/src/providers/multipartPlan.ts +43 -0
  66. package/tsconfig.json +42 -0
@@ -0,0 +1,305 @@
1
+ import Foundation
2
+
3
+ enum UploadState: String, Codable {
4
+ case created
5
+ case validating
6
+ case compressing
7
+ case queued
8
+ case connecting
9
+ case uploading
10
+ case verifying
11
+ case retrying
12
+ case completed
13
+ case paused
14
+ case failed
15
+ case cancelled
16
+ }
17
+
18
+ enum UploadNetworkPolicy: String, Codable {
19
+ case wait
20
+ case failFast
21
+ }
22
+
23
+ enum UploadErrorCode: String, Codable {
24
+ case FILE_NOT_FOUND
25
+ case FILE_TOO_LARGE
26
+ case NETWORK_ERROR
27
+ case AUTH_ERROR
28
+ case PROVIDER_ERROR
29
+ case MULTIPART_FAILED
30
+ case COMPRESSION_FAILED
31
+ case BACKGROUND_RESTRICTED
32
+ case CANCELLED
33
+ case UNKNOWN
34
+ }
35
+
36
+ struct UploadErrorPayload: Codable {
37
+ let code: UploadErrorCode
38
+ let message: String
39
+ let retryable: Bool
40
+ let taskId: String
41
+ let phase: String?
42
+ let httpStatus: Int?
43
+ let providerCode: String?
44
+ let providerMessage: String?
45
+ let nativeCause: String?
46
+
47
+ func toDictionary() -> [String: Any] {
48
+ var dict: [String: Any] = [
49
+ "code": code.rawValue,
50
+ "message": message,
51
+ "retryable": retryable,
52
+ "taskId": taskId,
53
+ ]
54
+ if let phase {
55
+ dict["phase"] = phase
56
+ }
57
+ if let httpStatus {
58
+ dict["httpStatus"] = httpStatus
59
+ }
60
+ if let providerCode {
61
+ dict["providerCode"] = providerCode
62
+ }
63
+ if let providerMessage {
64
+ dict["providerMessage"] = providerMessage
65
+ }
66
+ if let nativeCause {
67
+ dict["nativeCause"] = nativeCause
68
+ }
69
+ return dict
70
+ }
71
+ }
72
+
73
+ struct UploadProgress: Codable {
74
+ var bytesUploaded: Int
75
+ var totalBytes: Int
76
+ var percentage: Double
77
+ }
78
+
79
+ struct UploadFileMetadata: Codable {
80
+ let originalUri: String
81
+ let uploadUri: String
82
+ let isStaged: Bool
83
+ let sizeBytes: Int?
84
+
85
+ func toDictionary() -> [String: Any] {
86
+ var dict: [String: Any] = [
87
+ "originalUri": originalUri,
88
+ "uploadUri": uploadUri,
89
+ "isStaged": isStaged,
90
+ ]
91
+ if let sizeBytes {
92
+ dict["sizeBytes"] = sizeBytes
93
+ }
94
+ return dict
95
+ }
96
+ }
97
+
98
+ struct UploadTaskRecord: Codable {
99
+ let id: String
100
+ let localUri: String
101
+ var stagedFilePath: String?
102
+ var optimizedFilePath: String?
103
+ var compressionEnabled: Bool
104
+ var compressionPreset: String?
105
+ var originalSize: Int?
106
+ var optimizedSize: Int?
107
+ let uploadUrl: String
108
+ let method: String
109
+ var state: UploadState
110
+ var progress: UploadProgress
111
+ let background: Bool
112
+ let networkPolicy: UploadNetworkPolicy
113
+ var attempt: Int
114
+ let maxAttempts: Int
115
+ var nextRetryAt: TimeInterval?
116
+ let createdAt: TimeInterval
117
+ var updatedAt: TimeInterval
118
+ var headers: [String: String]
119
+ var sessionTaskIdentifier: Int?
120
+ var error: UploadErrorPayload?
121
+
122
+ init(
123
+ id: String,
124
+ localUri: String,
125
+ stagedFilePath: String? = nil,
126
+ optimizedFilePath: String? = nil,
127
+ compressionEnabled: Bool = false,
128
+ compressionPreset: String? = nil,
129
+ originalSize: Int? = nil,
130
+ optimizedSize: Int? = nil,
131
+ uploadUrl: String,
132
+ method: String,
133
+ state: UploadState,
134
+ progress: UploadProgress,
135
+ background: Bool,
136
+ networkPolicy: UploadNetworkPolicy,
137
+ attempt: Int,
138
+ maxAttempts: Int,
139
+ nextRetryAt: TimeInterval?,
140
+ createdAt: TimeInterval,
141
+ updatedAt: TimeInterval,
142
+ headers: [String: String],
143
+ sessionTaskIdentifier: Int?,
144
+ error: UploadErrorPayload?
145
+ ) {
146
+ self.id = id
147
+ self.localUri = localUri
148
+ self.stagedFilePath = stagedFilePath
149
+ self.optimizedFilePath = optimizedFilePath
150
+ self.compressionEnabled = compressionEnabled
151
+ self.compressionPreset = compressionPreset
152
+ self.originalSize = originalSize
153
+ self.optimizedSize = optimizedSize
154
+ self.uploadUrl = uploadUrl
155
+ self.method = method
156
+ self.state = state
157
+ self.progress = progress
158
+ self.background = background
159
+ self.networkPolicy = networkPolicy
160
+ self.attempt = attempt
161
+ self.maxAttempts = max(1, maxAttempts)
162
+ self.nextRetryAt = nextRetryAt
163
+ self.createdAt = createdAt
164
+ self.updatedAt = updatedAt
165
+ self.headers = headers
166
+ self.sessionTaskIdentifier = sessionTaskIdentifier
167
+ self.error = error
168
+ }
169
+
170
+ enum CodingKeys: String, CodingKey {
171
+ case id
172
+ case localUri
173
+ case stagedFilePath
174
+ case optimizedFilePath
175
+ case compressionEnabled
176
+ case compressionPreset
177
+ case originalSize
178
+ case optimizedSize
179
+ case uploadUrl
180
+ case method
181
+ case state
182
+ case progress
183
+ case background
184
+ case networkPolicy
185
+ case attempt
186
+ case maxAttempts
187
+ case nextRetryAt
188
+ case createdAt
189
+ case updatedAt
190
+ case headers
191
+ case sessionTaskIdentifier
192
+ case error
193
+ }
194
+
195
+ init(from decoder: Decoder) throws {
196
+ let container = try decoder.container(keyedBy: CodingKeys.self)
197
+ id = try container.decode(String.self, forKey: .id)
198
+ localUri = try container.decode(String.self, forKey: .localUri)
199
+ stagedFilePath = try container.decodeIfPresent(String.self, forKey: .stagedFilePath)
200
+ optimizedFilePath = try container.decodeIfPresent(String.self, forKey: .optimizedFilePath)
201
+ compressionEnabled = try container.decodeIfPresent(Bool.self, forKey: .compressionEnabled) ?? false
202
+ compressionPreset = try container.decodeIfPresent(String.self, forKey: .compressionPreset)
203
+ originalSize = try container.decodeIfPresent(Int.self, forKey: .originalSize)
204
+ optimizedSize = try container.decodeIfPresent(Int.self, forKey: .optimizedSize)
205
+ uploadUrl = try container.decode(String.self, forKey: .uploadUrl)
206
+ method = try container.decode(String.self, forKey: .method)
207
+ state = try container.decode(UploadState.self, forKey: .state)
208
+ progress = try container.decode(UploadProgress.self, forKey: .progress)
209
+ background = try container.decode(Bool.self, forKey: .background)
210
+ networkPolicy = try container.decodeIfPresent(UploadNetworkPolicy.self, forKey: .networkPolicy) ?? .wait
211
+ attempt = try container.decodeIfPresent(Int.self, forKey: .attempt) ?? 0
212
+ maxAttempts = max(1, try container.decodeIfPresent(Int.self, forKey: .maxAttempts) ?? 3)
213
+ nextRetryAt = try container.decodeIfPresent(TimeInterval.self, forKey: .nextRetryAt)
214
+ createdAt = try container.decode(TimeInterval.self, forKey: .createdAt)
215
+ updatedAt = try container.decode(TimeInterval.self, forKey: .updatedAt)
216
+ headers = try container.decodeIfPresent([String: String].self, forKey: .headers) ?? [:]
217
+ sessionTaskIdentifier = try container.decodeIfPresent(Int.self, forKey: .sessionTaskIdentifier)
218
+ error = try container.decodeIfPresent(UploadErrorPayload.self, forKey: .error)
219
+ }
220
+
221
+ func fileMetadata() -> UploadFileMetadata {
222
+ let uploadPath = optimizedFilePath ?? stagedFilePath
223
+ let uploadUri: String
224
+ if let uploadPath {
225
+ uploadUri = URL(fileURLWithPath: uploadPath).absoluteString
226
+ } else if localUri.hasPrefix("file://") {
227
+ uploadUri = localUri
228
+ } else {
229
+ uploadUri = URL(fileURLWithPath: localUri).absoluteString
230
+ }
231
+ let sizeBytes = optimizedSize ?? originalSize ?? (progress.totalBytes > 0 ? progress.totalBytes : nil)
232
+ return UploadFileMetadata(
233
+ originalUri: localUri,
234
+ uploadUri: uploadUri,
235
+ isStaged: stagedFilePath != nil || optimizedFilePath != nil,
236
+ sizeBytes: sizeBytes
237
+ )
238
+ }
239
+
240
+ func compressionStatsDictionary() -> [String: Any]? {
241
+ guard compressionEnabled,
242
+ let preset = compressionPreset,
243
+ let originalSize,
244
+ let optimizedSize else {
245
+ return nil
246
+ }
247
+ return [
248
+ "preset": preset,
249
+ "originalSize": originalSize,
250
+ "optimizedSize": optimizedSize,
251
+ "format": "jpeg",
252
+ ]
253
+ }
254
+
255
+ func toDictionary() -> [String: Any] {
256
+ let file = fileMetadata()
257
+ var dict: [String: Any] = [
258
+ "id": id,
259
+ "localUri": localUri,
260
+ "uploadUrl": uploadUrl,
261
+ "file": file.toDictionary(),
262
+ "method": method,
263
+ "state": state.rawValue,
264
+ "progress": [
265
+ "bytesUploaded": progress.bytesUploaded,
266
+ "totalBytes": progress.totalBytes,
267
+ "percentage": progress.percentage,
268
+ ],
269
+ "background": background,
270
+ "networkPolicy": networkPolicy.rawValue,
271
+ "attempt": attempt,
272
+ "maxAttempts": maxAttempts,
273
+ "createdAt": createdAt,
274
+ "updatedAt": updatedAt,
275
+ ]
276
+ if let nextRetryAt {
277
+ dict["nextRetryAt"] = nextRetryAt
278
+ }
279
+ if let originalSize {
280
+ dict["originalSize"] = originalSize
281
+ }
282
+ if let optimizedSize {
283
+ dict["optimizedSize"] = optimizedSize
284
+ }
285
+ if let compression = compressionStatsDictionary() {
286
+ dict["compression"] = compression
287
+ }
288
+ if let error {
289
+ dict["error"] = error.toDictionary()
290
+ }
291
+ return dict
292
+ }
293
+ }
294
+
295
+ struct CreateUploadOptions {
296
+ let localUri: String
297
+ let uploadUrl: String
298
+ let method: String
299
+ let headers: [String: String]
300
+ let background: Bool
301
+ let networkPolicy: UploadNetworkPolicy
302
+ let maxAttempts: Int
303
+ let compressionEnabled: Bool
304
+ let compressionPreset: CompressionPreset
305
+ }
@@ -0,0 +1,82 @@
1
+ import Foundation
2
+
3
+ final class UploadSessionDelegate: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
4
+ weak var manager: UploadManager?
5
+ private let responseDataLock = NSLock()
6
+ private var responseDataByTaskId: [String: Data] = [:]
7
+ private let maxResponseBodyBytes = 16 * 1024
8
+
9
+ func urlSession(
10
+ _ session: URLSession,
11
+ task: URLSessionTask,
12
+ didSendBodyData bytesSent: Int64,
13
+ totalBytesSent: Int64,
14
+ totalBytesExpectedToSend: Int64
15
+ ) {
16
+ guard let taskId = task.taskDescription else {
17
+ return
18
+ }
19
+ let totalBytes: Int
20
+ if totalBytesExpectedToSend > 0 {
21
+ totalBytes = Int(totalBytesExpectedToSend)
22
+ } else if let storedTotal = UploadTaskStore.shared.task(id: taskId)?.progress.totalBytes,
23
+ storedTotal > 0 {
24
+ totalBytes = storedTotal
25
+ } else {
26
+ return
27
+ }
28
+ manager?.handleProgress(
29
+ taskId: taskId,
30
+ bytesUploaded: Int(totalBytesSent),
31
+ totalBytes: totalBytes
32
+ )
33
+ }
34
+
35
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
36
+ guard let taskId = task.taskDescription else {
37
+ return
38
+ }
39
+ if let error {
40
+ manager?.handleFailure(taskId: taskId, error: error, response: task.response)
41
+ _ = takeResponseBody(taskId: taskId)
42
+ return
43
+ }
44
+ if let http = task.response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
45
+ manager?.handleHttpFailure(
46
+ taskId: taskId,
47
+ statusCode: http.statusCode,
48
+ responseBody: takeResponseBody(taskId: taskId)
49
+ )
50
+ return
51
+ }
52
+ _ = takeResponseBody(taskId: taskId)
53
+ manager?.handleCompletion(taskId: taskId)
54
+ }
55
+
56
+ func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
57
+ guard let taskId = dataTask.taskDescription else {
58
+ return
59
+ }
60
+ responseDataLock.lock()
61
+ var existing = responseDataByTaskId[taskId] ?? Data()
62
+ if existing.count < maxResponseBodyBytes {
63
+ existing.append(data.prefix(maxResponseBodyBytes - existing.count))
64
+ responseDataByTaskId[taskId] = existing
65
+ }
66
+ responseDataLock.unlock()
67
+ }
68
+
69
+ func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
70
+ UploadManager.shared.invokeBackgroundCompletionHandler(for: session.configuration.identifier)
71
+ }
72
+
73
+ private func takeResponseBody(taskId: String) -> String? {
74
+ responseDataLock.lock()
75
+ let data = responseDataByTaskId.removeValue(forKey: taskId)
76
+ responseDataLock.unlock()
77
+ guard let data, !data.isEmpty else {
78
+ return nil
79
+ }
80
+ return String(data: data, encoding: .utf8)
81
+ }
82
+ }
@@ -0,0 +1,92 @@
1
+ import Foundation
2
+
3
+ final class UploadTaskStore {
4
+ static let shared = UploadTaskStore()
5
+
6
+ private let fileName = "uploados-tasks.json"
7
+ private let queue = DispatchQueue(label: "expo.modules.uploados.store")
8
+ private var tasks: [String: UploadTaskRecord] = [:]
9
+
10
+ private init() {
11
+ loadFromDisk()
12
+ }
13
+
14
+ func allTasks() -> [UploadTaskRecord] {
15
+ queue.sync {
16
+ Array(tasks.values).sorted { $0.createdAt < $1.createdAt }
17
+ }
18
+ }
19
+
20
+ func task(id: String) -> UploadTaskRecord? {
21
+ queue.sync { tasks[id] }
22
+ }
23
+
24
+ func upsert(_ task: UploadTaskRecord) {
25
+ queue.sync {
26
+ tasks[task.id] = task
27
+ persistLocked()
28
+ }
29
+ }
30
+
31
+ func update(id: String, mutate: (inout UploadTaskRecord) -> Void) -> UploadTaskRecord? {
32
+ queue.sync {
33
+ guard var task = tasks[id] else {
34
+ return nil
35
+ }
36
+ mutate(&task)
37
+ task.updatedAt = Date().timeIntervalSince1970 * 1000
38
+ tasks[id] = task
39
+ persistLocked()
40
+ return task
41
+ }
42
+ }
43
+
44
+ func restorableTasks() -> [UploadTaskRecord] {
45
+ queue.sync {
46
+ tasks.values.filter { task in
47
+ switch task.state {
48
+ case .created, .validating, .compressing, .queued, .connecting, .uploading, .verifying, .retrying, .paused:
49
+ return true
50
+ default:
51
+ return false
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ private func loadFromDisk() {
58
+ queue.sync {
59
+ guard let url = storeURL(),
60
+ FileManager.default.fileExists(atPath: url.path) else {
61
+ return
62
+ }
63
+ do {
64
+ let data = try Data(contentsOf: url)
65
+ let decoded = try JSONDecoder().decode([UploadTaskRecord].self, from: data)
66
+ tasks = Dictionary(uniqueKeysWithValues: decoded.map { ($0.id, $0) })
67
+ } catch {
68
+ tasks = [:]
69
+ }
70
+ }
71
+ }
72
+
73
+ private func persistLocked() {
74
+ guard let url = storeURL() else {
75
+ return
76
+ }
77
+ do {
78
+ let directory = url.deletingLastPathComponent()
79
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
80
+ let data = try JSONEncoder().encode(Array(tasks.values))
81
+ try data.write(to: url, options: .atomic)
82
+ } catch {
83
+ // Persistence failures should not crash uploads; state may be lost on restart.
84
+ }
85
+ }
86
+
87
+ private func storeURL() -> URL? {
88
+ FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
89
+ .appendingPathComponent("uploados", isDirectory: true)
90
+ .appendingPathComponent(fileName)
91
+ }
92
+ }
@@ -0,0 +1,14 @@
1
+ import ExpoModulesCore
2
+
3
+ public class UploadosAppDelegate: ExpoAppDelegateSubscriber {
4
+ public func application(
5
+ _ application: UIApplication,
6
+ handleEventsForBackgroundURLSession identifier: String,
7
+ completionHandler: @escaping () -> Void
8
+ ) {
9
+ UploadManager.shared.registerBackgroundCompletionHandler(
10
+ for: identifier,
11
+ completionHandler: completionHandler
12
+ )
13
+ }
14
+ }
@@ -0,0 +1,23 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'Uploados'
3
+ s.version = '0.1.0'
4
+ s.summary = 'Native signed-URL upload engine for Expo'
5
+ s.description = 'iOS URLSession-based signed-URL upload engine with staging, persistence, and JPEG compression'
6
+ s.author = 'bessim boujebli'
7
+ s.homepage = 'https://github.com/bessim-dev/uploados'
8
+ s.platforms = {
9
+ :ios => '15.1',
10
+ :tvos => '15.1'
11
+ }
12
+ s.source = { git: 'https://github.com/bessim-dev/uploados.git' }
13
+ s.static_framework = true
14
+
15
+ s.dependency 'ExpoModulesCore'
16
+
17
+ # Swift/Objective-C compatibility
18
+ s.pod_target_xcconfig = {
19
+ 'DEFINES_MODULE' => 'YES',
20
+ }
21
+
22
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
23
+ end
@@ -0,0 +1,87 @@
1
+ import ExpoModulesCore
2
+
3
+ public class UploadosModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("Uploados")
6
+
7
+ Events("onUploadEvent")
8
+
9
+ OnCreate {
10
+ UploadManager.shared.setEventEmitter { [weak self] payload in
11
+ self?.sendEvent("onUploadEvent", payload)
12
+ }
13
+ _ = UploadManager.shared.restoreQueue()
14
+ }
15
+
16
+ AsyncFunction("createUploadAsync") { (options: [String: Any]) -> [String: Any] in
17
+ let parsed = try Self.parseCreateOptions(options)
18
+ let task = try UploadManager.shared.createUpload(options: parsed)
19
+ return task.toDictionary()
20
+ }
21
+
22
+ AsyncFunction("cancelUploadAsync") { (taskId: String) -> [String: Any] in
23
+ let task = try UploadManager.shared.cancelUpload(taskId: taskId)
24
+ return task.toDictionary()
25
+ }
26
+
27
+ AsyncFunction("retryUploadAsync") { (taskId: String) -> [String: Any] in
28
+ let task = try UploadManager.shared.retryUpload(taskId: taskId)
29
+ return task.toDictionary()
30
+ }
31
+
32
+ AsyncFunction("getTaskAsync") { (taskId: String) -> [String: Any]? in
33
+ UploadTaskStore.shared.task(id: taskId)?.toDictionary()
34
+ }
35
+
36
+ AsyncFunction("getAllTasksAsync") { () -> [[String: Any]] in
37
+ UploadTaskStore.shared.allTasks().map { $0.toDictionary() }
38
+ }
39
+
40
+ AsyncFunction("restoreQueueAsync") { () -> [[String: Any]] in
41
+ UploadManager.shared.restoreQueue().map { $0.toDictionary() }
42
+ }
43
+ }
44
+
45
+ private static func parseCreateOptions(_ options: [String: Any]) throws -> CreateUploadOptions {
46
+ guard let localUri = options["localUri"] as? String else {
47
+ throw InvalidOptionsException("localUri")
48
+ }
49
+ guard let uploadUrl = options["uploadUrl"] as? String else {
50
+ throw InvalidOptionsException("uploadUrl")
51
+ }
52
+ let method = (options["method"] as? String)?.uppercased() ?? "PUT"
53
+ guard method == "PUT" || method == "POST" else {
54
+ throw InvalidOptionsException("method")
55
+ }
56
+ let networkPolicyRaw = options["networkPolicy"] as? String ?? "wait"
57
+ guard let networkPolicy = UploadNetworkPolicy(rawValue: networkPolicyRaw) else {
58
+ throw InvalidOptionsException("networkPolicy")
59
+ }
60
+ let retry = options["retry"] as? [String: Any]
61
+ let maxAttemptsValue = retry?["maxAttempts"] as? NSNumber
62
+ let maxAttempts = max(1, maxAttemptsValue?.intValue ?? 3)
63
+ let headers = options["headers"] as? [String: String] ?? [:]
64
+ let background = options["background"] as? Bool ?? false
65
+ let compression = options["compression"] as? [String: Any]
66
+ let compressionEnabled = compression?["enabled"] as? Bool ?? false
67
+ let compressionPresetRaw = compression?["preset"] as? String ?? CompressionPreset.balanced.rawValue
68
+ let compressionPreset = CompressionPreset(raw: compressionPresetRaw)
69
+ return CreateUploadOptions(
70
+ localUri: localUri,
71
+ uploadUrl: uploadUrl,
72
+ method: method,
73
+ headers: headers,
74
+ background: background,
75
+ networkPolicy: background ? .wait : networkPolicy,
76
+ maxAttempts: maxAttempts,
77
+ compressionEnabled: compressionEnabled,
78
+ compressionPreset: compressionPreset
79
+ )
80
+ }
81
+ }
82
+
83
+ internal final class InvalidOptionsException: GenericException<String>, @unchecked Sendable {
84
+ override var reason: String {
85
+ "Missing or invalid upload option: \(param)"
86
+ }
87
+ }
package/jest.config.js ADDED
@@ -0,0 +1,15 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src'],
5
+ testMatch: ['**/__tests__/**/*.test.ts'],
6
+ transform: {
7
+ '^.+\\.(ts|tsx)$': [
8
+ 'babel-jest',
9
+ {
10
+ presets: ['babel-preset-expo'],
11
+ },
12
+ ],
13
+ },
14
+ modulePathIgnorePatterns: ['<rootDir>/build/'],
15
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "uploados",
3
+ "version": "0.1.0",
4
+ "description": "Native signed-URL upload engine for Expo apps",
5
+ "main": "build/index.js",
6
+ "types": "build/index.d.ts",
7
+ "scripts": {
8
+ "build": "node internal/module_scripts/build.js",
9
+ "clean": "node internal/module_scripts/clean.js",
10
+ "lint": "eslint src/",
11
+ "test": "node internal/module_scripts/test.js",
12
+ "prepare": "node internal/module_scripts/prepare.js",
13
+ "open:ios": "node internal/module_scripts/open-ios.js",
14
+ "open:android": "node internal/module_scripts/open-android.js"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "expo",
19
+ "uploads",
20
+ "presigned-url",
21
+ "background-uploads",
22
+ "image-compression",
23
+ "uploados",
24
+ "Uploados"
25
+ ],
26
+ "repository": "https://github.com/bessim-dev/uploados",
27
+ "bugs": {
28
+ "url": "https://github.com/bessim-dev/uploados/issues"
29
+ },
30
+ "author": "bessim boujebli <bessim.boujebli@gmail.com> (bessim-dev)",
31
+ "license": "MIT",
32
+ "homepage": "https://github.com/bessim-dev/uploados#readme",
33
+ "dependencies": {},
34
+ "devDependencies": {
35
+ "@babel/core": "^7.26.0",
36
+ "@babel/runtime": "^7.29.7",
37
+ "@types/jest": "^29.2.1",
38
+ "@types/react": "~19.2.14",
39
+ "babel-preset-expo": "~56.0.0",
40
+ "eslint": "~9.39.4",
41
+ "eslint-config-universe": "^15.0.3",
42
+ "expo": "^56.0.5",
43
+ "jest": "^29.7.0",
44
+ "jest-expo": "~56.0.4",
45
+ "prettier": "^3.0.0",
46
+ "react-native": "0.85.3",
47
+ "typescript": "~6.0.3"
48
+ },
49
+ "peerDependencies": {
50
+ "expo": ">=52.0.0 <57.0.0",
51
+ "react": ">=18.3.1 <20.0.0",
52
+ "react-native": ">=0.76.0 <0.86.0"
53
+ }
54
+ }