@viettelpost/react-native-ota 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 (56) hide show
  1. package/README.md +38 -0
  2. package/android/build.gradle +48 -0
  3. package/android/src/main/java/com/viettelpost/otakit/OTAHashUtils.kt +21 -0
  4. package/android/src/main/java/com/viettelpost/otakit/OTATestReceiver.kt +51 -0
  5. package/android/src/main/java/com/viettelpost/otakit/OTAUpdateBundleResolver.kt +405 -0
  6. package/android/src/main/java/com/viettelpost/otakit/OTAUpdateCleanup.kt +186 -0
  7. package/android/src/main/java/com/viettelpost/otakit/OTAUpdateDownloader.kt +649 -0
  8. package/android/src/main/java/com/viettelpost/otakit/OTAUpdateMetadata.kt +72 -0
  9. package/android/src/main/java/com/viettelpost/otakit/OTAUpdateModule.kt +140 -0
  10. package/android/src/main/java/com/viettelpost/otakit/OTAUpdatePackage.kt +30 -0
  11. package/android/src/main/java/com/viettelpost/otakit/OTAUpdateSignatureVerifier.kt +63 -0
  12. package/android/src/main/java/com/viettelpost/otakit/OTAUpdateStorage.kt +62 -0
  13. package/android/src/main/java/com/viettelpost/otakit/OTAZipUtils.kt +100 -0
  14. package/android/src/main/res/raw/ota_public_key.pem +9 -0
  15. package/bin/cli/assets-zip.js +77 -0
  16. package/bin/cli/bundle.js +72 -0
  17. package/bin/cli/deploy.js +224 -0
  18. package/bin/cli/sign.js +97 -0
  19. package/bin/cli/upload.js +109 -0
  20. package/bin/ota.js +200 -0
  21. package/docs/BACKEND_CONTRACT.md +93 -0
  22. package/docs/DEPLOY_CLI.md +39 -0
  23. package/docs/INTEGRATION_ANDROID.md +20 -0
  24. package/docs/INTEGRATION_IOS.md +21 -0
  25. package/docs/RELEASE_WORKFLOW.md +14 -0
  26. package/ios/OTAHashUtils.swift +22 -0
  27. package/ios/OTAUpdateBundleResolver.swift +359 -0
  28. package/ios/OTAUpdateCleanup.swift +269 -0
  29. package/ios/OTAUpdateDownloader.swift +709 -0
  30. package/ios/OTAUpdateMetadata.swift +47 -0
  31. package/ios/OTAUpdateModule.mm +190 -0
  32. package/ios/OTAUpdateSignatureVerifier.swift +81 -0
  33. package/ios/OTAUpdateStorage.swift +83 -0
  34. package/ios/OTAZipUtils.swift +103 -0
  35. package/ios/ota_public_key.pem +9 -0
  36. package/lib/NativeOTAUpdate.d.ts +77 -0
  37. package/lib/NativeOTAUpdate.js +59 -0
  38. package/lib/OTAClient.d.ts +27 -0
  39. package/lib/OTAClient.js +101 -0
  40. package/lib/config.d.ts +14 -0
  41. package/lib/config.js +29 -0
  42. package/lib/devtools.d.ts +10 -0
  43. package/lib/devtools.js +54 -0
  44. package/lib/index.d.ts +15 -0
  45. package/lib/index.js +32 -0
  46. package/lib/spec/NativeOTAUpdate.d.ts +16 -0
  47. package/lib/spec/NativeOTAUpdate.js +4 -0
  48. package/package.json +82 -0
  49. package/react-native-ota.podspec +21 -0
  50. package/scripts/run-bin.js +67 -0
  51. package/src/NativeOTAUpdate.ts +144 -0
  52. package/src/OTAClient.ts +151 -0
  53. package/src/config.ts +41 -0
  54. package/src/devtools.ts +64 -0
  55. package/src/index.ts +69 -0
  56. package/src/spec/NativeOTAUpdate.ts +21 -0
