Files
cell/scripts/cellfs.cm
2025-11-24 23:08:40 -06:00

272 lines
5.9 KiB
Plaintext

var cellfs = this
// CellFS: A filesystem implementation using miniz and raw OS filesystem
// Supports mounting multiple sources (fs, zip) and named mounts (@name)
var fd = use('fd')
var miniz = use('miniz')
// Internal state
var mounts = [] // Array of {source, type, handle, name}
// Helper to normalize paths
function normalize_path(path) {
if (!path) return ""
// Remove leading/trailing slashes and normalize
return path.replace(/^\/+|\/+$/g, "")
}
// Helper to get directory from path
function dirname(path) {
var idx = path.lastIndexOf("/")
if (idx == -1) return ""
return path.substring(0, idx)
}
// Helper to get basename from path
function basename(path) {
var idx = path.lastIndexOf("/")
if (idx == -1) return path
return path.substring(idx + 1)
}
// Helper to join paths
function join_paths(base, rel) {
base = base.replace(/\/+$/, "")
rel = rel.replace(/^\/+/, "")
if (!base) return rel
if (!rel) return base
return base + "/" + rel
}
// Check if a file exists in a specific mount
function mount_exists(mount, path) {
if (mount.type == 'zip') {
try {
mount.handle.mod(path)
return true
} catch (e) {
return false
}
} else { // fs
var full_path = join_paths(mount.source, path)
try {
var st = fd.stat(full_path)
return st.isFile
} catch (e) {
return false
}
}
}
// Resolve a path to a specific mount and relative path
// Returns { mount, path } or throws/returns null
function resolve(path, must_exist) {
path = normalize_path(path)
// Check for named mount
if (path.startsWith("@")) {
var idx = path.indexOf("/")
var mount_name = ""
var rel_path = ""
if (idx == -1) {
mount_name = path.substring(1)
rel_path = ""
} else {
mount_name = path.substring(1, idx)
rel_path = path.substring(idx + 1)
}
// Find named mount
var mount = null
for (var m of mounts) {
if (m.name == mount_name) {
mount = m
break
}
}
if (!mount) {
throw new Error("Unknown mount point: @" + mount_name)
}
return { mount: mount, path: rel_path }
}
// Search path
for (var mount of mounts) {
if (mount_exists(mount, path)) {
return { mount: mount, path: path }
}
}
if (must_exist) {
// throw new Error("File not found in any mount: " + path)
return null
}
return null
}
// Mount a source
function mount(source, name) {
// Check if source exists
var st = null
try {
st = fd.stat(source)
} catch (e) {
throw new Error("Mount source not found: " + source)
}
var mount_info = {
source: source,
name: name || null,
type: 'fs',
handle: null
}
if (st.isDirectory) {
mount_info.type = 'fs'
} else if (st.isFile) {
// Assume zip
var zip_data = null
// Always read as bytes for zip
var f = fd.open(source, 'r')
var s = fd.fstat(f)
zip_data = fd.read(f, s.size)
fd.close(f)
var zip = miniz.read(zip_data)
if (!zip || typeof zip.count != 'function') {
throw new Error("Invalid ZIP file: " + source)
}
mount_info.type = 'zip'
mount_info.handle = zip
} else {
throw new Error("Unsupported mount source type: " + source)
}
mounts.push(mount_info)
log.console(`Mounted ${source} ${name ? 'as @' + name : ''}`)
}
// Unmount
function unmount(name_or_source) {
for (var i = 0; i < mounts.length; i++) {
if (mounts[i].name == name_or_source || mounts[i].source == name_or_source) {
mounts.splice(i, 1)
return
}
}
throw new Error("Mount not found: " + name_or_source)
}
// Read file
function slurp(path) {
var res = resolve(path, true)
if (!res) throw new Error("File not found: " + path)
if (res.mount.type == 'zip') {
return res.mount.handle.slurp(res.path)
} else {
var full_path = join_paths(res.mount.source, res.path)
return fd.slurp(full_path)
}
}
// Read file as bytes
function slurpbytes(path) {
var res = resolve(path, true)
if (!res) throw new Error("File not found: " + path)
if (res.mount.type == 'zip') {
return res.mount.handle.slurp(res.path)
} else {
var full_path = join_paths(res.mount.source, res.path)
var f = fd.open(full_path, 'r')
var s = fd.fstat(f)
var data = fd.read(f, s.size)
fd.close(f)
return data
}
}
// Write file
function slurpwrite(path, data) {
if (!path.startsWith("@")) {
throw new Error("slurpwrite requires a named mount (e.g. @name/file.txt)")
}
var res = resolve(path, false)
// For named mounts, resolve returns the mount even if file doesn't exist
if (res.mount.type == 'zip') {
throw new Error("Cannot write to zip mount: @" + res.mount.name)
}
var full_path = join_paths(res.mount.source, res.path)
var f = fd.open(full_path, 'w')
fd.write(f, data)
fd.close(f)
}
// Check existence
function exists(path) {
var res = resolve(path, false)
if (path.startsWith("@")) {
return mount_exists(res.mount, res.path)
}
return res != null
}
// Stat
function stat(path) {
var res = resolve(path, true)
if (!res) throw new Error("File not found: " + path)
if (res.mount.type == 'zip') {
var mod = res.mount.handle.mod(res.path)
return {
filesize: 0,
modtime: mod * 1000,
isDirectory: false
}
} else {
var full_path = join_paths(res.mount.source, res.path)
var s = fd.stat(full_path)
return {
filesize: s.size,
modtime: s.mtime,
isDirectory: s.isDirectory
}
}
}
// Get search paths
function searchpath() {
var paths = []
for (var mount of mounts) {
paths.push(mount.source)
}
return paths
}
// Initialize
mount('.', 'cwd')
// Exports
cellfs.mount = mount
cellfs.unmount = unmount
cellfs.slurp = slurp
cellfs.slurpbytes = slurpbytes
cellfs.slurpwrite = slurpwrite
cellfs.exists = exists
cellfs.stat = stat
cellfs.searchpath = searchpath
return cellfs