fix(web): harden multimodal draft and storage lifecycle

This commit is contained in:
creatixchu
2026-07-31 17:15:08 +08:00
parent f415e16c83
commit 8b00482d7c
34 changed files with 303 additions and 129 deletions
@@ -2,8 +2,8 @@
import { createHash, randomUUID } from 'node:crypto'
import { constants } from 'node:fs'
import { chmod, link, mkdir, open, readFile, stat, unlink } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { chmod, link, mkdir, open, readFile, unlink } from 'node:fs/promises'
import { dirname, join, parse, resolve } from 'node:path'
import {
AttachmentError,
AttachmentId,
@@ -17,6 +17,7 @@ import type {
import { detectImage, probeImage } from './image.ts'
const ID_PATTERN = /^sha256:([a-f0-9]{64})$/
const durableHomes = new Set<string>()
function digest(data: Uint8Array): string {
return createHash('sha256').update(data).digest('hex')
@@ -83,31 +84,6 @@ async function syncDirectory(path: string): Promise<void> {
}
}
/**
* Walk up from a preferred boundary to the deepest ancestor that already
* exists. A first save may create DSH_HOME itself (recursive mkdir), and a
* directory this process creates is not durable until its parent entry syncs
* — so only a pre-existing directory may be vouched as the durable stop.
* @param path - preferred absolute boundary.
* @returns `path` when it exists, else its closest existing ancestor.
*/
async function existingBoundary(path: string): Promise<string> {
let level = resolve(path)
while (true) {
try {
await stat(level)
return level
} catch {
// Swallows only the stat probe's failure: a missing (or unreadable)
// level simply moves the boundary up; mkdir later surfaces real errors.
}
const parent = dirname(level)
/* v8 ignore next -- filesystem-root guard: the root directory always exists, so stat returns first. */
if (parent === level) return level
level = parent
}
}
/**
* Create one private directory tree and persist every ancestor entry up to a
* caller-vouched durable boundary. The walk deliberately ignores what mkdir
@@ -134,6 +110,20 @@ async function ensureDurableDirectory(path: string, boundary: string): Promise<v
}
}
/**
* Establish this process's proof that one DSH_HOME entry and every ancestor
* below the filesystem root are durable. Mere existence is insufficient: a
* concurrent process may have created the directory but not synced its parent.
*/
async function ensureDurableHome(path: string): Promise<string> {
const home = resolve(path)
if (!durableHomes.has(home)) {
await ensureDurableDirectory(home, parse(home).root)
durableHomes.add(home)
}
return home
}
/**
* Save and verify immutable image bytes below a versioned attachment root.
* @param root - absolute `DSH_HOME/attachments/v1` root.
@@ -147,12 +137,10 @@ 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')
// The preferred boundary is the root's grandparent (DSH_HOME for the
// documented `DSH_HOME/attachments/v1` layout): `attachments`/`v1` may be
// first-created by a concurrent save, so their entries sync on every path.
// When DSH_HOME itself does not exist yet, the boundary retreats to its
// closest existing ancestor so the first save syncs the new home entry too.
const boundary = await existingBoundary(dirname(dirname(resolve(root))))
// Establish DSH_HOME itself against the filesystem root once per process.
// Every process performs that proof independently, so observing a directory
// another process created can never be mistaken for durable publication.
const boundary = await ensureDurableHome(dirname(dirname(resolve(root))))
await ensureDurableDirectory(bucket, boundary)
await ensureDurableDirectory(staging, boundary)
const temporary = join(staging, randomUUID())