/** * 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 } } }