262 lines
6.7 KiB
JavaScript
262 lines
6.7 KiB
JavaScript
/**
|
|
* 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 '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<string, unknown> | 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 }
|
|
}
|