/** * Node fs.promises-shaped adapter over ctx.vfs for isomorphic-git. * Empty directories are represented by a hidden marker file (filtered from readdir). * Stat/lstat return Node-like objects (mtime/ctime, mtimeMs/ctimeMs, mode, dev, ino, uid, gid) * because isomorphic-git normalizeStats() requires them for the index. */ import path from '#host-path' const MARKER = '.bareos_empty' function err(code, message) { const e = new Error(message || code) // @ts-ignore e.code = code return e } /** Default Unix mode bits isomorphic-git expects for Git trees (see normalizeMode in upstream). */ const MODE_FILE = 0o100644 const MODE_DIR = 0o040755 const MODE_SYMLINK = 0o120000 /** * Node fs.Stats-shaped object for isomorphic-git (normalizeStats reads mtime/ctime and numeric ids). */ export class VfsGitStats { /** * @param {'file' | 'directory' | 'symlink'} type * @param {number} [size] * @param {{ mtimeMs?: number, ctimeMs?: number, mode?: number, dev?: number, ino?: number, uid?: number, gid?: number }} [opts] */ constructor(type, size = 0, opts = {}) { this.type = type this.size = size const ms = typeof opts.mtimeMs === 'number' ? opts.mtimeMs : Date.now() this.mtimeMs = ms this.ctimeMs = typeof opts.ctimeMs === 'number' ? opts.ctimeMs : ms this.atimeMs = ms this.birthtimeMs = this.ctimeMs this.mtime = new Date(this.mtimeMs) this.ctime = new Date(this.ctimeMs) this.atime = this.mtime this.birthtime = this.ctime this.dev = opts.dev ?? 0 this.ino = opts.ino ?? 0 this.mode = opts.mode ?? (type === 'directory' ? MODE_DIR : type === 'symlink' ? MODE_SYMLINK : MODE_FILE) this.uid = opts.uid ?? 0 this.gid = opts.gid ?? 0 this.blocks = 0 this.blksize = 4096 this.nlink = 1 this.rdev = 0 } isFile() { return this.type === 'file' } isDirectory() { return this.type === 'directory' } isSymbolicLink() { return this.type === 'symlink' } } /** * Map vfs lstat/stat result to Stats or throw ENOENT. * @param {Record | null} st */ function toStats(st) { if (!st) throw err('ENOENT', 'ENOENT') const ts = { mtimeMs: typeof st.mtimeMs === 'number' ? st.mtimeMs : undefined, ctimeMs: typeof st.ctimeMs === 'number' ? st.ctimeMs : undefined, mode: typeof st.mode === 'number' ? st.mode : undefined } if (st.type === 'file') return new VfsGitStats('file', Number(st.size) || 0, ts) if (st.type === 'directory') return new VfsGitStats('directory', 0, ts) if (st.type === 'symlink') return new VfsGitStats('symlink', Number(st.size) || 0, ts) throw err('ENOENT', 'ENOENT') } /** * @param {{ readFile: Function, writeFile: Function, unlink: Function, readdir: Function, stat: Function, lstat: Function, readlink: Function, symlink: Function }} vfs */ export function createGitFsFromVfs(vfs) { async function rawReaddir(p) { return vfs.readdir(p) } async function readDirFiltered(p) { const names = await rawReaddir(p) return names.filter((n) => n !== MARKER) } async function stat(p) { const st = await vfs.stat(p) return toStats(st) } async function lstat(p) { const st = await vfs.lstat(p) return toStats(st) } async function readFile(p, opts = {}) { const buf = await vfs.readFile(p) if (buf == null) throw err('ENOENT', 'ENOENT') const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf) if (opts.encoding === 'utf8' || opts.encoding === 'utf-8') { return Buffer.from(u8).toString('utf8') } return Buffer.from(u8) } async function writeFile(p, data, _opts = {}) { const body = typeof data === 'string' ? Buffer.from(data, 'utf8') : data instanceof Buffer ? data : Buffer.from(data) await vfs.writeFile(p, body) } async function unlink(p) { try { await vfs.unlink(p) } catch (e) { if (e && e.code === 'ENOENT') throw e throw e } } async function mkdir(p, options = {}) { const recursive = Boolean(options.recursive) const norm = path.posix.normalize(p) if (norm === '/' || norm === '') return try { const s = await stat(norm) if (s.isDirectory()) return if (s.isFile()) throw err('EEXIST', 'EEXIST') } catch (e) { if (e.code !== 'ENOENT') throw e const parent = path.posix.dirname(norm) if (parent !== norm && parent !== '/' && parent !== '') { try { const ps = await stat(parent) if (!ps.isDirectory()) throw err('ENOTDIR', 'ENOTDIR') } catch (pe) { if (pe.code === 'ENOENT') { if (recursive) await mkdir(parent, { recursive: true }) else throw err('ENOENT', 'ENOENT') } else throw pe } } const markerPath = path.posix.join(norm, MARKER) await vfs.writeFile(markerPath, Buffer.alloc(0)) } } async function rmdir(p, opts = {}) { if (opts.recursive) { await rm(p, { recursive: true, force: true }) return } const names = await readDirFiltered(p) if (names.length) throw err('ENOTEMPTY', 'ENOTEMPTY') const markerPath = path.posix.join(p, MARKER) try { await vfs.unlink(markerPath) } catch (e) { if (e && e.code !== 'ENOENT') throw e } } async function rm(p, opts = {}) { const recursive = Boolean(opts.recursive) const force = Boolean(opts.force) let st try { st = await lstat(p) } catch (e) { if (e.code === 'ENOENT' && force) return throw e } if (st.isSymbolicLink() || st.isFile()) { await unlink(p) return } if (!st.isDirectory()) { await unlink(p) return } if (!recursive) { await rmdir(p, {}) return } const children = await rawReaddir(p) for (const name of children) { const full = path.posix.join(p, name) await rm(full, { recursive: true, force: true }) } try { await vfs.unlink(path.posix.join(p, MARKER)) } catch { /* */ } try { await rmdir(p, {}) } catch (e) { if (e.code !== 'ENOENT' && e.code !== 'ENOTEMPTY') throw e } } async function readdir(p) { try { await stat(p) } catch (e) { if (e.code === 'ENOENT') return null throw e } return readDirFiltered(p) } async function readlink(p) { return vfs.readlink(p) } async function symlink(target, p) { return vfs.symlink(target, p) } async function chmod(p, mode) { if (typeof vfs.chmod === 'function') { await vfs.chmod(p, mode) } } const promises = { readFile, writeFile, unlink, mkdir, rmdir, rm, stat, lstat, readdir, readlink, symlink, chmod } return { promises } }