dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,78 @@
|
||||
import { link, open, rename, rm, stat } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import {
|
||||
FileManagerError, FileRevisionConflictError, MAX_TEXT_BYTES, fileError,
|
||||
fileManagerInternals, isManagedPath, normalizeAbsolutePath,
|
||||
} from './index.mjs'
|
||||
|
||||
async function syncDirectory(path) {
|
||||
const handle = await open(path, 'r')
|
||||
try { await handle.sync() } finally { await handle.close() }
|
||||
}
|
||||
|
||||
function bytes(value) {
|
||||
if (typeof value !== 'string') throw new FileManagerError('content must be a string')
|
||||
const result = Buffer.from(value, 'utf8')
|
||||
if (result.byteLength > MAX_TEXT_BYTES) throw new FileManagerError('file is too large to edit', 413, 'FILE_TOO_LARGE')
|
||||
if (result.includes(0)) throw new FileManagerError('content contains NUL', 415, 'FILE_TYPE_UNSUPPORTED')
|
||||
return result
|
||||
}
|
||||
|
||||
export class AtomicFileEditor {
|
||||
constructor({ isManaged = isManagedPath } = {}) {
|
||||
this.isManaged = isManaged
|
||||
}
|
||||
|
||||
async write(value, content, revision, { create = false } = {}) {
|
||||
const path = normalizeAbsolutePath(value)
|
||||
const body = bytes(content)
|
||||
const parent = dirname(path)
|
||||
let original
|
||||
try {
|
||||
original = await stat(path, { bigint: true })
|
||||
if (!original.isFile()) throw new FileManagerError('path is not a regular file', 415, 'FILE_TYPE_UNSUPPORTED')
|
||||
if (create) throw new FileManagerError('file already exists', 409, 'FILE_EXISTS')
|
||||
if (typeof revision !== 'string' || revision !== fileManagerInternals.revisionFor(path, original)) {
|
||||
throw new FileRevisionConflictError()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') throw fileError(error)
|
||||
if (!create) throw fileError(error)
|
||||
if (revision !== null && revision !== undefined) throw new FileManagerError('new file revision must be null')
|
||||
}
|
||||
const temporary = join(parent, `.dsh-edit-${randomUUID()}.tmp`)
|
||||
let handle
|
||||
try {
|
||||
handle = await open(temporary, 'wx', original === undefined ? 0o644 : Number(original.mode & 0o7777n))
|
||||
await handle.writeFile(body)
|
||||
await handle.sync()
|
||||
if (original !== undefined) {
|
||||
await handle.chmod(Number(original.mode & 0o7777n))
|
||||
const current = await handle.stat({ bigint: true })
|
||||
if (current.uid !== original.uid || current.gid !== original.gid) {
|
||||
try { await handle.chown(Number(original.uid), Number(original.gid)) } catch {
|
||||
throw new FileManagerError('cannot preserve file ownership; use the terminal', 403, 'OWNERSHIP_PRESERVE_FAILED')
|
||||
}
|
||||
}
|
||||
}
|
||||
await handle.close()
|
||||
handle = undefined
|
||||
if (original === undefined) {
|
||||
await link(temporary, path)
|
||||
await rm(temporary)
|
||||
} else await rename(temporary, path)
|
||||
await syncDirectory(parent)
|
||||
const saved = await stat(path, { bigint: true })
|
||||
return {
|
||||
path, revision: fileManagerInternals.revisionFor(path, saved), size: body.byteLength,
|
||||
mode: Number(saved.mode & 0o7777n), modifiedAt: new Date(Number(saved.mtimeNs / 1_000_000n)).toISOString(),
|
||||
managed: this.isManaged(path),
|
||||
}
|
||||
} catch (error) {
|
||||
await handle?.close().catch(() => {})
|
||||
await rm(temporary, { force: true }).catch(() => {})
|
||||
throw fileError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user