pinokiod 8.0.76 → 8.0.78
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/kernel/vault/automatic_scans.js +49 -16
- package/kernel/vault/registry_core.js +23 -4
- package/kernel/vault/sweeper.js +6 -1
- package/package.json +1 -1
- package/server/public/vault.css +5 -1
- package/test/vault-automatic-scans.test.js +153 -8
- package/test/vault-engine.test.js +7 -0
- package/test/vault-ui.test.js +2 -0
|
@@ -32,6 +32,12 @@ const pathKey = (value) => {
|
|
|
32
32
|
return process.platform === "win32" ? resolved.toLowerCase() : resolved
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const metadataKey = (entry) => [
|
|
36
|
+
Number(entry.mode) & 0o7777,
|
|
37
|
+
Number(entry.uid),
|
|
38
|
+
Number(entry.gid)
|
|
39
|
+
].join(":")
|
|
40
|
+
|
|
35
41
|
class AutomaticScans {
|
|
36
42
|
constructor(vault) {
|
|
37
43
|
this.vault = vault
|
|
@@ -270,7 +276,7 @@ class AutomaticScans {
|
|
|
270
276
|
if (!collected.size) this.discardChangedPaths(app)
|
|
271
277
|
}
|
|
272
278
|
|
|
273
|
-
async
|
|
279
|
+
async candidatePolicy() {
|
|
274
280
|
const cached = this.vault.lastScanCache &&
|
|
275
281
|
typeof this.vault.lastScanCache.get === "function"
|
|
276
282
|
? this.vault.lastScanCache.get("")
|
|
@@ -279,9 +285,24 @@ class AutomaticScans {
|
|
|
279
285
|
? await this.vault.registry.scanFor()
|
|
280
286
|
: null)
|
|
281
287
|
const threshold = scan ? Number(scan.candidate_min_bytes) : NaN
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
288
|
+
const hasLinkableDevices = !!(
|
|
289
|
+
scan && Array.isArray(scan.linkable_devices))
|
|
290
|
+
return {
|
|
291
|
+
threshold: Number.isFinite(threshold) && threshold >= 0
|
|
292
|
+
? threshold
|
|
293
|
+
: SIZE_THRESHOLD,
|
|
294
|
+
linkability_known: hasLinkableDevices,
|
|
295
|
+
linkable_devices: new Set(hasLinkableDevices
|
|
296
|
+
? scan.linkable_devices
|
|
297
|
+
.map((dev) => Number(dev))
|
|
298
|
+
.filter((dev) => Number.isFinite(dev))
|
|
299
|
+
: [])
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async candidateThreshold(policy = null) {
|
|
304
|
+
const current = policy || await this.candidatePolicy()
|
|
305
|
+
return current.threshold
|
|
285
306
|
}
|
|
286
307
|
|
|
287
308
|
statChangedPaths(paths, options, onSettled = null) {
|
|
@@ -910,7 +931,8 @@ class AutomaticScans {
|
|
|
910
931
|
}
|
|
911
932
|
this.checkpoint(active)
|
|
912
933
|
if (!current.isFile() || current.isSymbolicLink() ||
|
|
913
|
-
current.dev !== entry.dev || current.size !== entry.size
|
|
934
|
+
current.dev !== entry.dev || current.size !== entry.size ||
|
|
935
|
+
metadataKey(current) !== metadataKey(entry)) {
|
|
914
936
|
this.logVerificationSkip(
|
|
915
937
|
active.app, entry, "snapshot-changed", counts)
|
|
916
938
|
return null
|
|
@@ -1016,8 +1038,10 @@ class AutomaticScans {
|
|
|
1016
1038
|
hash_failures: 0,
|
|
1017
1039
|
unstable_hashes: 0
|
|
1018
1040
|
}
|
|
1019
|
-
markStage("
|
|
1020
|
-
const
|
|
1041
|
+
markStage("policy-read")
|
|
1042
|
+
const policy = await this.candidatePolicy()
|
|
1043
|
+
const threshold = await this.candidateThreshold(policy)
|
|
1044
|
+
const linkableDevices = policy.linkable_devices
|
|
1021
1045
|
const paths = [...new Set(active.paths || [])].filter((filePath) =>
|
|
1022
1046
|
typeof filePath === "string" && inside(root, filePath))
|
|
1023
1047
|
const verifiedHashes = new Map()
|
|
@@ -1025,8 +1049,10 @@ class AutomaticScans {
|
|
|
1025
1049
|
app,
|
|
1026
1050
|
root,
|
|
1027
1051
|
threshold_bytes: threshold,
|
|
1052
|
+
linkability_known: policy.linkability_known,
|
|
1053
|
+
linkable_devices: [...linkableDevices],
|
|
1028
1054
|
changed_paths: paths.length,
|
|
1029
|
-
policy: "metadata-prefilter-sha256"
|
|
1055
|
+
policy: "eligibility-metadata-prefilter-sha256"
|
|
1030
1056
|
})
|
|
1031
1057
|
markStage("staging-reset")
|
|
1032
1058
|
await this.vault.registry.beginAutomaticPrecheck(app)
|
|
@@ -1065,7 +1091,9 @@ class AutomaticScans {
|
|
|
1065
1091
|
if (!stat || !stat.isFile() || stat.isSymbolicLink()) continue
|
|
1066
1092
|
counts.files += 1
|
|
1067
1093
|
counts.bytes += Math.max(0, Number(stat.size) || 0)
|
|
1068
|
-
if (!isCandidateFileSize(stat.size, threshold)
|
|
1094
|
+
if (!isCandidateFileSize(stat.size, threshold) ||
|
|
1095
|
+
(policy.linkability_known &&
|
|
1096
|
+
!linkableDevices.has(stat.dev))) continue
|
|
1069
1097
|
candidates.push({
|
|
1070
1098
|
path: paths[index],
|
|
1071
1099
|
size: stat.size,
|
|
@@ -1119,8 +1147,8 @@ class AutomaticScans {
|
|
|
1119
1147
|
const preparePeers = () => {
|
|
1120
1148
|
if (!group || group.unmatched) return
|
|
1121
1149
|
group.unmatched = new Map()
|
|
1122
|
-
for (const [
|
|
1123
|
-
group.unmatched.set(
|
|
1150
|
+
for (const [match, value] of group.candidatesByMatch) {
|
|
1151
|
+
group.unmatched.set(match, {
|
|
1124
1152
|
identity: value.identities.values().next().value,
|
|
1125
1153
|
candidate: value.candidate
|
|
1126
1154
|
})
|
|
@@ -1149,10 +1177,11 @@ class AutomaticScans {
|
|
|
1149
1177
|
)
|
|
1150
1178
|
if (!verified) continue
|
|
1151
1179
|
const hash = verified.hash
|
|
1152
|
-
|
|
1180
|
+
const match = `${hash}\0${metadataKey(verified)}`
|
|
1181
|
+
let value = group.candidatesByMatch.get(match)
|
|
1153
1182
|
if (!value) {
|
|
1154
1183
|
value = { candidate: verified, identities: new Set() }
|
|
1155
|
-
group.
|
|
1184
|
+
group.candidatesByMatch.set(match, value)
|
|
1156
1185
|
}
|
|
1157
1186
|
value.identities.add(this.automaticIdentityKey(verified))
|
|
1158
1187
|
if (value.identities.size > 1) {
|
|
@@ -1197,7 +1226,7 @@ class AutomaticScans {
|
|
|
1197
1226
|
key,
|
|
1198
1227
|
candidates: [],
|
|
1199
1228
|
candidatesHashed: false,
|
|
1200
|
-
|
|
1229
|
+
candidatesByMatch: new Map(),
|
|
1201
1230
|
unmatched: null,
|
|
1202
1231
|
memory: new Map()
|
|
1203
1232
|
}
|
|
@@ -1222,13 +1251,17 @@ class AutomaticScans {
|
|
|
1222
1251
|
if (!await hashCandidates(identity)) continue
|
|
1223
1252
|
if (proof) return await complete()
|
|
1224
1253
|
if (!group.unmatched.size) continue
|
|
1254
|
+
const peerMetadata = metadataKey(peer.entry)
|
|
1225
1255
|
if (![...group.unmatched.values()].some((wanted) =>
|
|
1226
|
-
wanted.identity !== identity
|
|
1256
|
+
wanted.identity !== identity &&
|
|
1257
|
+
metadataKey(wanted.candidate) === peerMetadata)) continue
|
|
1227
1258
|
this.checkpoint(active)
|
|
1228
1259
|
markStage("peer-hash", entry)
|
|
1229
1260
|
const verified = await this.verifiedAutomaticHash(
|
|
1230
1261
|
active, entry, group.memory, verifiedHashes, counts, peer)
|
|
1231
|
-
const
|
|
1262
|
+
const match = verified &&
|
|
1263
|
+
`${verified.hash}\0${metadataKey(verified)}`
|
|
1264
|
+
const wanted = match && group.unmatched.get(match)
|
|
1232
1265
|
if (!wanted || wanted.identity === identity) {
|
|
1233
1266
|
continue
|
|
1234
1267
|
}
|
|
@@ -2807,6 +2807,9 @@ class RegistryCore {
|
|
|
2807
2807
|
WHERE peer.app = candidate.app
|
|
2808
2808
|
AND peer.dev = candidate.dev
|
|
2809
2809
|
AND peer.size = candidate.size
|
|
2810
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
2811
|
+
AND peer.uid = candidate.uid
|
|
2812
|
+
AND peer.gid = candidate.gid
|
|
2810
2813
|
AND pinokio_path_key(peer.path) !=
|
|
2811
2814
|
pinokio_path_key(candidate.path)
|
|
2812
2815
|
)
|
|
@@ -2814,7 +2817,10 @@ class RegistryCore {
|
|
|
2814
2817
|
SELECT 1 FROM files peer
|
|
2815
2818
|
WHERE peer.dev = candidate.dev
|
|
2816
2819
|
AND peer.size = candidate.size
|
|
2817
|
-
AND peer.unavailable_reason IS
|
|
2820
|
+
AND peer.unavailable_reason IS NULL
|
|
2821
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
2822
|
+
AND peer.uid = candidate.uid
|
|
2823
|
+
AND peer.gid = candidate.gid
|
|
2818
2824
|
AND pinokio_path_key(peer.path) !=
|
|
2819
2825
|
pinokio_path_key(candidate.path)
|
|
2820
2826
|
)
|
|
@@ -2823,6 +2829,9 @@ class RegistryCore {
|
|
|
2823
2829
|
WHERE peer.verified_at IS NOT NULL
|
|
2824
2830
|
AND peer.dev = candidate.dev
|
|
2825
2831
|
AND peer.size = candidate.size
|
|
2832
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
2833
|
+
AND peer.uid = candidate.uid
|
|
2834
|
+
AND peer.gid = candidate.gid
|
|
2826
2835
|
AND pinokio_path_key(peer.path) !=
|
|
2827
2836
|
pinokio_path_key(candidate.path)
|
|
2828
2837
|
)
|
|
@@ -2888,6 +2897,7 @@ class RegistryCore {
|
|
|
2888
2897
|
WHERE candidate.app = @app
|
|
2889
2898
|
AND candidate.dev = @dev
|
|
2890
2899
|
AND candidate.size = @size
|
|
2900
|
+
AND ${possiblePeer}
|
|
2891
2901
|
AND candidate.path > @path
|
|
2892
2902
|
ORDER BY candidate.path
|
|
2893
2903
|
LIMIT @limit
|
|
@@ -2948,7 +2958,7 @@ class RegistryCore {
|
|
|
2948
2958
|
WHERE peer.dev = @dev
|
|
2949
2959
|
AND peer.size = @size
|
|
2950
2960
|
AND peer.path > @path
|
|
2951
|
-
AND peer.unavailable_reason IS
|
|
2961
|
+
AND peer.unavailable_reason IS NULL
|
|
2952
2962
|
ORDER BY peer.path
|
|
2953
2963
|
LIMIT @limit
|
|
2954
2964
|
`).all({ dev, size, path: afterPath, limit: pageSize })
|
|
@@ -3030,7 +3040,7 @@ class RegistryCore {
|
|
|
3030
3040
|
hashes.set(canonicalPathKey(entry.path), entry.hash)
|
|
3031
3041
|
}
|
|
3032
3042
|
const rows = this.database.prepare(`
|
|
3033
|
-
SELECT path, dev, ino, size, mtime, ctime
|
|
3043
|
+
SELECT path, dev, ino, size, mtime, ctime, mode, uid, gid
|
|
3034
3044
|
FROM automatic_precheck_files
|
|
3035
3045
|
WHERE app = ?
|
|
3036
3046
|
ORDER BY pinokio_path_key(path), path, dev, ino, size, mtime, ctime
|
|
@@ -3048,7 +3058,10 @@ class RegistryCore {
|
|
|
3048
3058
|
row.ino,
|
|
3049
3059
|
row.size,
|
|
3050
3060
|
row.mtime,
|
|
3051
|
-
row.ctime
|
|
3061
|
+
row.ctime,
|
|
3062
|
+
Number(row.mode) & 0o7777,
|
|
3063
|
+
row.uid,
|
|
3064
|
+
row.gid
|
|
3052
3065
|
]) {
|
|
3053
3066
|
digest.update(String(value))
|
|
3054
3067
|
digest.update("\0")
|
|
@@ -3062,6 +3075,12 @@ class RegistryCore {
|
|
|
3062
3075
|
digest.update("\0")
|
|
3063
3076
|
digest.update(String(proof.row.size))
|
|
3064
3077
|
digest.update("\0")
|
|
3078
|
+
digest.update(String(Number(proof.row.mode) & 0o7777))
|
|
3079
|
+
digest.update("\0")
|
|
3080
|
+
digest.update(String(proof.row.uid))
|
|
3081
|
+
digest.update("\0")
|
|
3082
|
+
digest.update(String(proof.row.gid))
|
|
3083
|
+
digest.update("\0")
|
|
3065
3084
|
digest.update(proof.hash)
|
|
3066
3085
|
return {
|
|
3067
3086
|
signature: digest.digest("hex"),
|
package/kernel/vault/sweeper.js
CHANGED
|
@@ -155,7 +155,6 @@ class Sweeper {
|
|
|
155
155
|
: "complete"
|
|
156
156
|
this.state.phase = "publishing"
|
|
157
157
|
this.state.duration_ms = Date.now() - this.state.started
|
|
158
|
-
const metadata = this.scanMetadata(scopeId, outcome)
|
|
159
158
|
const stores = anchorStores
|
|
160
159
|
.filter((store) =>
|
|
161
160
|
store.available && Number.isFinite(store.dev))
|
|
@@ -165,6 +164,12 @@ class Sweeper {
|
|
|
165
164
|
can_link: store.mode !== "copy",
|
|
166
165
|
root: store.root
|
|
167
166
|
}))
|
|
167
|
+
const metadata = this.scanMetadata(scopeId, outcome)
|
|
168
|
+
if (!scopeId) {
|
|
169
|
+
metadata.linkable_devices = [...new Set(stores
|
|
170
|
+
.filter((store) => store.can_link)
|
|
171
|
+
.map((store) => store.dev))].sort((left, right) => left - right)
|
|
172
|
+
}
|
|
168
173
|
const publication = await registry.publishScan(
|
|
169
174
|
runId,
|
|
170
175
|
this.publicationSourceIds(scopeId),
|
package/package.json
CHANGED
package/server/public/vault.css
CHANGED
|
@@ -2133,7 +2133,11 @@ body.dark .vault-row-action .vault-text-button:hover:not(:disabled) {
|
|
|
2133
2133
|
.vault-event-time { color: var(--task-muted); font-size: 10.5px; }
|
|
2134
2134
|
.vault-unavailable { color: var(--task-muted); font-size: 11px; }
|
|
2135
2135
|
@media (max-width: 1140px) {
|
|
2136
|
-
.vault-overview {
|
|
2136
|
+
.vault-overview {
|
|
2137
|
+
min-height: auto;
|
|
2138
|
+
align-items: flex-start;
|
|
2139
|
+
flex-direction: column;
|
|
2140
|
+
}
|
|
2137
2141
|
.vault-overview-actions { width: 100%; justify-content: flex-start; }
|
|
2138
2142
|
.vault-scan-coachmark { right: auto; left: 0; }
|
|
2139
2143
|
.vault-metrics { width: 100%; }
|
|
@@ -48,6 +48,13 @@ const makeVault = async (home, options = {}) => {
|
|
|
48
48
|
: true
|
|
49
49
|
vault.globalScanReady = async () => globalScanReady
|
|
50
50
|
vault.automaticScans.globalScanReady = globalScanReady
|
|
51
|
+
if (globalScanReady) {
|
|
52
|
+
const homeStat = await fs.promises.stat(home)
|
|
53
|
+
vault.lastScanCache.set("", {
|
|
54
|
+
candidate_min_bytes: SIZE_THRESHOLD,
|
|
55
|
+
linkable_devices: [homeStat.dev]
|
|
56
|
+
})
|
|
57
|
+
}
|
|
51
58
|
return vault
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -1037,7 +1044,10 @@ describe("automatic app checks", () => {
|
|
|
1037
1044
|
["ino", 22],
|
|
1038
1045
|
["size", 201],
|
|
1039
1046
|
["mtime", 203],
|
|
1040
|
-
["ctime", 204]
|
|
1047
|
+
["ctime", 204],
|
|
1048
|
+
["mode", 0o640],
|
|
1049
|
+
["uid", 1],
|
|
1050
|
+
["gid", 1]
|
|
1041
1051
|
]) {
|
|
1042
1052
|
assert.notEqual(
|
|
1043
1053
|
signature([proof, Object.assign({}, unmatched, { [field]: value })]),
|
|
@@ -1161,6 +1171,103 @@ describe("automatic app checks", () => {
|
|
|
1161
1171
|
await close(vault)
|
|
1162
1172
|
})
|
|
1163
1173
|
|
|
1174
|
+
test("metadata-incompatible copies do not publish a result", async (t) => {
|
|
1175
|
+
if (process.platform === "win32") {
|
|
1176
|
+
t.skip("POSIX permission metadata is required")
|
|
1177
|
+
return
|
|
1178
|
+
}
|
|
1179
|
+
const home = await makeHome()
|
|
1180
|
+
const app = "metadata-incompatible-app"
|
|
1181
|
+
const appRoot = path.join(home, "api", app)
|
|
1182
|
+
const first = path.join(appRoot, "first.bin")
|
|
1183
|
+
const second = path.join(appRoot, "second.bin")
|
|
1184
|
+
await fs.promises.mkdir(appRoot)
|
|
1185
|
+
await fs.promises.writeFile(first, "same-content")
|
|
1186
|
+
await fs.promises.writeFile(second, "same-content")
|
|
1187
|
+
await fs.promises.chmod(first, 0o600)
|
|
1188
|
+
await fs.promises.chmod(second, 0o644)
|
|
1189
|
+
const vault = await makeVault(home)
|
|
1190
|
+
vault.automaticScans.candidateThreshold = async () => 1
|
|
1191
|
+
const hashedPaths = []
|
|
1192
|
+
const originalHashFile = vault.hashFile.bind(vault)
|
|
1193
|
+
vault.hashFile = async (filePath, options) => {
|
|
1194
|
+
hashedPaths.push(filePath)
|
|
1195
|
+
return originalHashFile(filePath, options)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
vault.automaticScans.queueApp(app, [first, second])
|
|
1199
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1200
|
+
!vault.automaticScans.entries.has(app))
|
|
1201
|
+
|
|
1202
|
+
assert.deepEqual(hashedPaths, [])
|
|
1203
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
1204
|
+
await close(vault)
|
|
1205
|
+
})
|
|
1206
|
+
|
|
1207
|
+
test("devices without hardlink support do not publish a result", async () => {
|
|
1208
|
+
const home = await makeHome()
|
|
1209
|
+
const app = "copy-only-device-app"
|
|
1210
|
+
const appRoot = path.join(home, "api", app)
|
|
1211
|
+
const first = path.join(appRoot, "first.bin")
|
|
1212
|
+
const second = path.join(appRoot, "second.bin")
|
|
1213
|
+
await fs.promises.mkdir(appRoot)
|
|
1214
|
+
await fs.promises.writeFile(first, "same-content")
|
|
1215
|
+
await fs.promises.writeFile(second, "same-content")
|
|
1216
|
+
const vault = await makeVault(home)
|
|
1217
|
+
vault.automaticScans.candidateThreshold = async () => 1
|
|
1218
|
+
vault.automaticScans.candidatePolicy = async () => ({
|
|
1219
|
+
threshold: 1,
|
|
1220
|
+
linkability_known: true,
|
|
1221
|
+
linkable_devices: new Set()
|
|
1222
|
+
})
|
|
1223
|
+
const hashedPaths = []
|
|
1224
|
+
const originalHashFile = vault.hashFile.bind(vault)
|
|
1225
|
+
vault.hashFile = async (filePath, options) => {
|
|
1226
|
+
hashedPaths.push(filePath)
|
|
1227
|
+
return originalHashFile(filePath, options)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
vault.automaticScans.queueApp(app, [first, second])
|
|
1231
|
+
await waitFor(() => !vault.automaticScans.active &&
|
|
1232
|
+
!vault.automaticScans.entries.has(app))
|
|
1233
|
+
|
|
1234
|
+
assert.deepEqual(hashedPaths, [])
|
|
1235
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
1236
|
+
await close(vault)
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
test("legacy global scans without linkability metadata preserve automatic checks", async () => {
|
|
1240
|
+
const home = await makeHome()
|
|
1241
|
+
const app = "legacy-global-scan-app"
|
|
1242
|
+
const appRoot = path.join(home, "api", app)
|
|
1243
|
+
const first = path.join(appRoot, "first.bin")
|
|
1244
|
+
const second = path.join(appRoot, "second.bin")
|
|
1245
|
+
await fs.promises.mkdir(appRoot)
|
|
1246
|
+
await fs.promises.writeFile(first, "same-content")
|
|
1247
|
+
await fs.promises.writeFile(second, "same-content")
|
|
1248
|
+
const vault = await makeVault(home)
|
|
1249
|
+
vault.lastScanCache.set("", { candidate_min_bytes: 1 })
|
|
1250
|
+
const hashedPaths = []
|
|
1251
|
+
const originalHashFile = vault.hashFile.bind(vault)
|
|
1252
|
+
vault.hashFile = async (filePath, options) => {
|
|
1253
|
+
hashedPaths.push(filePath)
|
|
1254
|
+
return originalHashFile(filePath, options)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
vault.automaticScans.queueApp(app, [first, second])
|
|
1258
|
+
await waitFor(() => {
|
|
1259
|
+
const row = vault.automaticScans.entries.get(app)
|
|
1260
|
+
return !vault.automaticScans.active && row && row.state === "result"
|
|
1261
|
+
})
|
|
1262
|
+
|
|
1263
|
+
assert.deepEqual(hashedPaths.sort(), [first, second].sort())
|
|
1264
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows.map((row) => ({
|
|
1265
|
+
app: row.app,
|
|
1266
|
+
state: row.state
|
|
1267
|
+
})), [{ app, state: "result" }])
|
|
1268
|
+
await close(vault)
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1164
1271
|
test("hash worker cancellation interrupts a read without poisoning the worker", async () => {
|
|
1165
1272
|
const home = await makeHome()
|
|
1166
1273
|
const large = path.join(home, "large.bin")
|
|
@@ -2113,7 +2220,7 @@ describe("automatic app checks", () => {
|
|
|
2113
2220
|
await close(vault)
|
|
2114
2221
|
})
|
|
2115
2222
|
|
|
2116
|
-
test("peer verification rejects changed
|
|
2223
|
+
test("peer verification rejects changed eligibility metadata", async (t) => {
|
|
2117
2224
|
const home = await makeHome()
|
|
2118
2225
|
const automatic = makeAutomatic({
|
|
2119
2226
|
enabled: true,
|
|
@@ -2141,6 +2248,18 @@ describe("automatic app checks", () => {
|
|
|
2141
2248
|
{
|
|
2142
2249
|
name: "size",
|
|
2143
2250
|
stat: { file: true, link: false, dev: 1, size: 5 }
|
|
2251
|
+
},
|
|
2252
|
+
{
|
|
2253
|
+
name: "permissions",
|
|
2254
|
+
stat: { file: true, link: false, dev: 1, size: 4, mode: 1 }
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
name: "owner",
|
|
2258
|
+
stat: { file: true, link: false, dev: 1, size: 4, uid: 1 }
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
name: "group",
|
|
2262
|
+
stat: { file: true, link: false, dev: 1, size: 4, gid: 1 }
|
|
2144
2263
|
}
|
|
2145
2264
|
]
|
|
2146
2265
|
const currentStats = new Map(cases.map(({ name, stat }) => [
|
|
@@ -2154,9 +2273,9 @@ describe("automatic app checks", () => {
|
|
|
2154
2273
|
mtimeMs: 1,
|
|
2155
2274
|
ctimeMs: 1,
|
|
2156
2275
|
nlink: 1,
|
|
2157
|
-
mode: 0,
|
|
2158
|
-
uid: 0,
|
|
2159
|
-
gid: 0
|
|
2276
|
+
mode: stat.mode || 0,
|
|
2277
|
+
uid: stat.uid || 0,
|
|
2278
|
+
gid: stat.gid || 0
|
|
2160
2279
|
}
|
|
2161
2280
|
]))
|
|
2162
2281
|
const lstat = fs.promises.lstat
|
|
@@ -2172,7 +2291,10 @@ describe("automatic app checks", () => {
|
|
|
2172
2291
|
size: 4,
|
|
2173
2292
|
ino: 1,
|
|
2174
2293
|
mtime: 1,
|
|
2175
|
-
ctime: 1
|
|
2294
|
+
ctime: 1,
|
|
2295
|
+
mode: 0,
|
|
2296
|
+
uid: 0,
|
|
2297
|
+
gid: 0
|
|
2176
2298
|
}, counts), null)
|
|
2177
2299
|
}
|
|
2178
2300
|
|
|
@@ -3426,7 +3548,11 @@ describe("automatic app checks", () => {
|
|
|
3426
3548
|
candidate("hardlink-a.bin", 108, 7),
|
|
3427
3549
|
candidate("hardlink-b.bin", 108, 7),
|
|
3428
3550
|
candidate("file-peer.bin", 109, 8),
|
|
3429
|
-
candidate("anchor-peer.bin", 110, 9)
|
|
3551
|
+
candidate("anchor-peer.bin", 110, 9),
|
|
3552
|
+
candidate("unavailable-peer.bin", 111, 10),
|
|
3553
|
+
Object.assign(candidate("metadata-mismatch.bin", 108, 11), {
|
|
3554
|
+
mode: 1
|
|
3555
|
+
})
|
|
3430
3556
|
]
|
|
3431
3557
|
const file = (filePath, size, dev, ino, unavailableReason = null) => ({
|
|
3432
3558
|
path: filePath,
|
|
@@ -3454,6 +3580,13 @@ describe("automatic app checks", () => {
|
|
|
3454
3580
|
path.join(home, "stale-peer.bin"), 106, 1, 23, "stale"))
|
|
3455
3581
|
await registry.upsertFile(file(
|
|
3456
3582
|
path.join(home, "valid-file-peer.bin"), 109, 1, 24))
|
|
3583
|
+
const unavailableWithinEligibleGroup = path.join(
|
|
3584
|
+
home, "unavailable-within-eligible-group.bin")
|
|
3585
|
+
await registry.upsertFile(file(
|
|
3586
|
+
unavailableWithinEligibleGroup, 109, 1, 25, "metadata"))
|
|
3587
|
+
await registry.upsertFile(file(
|
|
3588
|
+
path.join(home, "unavailable-file-peer.bin"), 111, 1, 26,
|
|
3589
|
+
"metadata"))
|
|
3457
3590
|
|
|
3458
3591
|
for (const [name, size, verifiedAt] of [
|
|
3459
3592
|
["unverified", 107, null],
|
|
@@ -3497,6 +3630,9 @@ describe("automatic app checks", () => {
|
|
|
3497
3630
|
WHERE peer.app = candidate.app
|
|
3498
3631
|
AND peer.dev = candidate.dev
|
|
3499
3632
|
AND peer.size = candidate.size
|
|
3633
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
3634
|
+
AND peer.uid = candidate.uid
|
|
3635
|
+
AND peer.gid = candidate.gid
|
|
3500
3636
|
AND pinokio_path_key(peer.path) !=
|
|
3501
3637
|
pinokio_path_key(candidate.path)
|
|
3502
3638
|
)
|
|
@@ -3504,7 +3640,10 @@ describe("automatic app checks", () => {
|
|
|
3504
3640
|
SELECT 1 FROM files peer
|
|
3505
3641
|
WHERE peer.dev = candidate.dev
|
|
3506
3642
|
AND peer.size = candidate.size
|
|
3507
|
-
AND peer.unavailable_reason IS
|
|
3643
|
+
AND peer.unavailable_reason IS NULL
|
|
3644
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
3645
|
+
AND peer.uid = candidate.uid
|
|
3646
|
+
AND peer.gid = candidate.gid
|
|
3508
3647
|
AND pinokio_path_key(peer.path) !=
|
|
3509
3648
|
pinokio_path_key(candidate.path)
|
|
3510
3649
|
)
|
|
@@ -3513,6 +3652,9 @@ describe("automatic app checks", () => {
|
|
|
3513
3652
|
WHERE peer.verified_at IS NOT NULL
|
|
3514
3653
|
AND peer.dev = candidate.dev
|
|
3515
3654
|
AND peer.size = candidate.size
|
|
3655
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
3656
|
+
AND peer.uid = candidate.uid
|
|
3657
|
+
AND peer.gid = candidate.gid
|
|
3516
3658
|
AND pinokio_path_key(peer.path) !=
|
|
3517
3659
|
pinokio_path_key(candidate.path)
|
|
3518
3660
|
)
|
|
@@ -3525,9 +3667,11 @@ describe("automatic app checks", () => {
|
|
|
3525
3667
|
assert.match(plan, /files_size_device_path_idx/)
|
|
3526
3668
|
assert.match(plan, /anchors_size_device_path_idx/)
|
|
3527
3669
|
const changed = []
|
|
3670
|
+
const returnedPaths = []
|
|
3528
3671
|
let cursor = null
|
|
3529
3672
|
do {
|
|
3530
3673
|
const page = registry.automaticPrecheckEntries(app, cursor, 2)
|
|
3674
|
+
returnedPaths.push(...page.entries.map((entry) => entry.path))
|
|
3531
3675
|
changed.push(...page.entries
|
|
3532
3676
|
.filter((entry) => entry.kind === "changed")
|
|
3533
3677
|
.map((entry) => entry.path))
|
|
@@ -3540,6 +3684,7 @@ describe("automatic app checks", () => {
|
|
|
3540
3684
|
entries[8].path,
|
|
3541
3685
|
entries[9].path
|
|
3542
3686
|
].sort())
|
|
3687
|
+
assert.equal(returnedPaths.includes(unavailableWithinEligibleGroup), false)
|
|
3543
3688
|
registry.close()
|
|
3544
3689
|
})
|
|
3545
3690
|
|
|
@@ -189,6 +189,13 @@ describe("Save Space engine", () => {
|
|
|
189
189
|
|
|
190
190
|
assert.equal((await vault.status()).global_candidate_min_bytes, selected)
|
|
191
191
|
assert.equal(await vault.automaticScans.candidateThreshold(), selected)
|
|
192
|
+
const scan = await vault.registry.scanFor()
|
|
193
|
+
const expectedDevices = vault.anchorStores()
|
|
194
|
+
.filter((store) => store.available && Number.isFinite(store.dev) &&
|
|
195
|
+
store.mode !== "copy")
|
|
196
|
+
.map((store) => store.dev)
|
|
197
|
+
.sort((left, right) => left - right)
|
|
198
|
+
assert.deepEqual(scan.linkable_devices, expectedDevices)
|
|
192
199
|
|
|
193
200
|
await close(vault)
|
|
194
201
|
})
|
package/test/vault-ui.test.js
CHANGED
|
@@ -546,6 +546,8 @@ describe("Save Space interface", () => {
|
|
|
546
546
|
assert.match(combined, /unique to this app/)
|
|
547
547
|
assert.match(vaultCss,
|
|
548
548
|
/body\.vault-page \.vault-view-tabs\s*\{[^}]*padding:\s*0;/s)
|
|
549
|
+
assert.match(vaultCss,
|
|
550
|
+
/@media \(max-width: 1140px\)[\s\S]*?\.vault-overview\s*\{[^}]*min-height:\s*auto;/s)
|
|
549
551
|
assert.match(vaultCss,
|
|
550
552
|
/body\.dark\.vault-page\s*\{[^}]*--task-panel:\s*var\(--pinokio-sidebar-tabbar-bg\);/s)
|
|
551
553
|
assert.match(vaultCss,
|