feat(util): extract dsh-atomic-write and migrate settings-local writes

writeFileAtomic: exclusive-create random-suffix temp + rename carrying the
caller-stated mode; settings-local persistSection now consumes it. The
credentials-local store shares it next.
This commit is contained in:
Yichen Jiang
2026-07-29 12:59:41 +08:00
parent 42de60347a
commit ba37180946
13 changed files with 281 additions and 16 deletions
+5 -16
View File
@@ -8,10 +8,10 @@
import { Context, Service } from 'cordis'
import z from 'schemastery'
import { watch as chokidarWatch } from 'chokidar'
import { randomBytes } from 'node:crypto'
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
import { dirname, extname, join, resolve } from 'node:path'
import { readFile } from 'node:fs/promises'
import { extname, join, resolve } from 'node:path'
import { Document, parseDocument } from 'yaml'
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
import { Settings, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
@@ -137,19 +137,8 @@ export class SettingsLocal extends Settings {
const output = this.spec.format === 'yaml'
? this.renderYaml(ns, section)
: this.renderJson(ns, section)
await mkdir(dirname(this.spec.filename), { recursive: true })
// Exclusive-create (`wx`) a random-suffix sibling: the open refuses to
// follow any planted symlink at a guessable temp path, and the fresh inode
// carries owner-only permissions that survive the rename — a document that
// may hold personal values is never world-readable and never a symlink.
const temp = `${this.spec.filename}.${randomBytes(6).toString('hex')}.tmp`
try {
await writeFile(temp, output, { mode: 0o600, flag: 'wx' })
await rename(temp, this.spec.filename)
} catch (error) {
await rm(temp, { force: true })
throw error
}
// 0600: a document that may hold personal values is never world-readable.
await writeFileAtomic(this.spec.filename, output, { mode: 0o600 })
this.text = output
}