@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,269 @@
1
+ import Foundation
2
+
3
+ struct OTADiskUsageBundle: Codable {
4
+ let version: String
5
+ let bytes: UInt64
6
+ let isActive: Bool
7
+ let isPending: Bool
8
+ let isFailed: Bool
9
+ }
10
+
11
+ struct OTADiskUsage: Codable {
12
+ let otaRootPath: String
13
+ let totalBytes: UInt64
14
+ let tmpBytes: UInt64
15
+ let bundlesBytes: UInt64
16
+ let bundles: [OTADiskUsageBundle]
17
+ }
18
+
19
+ @objc(OTAUpdateCleanup)
20
+ final class OTAUpdateCleanup: NSObject {
21
+ @objc static let shared = OTAUpdateCleanup()
22
+
23
+ private let fileManager: FileManager
24
+ private let encoder: JSONEncoder
25
+ private let storage: OTAUpdateStorage
26
+
27
+ private let embeddedVersion = "embedded"
28
+ private let metadataFileName = "metadata.json"
29
+
30
+ override init() {
31
+ fileManager = .default
32
+ encoder = JSONEncoder()
33
+ storage = OTAUpdateStorage.shared
34
+ super.init()
35
+ }
36
+
37
+ func cleanupAfterSuccessfulActivation(activeVersion: String) throws -> Bool {
38
+ if isEmbeddedVersion(activeVersion) {
39
+ return try cleanupOrphanedBundles(keepVersions: [])
40
+ }
41
+ return try cleanupOrphanedBundles(keepVersions: [activeVersion])
42
+ }
43
+
44
+ func cleanupFailedPendingBundle(failedVersion: String?) throws -> Bool {
45
+ guard let failedVersion = failedVersion, !isEmbeddedVersion(failedVersion) else {
46
+ return true
47
+ }
48
+
49
+ let bundleDeleted = try safeDeleteUnderOTARoot(bundleDirectoryURL(version: failedVersion))
50
+ let tempDeleted = try cleanupTemp(version: failedVersion)
51
+ return bundleDeleted && tempDeleted
52
+ }
53
+
54
+ func cleanupTemp(version: String?) throws -> Bool {
55
+ guard let version = version, !version.isEmpty else {
56
+ return true
57
+ }
58
+ return try safeDeleteUnderOTARoot(tempDirectoryURL(version: version))
59
+ }
60
+
61
+ func cleanupAllTemp() throws -> Bool {
62
+ try safeDeleteChildrenUnderOTARoot(tmpRootURL())
63
+ }
64
+
65
+ func cleanupOrphanedBundles(keepVersions: Set<String>) throws -> Bool {
66
+ let normalizedKeepVersions = Set(keepVersions.filter { !isEmbeddedVersion($0) })
67
+ return try safeDeleteChildrenUnderOTARoot(bundleRootURL()) { childURL in
68
+ !normalizedKeepVersions.contains(childURL.lastPathComponent)
69
+ }
70
+ }
71
+
72
+ @objc func cleanupOTAStorage() throws -> NSNumber {
73
+ let metadata = try storage.readMetadata()
74
+ var keepVersions = Set<String>()
75
+ if !isEmbeddedVersion(metadata.activeBundleVersion) {
76
+ keepVersions.insert(metadata.activeBundleVersion)
77
+ }
78
+ if let pendingVersion = metadata.pendingBundleVersion, !isEmbeddedVersion(pendingVersion) {
79
+ keepVersions.insert(pendingVersion)
80
+ }
81
+
82
+ var didCleanup = try cleanupAllTemp()
83
+ if let failedVersion = metadata.failedBundleVersion,
84
+ !isEmbeddedVersion(failedVersion),
85
+ !keepVersions.contains(failedVersion) {
86
+ didCleanup = (try cleanupFailedPendingBundle(failedVersion: failedVersion)) && didCleanup
87
+ }
88
+ didCleanup = (try cleanupOrphanedBundles(keepVersions: keepVersions)) && didCleanup
89
+ return didCleanup as NSNumber
90
+ }
91
+
92
+ @objc func getOTADiskUsageJSONString() throws -> String {
93
+ let usage = try getOTADiskUsage()
94
+ let data = try encoder.encode(usage)
95
+ guard let json = String(data: data, encoding: .utf8) else {
96
+ throw OTAUpdateCleanupError.jsonEncodingFailed
97
+ }
98
+ return json
99
+ }
100
+
101
+ func deleteFinalBundleForFailedInstall(version: String?) throws -> Bool {
102
+ guard let version = version, !isEmbeddedVersion(version) else {
103
+ return true
104
+ }
105
+ return try safeDeleteUnderOTARoot(bundleDirectoryURL(version: version))
106
+ }
107
+
108
+ private func getOTADiskUsage() throws -> OTADiskUsage {
109
+ let metadata = try storage.readMetadata()
110
+ let otaRoot = try otaDirectoryURL()
111
+ let tmpRoot = try tmpRootURL()
112
+ let bundlesRoot = try bundleRootURL()
113
+ let bundleURLs = (try? fileManager.contentsOfDirectory(
114
+ at: bundlesRoot,
115
+ includingPropertiesForKeys: [.isDirectoryKey],
116
+ options: [.skipsHiddenFiles]
117
+ )) ?? []
118
+
119
+ let bundles = bundleURLs
120
+ .filter { isDirectory($0) }
121
+ .sorted { $0.lastPathComponent < $1.lastPathComponent }
122
+ .map { bundleURL in
123
+ OTADiskUsageBundle(
124
+ version: bundleURL.lastPathComponent,
125
+ bytes: directorySize(bundleURL),
126
+ isActive: bundleURL.lastPathComponent == metadata.activeBundleVersion,
127
+ isPending: bundleURL.lastPathComponent == metadata.pendingBundleVersion,
128
+ isFailed: bundleURL.lastPathComponent == metadata.failedBundleVersion
129
+ )
130
+ }
131
+
132
+ return OTADiskUsage(
133
+ otaRootPath: otaRoot.path,
134
+ totalBytes: directorySize(otaRoot),
135
+ tmpBytes: directorySize(tmpRoot),
136
+ bundlesBytes: directorySize(bundlesRoot),
137
+ bundles: bundles
138
+ )
139
+ }
140
+
141
+ private func safeDeleteChildrenUnderOTARoot(
142
+ _ directoryURL: URL,
143
+ shouldDelete: (URL) -> Bool = { _ in true }
144
+ ) throws -> Bool {
145
+ guard fileManager.fileExists(atPath: directoryURL.path) else {
146
+ return true
147
+ }
148
+
149
+ let children = (try? fileManager.contentsOfDirectory(
150
+ at: directoryURL,
151
+ includingPropertiesForKeys: nil,
152
+ options: []
153
+ )) ?? []
154
+
155
+ var didDeleteAll = true
156
+ for child in children where shouldDelete(child) {
157
+ didDeleteAll = (try safeDeleteUnderOTARoot(child)) && didDeleteAll
158
+ }
159
+ return didDeleteAll
160
+ }
161
+
162
+ private func safeDeleteUnderOTARoot(_ targetURL: URL) throws -> Bool {
163
+ guard fileManager.fileExists(atPath: targetURL.path) else {
164
+ return true
165
+ }
166
+
167
+ let rootURL = try otaDirectoryURL().standardizedFileURL.resolvingSymlinksInPath()
168
+ let targetURL = targetURL.standardizedFileURL.resolvingSymlinksInPath()
169
+ let rootPath = rootURL.path
170
+ let targetPath = targetURL.path
171
+
172
+ // Cleanup must never remove metadata, the OTA root, or any path outside OTA storage.
173
+ if targetURL.lastPathComponent == metadataFileName ||
174
+ targetPath == rootPath ||
175
+ !targetPath.hasPrefix(rootPath + "/") {
176
+ NSLog("OTAKit cleanup refused unsafe delete; path=%@", targetPath)
177
+ return false
178
+ }
179
+
180
+ do {
181
+ try fileManager.removeItem(at: targetURL)
182
+ NSLog("OTAKit cleanup deleted %@", targetPath)
183
+ return true
184
+ } catch {
185
+ NSLog("OTAKit cleanup failed for %@: %@", targetPath, error.localizedDescription)
186
+ return false
187
+ }
188
+ }
189
+
190
+ private func directorySize(_ url: URL) -> UInt64 {
191
+ var isDirectory: ObjCBool = false
192
+ guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) else {
193
+ return 0
194
+ }
195
+ if !isDirectory.boolValue {
196
+ return fileSize(url)
197
+ }
198
+
199
+ guard let enumerator = fileManager.enumerator(
200
+ at: url,
201
+ includingPropertiesForKeys: [.fileSizeKey, .isRegularFileKey],
202
+ options: [],
203
+ errorHandler: { _, _ in true }
204
+ ) else {
205
+ return 0
206
+ }
207
+
208
+ var totalSize: UInt64 = 0
209
+ for case let fileURL as URL in enumerator {
210
+ let values = try? fileURL.resourceValues(forKeys: [.fileSizeKey, .isRegularFileKey])
211
+ if values?.isRegularFile == true, let fileSize = values?.fileSize {
212
+ totalSize += UInt64(fileSize)
213
+ }
214
+ }
215
+ return totalSize
216
+ }
217
+
218
+ private func fileSize(_ url: URL) -> UInt64 {
219
+ guard let attributes = try? fileManager.attributesOfItem(atPath: url.path),
220
+ let size = attributes[.size] as? NSNumber else {
221
+ return 0
222
+ }
223
+ return size.uint64Value
224
+ }
225
+
226
+ private func isDirectory(_ url: URL) -> Bool {
227
+ var isDirectory: ObjCBool = false
228
+ return fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) && isDirectory.boolValue
229
+ }
230
+
231
+ private func bundleRootURL() throws -> URL {
232
+ try otaDirectoryURL().appendingPathComponent("bundles", isDirectory: true)
233
+ }
234
+
235
+ private func tmpRootURL() throws -> URL {
236
+ try otaDirectoryURL().appendingPathComponent("tmp", isDirectory: true)
237
+ }
238
+
239
+ private func bundleDirectoryURL(version: String) throws -> URL {
240
+ try bundleRootURL().appendingPathComponent(version, isDirectory: true)
241
+ }
242
+
243
+ private func tempDirectoryURL(version: String) throws -> URL {
244
+ try tmpRootURL().appendingPathComponent(version, isDirectory: true)
245
+ }
246
+
247
+ private func otaDirectoryURL() throws -> URL {
248
+ let supportURL = try fileManager.url(
249
+ for: .applicationSupportDirectory,
250
+ in: .userDomainMask,
251
+ appropriateFor: nil,
252
+ create: true
253
+ )
254
+ let otaURL = supportURL.appendingPathComponent("OTA", isDirectory: true)
255
+ try fileManager.createDirectory(at: otaURL, withIntermediateDirectories: true, attributes: nil)
256
+ return otaURL
257
+ }
258
+
259
+ private func isEmbeddedVersion(_ version: String?) -> Bool {
260
+ guard let version = version, !version.isEmpty else {
261
+ return true
262
+ }
263
+ return version == embeddedVersion
264
+ }
265
+ }
266
+
267
+ enum OTAUpdateCleanupError: Error {
268
+ case jsonEncodingFailed
269
+ }