fix(web): harden multimodal draft and storage lifecycle
This commit is contained in:
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user