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,209 @@
1
+ package expo.modules.uploados.upload
2
+
3
+ import java.io.File
4
+
5
+ enum class UploadState(val raw: String) {
6
+ CREATED("created"),
7
+ VALIDATING("validating"),
8
+ COMPRESSING("compressing"),
9
+ QUEUED("queued"),
10
+ CONNECTING("connecting"),
11
+ UPLOADING("uploading"),
12
+ VERIFYING("verifying"),
13
+ RETRYING("retrying"),
14
+ COMPLETED("completed"),
15
+ PAUSED("paused"),
16
+ FAILED("failed"),
17
+ CANCELLED("cancelled");
18
+
19
+ companion object {
20
+ fun fromRaw(value: String): UploadState? =
21
+ entries.firstOrNull { it.raw == value }
22
+ }
23
+ }
24
+
25
+ enum class UploadNetworkPolicy(val raw: String) {
26
+ WAIT("wait"),
27
+ FAIL_FAST("failFast");
28
+
29
+ companion object {
30
+ fun fromRaw(value: String): UploadNetworkPolicy? =
31
+ entries.firstOrNull { it.raw == value }
32
+ }
33
+ }
34
+
35
+ enum class UploadExecutionResult {
36
+ FINISHED,
37
+ RETRY,
38
+ }
39
+
40
+ enum class UploadErrorCode(val raw: String) {
41
+ FILE_NOT_FOUND("FILE_NOT_FOUND"),
42
+ FILE_TOO_LARGE("FILE_TOO_LARGE"),
43
+ NETWORK_ERROR("NETWORK_ERROR"),
44
+ AUTH_ERROR("AUTH_ERROR"),
45
+ PROVIDER_ERROR("PROVIDER_ERROR"),
46
+ MULTIPART_FAILED("MULTIPART_FAILED"),
47
+ COMPRESSION_FAILED("COMPRESSION_FAILED"),
48
+ BACKGROUND_RESTRICTED("BACKGROUND_RESTRICTED"),
49
+ CANCELLED("CANCELLED"),
50
+ UNKNOWN("UNKNOWN");
51
+
52
+ companion object {
53
+ fun fromRaw(value: String): UploadErrorCode? =
54
+ entries.firstOrNull { it.raw == value }
55
+ }
56
+ }
57
+
58
+ data class UploadErrorPayload(
59
+ val code: UploadErrorCode,
60
+ val message: String,
61
+ val retryable: Boolean,
62
+ val taskId: String,
63
+ val phase: String? = null,
64
+ val httpStatus: Int? = null,
65
+ val providerCode: String? = null,
66
+ val providerMessage: String? = null,
67
+ val nativeCause: String? = null,
68
+ ) {
69
+ fun toMap(): Map<String, Any> {
70
+ val map = mutableMapOf<String, Any>(
71
+ "code" to code.raw,
72
+ "message" to message,
73
+ "retryable" to retryable,
74
+ "taskId" to taskId,
75
+ )
76
+ phase?.let { map["phase"] = it }
77
+ httpStatus?.let { map["httpStatus"] = it }
78
+ providerCode?.let { map["providerCode"] = it }
79
+ providerMessage?.let { map["providerMessage"] = it }
80
+ nativeCause?.let { map["nativeCause"] = it }
81
+ return map
82
+ }
83
+ }
84
+
85
+ data class UploadFileMetadata(
86
+ val originalUri: String,
87
+ val uploadUri: String,
88
+ val isStaged: Boolean,
89
+ val sizeBytes: Long? = null,
90
+ ) {
91
+ fun toMap(): Map<String, Any> {
92
+ val map = mutableMapOf<String, Any>(
93
+ "originalUri" to originalUri,
94
+ "uploadUri" to uploadUri,
95
+ "isStaged" to isStaged,
96
+ )
97
+ sizeBytes?.let { map["sizeBytes"] = it }
98
+ return map
99
+ }
100
+ }
101
+
102
+ data class UploadProgress(
103
+ var bytesUploaded: Long,
104
+ var totalBytes: Long,
105
+ var percentage: Double,
106
+ ) {
107
+ fun toMap(): Map<String, Any> = mapOf(
108
+ "bytesUploaded" to bytesUploaded,
109
+ "totalBytes" to totalBytes,
110
+ "percentage" to percentage,
111
+ )
112
+ }
113
+
114
+ data class UploadTaskRecord(
115
+ val id: String,
116
+ val localUri: String,
117
+ var stagedFilePath: String? = null,
118
+ var optimizedFilePath: String? = null,
119
+ val compressionEnabled: Boolean = false,
120
+ var compressionPreset: String? = null,
121
+ var originalSize: Long? = null,
122
+ var optimizedSize: Long? = null,
123
+ val uploadUrl: String,
124
+ val method: String,
125
+ var state: UploadState,
126
+ var progress: UploadProgress,
127
+ val background: Boolean,
128
+ val networkPolicy: UploadNetworkPolicy,
129
+ var attempt: Int,
130
+ val maxAttempts: Int,
131
+ var nextRetryAt: Double? = null,
132
+ val createdAt: Double,
133
+ var updatedAt: Double,
134
+ var headers: Map<String, String>,
135
+ var error: UploadErrorPayload? = null,
136
+ ) {
137
+ fun fileMetadata(): UploadFileMetadata {
138
+ val uploadPath = optimizedFilePath ?: stagedFilePath
139
+ val uploadUri = uploadPath?.let { File(it).toURI().toString() }
140
+ ?: if (localUri.startsWith("file://") || localUri.startsWith("content://")) {
141
+ localUri
142
+ } else {
143
+ File(localUri).toURI().toString()
144
+ }
145
+ val sizeBytes = optimizedSize ?: originalSize ?: progress.totalBytes.takeIf { it > 0 }
146
+ return UploadFileMetadata(
147
+ originalUri = localUri,
148
+ uploadUri = uploadUri,
149
+ isStaged = stagedFilePath != null || optimizedFilePath != null,
150
+ sizeBytes = sizeBytes,
151
+ )
152
+ }
153
+
154
+ fun compressionStatsMap(): Map<String, Any>? {
155
+ val preset = compressionPreset
156
+ val original = originalSize
157
+ val optimized = optimizedSize
158
+ if (!compressionEnabled || preset == null || original == null || optimized == null) {
159
+ return null
160
+ }
161
+ return mapOf(
162
+ "preset" to preset,
163
+ "originalSize" to original,
164
+ "optimizedSize" to optimized,
165
+ "format" to "jpeg",
166
+ )
167
+ }
168
+
169
+ fun toMap(): Map<String, Any?> {
170
+ val map = mutableMapOf<String, Any?>(
171
+ "id" to id,
172
+ "localUri" to localUri,
173
+ "file" to fileMetadata().toMap(),
174
+ "uploadUrl" to uploadUrl,
175
+ "method" to method,
176
+ "state" to state.raw,
177
+ "progress" to progress.toMap(),
178
+ "background" to background,
179
+ "networkPolicy" to networkPolicy.raw,
180
+ "attempt" to attempt,
181
+ "maxAttempts" to maxAttempts,
182
+ "createdAt" to createdAt,
183
+ "updatedAt" to updatedAt,
184
+ )
185
+ nextRetryAt?.let { map["nextRetryAt"] = it }
186
+ originalSize?.let { map["originalSize"] = it }
187
+ optimizedSize?.let { map["optimizedSize"] = it }
188
+ compressionStatsMap()?.let { map["compression"] = it }
189
+ error?.let { map["error"] = it.toMap() }
190
+ return map
191
+ }
192
+ }
193
+
194
+ data class CreateUploadOptions(
195
+ val localUri: String,
196
+ val uploadUrl: String,
197
+ val method: String,
198
+ val headers: Map<String, String>,
199
+ val background: Boolean,
200
+ val networkPolicy: UploadNetworkPolicy,
201
+ val maxAttempts: Int,
202
+ val compressionEnabled: Boolean = false,
203
+ val compressionPreset: CompressionPreset = CompressionPreset.BALANCED,
204
+ )
205
+
206
+ class UploadManagerException(
207
+ val errorCode: UploadErrorCode,
208
+ override val message: String,
209
+ ) : Exception(message)
@@ -0,0 +1,93 @@
1
+ package expo.modules.uploados.upload
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.content.Context
7
+ import android.content.pm.ServiceInfo
8
+ import android.os.Build
9
+ import androidx.core.app.NotificationCompat
10
+ import androidx.work.ForegroundInfo
11
+ import kotlin.math.roundToInt
12
+
13
+ object UploadNotificationHelper {
14
+ private const val CHANNEL_ID = "uploados_uploads"
15
+ private const val CHANNEL_NAME = "Uploads"
16
+
17
+ fun foregroundInfo(context: Context, task: UploadTaskRecord): ForegroundInfo {
18
+ val notification = buildNotification(context, task)
19
+ val notificationId = notificationId(task.id)
20
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
21
+ ForegroundInfo(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
22
+ } else {
23
+ ForegroundInfo(notificationId, notification)
24
+ }
25
+ }
26
+
27
+ fun showProgress(context: Context, task: UploadTaskRecord) {
28
+ try {
29
+ notificationManager(context).notify(notificationId(task.id), buildNotification(context, task))
30
+ } catch (_: RuntimeException) {
31
+ // Notification updates are best-effort; upload state remains the source of truth.
32
+ }
33
+ }
34
+
35
+ fun cancel(context: Context, taskId: String) {
36
+ try {
37
+ notificationManager(context).cancel(notificationId(taskId))
38
+ } catch (_: RuntimeException) {
39
+ // Notification cleanup should not affect completed, failed, or cancelled uploads.
40
+ }
41
+ }
42
+
43
+ private fun buildNotification(context: Context, task: UploadTaskRecord): Notification {
44
+ ensureChannel(context)
45
+
46
+ val percentage = task.progress.percentage.roundToInt().coerceIn(0, 100)
47
+ val hasTotal = task.progress.totalBytes > 0
48
+ val contentText = if (hasTotal) {
49
+ "$percentage% uploaded"
50
+ } else {
51
+ "Preparing upload"
52
+ }
53
+
54
+ return NotificationCompat.Builder(context, CHANNEL_ID)
55
+ .setSmallIcon(notificationIcon(context))
56
+ .setContentTitle("Upload in progress")
57
+ .setContentText(contentText)
58
+ .setOngoing(task.state != UploadState.COMPLETED && task.state != UploadState.FAILED)
59
+ .setOnlyAlertOnce(true)
60
+ .setCategory(NotificationCompat.CATEGORY_PROGRESS)
61
+ .setPriority(NotificationCompat.PRIORITY_LOW)
62
+ .setProgress(100, percentage, !hasTotal)
63
+ .build()
64
+ }
65
+
66
+ private fun ensureChannel(context: Context) {
67
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
68
+ return
69
+ }
70
+ val manager = notificationManager(context)
71
+ if (manager.getNotificationChannel(CHANNEL_ID) != null) {
72
+ return
73
+ }
74
+ val channel = NotificationChannel(
75
+ CHANNEL_ID,
76
+ CHANNEL_NAME,
77
+ NotificationManager.IMPORTANCE_LOW,
78
+ )
79
+ channel.setShowBadge(false)
80
+ manager.createNotificationChannel(channel)
81
+ }
82
+
83
+ private fun notificationManager(context: Context): NotificationManager =
84
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
85
+
86
+ private fun notificationIcon(context: Context): Int {
87
+ val appIcon = context.applicationInfo.icon
88
+ return if (appIcon != 0) appIcon else android.R.drawable.stat_sys_upload
89
+ }
90
+
91
+ private fun notificationId(taskId: String): Int =
92
+ taskId.hashCode().and(Int.MAX_VALUE).coerceAtLeast(1)
93
+ }
@@ -0,0 +1,224 @@
1
+ package expo.modules.uploados.upload
2
+
3
+ import android.content.Context
4
+ import org.json.JSONArray
5
+ import org.json.JSONObject
6
+ import java.io.File
7
+
8
+ class UploadTaskStore private constructor(context: Context) {
9
+ private val fileName = "uploados-tasks.json"
10
+ private val lock = Any()
11
+ private val tasks = mutableMapOf<String, UploadTaskRecord>()
12
+ private val storeDir: File =
13
+ File(context.filesDir, "uploados").also { it.mkdirs() }
14
+
15
+ init {
16
+ loadFromDisk()
17
+ }
18
+
19
+ fun allTasks(): List<UploadTaskRecord> = synchronized(lock) {
20
+ tasks.values.sortedBy { it.createdAt }
21
+ }
22
+
23
+ fun task(id: String): UploadTaskRecord? = synchronized(lock) {
24
+ tasks[id]
25
+ }
26
+
27
+ fun upsert(task: UploadTaskRecord) {
28
+ synchronized(lock) {
29
+ tasks[task.id] = task
30
+ persistLocked()
31
+ }
32
+ }
33
+
34
+ fun update(id: String, mutate: (UploadTaskRecord) -> Unit): UploadTaskRecord? {
35
+ synchronized(lock) {
36
+ val existing = tasks[id] ?: return null
37
+ val copy = existing.copy(
38
+ stagedFilePath = existing.stagedFilePath,
39
+ optimizedFilePath = existing.optimizedFilePath,
40
+ compressionPreset = existing.compressionPreset,
41
+ originalSize = existing.originalSize,
42
+ optimizedSize = existing.optimizedSize,
43
+ progress = existing.progress.copy(),
44
+ headers = existing.headers.toMap(),
45
+ error = existing.error,
46
+ )
47
+ mutate(copy)
48
+ copy.updatedAt = System.currentTimeMillis().toDouble()
49
+ tasks[id] = copy
50
+ persistLocked()
51
+ return copy
52
+ }
53
+ }
54
+
55
+ fun restorableTasks(): List<UploadTaskRecord> = synchronized(lock) {
56
+ tasks.values.filter { task ->
57
+ when (task.state) {
58
+ UploadState.CREATED,
59
+ UploadState.VALIDATING,
60
+ UploadState.COMPRESSING,
61
+ UploadState.QUEUED,
62
+ UploadState.CONNECTING,
63
+ UploadState.UPLOADING,
64
+ UploadState.VERIFYING,
65
+ UploadState.RETRYING,
66
+ UploadState.PAUSED -> true
67
+ else -> false
68
+ }
69
+ }
70
+ }
71
+
72
+ private fun loadFromDisk() {
73
+ synchronized(lock) {
74
+ val file = storeFile()
75
+ if (!file.exists()) {
76
+ return
77
+ }
78
+ try {
79
+ val json = JSONArray(file.readText())
80
+ tasks.clear()
81
+ for (i in 0 until json.length()) {
82
+ val record = decodeTask(json.getJSONObject(i))
83
+ tasks[record.id] = record
84
+ }
85
+ } catch (_: Exception) {
86
+ tasks.clear()
87
+ }
88
+ }
89
+ }
90
+
91
+ private fun persistLocked() {
92
+ val file = storeFile()
93
+ val tmpFile = File(storeDir, "$fileName.tmp")
94
+ val backupFile = File(storeDir, "$fileName.bak")
95
+ try {
96
+ val array = JSONArray()
97
+ tasks.values.forEach { array.put(encodeTask(it)) }
98
+ tmpFile.writeText(array.toString())
99
+ if (file.exists()) {
100
+ file.copyTo(backupFile, overwrite = true)
101
+ }
102
+ if (file.exists() && !file.delete()) {
103
+ throw IllegalStateException("Could not replace upload task store.")
104
+ }
105
+ if (!tmpFile.renameTo(file)) {
106
+ if (backupFile.exists()) {
107
+ backupFile.copyTo(file, overwrite = true)
108
+ }
109
+ throw IllegalStateException("Could not persist upload task store.")
110
+ }
111
+ backupFile.delete()
112
+ } catch (_: Exception) {
113
+ tmpFile.delete()
114
+ // Persistence failures should not crash uploads.
115
+ }
116
+ }
117
+
118
+ private fun storeFile(): File = File(storeDir, fileName)
119
+
120
+ companion object {
121
+ @Volatile
122
+ private var instance: UploadTaskStore? = null
123
+
124
+ fun getInstance(context: Context): UploadTaskStore {
125
+ return instance ?: synchronized(this) {
126
+ instance ?: UploadTaskStore(context.applicationContext).also { instance = it }
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ private fun encodeTask(task: UploadTaskRecord): JSONObject {
133
+ val json = JSONObject()
134
+ json.put("id", task.id)
135
+ json.put("localUri", task.localUri)
136
+ task.stagedFilePath?.let { json.put("stagedFilePath", it) }
137
+ task.optimizedFilePath?.let { json.put("optimizedFilePath", it) }
138
+ json.put("compressionEnabled", task.compressionEnabled)
139
+ task.compressionPreset?.let { json.put("compressionPreset", it) }
140
+ task.originalSize?.let { json.put("originalSize", it) }
141
+ task.optimizedSize?.let { json.put("optimizedSize", it) }
142
+ json.put("uploadUrl", task.uploadUrl)
143
+ json.put("method", task.method)
144
+ json.put("state", task.state.raw)
145
+ json.put("background", task.background)
146
+ json.put("networkPolicy", task.networkPolicy.raw)
147
+ json.put("attempt", task.attempt)
148
+ json.put("maxAttempts", task.maxAttempts)
149
+ task.nextRetryAt?.let { json.put("nextRetryAt", it) }
150
+ json.put("createdAt", task.createdAt)
151
+ json.put("updatedAt", task.updatedAt)
152
+ val progress = JSONObject()
153
+ progress.put("bytesUploaded", task.progress.bytesUploaded)
154
+ progress.put("totalBytes", task.progress.totalBytes)
155
+ progress.put("percentage", task.progress.percentage)
156
+ json.put("progress", progress)
157
+ val headers = JSONObject()
158
+ task.headers.forEach { (key, value) -> headers.put(key, value) }
159
+ json.put("headers", headers)
160
+ task.error?.let { error ->
161
+ val errorJson = JSONObject()
162
+ errorJson.put("code", error.code.raw)
163
+ errorJson.put("message", error.message)
164
+ errorJson.put("retryable", error.retryable)
165
+ errorJson.put("taskId", error.taskId)
166
+ error.phase?.let { errorJson.put("phase", it) }
167
+ error.httpStatus?.let { errorJson.put("httpStatus", it) }
168
+ error.providerCode?.let { errorJson.put("providerCode", it) }
169
+ error.providerMessage?.let { errorJson.put("providerMessage", it) }
170
+ error.nativeCause?.let { errorJson.put("nativeCause", it) }
171
+ json.put("error", errorJson)
172
+ }
173
+ return json
174
+ }
175
+
176
+ private fun decodeTask(json: JSONObject): UploadTaskRecord {
177
+ val progressJson = json.getJSONObject("progress")
178
+ val headersJson = json.optJSONObject("headers") ?: JSONObject()
179
+ val headers = mutableMapOf<String, String>()
180
+ headersJson.keys().forEach { key ->
181
+ headers[key] = headersJson.getString(key)
182
+ }
183
+ val error = json.optJSONObject("error")?.let { errorJson ->
184
+ UploadErrorPayload(
185
+ code = UploadErrorCode.fromRaw(errorJson.getString("code")) ?: UploadErrorCode.UNKNOWN,
186
+ message = errorJson.getString("message"),
187
+ retryable = errorJson.getBoolean("retryable"),
188
+ taskId = errorJson.getString("taskId"),
189
+ phase = errorJson.optString("phase").takeIf { it.isNotEmpty() },
190
+ httpStatus = if (errorJson.has("httpStatus")) errorJson.getInt("httpStatus") else null,
191
+ providerCode = errorJson.optString("providerCode").takeIf { it.isNotEmpty() },
192
+ providerMessage = errorJson.optString("providerMessage").takeIf { it.isNotEmpty() },
193
+ nativeCause = errorJson.optString("nativeCause").takeIf { it.isNotEmpty() },
194
+ )
195
+ }
196
+ return UploadTaskRecord(
197
+ id = json.getString("id"),
198
+ localUri = json.getString("localUri"),
199
+ stagedFilePath = json.optString("stagedFilePath").takeIf { it.isNotEmpty() },
200
+ optimizedFilePath = json.optString("optimizedFilePath").takeIf { it.isNotEmpty() },
201
+ compressionEnabled = json.optBoolean("compressionEnabled", false),
202
+ compressionPreset = json.optString("compressionPreset").takeIf { it.isNotEmpty() },
203
+ originalSize = if (json.has("originalSize")) json.getLong("originalSize") else null,
204
+ optimizedSize = if (json.has("optimizedSize")) json.getLong("optimizedSize") else null,
205
+ uploadUrl = json.getString("uploadUrl"),
206
+ method = json.getString("method"),
207
+ state = UploadState.fromRaw(json.getString("state")) ?: UploadState.FAILED,
208
+ progress = UploadProgress(
209
+ bytesUploaded = progressJson.getLong("bytesUploaded"),
210
+ totalBytes = progressJson.getLong("totalBytes"),
211
+ percentage = progressJson.getDouble("percentage"),
212
+ ),
213
+ background = json.getBoolean("background"),
214
+ networkPolicy = UploadNetworkPolicy.fromRaw(json.optString("networkPolicy", UploadNetworkPolicy.WAIT.raw))
215
+ ?: UploadNetworkPolicy.WAIT,
216
+ attempt = json.optInt("attempt", 0),
217
+ maxAttempts = json.optInt("maxAttempts", 3).coerceAtLeast(1),
218
+ nextRetryAt = if (json.has("nextRetryAt")) json.getDouble("nextRetryAt") else null,
219
+ createdAt = json.getDouble("createdAt"),
220
+ updatedAt = json.getDouble("updatedAt"),
221
+ headers = headers,
222
+ error = error,
223
+ )
224
+ }
@@ -0,0 +1,31 @@
1
+ package expo.modules.uploados.upload
2
+
3
+ import android.content.Context
4
+ import androidx.work.CoroutineWorker
5
+ import androidx.work.WorkerParameters
6
+
7
+ class UploadWorker(
8
+ appContext: Context,
9
+ workerParams: WorkerParameters,
10
+ ) : CoroutineWorker(appContext, workerParams) {
11
+ override suspend fun doWork(): Result {
12
+ val taskId = inputData.getString(UploadManager.WORK_INPUT_TASK_ID)
13
+ ?: return Result.failure()
14
+ return try {
15
+ val store = UploadTaskStore.getInstance(applicationContext)
16
+ store.task(taskId)?.let { task ->
17
+ setForeground(UploadNotificationHelper.foregroundInfo(applicationContext, task))
18
+ }
19
+ when (UploadManager.getInstance(applicationContext).executeUpload(taskId)) {
20
+ UploadExecutionResult.RETRY -> Result.retry()
21
+ UploadExecutionResult.FINISHED -> Result.success()
22
+ }
23
+ } catch (_: UploadCancelledException) {
24
+ Result.failure()
25
+ } catch (_: Exception) {
26
+ Result.failure()
27
+ }
28
+ }
29
+ }
30
+
31
+ class UploadCancelledException : Exception()