@@ -0,0 +1,186 @@
1
+ package com.viettelpost.otakit
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+ import org.json.JSONArray
6
+ import org.json.JSONObject
7
+ import java.io.File
8
+
9
+ object OTAUpdateCleanup {
10
+ private const val TAG = "OTAKit"
11
+ private const val EMBEDDED_VERSION = "embedded"
12
+ private const val BUNDLES_DIR_NAME = "bundles"
13
+ private const val TMP_DIR_NAME = "tmp"
14
+ private const val METADATA_FILE_NAME = "metadata.json"
15
+
16
+ fun cleanupAfterSuccessfulActivation(context: Context, activeVersion: String): Boolean {
17
+ if (isEmbeddedVersion(activeVersion)) {
18
+ return cleanupOrphanedBundles(context, emptySet())
19
+ }
20
+ return cleanupOrphanedBundles(context, setOf(activeVersion))
21
+ }
22
+
23
+ fun cleanupFailedPendingBundle(context: Context, failedVersion: String?): Boolean {
24
+ if (isEmbeddedVersion(failedVersion)) {
25
+ return true
26
+ }
27
+ val version = failedVersion.orEmpty()
28
+ val bundleDeleted = safeDeleteUnderOTARoot(context, bundleDirectory(context, version))
29
+ val tempDeleted = cleanupTemp(context, version)
30
+ return bundleDeleted && tempDeleted
31
+ }
32
+
33
+ fun cleanupTemp(context: Context, version: String?): Boolean {
34
+ if (version.isNullOrBlank()) {
35
+ return true
36
+ }
37
+ return safeDeleteUnderOTARoot(context, tempDirectory(context, version))
38
+ }
39
+
40
+ fun cleanupAllTemp(context: Context): Boolean =
41
+ safeDeleteChildrenUnderOTARoot(tmpRoot(context))
42
+
43
+ fun cleanupOrphanedBundles(context: Context, keepVersions: Set<String>): Boolean {
44
+ val normalizedKeepVersions = keepVersions
45
+ .filterNot { isEmbeddedVersion(it) }
46
+ .toSet()
47
+ return safeDeleteChildrenUnderOTARoot(bundleRoot(context)) { child ->
48
+ child.name !in normalizedKeepVersions
49
+ }
50
+ }
51
+
52
+ fun cleanupOTAStorage(context: Context): Boolean {
53
+ val metadata = OTAUpdateStorage.readMetadata(context)
54
+ val keepVersions = mutableSetOf<String>()
55
+ if (!isEmbeddedVersion(metadata.activeBundleVersion)) {
56
+ keepVersions.add(metadata.activeBundleVersion)
57
+ }
58
+ if (!isEmbeddedVersion(metadata.pendingBundleVersion)) {
59
+ keepVersions.add(metadata.pendingBundleVersion.orEmpty())
60
+ }
61
+
62
+ var didCleanup = cleanupAllTemp(context)
63
+ if (!isEmbeddedVersion(metadata.failedBundleVersion) &&
64
+ metadata.failedBundleVersion !in keepVersions
65
+ ) {
66
+ didCleanup = cleanupFailedPendingBundle(context, metadata.failedBundleVersion) && didCleanup
67
+ }
68
+ didCleanup = cleanupOrphanedBundles(context, keepVersions) && didCleanup
69
+ return didCleanup
70
+ }
71
+
72
+ fun getOTADiskUsage(context: Context): JSONObject {
73
+ val metadata = OTAUpdateStorage.readMetadata(context)
74
+ val otaRoot = OTAUpdateBundleResolver.otaDirectory(context)
75
+ val tmpRoot = tmpRoot(context)
76
+ val bundlesRoot = bundleRoot(context)
77
+ val bundles = JSONArray()
78
+
79
+ bundlesRoot.listFiles()
80
+ ?.filter { it.isDirectory }
81
+ ?.sortedBy { it.name }
82
+ ?.forEach { directory ->
83
+ bundles.put(
84
+ JSONObject().apply {
85
+ put("version", directory.name)
86
+ put("bytes", directorySize(directory))
87
+ put("isActive", directory.name == metadata.activeBundleVersion)
88
+ put("isPending", directory.name == metadata.pendingBundleVersion)
89
+ put("isFailed", directory.name == metadata.failedBundleVersion)
90
+ },
91
+ )
92
+ }
93
+
94
+ return JSONObject().apply {
95
+ put("otaRootPath", otaRoot.absolutePath)
96
+ put("totalBytes", directorySize(otaRoot))
97
+ put("tmpBytes", directorySize(tmpRoot))
98
+ put("bundlesBytes", directorySize(bundlesRoot))
99
+ put("bundles", bundles)
100
+ }
101
+ }
102
+
103
+ fun deleteFinalBundleForFailedInstall(context: Context, version: String?): Boolean {
104
+ if (isEmbeddedVersion(version)) {
105
+ return true
106
+ }
107
+ return safeDeleteUnderOTARoot(context, bundleDirectory(context, version.orEmpty()))
108
+ }
109
+
110
+ private fun safeDeleteChildrenUnderOTARoot(
111
+ directory: File,
112
+ shouldDelete: (File) -> Boolean = { true },
113
+ ): Boolean {
114
+ if (!directory.exists()) {
115
+ return true
116
+ }
117
+ val children = directory.listFiles() ?: return true
118
+ var didDeleteAll = true
119
+ children.forEach { child ->
120
+ if (shouldDelete(child)) {
121
+ didDeleteAll = safeDeleteUnderOTARoot(directory.parentFile ?: directory, child) && didDeleteAll
122
+ }
123
+ }
124
+ return didDeleteAll
125
+ }
126
+
127
+ private fun safeDeleteUnderOTARoot(context: Context, target: File): Boolean =
128
+ safeDeleteUnderOTARoot(OTAUpdateBundleResolver.otaDirectory(context), target)
129
+
130
+ private fun safeDeleteUnderOTARoot(otaRoot: File, target: File): Boolean {
131
+ if (!target.exists()) {
132
+ return true
133
+ }
134
+
135
+ return try {
136
+ val rootPath = otaRoot.canonicalFile.toPath()
137
+ val targetPath = target.canonicalFile.toPath()
138
+
139
+ // Cleanup must never remove metadata, the OTA root, or any path outside OTA storage.
140
+ if (target.name == METADATA_FILE_NAME || targetPath == rootPath || !targetPath.startsWith(rootPath)) {
141
+ Log.w(TAG, "OTA cleanup refused unsafe delete; path=${target.absolutePath}")
142
+ return false
143
+ }
144
+
145
+ if (!target.deleteRecursively()) {
146
+ Log.w(TAG, "OTA cleanup could not delete ${target.absolutePath}")
147
+ return false
148
+ }
149
+ Log.d(TAG, "OTA cleanup deleted ${target.absolutePath}")
150
+ true
151
+ } catch (error: Exception) {
152
+ Log.w(TAG, "OTA cleanup failed for ${target.absolutePath}", error)
153
+ false
154
+ }
155
+ }
156
+
157
+ private fun directorySize(file: File): Long {
158
+ if (!file.exists()) {
159
+ return 0L
160
+ }
161
+ if (file.isFile) {
162
+ return file.length()
163
+ }
164
+
165
+ var total = 0L
166
+ file.listFiles()?.forEach { child ->
167
+ total += directorySize(child)
168
+ }
169
+ return total
170
+ }
171
+
172
+ private fun bundleRoot(context: Context): File =
173
+ File(OTAUpdateBundleResolver.otaDirectory(context), BUNDLES_DIR_NAME)
174
+
175
+ private fun tmpRoot(context: Context): File =
176
+ File(OTAUpdateBundleResolver.otaDirectory(context), TMP_DIR_NAME)
177
+
178
+ private fun bundleDirectory(context: Context, version: String): File =
179
+ File(bundleRoot(context), version)
180
+
181
+ private fun tempDirectory(context: Context, version: String): File =
182
+ File(tmpRoot(context), version)
183
+
184
+ private fun isEmbeddedVersion(version: String?): Boolean =
185
+ version.isNullOrBlank() || version == EMBEDDED_VERSION
186
+ }