pinokiod 8.0.50 → 8.0.51
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 +84 -74
- package/package.json +1 -1
- package/server/public/vault.js +20 -1
- package/test/vault-sweep.test.js +36 -25
- package/test/vault-ui.test.js +16 -23
package/kernel/vault/index.js
CHANGED
|
@@ -103,8 +103,8 @@ class Vault {
|
|
|
103
103
|
get blobRoot() {
|
|
104
104
|
return path.resolve(this.root, "sha256")
|
|
105
105
|
}
|
|
106
|
-
get
|
|
107
|
-
return path.resolve(this.root, "sources")
|
|
106
|
+
get externalSourcesPath() {
|
|
107
|
+
return path.resolve(this.root, "external-sources.json")
|
|
108
108
|
}
|
|
109
109
|
storePathFor(hash) {
|
|
110
110
|
if (typeof hash !== "string" || !SHA256_RE.test(hash)) {
|
|
@@ -129,6 +129,43 @@ class Vault {
|
|
|
129
129
|
if (!st) throw unsafeStoragePath(directory)
|
|
130
130
|
return st
|
|
131
131
|
}
|
|
132
|
+
async readExternalSourcePaths() {
|
|
133
|
+
let raw
|
|
134
|
+
try {
|
|
135
|
+
raw = await this.registry.readFile(this.externalSourcesPath)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (isMissingError(error)) return []
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
140
|
+
let parsed
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(raw)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw new Error("External folder settings are invalid.")
|
|
145
|
+
}
|
|
146
|
+
if (!parsed || !Array.isArray(parsed.paths)) {
|
|
147
|
+
throw new Error("External folder settings are invalid.")
|
|
148
|
+
}
|
|
149
|
+
const paths = []
|
|
150
|
+
for (const value of parsed.paths) {
|
|
151
|
+
if (typeof value !== "string" || !path.isAbsolute(value.trim())) continue
|
|
152
|
+
const resolved = path.resolve(value.trim())
|
|
153
|
+
if (!paths.some((existing) => samePath(existing, resolved))) paths.push(resolved)
|
|
154
|
+
}
|
|
155
|
+
return paths
|
|
156
|
+
}
|
|
157
|
+
async writeExternalSourcePaths(folderPaths) {
|
|
158
|
+
const paths = []
|
|
159
|
+
for (const value of folderPaths) {
|
|
160
|
+
if (typeof value !== "string" || !path.isAbsolute(value.trim())) continue
|
|
161
|
+
const resolved = path.resolve(value.trim())
|
|
162
|
+
if (!paths.some((existing) => samePath(existing, resolved))) paths.push(resolved)
|
|
163
|
+
}
|
|
164
|
+
await this.registry.atomicWrite(
|
|
165
|
+
this.externalSourcesPath,
|
|
166
|
+
JSON.stringify({ paths }, null, 2)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
132
169
|
async storeStatIfPresent(storePath, options = {}) {
|
|
133
170
|
if (!isPathWithin(this.blobRoot, storePath)) throw unsafeStoragePath(storePath)
|
|
134
171
|
if (!await this.directoryIfSafe(this.root) || !await this.directoryIfSafe(this.blobRoot)) {
|
|
@@ -205,6 +242,11 @@ class Vault {
|
|
|
205
242
|
}
|
|
206
243
|
}
|
|
207
244
|
}
|
|
245
|
+
case "remove_source":
|
|
246
|
+
if (typeof payload.source_id !== "string" || !payload.source_id) {
|
|
247
|
+
return { error: "Choose an external folder to remove." }
|
|
248
|
+
}
|
|
249
|
+
return this.runMutation(() => this.removeExternalSource(payload.source_id))
|
|
208
250
|
case "scan": {
|
|
209
251
|
if (payload.scope_id && !this.scanSource(payload.scope_id)) {
|
|
210
252
|
return { error: "That scan location is no longer available." }
|
|
@@ -313,34 +355,6 @@ class Vault {
|
|
|
313
355
|
return source
|
|
314
356
|
}
|
|
315
357
|
|
|
316
|
-
const seenExternalRoots = new Set()
|
|
317
|
-
const addExternalEntries = async (importRoot, idPrefix = "") => {
|
|
318
|
-
let entries = []
|
|
319
|
-
try {
|
|
320
|
-
entries = await fs.promises.readdir(importRoot, { withFileTypes: true })
|
|
321
|
-
} catch (error) {
|
|
322
|
-
if (!isMissingError(error)) throw error
|
|
323
|
-
}
|
|
324
|
-
for (const entry of entries) {
|
|
325
|
-
if (!entry.isSymbolicLink()) continue
|
|
326
|
-
const mountPath = path.resolve(importRoot, entry.name)
|
|
327
|
-
try {
|
|
328
|
-
const root = await fs.promises.realpath(mountPath)
|
|
329
|
-
const st = await fs.promises.stat(root)
|
|
330
|
-
if (!st.isDirectory()) continue
|
|
331
|
-
const canonical = path.resolve(root)
|
|
332
|
-
if (seenExternalRoots.has(canonical)) continue
|
|
333
|
-
seenExternalRoots.add(canonical)
|
|
334
|
-
sources.push(await decorate({
|
|
335
|
-
id: sourceId("external", `${idPrefix}${entry.name}`), kind: "external", label: entry.name,
|
|
336
|
-
root: canonical, mount_path: mountPath, parent_id: "external"
|
|
337
|
-
}))
|
|
338
|
-
} catch (error) {
|
|
339
|
-
if (!isMissingError(error)) throw error
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
358
|
let apiEntries = []
|
|
345
359
|
try {
|
|
346
360
|
apiEntries = await fs.promises.readdir(apiRoot, { withFileTypes: true })
|
|
@@ -356,9 +370,15 @@ class Vault {
|
|
|
356
370
|
}))
|
|
357
371
|
}
|
|
358
372
|
}
|
|
359
|
-
await
|
|
360
|
-
|
|
361
|
-
|
|
373
|
+
for (const configuredPath of await this.readExternalSourcePaths()) {
|
|
374
|
+
sources.push(await decorate({
|
|
375
|
+
id: sourceId("external", configuredPath),
|
|
376
|
+
kind: "external",
|
|
377
|
+
label: path.basename(configuredPath) || configuredPath,
|
|
378
|
+
root: configuredPath,
|
|
379
|
+
parent_id: "external",
|
|
380
|
+
configured: true
|
|
381
|
+
}))
|
|
362
382
|
}
|
|
363
383
|
|
|
364
384
|
let homeEntries = []
|
|
@@ -421,49 +441,38 @@ class Vault {
|
|
|
421
441
|
return { created: false, source: existing }
|
|
422
442
|
}
|
|
423
443
|
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
try
|
|
432
|
-
await fs.promises.symlink(canonical, candidate, this.kernel.platform === "win32" ? "junction" : "dir")
|
|
433
|
-
mountPath = candidate
|
|
434
|
-
break
|
|
435
|
-
} catch (error) {
|
|
436
|
-
if (error && error.code === "EEXIST") continue
|
|
437
|
-
if (error && (error.code === "EACCES" || error.code === "EPERM")) {
|
|
438
|
-
throw new Error("Pinokio does not have permission to add that folder.")
|
|
439
|
-
}
|
|
440
|
-
throw new Error("Pinokio could not add that folder.")
|
|
441
|
-
}
|
|
444
|
+
const configuredPaths = await this.readExternalSourcePaths()
|
|
445
|
+
configuredPaths.push(canonical)
|
|
446
|
+
await this.writeExternalSourcePaths(configuredPaths)
|
|
447
|
+
await this.refreshSources()
|
|
448
|
+
const source = this._sources.find((item) =>
|
|
449
|
+
item.kind === "external" && item.configured && samePath(item.root, canonical))
|
|
450
|
+
if (!source) {
|
|
451
|
+
throw new Error("The folder could not be added. Restart Pinokio and try again.")
|
|
442
452
|
}
|
|
443
|
-
|
|
453
|
+
return { created: true, source }
|
|
454
|
+
}
|
|
455
|
+
async removeExternalSource(sourceIdToRemove) {
|
|
456
|
+
await this.refreshSources()
|
|
457
|
+
const source = this._sources.find((item) =>
|
|
458
|
+
item.id === sourceIdToRemove && item.kind === "external" && item.configured)
|
|
459
|
+
if (!source) return { error: "That external folder is no longer configured." }
|
|
444
460
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return { created: true, source }
|
|
450
|
-
} catch (error) {
|
|
451
|
-
// Adding a source is transactional. Roll back only the exact directory
|
|
452
|
-
// link created above; preserve any path an external writer replaced.
|
|
453
|
-
try {
|
|
454
|
-
const [mounted, target] = await Promise.all([
|
|
455
|
-
fs.promises.lstat(mountPath),
|
|
456
|
-
fs.promises.realpath(mountPath)
|
|
457
|
-
])
|
|
458
|
-
if (mounted.isSymbolicLink() && samePath(target, canonical)) {
|
|
459
|
-
await fs.promises.unlink(mountPath)
|
|
460
|
-
}
|
|
461
|
-
} catch {
|
|
462
|
-
// The original error remains authoritative; an unverified path is
|
|
463
|
-
// deliberately preserved rather than deleted.
|
|
464
|
-
}
|
|
465
|
-
throw error
|
|
461
|
+
const configuredPaths = await this.readExternalSourcePaths()
|
|
462
|
+
const remaining = configuredPaths.filter((folderPath) => !samePath(folderPath, source.root))
|
|
463
|
+
if (remaining.length === configuredPaths.length) {
|
|
464
|
+
return { error: "That external folder is no longer configured." }
|
|
466
465
|
}
|
|
466
|
+
await this.writeExternalSourcePaths(remaining)
|
|
467
|
+
await this.refreshSources()
|
|
468
|
+
this.reconcileConfiguredSources()
|
|
469
|
+
for (const [filePath, entry] of [...this.registry.excluded]) {
|
|
470
|
+
if (entry.source_id === source.id) this.registry.excluded.delete(filePath)
|
|
471
|
+
}
|
|
472
|
+
this.registry.sourceScans.delete(source.id)
|
|
473
|
+
this.registry.lastScan = null
|
|
474
|
+
this.registry.schedulePersist()
|
|
475
|
+
return { removed: true, source_id: source.id, label: source.label }
|
|
467
476
|
}
|
|
468
477
|
sourceForPath(filePath, preferredId) {
|
|
469
478
|
let cursor = path.resolve(filePath)
|
|
@@ -1811,7 +1820,8 @@ class Vault {
|
|
|
1811
1820
|
display_path: source.kind === "pinokio" ? source.root : (source.mount_path || source.root),
|
|
1812
1821
|
target_path: source.kind === "external" ? source.root : null,
|
|
1813
1822
|
parent_id: scopeId ? null : source.parent_id, app: source.app || null,
|
|
1814
|
-
available: source.available !== false, shareable: source.kind === "virtual" ? null : !!source.shareable
|
|
1823
|
+
available: source.available !== false, shareable: source.kind === "virtual" ? null : !!source.shareable,
|
|
1824
|
+
removable: source.kind === "external" && source.configured === true
|
|
1815
1825
|
}))
|
|
1816
1826
|
const result = {
|
|
1817
1827
|
enabled: true,
|
package/package.json
CHANGED
package/server/public/vault.js
CHANGED
|
@@ -20,6 +20,9 @@ const COPY = {
|
|
|
20
20
|
external_added: "Added to Locations. Run a scan when you’re ready.",
|
|
21
21
|
external_exists: "That folder is already in Locations.",
|
|
22
22
|
external_other_disk: "Added to Locations. It can be scanned, but files on this disk cannot be deduplicated with files in your Pinokio folder.",
|
|
23
|
+
remove_external_folder: "Remove from Locations",
|
|
24
|
+
remove_external_confirm: "Remove “{name}” from Locations? This does not delete or modify any files.",
|
|
25
|
+
external_removed: "Removed from Locations. No files were changed.",
|
|
23
26
|
pinokio_folder: "Pinokio folder",
|
|
24
27
|
save_space: "Save space",
|
|
25
28
|
disk_space_freed: "of disk space freed",
|
|
@@ -530,6 +533,9 @@ const batchAction = (source) => {
|
|
|
530
533
|
if (!source.shareable || !shareable.length) return `<div class="vault-pane-action-note">${esc(COPY.sharing_unavailable)}</div>`
|
|
531
534
|
return `<button class="vault-button" type="button" data-deduplicate-scope="${attr(source.id)}">${esc(COPY.deduplicate)} ${countLabel(shareable.length)}</button>`
|
|
532
535
|
}
|
|
536
|
+
const removeSourceAction = (source) => source && source.removable
|
|
537
|
+
? `<button class="vault-button" type="button" data-remove-source="${attr(source.id)}">${esc(COPY.remove_external_folder)}</button>`
|
|
538
|
+
: ""
|
|
533
539
|
|
|
534
540
|
const toolbarSummary = (visibleItems) => {
|
|
535
541
|
if (state.view === "duplicates") {
|
|
@@ -588,7 +594,8 @@ const renderToolbar = (visibleItems, allItems) => {
|
|
|
588
594
|
${displayModeControl()}
|
|
589
595
|
${descriptionMarkup}
|
|
590
596
|
<span class="vault-toolbar-count" id="vault-toolbar-summary">${esc(toolbarSummary(visibleItems))}</span>
|
|
591
|
-
${state.view === "all" ? batchAction(source) : bulkDeduplicationAction(allItems)}
|
|
597
|
+
${state.view === "all" ? batchAction(source) : bulkDeduplicationAction(allItems)}
|
|
598
|
+
${removeSourceAction(source)}`
|
|
592
599
|
}
|
|
593
600
|
|
|
594
601
|
const statusMarkup = (item) => {
|
|
@@ -1582,6 +1589,18 @@ document.addEventListener("click", async (event) => {
|
|
|
1582
1589
|
if (state.expandedFiles.has(file)) state.expandedFiles.delete(file)
|
|
1583
1590
|
else state.expandedFiles.add(file)
|
|
1584
1591
|
render()
|
|
1592
|
+
} else if (target.dataset.removeSource) {
|
|
1593
|
+
const source = sourceById(target.dataset.removeSource)
|
|
1594
|
+
if (!source) return
|
|
1595
|
+
const message = COPY.remove_external_confirm.replace("{name}", source.label)
|
|
1596
|
+
if (!window.confirm(message)) return
|
|
1597
|
+
await runAction({
|
|
1598
|
+
action: "remove_source",
|
|
1599
|
+
source_id: source.id
|
|
1600
|
+
}, () => {
|
|
1601
|
+
state.sourceId = null
|
|
1602
|
+
return COPY.external_removed
|
|
1603
|
+
})
|
|
1585
1604
|
} else if (target.id === "btn-add-source") {
|
|
1586
1605
|
target.disabled = true
|
|
1587
1606
|
try {
|
package/test/vault-sweep.test.js
CHANGED
|
@@ -265,7 +265,7 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
265
265
|
assert.strictEqual((await fs.promises.stat(outside)).nlink, 1)
|
|
266
266
|
})
|
|
267
267
|
|
|
268
|
-
test('top-level
|
|
268
|
+
test('top-level api links are not adopted as external sources', async (t) => {
|
|
269
269
|
const { home, vault } = await makeEnv()
|
|
270
270
|
const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-external-'))
|
|
271
271
|
const nested = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-external-nested-'))
|
|
@@ -286,22 +286,20 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
286
286
|
const source = vault.sources().find((item) => item.kind === 'external' && item.label === 'linked-models')
|
|
287
287
|
const canonicalImported = path.resolve(await fs.promises.realpath(external), 'models', 'm.bin')
|
|
288
288
|
const canonicalNestedFile = path.resolve(await fs.promises.realpath(nested), 'should-not-scan.bin')
|
|
289
|
-
assert.
|
|
290
|
-
assert.strictEqual(
|
|
291
|
-
assert.ok(vault.registry.scanIndex.has(canonicalImported), 'external root was scanned')
|
|
289
|
+
assert.strictEqual(source, undefined, 'api links are not Vault configuration')
|
|
290
|
+
assert.strictEqual(vault.registry.scanIndex.has(canonicalImported), false)
|
|
292
291
|
assert.strictEqual(vault.registry.scanIndex.has(canonicalNestedFile), false, 'nested directory link was not followed')
|
|
293
|
-
assert.strictEqual(vault.registry.duplicates.get(canonicalImported).source_id, source.id)
|
|
294
292
|
assert.strictEqual((await fs.promises.stat(local)).nlink, 2)
|
|
295
293
|
|
|
296
294
|
const status = await vault.status()
|
|
297
|
-
assert.
|
|
298
|
-
assert.strictEqual(status.duplicates.
|
|
295
|
+
assert.strictEqual(status.sources.some((item) => item.kind === 'external'), false)
|
|
296
|
+
assert.strictEqual(status.duplicates.some((item) => item.path === canonicalImported), false)
|
|
299
297
|
assert.strictEqual(status.last_scan.home_bytes_total, content.length, 'Pinokio metric excludes external files')
|
|
300
|
-
assert.strictEqual(status.last_scan.
|
|
298
|
+
assert.strictEqual(status.last_scan.bytes_total, content.length)
|
|
301
299
|
})
|
|
302
300
|
|
|
303
|
-
test('adding an external source
|
|
304
|
-
const { home, vault } = await makeEnv()
|
|
301
|
+
test('adding and removing an external source persists only its path and never creates a link', async () => {
|
|
302
|
+
const { home, vault, kernel } = await makeEnv()
|
|
305
303
|
const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-added-external-'))
|
|
306
304
|
homes.push(external)
|
|
307
305
|
const content = crypto.randomBytes(4096)
|
|
@@ -312,27 +310,42 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
312
310
|
assert.strictEqual(added.source.kind, 'external')
|
|
313
311
|
assert.strictEqual(added.source.parent_id, 'external')
|
|
314
312
|
assert.strictEqual(vault.registry.scanIndex.size, 0, 'adding a source never scans it')
|
|
315
|
-
assert.strictEqual((await fs.promises.
|
|
316
|
-
assert.strictEqual(
|
|
313
|
+
assert.strictEqual(added.source.root, path.resolve(await fs.promises.realpath(external)))
|
|
314
|
+
assert.strictEqual(added.source.mount_path, undefined)
|
|
315
|
+
assert.deepStrictEqual(
|
|
316
|
+
JSON.parse(await fs.promises.readFile(vault.externalSourcesPath, 'utf8')).paths,
|
|
317
|
+
[added.source.root]
|
|
318
|
+
)
|
|
319
|
+
assert.strictEqual(fs.existsSync(path.resolve(home, 'vault', 'sources')), false)
|
|
317
320
|
assert.deepStrictEqual(await fs.promises.readdir(path.resolve(home, 'api')), [],
|
|
318
|
-
'a
|
|
321
|
+
'adding a Vault source never creates an api link')
|
|
319
322
|
|
|
320
323
|
const repeated = await vault.addExternalSource(external)
|
|
321
324
|
assert.strictEqual(repeated.created, false, 'the same physical folder is not imported twice')
|
|
322
325
|
assert.strictEqual(vault.sources().filter((source) => source.kind === 'external').length, 1)
|
|
323
326
|
|
|
327
|
+
const fresh = new Vault(kernel)
|
|
328
|
+
await fresh.init()
|
|
329
|
+
assert.ok(fresh.sources().some((source) =>
|
|
330
|
+
source.kind === 'external' && source.root === added.source.root))
|
|
331
|
+
|
|
324
332
|
await vault.sweeper.scan()
|
|
325
333
|
const canonicalImported = path.resolve(await fs.promises.realpath(imported))
|
|
326
334
|
assert.ok(vault.registry.scanIndex.has(canonicalImported))
|
|
327
335
|
|
|
328
|
-
await
|
|
329
|
-
|
|
336
|
+
const removed = await vault.perform('remove_source', { source_id: added.source.id })
|
|
337
|
+
assert.strictEqual(removed.removed, true)
|
|
338
|
+
assert.deepStrictEqual(
|
|
339
|
+
JSON.parse(await fs.promises.readFile(vault.externalSourcesPath, 'utf8')).paths,
|
|
340
|
+
[]
|
|
341
|
+
)
|
|
330
342
|
const status = await vault.status()
|
|
331
343
|
assert.strictEqual(vault.registry.links.has(canonicalImported), false)
|
|
332
344
|
assert.strictEqual(vault.registry.duplicates.has(canonicalImported), false)
|
|
333
345
|
assert.strictEqual(vault.registry.scanIndex.has(canonicalImported), false)
|
|
334
346
|
assert.strictEqual(status.sources.some((source) => source.kind === 'external'), false)
|
|
335
347
|
assert.strictEqual(status.duplicates.some((item) => item.path === canonicalImported), false)
|
|
348
|
+
assert.deepStrictEqual(await fs.promises.readFile(imported), content)
|
|
336
349
|
})
|
|
337
350
|
|
|
338
351
|
test('nested external sources are walked once and attributed to the deepest source', async () => {
|
|
@@ -393,7 +406,7 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
393
406
|
const realStat = fs.promises.stat
|
|
394
407
|
let externalStats = 0
|
|
395
408
|
fs.promises.stat = async (target, options) => {
|
|
396
|
-
if (path.resolve(String(target)) === path.resolve(canonicalExternal) && ++externalStats ===
|
|
409
|
+
if (path.resolve(String(target)) === path.resolve(canonicalExternal) && ++externalStats === 1) {
|
|
397
410
|
const error = new Error('temporary external metadata failure')
|
|
398
411
|
error.code = 'EIO'
|
|
399
412
|
throw error
|
|
@@ -452,28 +465,26 @@ describe('vault manual scan (phase 3)', () => {
|
|
|
452
465
|
assert.strictEqual(vault.registry.lastScan.files, 2)
|
|
453
466
|
})
|
|
454
467
|
|
|
455
|
-
test('a failed source
|
|
468
|
+
test('a failed external-source settings write creates no configuration', async () => {
|
|
456
469
|
const { vault } = await makeEnv()
|
|
457
470
|
const external = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-add-rollback-'))
|
|
458
471
|
homes.push(external)
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
fs.promises.readdir = async (target, options) => {
|
|
463
|
-
if (path.resolve(target) === importRoot && ++importReads === 1) {
|
|
472
|
+
const realAtomicWrite = vault.registry.atomicWrite
|
|
473
|
+
vault.registry.atomicWrite = async (target, contents) => {
|
|
474
|
+
if (path.resolve(target) === vault.externalSourcesPath) {
|
|
464
475
|
const error = new Error('transient read failure')
|
|
465
476
|
error.code = 'EIO'
|
|
466
477
|
throw error
|
|
467
478
|
}
|
|
468
|
-
return
|
|
479
|
+
return realAtomicWrite.call(vault.registry, target, contents)
|
|
469
480
|
}
|
|
470
481
|
try {
|
|
471
482
|
await assert.rejects(vault.addExternalSource(external), (error) => error.code === 'EIO')
|
|
472
483
|
} finally {
|
|
473
|
-
|
|
484
|
+
vault.registry.atomicWrite = realAtomicWrite
|
|
474
485
|
}
|
|
475
486
|
|
|
476
|
-
assert.
|
|
487
|
+
assert.strictEqual(fs.existsSync(vault.externalSourcesPath), false)
|
|
477
488
|
assert.strictEqual(vault.sources().some((source) => source.kind === 'external'), false)
|
|
478
489
|
})
|
|
479
490
|
|
package/test/vault-ui.test.js
CHANGED
|
@@ -1334,6 +1334,8 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
1334
1334
|
assert.match(vaultPage, /id='btn-add-source'/)
|
|
1335
1335
|
assert.match(vaultPage, /<script src="\/Socket\.js"><\/script>/)
|
|
1336
1336
|
assert.match(vaultPage, /action:\s*"add_source"/)
|
|
1337
|
+
assert.match(vaultPage, /action:\s*"remove_source"/)
|
|
1338
|
+
assert.match(vaultPage, /remove_external_folder:\s*"Remove from Locations"/)
|
|
1337
1339
|
assert.match(vaultPage, /identical_contents_at:\s*"Identical contents at"/)
|
|
1338
1340
|
assert.doesNotMatch(vaultPage, /Matching locations/)
|
|
1339
1341
|
assert.match(vaultPage, /scan_waiting:\s*"Waiting for scan results"/)
|
|
@@ -2592,7 +2594,7 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
2592
2594
|
assert.doesNotMatch(actionRoute, /convertPending|runExclusive/)
|
|
2593
2595
|
})
|
|
2594
2596
|
|
|
2595
|
-
test('a stale external source id cannot authorize files outside its current target', async (
|
|
2597
|
+
test('a stale external source id cannot authorize files outside its current target', async () => {
|
|
2596
2598
|
const { home, vault } = await makeEnv()
|
|
2597
2599
|
const firstTarget = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-scope-old-'))
|
|
2598
2600
|
const secondTarget = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-scope-new-'))
|
|
@@ -2601,16 +2603,8 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
2601
2603
|
const hash = sha256(content)
|
|
2602
2604
|
const original = await writeFile(path.resolve(home, 'api', 'appA', 'model.bin'), content)
|
|
2603
2605
|
const outside = await writeFile(path.resolve(firstTarget, 'model.bin'), content)
|
|
2604
|
-
const
|
|
2605
|
-
try {
|
|
2606
|
-
await fs.promises.symlink(firstTarget, mount, process.platform === 'win32' ? 'junction' : 'dir')
|
|
2607
|
-
} catch (error) {
|
|
2608
|
-
t.skip(`directory links unavailable: ${error.message}`)
|
|
2609
|
-
return
|
|
2610
|
-
}
|
|
2611
|
-
await vault.refreshSources()
|
|
2606
|
+
const externalSource = (await vault.addExternalSource(firstTarget)).source
|
|
2612
2607
|
const originalSource = vault.sources().find((source) => source.kind === 'app' && source.app === 'appA')
|
|
2613
|
-
const externalSource = vault.sources().find((source) => source.kind === 'external' && source.label === 'external-models')
|
|
2614
2608
|
await vault.adopt(original, hash, { app: 'appA', source_id: originalSource.id })
|
|
2615
2609
|
const outsideStat = await fs.promises.stat(outside)
|
|
2616
2610
|
vault.registry.duplicates.set(outside, {
|
|
@@ -2618,12 +2612,12 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
2618
2612
|
dev: outsideStat.dev, ino: outsideStat.ino,
|
|
2619
2613
|
mtime: outsideStat.mtimeMs, ctime: outsideStat.ctimeMs
|
|
2620
2614
|
})
|
|
2621
|
-
await
|
|
2622
|
-
await
|
|
2615
|
+
await vault.writeExternalSourcePaths([secondTarget])
|
|
2616
|
+
await vault.refreshSources()
|
|
2623
2617
|
|
|
2624
2618
|
const result = await vault.perform('deduplicate', { scope_id: externalSource.id })
|
|
2625
2619
|
|
|
2626
|
-
assert.
|
|
2620
|
+
assert.match(result.error, /no longer available/i)
|
|
2627
2621
|
assert.strictEqual((await fs.promises.stat(outside)).nlink, 1)
|
|
2628
2622
|
assert.ok(vault.registry.duplicates.has(outside))
|
|
2629
2623
|
})
|
|
@@ -2772,16 +2766,13 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
2772
2766
|
assert.strictEqual(vault.registry.links.has(pending), false)
|
|
2773
2767
|
})
|
|
2774
2768
|
|
|
2775
|
-
test('status separates app, Pinokio folder, and external source metadata', async (
|
|
2769
|
+
test('status separates app, Pinokio folder, and external source metadata', async () => {
|
|
2776
2770
|
const { home, vault } = await makeEnv()
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
t.skip(`directory links unavailable: ${error.message}`)
|
|
2783
|
-
return
|
|
2784
|
-
}
|
|
2771
|
+
const externalParent = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'pinokio-ui-external-'))
|
|
2772
|
+
const external = path.resolve(externalParent, 'linked-models')
|
|
2773
|
+
homes.push(externalParent)
|
|
2774
|
+
await fs.promises.mkdir(external)
|
|
2775
|
+
await vault.addExternalSource(external)
|
|
2785
2776
|
await fs.promises.mkdir(path.resolve(home, 'api', 'local-app'), { recursive: true })
|
|
2786
2777
|
await fs.promises.mkdir(path.resolve(home, 'cache'), { recursive: true })
|
|
2787
2778
|
await vault.refreshSources()
|
|
@@ -2789,6 +2780,8 @@ describe('vault dashboard backend (phase 4)', () => {
|
|
|
2789
2780
|
const byKind = new Map(status.sources.map((source) => [source.id, source]))
|
|
2790
2781
|
assert.ok([...byKind.values()].some((source) => source.kind === 'app' && source.label === 'local-app' && source.parent_id === 'apps'))
|
|
2791
2782
|
assert.ok([...byKind.values()].some((source) => source.kind === 'folder' && source.label === 'cache' && source.parent_id === 'pinokio'))
|
|
2792
|
-
assert.ok([...byKind.values()].some((source) =>
|
|
2783
|
+
assert.ok([...byKind.values()].some((source) =>
|
|
2784
|
+
source.kind === 'external' && source.label === 'linked-models' &&
|
|
2785
|
+
source.parent_id === 'external' && source.removable === true))
|
|
2793
2786
|
})
|
|
2794
2787
|
})
|