fix(gui): close attachment durability gaps

This commit is contained in:
Yichen Jiang
2026-07-27 14:29:19 +08:00
parent b599d1d0af
commit a1835c3228
5 changed files with 142 additions and 16 deletions

View File

@@ -3,7 +3,7 @@
import { createHash, randomUUID } from 'node:crypto'
import { constants } from 'node:fs'
import { chmod, link, mkdir, open, readFile, unlink } from 'node:fs/promises'
import { basename, join } from 'node:path'
import { basename, dirname, join, resolve } from 'node:path'
import {
AttachmentError,
AttachmentId,
@@ -78,6 +78,25 @@ async function syncDirectory(path: string): Promise<void> {
}
}
/**
* Create one private directory tree and persist every newly published ancestor.
* @param path - absolute directory to create.
*/
async function ensureDurableDirectory(path: string): Promise<void> {
const target = resolve(path)
const firstCreated = await mkdir(target, { recursive: true, mode: 0o700 })
await chmod(target, 0o700)
if (firstCreated === undefined) return
const highestCreated = resolve(firstCreated)
let created = target
while (true) {
await syncDirectory(dirname(created))
if (created === highestCreated) return
created = dirname(created)
}
}
/**
* Save and verify immutable image bytes below a versioned attachment root.
* @param root - absolute `DSH_HOME/attachments/v1` root.
@@ -91,10 +110,8 @@ export async function saveImageFile(root: string, input: SaveImageAttachment, li
const sha256 = digest(input.data)
const bucket = join(root, 'objects', sha256.slice(0, 2))
const staging = join(root, 'tmp')
await mkdir(bucket, { recursive: true, mode: 0o700 })
await mkdir(staging, { recursive: true, mode: 0o700 })
await chmod(bucket, 0o700)
await chmod(staging, 0o700)
await ensureDurableDirectory(bucket)
await ensureDurableDirectory(staging)
const temporary = join(staging, randomUUID())
const target = objectPath(root, sha256)
let handle
@@ -112,11 +129,10 @@ export async function saveImageFile(root: string, input: SaveImageAttachment, li
const existing = new Uint8Array(await readFile(target))
if (digest(existing) !== sha256) throw new AttachmentError('Stored attachment failed integrity verification.', 'ATTACHMENT_CORRUPT')
}
// The synced file becomes durable only once its directory entries are: sync
// the bucket (the new object entry) and its parent (the possibly new bucket
// entry) before this reference can reach a session checkpoint. The dedup
// path syncs too — the earlier save that created the entry may have crashed
// before its own directory sync.
// Persist the target entry and close a concurrent bucket-creation window
// before the reference can reach a session checkpoint. The dedup path
// repeats both syncs because it may observe another writer's link before
// that writer reaches its own durability boundary.
await syncDirectory(bucket)
await syncDirectory(join(root, 'objects'))
await unlink(temporary)