Files
bare-operating-system/packages/bare-os-coreutils/src/mv.js
T
Raven Scott 15209b4837 feat(booter): hrpc allowlist parser, snapshot hints parity, shell pipefix, safer mv staging
- Add bare-os-hrpc-allowlist.js; wire stock bareOsHrpcRequest + bareOsHrpcAllowlistProbe;
  passthrough BARE_OS_HRPC_ALLOWLIST_JSON and operator env for snapshot/blind-relay/mirror/pear-doctor
- Merge BARE_OS_CORESTORE_SNAPSHOT_WORKFLOW_JSON into bareOsReadSnapshotHintsJson (match proc)
- Shell: pipefail runs all pipeline stages; aggregate first non-zero exit after assign prefixes
- mv: stage renames via vfs.resolveLogical so temp files stay in writable destination dir
- coreutils: POSIX-style split suffixes past zz; xargs doc for -P vs host subprocess
- identity-session: stable Error.code for no-account / passphrase failures
- Docs: KERNEL_CONTRACT, POSIX profile, handbook 5/7/9, kernel-extensions strict-boot table,
  env appendix, developer-guide + scripts README, protocol/booter CHANGELOGs; hrpc allowlist schema
- Tests: hrpc allowlist, pear_doctor schema 2, split suffix, pipefail pipeline; fix PLACEHOLDER_BASELINE link

Pretest: refreshed kernel bins, man.json, posix dashboard, bundle health, seeder parity
2026-04-05 01:31:03 -04:00

164 lines
5.4 KiB
JavaScript

/**
* Move/rename via copy + delete. Hyperdrive has no single-key rename across paths, so
* directory trees and cross-location moves are duplicated then removed. A single regular
* file to a new non-directory path uses a two-phase write: staging buffer bytes under a
* unique **`.bare-os-mv-tmp.*`** name in the **resolved destination directory**, writing the final name,
* removing the staging file, then unlinking the source — so a failed final write leaves
* the source path intact (staging is best-effort removed).
*
* Documented limitations: cross-volume moves always copy+delete; EXDEV-style behavior is
* implicit. Busy targets, partial copy failures, and union read-only trees surface as
* generic errors from the VFS. Prefer same-directory renames for smallest blast radius.
*/
function mvStagingPathForTarget(ctx, target) {
const raw =
ctx.vfs && typeof ctx.vfs.resolveLogical === 'function'
? ctx.vfs.resolveLogical(target)
: String(target || '')
const trimmed = String(raw || '').replace(/\/+$/, '') || '/'
const last = trimmed.lastIndexOf('/')
const dir =
last < 0 ? '/' : last === 0 ? '/' : trimmed.slice(0, last) || '/'
const base =
last < 0 ? trimmed : trimmed.slice(last + 1) || 'file'
const tag =
Date.now().toString(36) +
'-' +
Math.random().toString(36).slice(2, 10) +
Math.random().toString(36).slice(2, 6)
const name = '.bare-os-mv-tmp.' + tag + '.' + base
return dir === '/' ? '/' + name : dir + '/' + name
}
async function mvCopyPath(ctx, from, to, recursive, followSymlink) {
const st = await ctx.vfs.lstat(from)
if (!st) return false
if (st.type === 'symlink') {
if (!followSymlink) {
await ctx.vfs.symlink(await ctx.vfs.readlink(from), to)
return true
}
const fst = await ctx.vfs.stat(from)
if (fst.type === 'file') {
const buf = await ctx.vfs.readFile(from)
if (!buf) return false
await ctx.vfs.writeFile(to, buf)
return true
}
if (fst.type === 'directory') {
if (!recursive) return false
await ctx.vfs.mkdir(to, { recursive: true })
const names = await ctx.vfs.readdir(from)
for (const n of names) {
if (n === '.bareos_empty') continue
const f = from.replace(/\/+$/, '') + '/' + n
const t = to.replace(/\/+$/, '') + '/' + n
if (!(await mvCopyPath(ctx, f, t, true, followSymlink))) return false
}
return true
}
return false
}
if (st.type === 'file') {
const buf = await ctx.vfs.readFile(from)
if (!buf) return false
await ctx.vfs.writeFile(to, buf)
return true
}
if (st.type === 'directory') {
if (!recursive) return false
await ctx.vfs.mkdir(to, { recursive: true })
const names = await ctx.vfs.readdir(from)
for (const n of names) {
if (n === '.bareos_empty') continue
const f = from.replace(/\/+$/, '') + '/' + n
const t = to.replace(/\/+$/, '') + '/' + n
if (!(await mvCopyPath(ctx, f, t, true, followSymlink))) return false
}
return true
}
return false
}
async function run(ctx, argv) {
let followSymlink = false
const paths = []
for (let i = 1; i < argv.length; i++) {
const a = argv[i]
if (a === '-L' || a === '--dereference') {
followSymlink = true
continue
}
if (a === '-P' || a === '--no-dereference') {
followSymlink = false
continue
}
if (a === '--') {
paths.push(...argv.slice(i + 1))
break
}
if (a.startsWith('-')) {
ctx.console.error('mv: unsupported option ' + a)
ctx.exitCode = 1
return
}
paths.push(a)
}
if (paths.length < 2) {
ctx.console.error('usage: mv [-L|-P] SOURCE... DEST')
ctx.exitCode = 1
return
}
const dest = paths.pop()
const sources = paths
let destIsDir = false
try {
const dst = await ctx.vfs.lstat(dest)
destIsDir = !!(dst && dst.type === 'directory')
} catch {
destIsDir = false
}
if (sources.length > 1 && !destIsDir) {
ctx.console.error('mv: target is not a directory')
ctx.exitCode = 1
return
}
for (const src of sources) {
const base = src.replace(/\/+$/, '').split('/').pop() || src
const target =
destIsDir || sources.length > 1
? dest.replace(/\/+$/, '') + '/' + base
: dest
try {
const st = await ctx.vfs.lstat(src)
let followReg = st && st.type === 'file'
if (!followReg && followSymlink && st && st.type === 'symlink') {
const fs = await ctx.vfs.stat(src).catch(() => null)
followReg = !!(fs && fs.type === 'file')
}
const singleFileToFile = sources.length === 1 && !destIsDir && followReg
if (singleFileToFile) {
const buf = await ctx.vfs.readFile(src)
if (!buf) throw new Error('cannot read source')
const stage = mvStagingPathForTarget(ctx, target)
await ctx.vfs.writeFile(stage, buf)
try {
await ctx.vfs.writeFile(target, buf)
} catch (e) {
await ctx.vfs.unlink(stage).catch(() => {})
throw e
}
await ctx.vfs.unlink(stage).catch(() => {})
await ctx.vfs.unlink(src)
continue
}
if (!(await mvCopyPath(ctx, src, target, true, followSymlink))) {
throw new Error('cannot copy')
}
await ctx.vfs.rm(src, { recursive: true, force: true })
} catch (e) {
ctx.console.error('mv: ' + src + ': ' + ((e && e.message) || e))
ctx.exitCode = 1
}
}
}