pinokiod 8.0.76 → 8.0.77
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 +148 -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,98 @@ 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 fail closed", 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(() => !vault.automaticScans.active &&
|
|
1259
|
+
!vault.automaticScans.entries.has(app))
|
|
1260
|
+
|
|
1261
|
+
assert.deepEqual(hashedPaths, [])
|
|
1262
|
+
assert.deepEqual(vault.automaticScans.snapshot().rows, [])
|
|
1263
|
+
await close(vault)
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1164
1266
|
test("hash worker cancellation interrupts a read without poisoning the worker", async () => {
|
|
1165
1267
|
const home = await makeHome()
|
|
1166
1268
|
const large = path.join(home, "large.bin")
|
|
@@ -2113,7 +2215,7 @@ describe("automatic app checks", () => {
|
|
|
2113
2215
|
await close(vault)
|
|
2114
2216
|
})
|
|
2115
2217
|
|
|
2116
|
-
test("peer verification rejects changed
|
|
2218
|
+
test("peer verification rejects changed eligibility metadata", async (t) => {
|
|
2117
2219
|
const home = await makeHome()
|
|
2118
2220
|
const automatic = makeAutomatic({
|
|
2119
2221
|
enabled: true,
|
|
@@ -2141,6 +2243,18 @@ describe("automatic app checks", () => {
|
|
|
2141
2243
|
{
|
|
2142
2244
|
name: "size",
|
|
2143
2245
|
stat: { file: true, link: false, dev: 1, size: 5 }
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
name: "permissions",
|
|
2249
|
+
stat: { file: true, link: false, dev: 1, size: 4, mode: 1 }
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
name: "owner",
|
|
2253
|
+
stat: { file: true, link: false, dev: 1, size: 4, uid: 1 }
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
name: "group",
|
|
2257
|
+
stat: { file: true, link: false, dev: 1, size: 4, gid: 1 }
|
|
2144
2258
|
}
|
|
2145
2259
|
]
|
|
2146
2260
|
const currentStats = new Map(cases.map(({ name, stat }) => [
|
|
@@ -2154,9 +2268,9 @@ describe("automatic app checks", () => {
|
|
|
2154
2268
|
mtimeMs: 1,
|
|
2155
2269
|
ctimeMs: 1,
|
|
2156
2270
|
nlink: 1,
|
|
2157
|
-
mode: 0,
|
|
2158
|
-
uid: 0,
|
|
2159
|
-
gid: 0
|
|
2271
|
+
mode: stat.mode || 0,
|
|
2272
|
+
uid: stat.uid || 0,
|
|
2273
|
+
gid: stat.gid || 0
|
|
2160
2274
|
}
|
|
2161
2275
|
]))
|
|
2162
2276
|
const lstat = fs.promises.lstat
|
|
@@ -2172,7 +2286,10 @@ describe("automatic app checks", () => {
|
|
|
2172
2286
|
size: 4,
|
|
2173
2287
|
ino: 1,
|
|
2174
2288
|
mtime: 1,
|
|
2175
|
-
ctime: 1
|
|
2289
|
+
ctime: 1,
|
|
2290
|
+
mode: 0,
|
|
2291
|
+
uid: 0,
|
|
2292
|
+
gid: 0
|
|
2176
2293
|
}, counts), null)
|
|
2177
2294
|
}
|
|
2178
2295
|
|
|
@@ -3426,7 +3543,11 @@ describe("automatic app checks", () => {
|
|
|
3426
3543
|
candidate("hardlink-a.bin", 108, 7),
|
|
3427
3544
|
candidate("hardlink-b.bin", 108, 7),
|
|
3428
3545
|
candidate("file-peer.bin", 109, 8),
|
|
3429
|
-
candidate("anchor-peer.bin", 110, 9)
|
|
3546
|
+
candidate("anchor-peer.bin", 110, 9),
|
|
3547
|
+
candidate("unavailable-peer.bin", 111, 10),
|
|
3548
|
+
Object.assign(candidate("metadata-mismatch.bin", 108, 11), {
|
|
3549
|
+
mode: 1
|
|
3550
|
+
})
|
|
3430
3551
|
]
|
|
3431
3552
|
const file = (filePath, size, dev, ino, unavailableReason = null) => ({
|
|
3432
3553
|
path: filePath,
|
|
@@ -3454,6 +3575,13 @@ describe("automatic app checks", () => {
|
|
|
3454
3575
|
path.join(home, "stale-peer.bin"), 106, 1, 23, "stale"))
|
|
3455
3576
|
await registry.upsertFile(file(
|
|
3456
3577
|
path.join(home, "valid-file-peer.bin"), 109, 1, 24))
|
|
3578
|
+
const unavailableWithinEligibleGroup = path.join(
|
|
3579
|
+
home, "unavailable-within-eligible-group.bin")
|
|
3580
|
+
await registry.upsertFile(file(
|
|
3581
|
+
unavailableWithinEligibleGroup, 109, 1, 25, "metadata"))
|
|
3582
|
+
await registry.upsertFile(file(
|
|
3583
|
+
path.join(home, "unavailable-file-peer.bin"), 111, 1, 26,
|
|
3584
|
+
"metadata"))
|
|
3457
3585
|
|
|
3458
3586
|
for (const [name, size, verifiedAt] of [
|
|
3459
3587
|
["unverified", 107, null],
|
|
@@ -3497,6 +3625,9 @@ describe("automatic app checks", () => {
|
|
|
3497
3625
|
WHERE peer.app = candidate.app
|
|
3498
3626
|
AND peer.dev = candidate.dev
|
|
3499
3627
|
AND peer.size = candidate.size
|
|
3628
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
3629
|
+
AND peer.uid = candidate.uid
|
|
3630
|
+
AND peer.gid = candidate.gid
|
|
3500
3631
|
AND pinokio_path_key(peer.path) !=
|
|
3501
3632
|
pinokio_path_key(candidate.path)
|
|
3502
3633
|
)
|
|
@@ -3504,7 +3635,10 @@ describe("automatic app checks", () => {
|
|
|
3504
3635
|
SELECT 1 FROM files peer
|
|
3505
3636
|
WHERE peer.dev = candidate.dev
|
|
3506
3637
|
AND peer.size = candidate.size
|
|
3507
|
-
AND peer.unavailable_reason IS
|
|
3638
|
+
AND peer.unavailable_reason IS NULL
|
|
3639
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
3640
|
+
AND peer.uid = candidate.uid
|
|
3641
|
+
AND peer.gid = candidate.gid
|
|
3508
3642
|
AND pinokio_path_key(peer.path) !=
|
|
3509
3643
|
pinokio_path_key(candidate.path)
|
|
3510
3644
|
)
|
|
@@ -3513,6 +3647,9 @@ describe("automatic app checks", () => {
|
|
|
3513
3647
|
WHERE peer.verified_at IS NOT NULL
|
|
3514
3648
|
AND peer.dev = candidate.dev
|
|
3515
3649
|
AND peer.size = candidate.size
|
|
3650
|
+
AND (peer.mode & 4095) = (candidate.mode & 4095)
|
|
3651
|
+
AND peer.uid = candidate.uid
|
|
3652
|
+
AND peer.gid = candidate.gid
|
|
3516
3653
|
AND pinokio_path_key(peer.path) !=
|
|
3517
3654
|
pinokio_path_key(candidate.path)
|
|
3518
3655
|
)
|
|
@@ -3525,9 +3662,11 @@ describe("automatic app checks", () => {
|
|
|
3525
3662
|
assert.match(plan, /files_size_device_path_idx/)
|
|
3526
3663
|
assert.match(plan, /anchors_size_device_path_idx/)
|
|
3527
3664
|
const changed = []
|
|
3665
|
+
const returnedPaths = []
|
|
3528
3666
|
let cursor = null
|
|
3529
3667
|
do {
|
|
3530
3668
|
const page = registry.automaticPrecheckEntries(app, cursor, 2)
|
|
3669
|
+
returnedPaths.push(...page.entries.map((entry) => entry.path))
|
|
3531
3670
|
changed.push(...page.entries
|
|
3532
3671
|
.filter((entry) => entry.kind === "changed")
|
|
3533
3672
|
.map((entry) => entry.path))
|
|
@@ -3540,6 +3679,7 @@ describe("automatic app checks", () => {
|
|
|
3540
3679
|
entries[8].path,
|
|
3541
3680
|
entries[9].path
|
|
3542
3681
|
].sort())
|
|
3682
|
+
assert.equal(returnedPaths.includes(unavailableWithinEligibleGroup), false)
|
|
3543
3683
|
registry.close()
|
|
3544
3684
|
})
|
|
3545
3685
|
|
|
@@ -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,
|