/**
* locations.json must travel with the release it describes (org #146).
*
* A vdrive stores `index -> contentHash`. Serving needs the other half,
* `contentHash -> (file, offset, length)`, and rebuilding that by re-chunking
* every published file is O(all bytes) on every boot. So it is written once at
* publish time as `locations.json`.
*
* It lived only at `<releaseDir>/<repo>/.vdrive/locations.json` — OUTSIDE the
* version dir, so it is not part of what replicates with a release. A mirror
* therefore holds the artifacts and the key (which rides in manifest.json) but
* cannot resolve a single block, and has to pay for a second full copy of every
* byte to seed a drive it could otherwise serve for ~281KB.
*
* The index is CUMULATIVE: publishTree loads the prior index, adds the new
* version's chunks, and saves the union. So the copy written into a version dir
* is a snapshot of the whole drive at that point, not a per-version delta —
* which is what makes the NEWEST version's copy sufficient on its own.
*/
import test from 'brittle'
import fsp from '#node:fs/promises'
import path from '#node:path'
import os from '#node:os'
import { publishVersionToDrive, vdriveStoreDir } from '../lib/vdrive-publish.js'
function tmpdir (name) {
return fsp.mkdtemp(path.join(os.tmpdir(), 'pgv3-vdloc-' + name + '-'))
}
async function publish (releaseDir, repo, version, files) {
const versionDir = path.join(releaseDir, repo, 'versions', version)
await fsp.mkdir(versionDir, { recursive: true })
for (const [name, data] of Object.entries(files)) {
await fsp.writeFile(path.join(versionDir, name), data)
}
const { chunk } = await import('hyper-overlay/chunker')
const res = await publishVersionToDrive({
releaseDir,
repo,
version,
channel: 'production',
vdrive: { enabled: true, channels: null },
chunk
})
return { res, versionDir }
}
test('locations.json is written into the version dir so it replicates', async (t) => {
const releaseDir = await tmpdir('travel')
const repo = 'demo'
const { res, versionDir } = await publish(releaseDir, repo, '1.0.0', {
'app.bin': Buffer.alloc(512 * 1024, 3)
})
t.is(res.ok, true, 'published')
const inVersion = path.join(versionDir, 'locations.json')
const inStore = path.join(vdriveStoreDir(releaseDir, repo), 'locations.json')
t.ok(await exists(inStore), 'the drive keeps its working copy')
t.ok(await exists(inVersion), 'and a copy travels with the version')
const a = JSON.parse(await fsp.readFile(inStore, 'utf8'))
const b = JSON.parse(await fsp.readFile(inVersion, 'utf8'))
t.alike(b, a, 'the travelling copy is the same index')
t.ok(b.entries.length > 0, 'and it is not empty')
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test('every recorded path is relative and inside the root', async (t) => {
// An absolute path would not resolve on another machine and would bake this
// machine's layout into an artifact peers can read.
const releaseDir = await tmpdir('rel')
const repo = 'demo'
const { versionDir } = await publish(releaseDir, repo, '1.0.0', {
'app.bin': Buffer.alloc(256 * 1024, 9),
'notes.txt': 'hello'
})
const idx = JSON.parse(await fsp.readFile(path.join(versionDir, 'locations.json'), 'utf8'))
for (const [, rel] of idx.entries) {
t.absent(path.isAbsolute(rel), 'relative: ' + rel)
t.absent(rel.startsWith('..'), 'inside the root: ' + rel)
}
await fsp.rm(releaseDir, { recursive: true, force: true })
})
test("the newest version's copy covers earlier versions too", async (t) => {
// The index accumulates, so a mirror needs only the latest version's copy to
// serve the whole drive. If this ever became a per-version delta, a mirror
// would silently fail to resolve blocks belonging to older releases.
const releaseDir = await tmpdir('accum')
const repo = 'demo'
const first = await publish(releaseDir, repo, '1.0.0', {
'one.bin': Buffer.alloc(256 * 1024, 1)
})
t.is(first.res.ok, true, 'first published')
const second = await publish(releaseDir, repo, '2.0.0', {
'two.bin': Buffer.alloc(256 * 1024, 2)
})
t.is(second.res.ok, true, 'second published')
const older = JSON.parse(
await fsp.readFile(path.join(first.versionDir, 'locations.json'), 'utf8')
)
const newest = JSON.parse(
await fsp.readFile(path.join(second.versionDir, 'locations.json'), 'utf8')
)
const newestHashes = new Set(newest.entries.map((e) => e[0]))
for (const [hash] of older.entries) {
t.ok(newestHashes.has(hash), 'newest snapshot still carries ' + hash.slice(0, 12))
}
t.ok(newest.entries.length >= older.entries.length, 'and it only grew')
await fsp.rm(releaseDir, { recursive: true, force: true })
})
async function exists (p) {
try {
await fsp.stat(p)
return true
} catch {
return false
}
}