@sigx/lynx-updates 0.6.1 → 0.7.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/LICENSE +21 -21
- package/README.md +162 -162
- package/android/com/sigx/updates/UpdateDownloader.kt +154 -154
- package/android/com/sigx/updates/UpdateStore.kt +367 -367
- package/android/com/sigx/updates/UpdatesActivityHook.kt +25 -25
- package/android/com/sigx/updates/UpdatesBundleResolver.kt +18 -18
- package/android/com/sigx/updates/UpdatesEventBus.kt +54 -54
- package/android/com/sigx/updates/UpdatesLifecyclePublisher.kt +42 -42
- package/android/com/sigx/updates/UpdatesModule.kt +235 -235
- package/dist/controller.js +1 -1
- package/dist/controller.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/updates.d.ts +20 -11
- package/dist/updates.d.ts.map +1 -1
- package/dist/updates.js +22 -13
- package/dist/updates.js.map +1 -1
- package/ios/UpdateDownloader.swift +152 -152
- package/ios/UpdateStore.swift +286 -286
- package/ios/UpdatesBundleResolver.swift +15 -15
- package/ios/UpdatesEventBus.swift +59 -59
- package/ios/UpdatesLifecyclePublisher.swift +48 -48
- package/ios/UpdatesModule.swift +178 -178
- package/package.json +3 -3
- package/signalx-module.json +35 -35
|
@@ -1,154 +1,154 @@
|
|
|
1
|
-
package com.sigx.updates
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.util.Log
|
|
5
|
-
import java.io.File
|
|
6
|
-
import java.io.FileOutputStream
|
|
7
|
-
import java.net.HttpURLConnection
|
|
8
|
-
import java.net.URL
|
|
9
|
-
import java.security.MessageDigest
|
|
10
|
-
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
|
-
import org.json.JSONObject
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Streaming bundle downloader: bytes go straight to `tmp/<id>.partial` with
|
|
15
|
-
* an incremental SHA-256, then atomically move into `updates/<id>/` once the
|
|
16
|
-
* hash matches. Single-flight — concurrent calls beyond the first fail fast.
|
|
17
|
-
*/
|
|
18
|
-
object UpdateDownloader {
|
|
19
|
-
|
|
20
|
-
private const val TAG = "SigxUpdates"
|
|
21
|
-
private const val CONNECT_TIMEOUT_MS = 15_000
|
|
22
|
-
private const val READ_TIMEOUT_MS = 30_000
|
|
23
|
-
private const val PROGRESS_INTERVAL_MS = 150L
|
|
24
|
-
|
|
25
|
-
private val inFlight = AtomicBoolean(false)
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @return null on success, or an error message. `code` semantics ride in
|
|
29
|
-
* the message prefix; the module maps them to bridge errors.
|
|
30
|
-
*/
|
|
31
|
-
fun download(
|
|
32
|
-
context: Context,
|
|
33
|
-
url: String,
|
|
34
|
-
expectedSha256: String,
|
|
35
|
-
updateId: String,
|
|
36
|
-
headers: Map<String, String>,
|
|
37
|
-
manifestJson: String,
|
|
38
|
-
): String? {
|
|
39
|
-
// Already on disk and intact → success without a byte transferred.
|
|
40
|
-
if (UpdateStore.bundleFile(context, updateId).exists() &&
|
|
41
|
-
UpdateStore.verifySha256(context, updateId)
|
|
42
|
-
) {
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!inFlight.compareAndSet(false, true)) {
|
|
47
|
-
return "E_DOWNLOAD_IN_PROGRESS: another download is running"
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
return downloadLocked(context, url, expectedSha256, updateId, headers, manifestJson)
|
|
51
|
-
} finally {
|
|
52
|
-
inFlight.set(false)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
private fun downloadLocked(
|
|
57
|
-
context: Context,
|
|
58
|
-
url: String,
|
|
59
|
-
expectedSha256: String,
|
|
60
|
-
updateId: String,
|
|
61
|
-
headers: Map<String, String>,
|
|
62
|
-
manifestJson: String,
|
|
63
|
-
): String? {
|
|
64
|
-
val tmpDir = UpdateStore.tmpDir(context)
|
|
65
|
-
tmpDir.mkdirs()
|
|
66
|
-
val partial = File(tmpDir, "$updateId.partial")
|
|
67
|
-
|
|
68
|
-
var connection: HttpURLConnection? = null
|
|
69
|
-
try {
|
|
70
|
-
connection = URL(url).openConnection() as HttpURLConnection
|
|
71
|
-
connection.connectTimeout = CONNECT_TIMEOUT_MS
|
|
72
|
-
connection.readTimeout = READ_TIMEOUT_MS
|
|
73
|
-
connection.instanceFollowRedirects = true
|
|
74
|
-
for ((key, value) in headers) {
|
|
75
|
-
connection.setRequestProperty(key, value)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
val status = connection.responseCode
|
|
79
|
-
if (status !in 200..299) {
|
|
80
|
-
return "Download failed: HTTP $status"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
val totalBytes = connection.contentLengthLong.takeIf { it >= 0 }
|
|
84
|
-
val digest = MessageDigest.getInstance("SHA-256")
|
|
85
|
-
var receivedBytes = 0L
|
|
86
|
-
var lastProgressAt = 0L
|
|
87
|
-
|
|
88
|
-
connection.inputStream.use { input ->
|
|
89
|
-
FileOutputStream(partial).use { out ->
|
|
90
|
-
val buffer = ByteArray(64 * 1024)
|
|
91
|
-
while (true) {
|
|
92
|
-
val read = input.read(buffer)
|
|
93
|
-
if (read < 0) break
|
|
94
|
-
out.write(buffer, 0, read)
|
|
95
|
-
digest.update(buffer, 0, read)
|
|
96
|
-
receivedBytes += read
|
|
97
|
-
val now = System.currentTimeMillis()
|
|
98
|
-
if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {
|
|
99
|
-
lastProgressAt = now
|
|
100
|
-
UpdatesEventBus.emitProgress(receivedBytes, totalBytes)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
out.fd.sync()
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
UpdatesEventBus.emitProgress(receivedBytes, totalBytes ?: receivedBytes)
|
|
107
|
-
|
|
108
|
-
val actual = digest.digest().joinToString("") { "%02x".format(it) }
|
|
109
|
-
if (!actual.equals(expectedSha256, ignoreCase = true)) {
|
|
110
|
-
partial.delete()
|
|
111
|
-
return "E_HASH_MISMATCH: expected $expectedSha256, got $actual"
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Atomic-ish promote: write metadata first, bundle rename last.
|
|
115
|
-
val dir = UpdateStore.updateDir(context, updateId)
|
|
116
|
-
dir.deleteRecursively()
|
|
117
|
-
dir.mkdirs()
|
|
118
|
-
val meta = try {
|
|
119
|
-
JSONObject(manifestJson)
|
|
120
|
-
} catch (e: Exception) {
|
|
121
|
-
JSONObject()
|
|
122
|
-
}
|
|
123
|
-
meta.put("sha256", expectedSha256.lowercase())
|
|
124
|
-
meta.put("sizeBytes", receivedBytes)
|
|
125
|
-
meta.put("sourceUrl", url)
|
|
126
|
-
meta.put("downloadedAt", System.currentTimeMillis())
|
|
127
|
-
// Atomic + fsynced: a truncated update.json would fail
|
|
128
|
-
// verifySha256() on next launch and roll back a perfectly good
|
|
129
|
-
// bundle as "corrupt".
|
|
130
|
-
val metaFile = UpdateStore.updateJsonFile(context, updateId)
|
|
131
|
-
val metaTmp = File(dir, "update.json.tmp")
|
|
132
|
-
FileOutputStream(metaTmp).use { out ->
|
|
133
|
-
out.write(meta.toString().toByteArray(Charsets.UTF_8))
|
|
134
|
-
out.fd.sync()
|
|
135
|
-
}
|
|
136
|
-
if (!metaTmp.renameTo(metaFile)) {
|
|
137
|
-
metaFile.delete()
|
|
138
|
-
metaTmp.renameTo(metaFile)
|
|
139
|
-
}
|
|
140
|
-
val bundle = UpdateStore.bundleFile(context, updateId)
|
|
141
|
-
if (!partial.renameTo(bundle)) {
|
|
142
|
-
partial.copyTo(bundle, overwrite = true)
|
|
143
|
-
partial.delete()
|
|
144
|
-
}
|
|
145
|
-
Log.i(TAG, "Downloaded update $updateId ($receivedBytes bytes)")
|
|
146
|
-
return null
|
|
147
|
-
} catch (e: Exception) {
|
|
148
|
-
partial.delete()
|
|
149
|
-
return "Download failed: ${e.message}"
|
|
150
|
-
} finally {
|
|
151
|
-
connection?.disconnect()
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
1
|
+
package com.sigx.updates
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import java.io.File
|
|
6
|
+
import java.io.FileOutputStream
|
|
7
|
+
import java.net.HttpURLConnection
|
|
8
|
+
import java.net.URL
|
|
9
|
+
import java.security.MessageDigest
|
|
10
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
11
|
+
import org.json.JSONObject
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Streaming bundle downloader: bytes go straight to `tmp/<id>.partial` with
|
|
15
|
+
* an incremental SHA-256, then atomically move into `updates/<id>/` once the
|
|
16
|
+
* hash matches. Single-flight — concurrent calls beyond the first fail fast.
|
|
17
|
+
*/
|
|
18
|
+
object UpdateDownloader {
|
|
19
|
+
|
|
20
|
+
private const val TAG = "SigxUpdates"
|
|
21
|
+
private const val CONNECT_TIMEOUT_MS = 15_000
|
|
22
|
+
private const val READ_TIMEOUT_MS = 30_000
|
|
23
|
+
private const val PROGRESS_INTERVAL_MS = 150L
|
|
24
|
+
|
|
25
|
+
private val inFlight = AtomicBoolean(false)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @return null on success, or an error message. `code` semantics ride in
|
|
29
|
+
* the message prefix; the module maps them to bridge errors.
|
|
30
|
+
*/
|
|
31
|
+
fun download(
|
|
32
|
+
context: Context,
|
|
33
|
+
url: String,
|
|
34
|
+
expectedSha256: String,
|
|
35
|
+
updateId: String,
|
|
36
|
+
headers: Map<String, String>,
|
|
37
|
+
manifestJson: String,
|
|
38
|
+
): String? {
|
|
39
|
+
// Already on disk and intact → success without a byte transferred.
|
|
40
|
+
if (UpdateStore.bundleFile(context, updateId).exists() &&
|
|
41
|
+
UpdateStore.verifySha256(context, updateId)
|
|
42
|
+
) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!inFlight.compareAndSet(false, true)) {
|
|
47
|
+
return "E_DOWNLOAD_IN_PROGRESS: another download is running"
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
return downloadLocked(context, url, expectedSha256, updateId, headers, manifestJson)
|
|
51
|
+
} finally {
|
|
52
|
+
inFlight.set(false)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun downloadLocked(
|
|
57
|
+
context: Context,
|
|
58
|
+
url: String,
|
|
59
|
+
expectedSha256: String,
|
|
60
|
+
updateId: String,
|
|
61
|
+
headers: Map<String, String>,
|
|
62
|
+
manifestJson: String,
|
|
63
|
+
): String? {
|
|
64
|
+
val tmpDir = UpdateStore.tmpDir(context)
|
|
65
|
+
tmpDir.mkdirs()
|
|
66
|
+
val partial = File(tmpDir, "$updateId.partial")
|
|
67
|
+
|
|
68
|
+
var connection: HttpURLConnection? = null
|
|
69
|
+
try {
|
|
70
|
+
connection = URL(url).openConnection() as HttpURLConnection
|
|
71
|
+
connection.connectTimeout = CONNECT_TIMEOUT_MS
|
|
72
|
+
connection.readTimeout = READ_TIMEOUT_MS
|
|
73
|
+
connection.instanceFollowRedirects = true
|
|
74
|
+
for ((key, value) in headers) {
|
|
75
|
+
connection.setRequestProperty(key, value)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
val status = connection.responseCode
|
|
79
|
+
if (status !in 200..299) {
|
|
80
|
+
return "Download failed: HTTP $status"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
val totalBytes = connection.contentLengthLong.takeIf { it >= 0 }
|
|
84
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
85
|
+
var receivedBytes = 0L
|
|
86
|
+
var lastProgressAt = 0L
|
|
87
|
+
|
|
88
|
+
connection.inputStream.use { input ->
|
|
89
|
+
FileOutputStream(partial).use { out ->
|
|
90
|
+
val buffer = ByteArray(64 * 1024)
|
|
91
|
+
while (true) {
|
|
92
|
+
val read = input.read(buffer)
|
|
93
|
+
if (read < 0) break
|
|
94
|
+
out.write(buffer, 0, read)
|
|
95
|
+
digest.update(buffer, 0, read)
|
|
96
|
+
receivedBytes += read
|
|
97
|
+
val now = System.currentTimeMillis()
|
|
98
|
+
if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) {
|
|
99
|
+
lastProgressAt = now
|
|
100
|
+
UpdatesEventBus.emitProgress(receivedBytes, totalBytes)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
out.fd.sync()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
UpdatesEventBus.emitProgress(receivedBytes, totalBytes ?: receivedBytes)
|
|
107
|
+
|
|
108
|
+
val actual = digest.digest().joinToString("") { "%02x".format(it) }
|
|
109
|
+
if (!actual.equals(expectedSha256, ignoreCase = true)) {
|
|
110
|
+
partial.delete()
|
|
111
|
+
return "E_HASH_MISMATCH: expected $expectedSha256, got $actual"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Atomic-ish promote: write metadata first, bundle rename last.
|
|
115
|
+
val dir = UpdateStore.updateDir(context, updateId)
|
|
116
|
+
dir.deleteRecursively()
|
|
117
|
+
dir.mkdirs()
|
|
118
|
+
val meta = try {
|
|
119
|
+
JSONObject(manifestJson)
|
|
120
|
+
} catch (e: Exception) {
|
|
121
|
+
JSONObject()
|
|
122
|
+
}
|
|
123
|
+
meta.put("sha256", expectedSha256.lowercase())
|
|
124
|
+
meta.put("sizeBytes", receivedBytes)
|
|
125
|
+
meta.put("sourceUrl", url)
|
|
126
|
+
meta.put("downloadedAt", System.currentTimeMillis())
|
|
127
|
+
// Atomic + fsynced: a truncated update.json would fail
|
|
128
|
+
// verifySha256() on next launch and roll back a perfectly good
|
|
129
|
+
// bundle as "corrupt".
|
|
130
|
+
val metaFile = UpdateStore.updateJsonFile(context, updateId)
|
|
131
|
+
val metaTmp = File(dir, "update.json.tmp")
|
|
132
|
+
FileOutputStream(metaTmp).use { out ->
|
|
133
|
+
out.write(meta.toString().toByteArray(Charsets.UTF_8))
|
|
134
|
+
out.fd.sync()
|
|
135
|
+
}
|
|
136
|
+
if (!metaTmp.renameTo(metaFile)) {
|
|
137
|
+
metaFile.delete()
|
|
138
|
+
metaTmp.renameTo(metaFile)
|
|
139
|
+
}
|
|
140
|
+
val bundle = UpdateStore.bundleFile(context, updateId)
|
|
141
|
+
if (!partial.renameTo(bundle)) {
|
|
142
|
+
partial.copyTo(bundle, overwrite = true)
|
|
143
|
+
partial.delete()
|
|
144
|
+
}
|
|
145
|
+
Log.i(TAG, "Downloaded update $updateId ($receivedBytes bytes)")
|
|
146
|
+
return null
|
|
147
|
+
} catch (e: Exception) {
|
|
148
|
+
partial.delete()
|
|
149
|
+
return "Download failed: ${e.message}"
|
|
150
|
+
} finally {
|
|
151
|
+
connection?.disconnect()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|