pinokiod 8.0.111 → 8.0.112
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/index.js +9 -2
- package/kernel/vault/registry_core.js +27 -12
- package/package.json +1 -1
- package/test/vault-engine.test.js +51 -0
package/kernel/vault/index.js
CHANGED
|
@@ -3408,7 +3408,10 @@ class Vault {
|
|
|
3408
3408
|
.replace(/[\\/]+$/, "")
|
|
3409
3409
|
.slice(0, 4096)
|
|
3410
3410
|
const root = source.root.replace(/[\\/]+$/, "")
|
|
3411
|
-
|
|
3411
|
+
// Every source root is produced by path.resolve, so it is already native
|
|
3412
|
+
// and the platform separator is the right one -- no need to infer it from
|
|
3413
|
+
// the string, which a directory named with a backslash would fool.
|
|
3414
|
+
const separator = path.sep
|
|
3412
3415
|
const prefix = parent
|
|
3413
3416
|
? `${root}${separator}${parent.split("/").join(separator)}${separator}`
|
|
3414
3417
|
: `${root}${separator}`
|
|
@@ -3419,6 +3422,7 @@ class Vault {
|
|
|
3419
3422
|
const level = await this.registry.treeLevel({
|
|
3420
3423
|
sourceId: source.id,
|
|
3421
3424
|
prefix,
|
|
3425
|
+
separator,
|
|
3422
3426
|
view,
|
|
3423
3427
|
statusFilter,
|
|
3424
3428
|
query,
|
|
@@ -3438,7 +3442,10 @@ class Vault {
|
|
|
3438
3442
|
// A run of folders holding nothing but one another reads as one row.
|
|
3439
3443
|
// The shared prefix of the first and last path underneath is exactly
|
|
3440
3444
|
// that run, because the two are the extremes of a sorted list.
|
|
3441
|
-
const
|
|
3445
|
+
const native = (value) => separator === "/"
|
|
3446
|
+
? String(value || "")
|
|
3447
|
+
: String(value || "").split(separator).join("/")
|
|
3448
|
+
const shared = commonDirectory(native(row.lo), native(row.hi))
|
|
3442
3449
|
const label = shared && shared.startsWith(`${row.name}/`)
|
|
3443
3450
|
? shared
|
|
3444
3451
|
: row.name
|
|
@@ -22,7 +22,9 @@ const canonicalPathKey = (value) => {
|
|
|
22
22
|
// user cannot see a reason for. This narrows the match to the last segment.
|
|
23
23
|
const pathNameKey = (value) => {
|
|
24
24
|
const filePath = String(value || "")
|
|
25
|
-
|
|
25
|
+
// Only the platform's own separator: registry paths are always native, and
|
|
26
|
+
// on POSIX a backslash is an ordinary character in a file name.
|
|
27
|
+
const cut = filePath.lastIndexOf(path.sep)
|
|
26
28
|
return (cut < 0 ? filePath : filePath.slice(cut + 1)).toLowerCase()
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -5175,7 +5177,8 @@ class RegistryCore {
|
|
|
5175
5177
|
limit,
|
|
5176
5178
|
externalSourceIds,
|
|
5177
5179
|
nameOnly,
|
|
5178
|
-
parentPrefix
|
|
5180
|
+
parentPrefix,
|
|
5181
|
+
parentSeparator
|
|
5179
5182
|
} = options
|
|
5180
5183
|
const where = ["unavailable_reason IS NOT 'stale'"]
|
|
5181
5184
|
const values = []
|
|
@@ -5202,8 +5205,8 @@ class RegistryCore {
|
|
|
5202
5205
|
const [low, high] = this.prefixRange(parentPrefix)
|
|
5203
5206
|
where.push("path >= ? AND path < ?")
|
|
5204
5207
|
values.push(low, high)
|
|
5205
|
-
where.push("instr(
|
|
5206
|
-
values.push(String(parentPrefix).length + 1)
|
|
5208
|
+
where.push("instr(substr(path, ?), ?) = 0")
|
|
5209
|
+
values.push(String(parentPrefix).length + 1, parentSeparator)
|
|
5207
5210
|
}
|
|
5208
5211
|
if (cursor && typeof cursor.path === "string") {
|
|
5209
5212
|
if (sort === "asc" && Number.isFinite(cursor.size)) {
|
|
@@ -5246,7 +5249,8 @@ class RegistryCore {
|
|
|
5246
5249
|
pageSize,
|
|
5247
5250
|
externalSourceIds,
|
|
5248
5251
|
nameOnly,
|
|
5249
|
-
parentPrefix
|
|
5252
|
+
parentPrefix,
|
|
5253
|
+
parentSeparator
|
|
5250
5254
|
} = options
|
|
5251
5255
|
const sources = unrestricted
|
|
5252
5256
|
? [null]
|
|
@@ -5282,7 +5286,8 @@ class RegistryCore {
|
|
|
5282
5286
|
limit,
|
|
5283
5287
|
externalSourceIds,
|
|
5284
5288
|
nameOnly,
|
|
5285
|
-
parentPrefix
|
|
5289
|
+
parentPrefix,
|
|
5290
|
+
parentSeparator
|
|
5286
5291
|
})
|
|
5287
5292
|
stream.rows.push(...rows)
|
|
5288
5293
|
if (rows.length < limit) stream.exhausted = true
|
|
@@ -5356,6 +5361,7 @@ class RegistryCore {
|
|
|
5356
5361
|
statuses,
|
|
5357
5362
|
query,
|
|
5358
5363
|
nameOnly,
|
|
5364
|
+
separator,
|
|
5359
5365
|
sizeSort,
|
|
5360
5366
|
nameSort,
|
|
5361
5367
|
limit,
|
|
@@ -5382,14 +5388,16 @@ class RegistryCore {
|
|
|
5382
5388
|
: sizeSort === "desc"
|
|
5383
5389
|
? "bytes DESC, name ASC"
|
|
5384
5390
|
: nameSort === "desc" ? "name DESC" : "name ASC"
|
|
5391
|
+
// Values bind in the order the placeholders appear: the CTE's substr, then
|
|
5392
|
+
// its WHERE, then the two separators in the outer select and filter.
|
|
5385
5393
|
return this.database.prepare(`
|
|
5386
5394
|
WITH scoped AS (
|
|
5387
|
-
SELECT
|
|
5395
|
+
SELECT substr(path, ?) AS rel, size, status
|
|
5388
5396
|
FROM files
|
|
5389
5397
|
WHERE ${where.join(" AND ")}
|
|
5390
5398
|
)
|
|
5391
5399
|
SELECT
|
|
5392
|
-
substr(rel, 1, instr(rel,
|
|
5400
|
+
substr(rel, 1, instr(rel, ?) - 1) AS name,
|
|
5393
5401
|
COUNT(*) AS file_count,
|
|
5394
5402
|
SUM(size) AS bytes,
|
|
5395
5403
|
SUM(CASE WHEN status = 'duplicate' THEN 1 ELSE 0 END)
|
|
@@ -5401,13 +5409,15 @@ class RegistryCore {
|
|
|
5401
5409
|
MIN(rel) AS lo,
|
|
5402
5410
|
MAX(rel) AS hi
|
|
5403
5411
|
FROM scoped
|
|
5404
|
-
WHERE instr(rel,
|
|
5412
|
+
WHERE instr(rel, ?) > 0
|
|
5405
5413
|
GROUP BY name
|
|
5406
5414
|
ORDER BY ${order}
|
|
5407
5415
|
LIMIT ? OFFSET ?
|
|
5408
5416
|
`).all(
|
|
5409
5417
|
String(prefix).length + 1,
|
|
5410
5418
|
...values,
|
|
5419
|
+
separator,
|
|
5420
|
+
separator,
|
|
5411
5421
|
limit,
|
|
5412
5422
|
Math.max(0, Number(offset) || 0)
|
|
5413
5423
|
)
|
|
@@ -5495,6 +5505,7 @@ class RegistryCore {
|
|
|
5495
5505
|
const {
|
|
5496
5506
|
sourceId,
|
|
5497
5507
|
prefix,
|
|
5508
|
+
separator,
|
|
5498
5509
|
view,
|
|
5499
5510
|
statusFilter,
|
|
5500
5511
|
query,
|
|
@@ -5510,6 +5521,7 @@ class RegistryCore {
|
|
|
5510
5521
|
directories: this.treeDirectoryRows({
|
|
5511
5522
|
sourceId,
|
|
5512
5523
|
prefix,
|
|
5524
|
+
separator,
|
|
5513
5525
|
statuses: this.statusesForView(view, statusFilter),
|
|
5514
5526
|
query,
|
|
5515
5527
|
nameOnly,
|
|
@@ -5530,7 +5542,8 @@ class RegistryCore {
|
|
|
5530
5542
|
unrestricted: false,
|
|
5531
5543
|
externalSourceIds,
|
|
5532
5544
|
nameOnly,
|
|
5533
|
-
parentPrefix: prefix
|
|
5545
|
+
parentPrefix: prefix,
|
|
5546
|
+
parentSeparator: separator
|
|
5534
5547
|
})
|
|
5535
5548
|
}
|
|
5536
5549
|
}
|
|
@@ -5548,7 +5561,8 @@ class RegistryCore {
|
|
|
5548
5561
|
unrestricted,
|
|
5549
5562
|
externalSourceIds,
|
|
5550
5563
|
nameOnly,
|
|
5551
|
-
parentPrefix
|
|
5564
|
+
parentPrefix,
|
|
5565
|
+
parentSeparator
|
|
5552
5566
|
} = options
|
|
5553
5567
|
// Size and name sorting are one server-side order, so only one can win.
|
|
5554
5568
|
const sort = sizeSort === "asc" || sizeSort === "desc"
|
|
@@ -5564,7 +5578,8 @@ class RegistryCore {
|
|
|
5564
5578
|
pageSize,
|
|
5565
5579
|
externalSourceIds,
|
|
5566
5580
|
nameOnly,
|
|
5567
|
-
parentPrefix
|
|
5581
|
+
parentPrefix,
|
|
5582
|
+
parentSeparator
|
|
5568
5583
|
})
|
|
5569
5584
|
const hasMore = rows.length > pageSize
|
|
5570
5585
|
if (hasMore) rows.pop()
|
package/package.json
CHANGED
|
@@ -375,6 +375,57 @@ describe("Save Space engine", () => {
|
|
|
375
375
|
await close(reopened)
|
|
376
376
|
})
|
|
377
377
|
|
|
378
|
+
test("the folder tree reads Windows paths", async () => {
|
|
379
|
+
// Backslash separators cannot be exercised through the filesystem on a
|
|
380
|
+
// POSIX runner, so this drives the registry with Windows-shaped rows. It
|
|
381
|
+
// guards the separator, which used to be written into the SQL text where
|
|
382
|
+
// JavaScript and SQLite escaped it differently and neither matched.
|
|
383
|
+
const home = await makeHome()
|
|
384
|
+
const RegistryCore = require("../kernel/vault/registry_core")
|
|
385
|
+
const core = new RegistryCore(home)
|
|
386
|
+
await core.load()
|
|
387
|
+
const root = "C:\\Users\\x\\pinokio\\api\\demo"
|
|
388
|
+
const insert = core.database.prepare(`INSERT INTO files
|
|
389
|
+
(path, hash, size, mtime, ctime, dev, ino, mode, uid, gid,
|
|
390
|
+
source_id, app, status, updated_at)
|
|
391
|
+
VALUES (?, ?, ?, 0, 0, 1, ?, 0, 0, 0, 'app:demo', 'demo',
|
|
392
|
+
'reference', 0)`);
|
|
393
|
+
[
|
|
394
|
+
[`${root}\\app\\models\\big.bin`, 4000],
|
|
395
|
+
[`${root}\\app\\models\\small.bin`, 10],
|
|
396
|
+
[`${root}\\cache\\only\\deep\\x.bin`, 50],
|
|
397
|
+
[`${root}\\loose.bin`, 700]
|
|
398
|
+
].forEach(([filePath, size], index) =>
|
|
399
|
+
insert.run(filePath, `h${index}`, size, index + 1))
|
|
400
|
+
|
|
401
|
+
const directories = core.treeDirectoryRows({
|
|
402
|
+
sourceId: "app:demo",
|
|
403
|
+
prefix: `${root}\\`,
|
|
404
|
+
separator: "\\",
|
|
405
|
+
statuses: [],
|
|
406
|
+
query: "",
|
|
407
|
+
nameOnly: false,
|
|
408
|
+
sizeSort: "desc",
|
|
409
|
+
limit: 50,
|
|
410
|
+
offset: 0
|
|
411
|
+
})
|
|
412
|
+
assert.deepEqual(directories.map((row) => row.name), ["app", "cache"])
|
|
413
|
+
assert.equal(directories[0].bytes, 4010)
|
|
414
|
+
// The collapse reads these, so they have to carry the whole subpath.
|
|
415
|
+
assert.equal(directories[1].lo, "cache\\only\\deep\\x.bin")
|
|
416
|
+
|
|
417
|
+
const files = core.fileStreamRows({
|
|
418
|
+
sourceId: "app:demo",
|
|
419
|
+
query: "",
|
|
420
|
+
sort: "path",
|
|
421
|
+
limit: 50,
|
|
422
|
+
parentPrefix: `${root}\\`,
|
|
423
|
+
parentSeparator: "\\"
|
|
424
|
+
})
|
|
425
|
+
assert.deepEqual(files.map((row) => row.path), [`${root}\\loose.bin`])
|
|
426
|
+
core.database.close()
|
|
427
|
+
})
|
|
428
|
+
|
|
378
429
|
test("the folder tree resolves a group of locations to its members", async () => {
|
|
379
430
|
const { home, vault } = await makeVault()
|
|
380
431
|
await writeCandidate(
|