@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.
- package/README.md +38 -0
- package/android/build.gradle +48 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAHashUtils.kt +21 -0
- package/android/src/main/java/com/viettelpost/otakit/OTATestReceiver.kt +51 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateBundleResolver.kt +405 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateCleanup.kt +186 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateDownloader.kt +649 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateMetadata.kt +72 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateModule.kt +140 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdatePackage.kt +30 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateSignatureVerifier.kt +63 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAUpdateStorage.kt +62 -0
- package/android/src/main/java/com/viettelpost/otakit/OTAZipUtils.kt +100 -0
- package/android/src/main/res/raw/ota_public_key.pem +9 -0
- package/bin/cli/assets-zip.js +77 -0
- package/bin/cli/bundle.js +72 -0
- package/bin/cli/deploy.js +224 -0
- package/bin/cli/sign.js +97 -0
- package/bin/cli/upload.js +109 -0
- package/bin/ota.js +200 -0
- package/docs/BACKEND_CONTRACT.md +93 -0
- package/docs/DEPLOY_CLI.md +39 -0
- package/docs/INTEGRATION_ANDROID.md +20 -0
- package/docs/INTEGRATION_IOS.md +21 -0
- package/docs/RELEASE_WORKFLOW.md +14 -0
- package/ios/OTAHashUtils.swift +22 -0
- package/ios/OTAUpdateBundleResolver.swift +359 -0
- package/ios/OTAUpdateCleanup.swift +269 -0
- package/ios/OTAUpdateDownloader.swift +709 -0
- package/ios/OTAUpdateMetadata.swift +47 -0
- package/ios/OTAUpdateModule.mm +190 -0
- package/ios/OTAUpdateSignatureVerifier.swift +81 -0
- package/ios/OTAUpdateStorage.swift +83 -0
- package/ios/OTAZipUtils.swift +103 -0
- package/ios/ota_public_key.pem +9 -0
- package/lib/NativeOTAUpdate.d.ts +77 -0
- package/lib/NativeOTAUpdate.js +59 -0
- package/lib/OTAClient.d.ts +27 -0
- package/lib/OTAClient.js +101 -0
- package/lib/config.d.ts +14 -0
- package/lib/config.js +29 -0
- package/lib/devtools.d.ts +10 -0
- package/lib/devtools.js +54 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +32 -0
- package/lib/spec/NativeOTAUpdate.d.ts +16 -0
- package/lib/spec/NativeOTAUpdate.js +4 -0
- package/package.json +82 -0
- package/react-native-ota.podspec +21 -0
- package/scripts/run-bin.js +67 -0
- package/src/NativeOTAUpdate.ts +144 -0
- package/src/OTAClient.ts +151 -0
- package/src/config.ts +41 -0
- package/src/devtools.ts +64 -0
- package/src/index.ts +69 -0
- 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
|
+
}
|