turn into a cell package
This commit is contained in:
482
cellfs.cm
Normal file
482
cellfs.cm
Normal file
@@ -0,0 +1,482 @@
|
||||
var cellfs = {}
|
||||
|
||||
// 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')
|
||||
var qop = use('qop')
|
||||
var wildstar = use('wildstar')
|
||||
|
||||
// Internal state
|
||||
var mounts = [] // Array of {source, type, handle, name}
|
||||
|
||||
var writepath = "."
|
||||
|
||||
// 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 if (mount.type == 'qop') {
|
||||
try {
|
||||
return mount.handle.stat(path) != null
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
} else { // fs
|
||||
var full_path = join_paths(mount.source, path)
|
||||
try {
|
||||
var st = fd.stat(full_path)
|
||||
return st.isFile || st.isDirectory
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a path refers to a directory in a specific mount
|
||||
function is_directory(path) {
|
||||
var res = resolve(path)
|
||||
var mount = res.mount
|
||||
if (mount.type == 'zip') {
|
||||
try {
|
||||
return mount.handle.is_directory(path);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
} else if (mount.type == 'qop') {
|
||||
try {
|
||||
return mount.handle.is_directory(path);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
} else { // fs
|
||||
var full_path = join_paths(mount.source, path)
|
||||
try {
|
||||
var st = fd.stat(full_path)
|
||||
return st.isDirectory
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Mount a source
|
||||
function mount(source, name) {
|
||||
// Check if source exists
|
||||
var st = fd.stat(source)
|
||||
|
||||
var mount_info = {
|
||||
source: source,
|
||||
name: name || null,
|
||||
type: 'fs',
|
||||
handle: null,
|
||||
zip_blob: null
|
||||
}
|
||||
|
||||
if (st.isDirectory) {
|
||||
mount_info.type = 'fs'
|
||||
} else if (st.isFile) {
|
||||
var blob = fd.slurp(source)
|
||||
|
||||
// Try QOP first (it's likely faster to fail?) or Zip?
|
||||
// QOP open checks magic.
|
||||
var qop_archive = null
|
||||
try {
|
||||
qop_archive = qop.open(blob)
|
||||
} catch(e) {}
|
||||
|
||||
if (qop_archive) {
|
||||
mount_info.type = 'qop'
|
||||
mount_info.handle = qop_archive
|
||||
mount_info.zip_blob = blob // keep blob alive
|
||||
} else {
|
||||
var zip = miniz.read(blob)
|
||||
if (!zip || typeof zip.count != 'function') {
|
||||
throw new Error("Invalid archive file (not zip or qop): " + source)
|
||||
}
|
||||
|
||||
mount_info.type = 'zip'
|
||||
mount_info.handle = zip
|
||||
mount_info.zip_blob = blob // keep blob alive
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unsupported mount source type: " + source)
|
||||
}
|
||||
|
||||
mounts.push(mount_info)
|
||||
}
|
||||
|
||||
// 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 if (res.mount.type == 'qop') {
|
||||
var data = res.mount.handle.read(res.path)
|
||||
if (!data) throw new Error("File not found in qop: " + path)
|
||||
return data
|
||||
} else {
|
||||
var full_path = join_paths(res.mount.source, res.path)
|
||||
return fd.slurp(full_path)
|
||||
}
|
||||
}
|
||||
|
||||
// Write file
|
||||
function slurpwrite(path, data) {
|
||||
var full_path = writepath + "/" + 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 if (res.mount.type == 'qop') {
|
||||
var s = res.mount.handle.stat(res.path)
|
||||
if (!s) throw new Error("File not found in qop: " + path)
|
||||
return {
|
||||
filesize: s.size,
|
||||
modtime: s.modtime,
|
||||
isDirectory: s.isDirectory
|
||||
}
|
||||
} 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() {
|
||||
return mounts.slice()
|
||||
}
|
||||
|
||||
// Mount a package using the shop system
|
||||
function mount_package(name) {
|
||||
if (name == null) {
|
||||
mount('.', null)
|
||||
return
|
||||
}
|
||||
|
||||
var shop = use('shop')
|
||||
var dir = shop.get_module_dir(name)
|
||||
|
||||
if (!dir) {
|
||||
throw new Error("Package not found: " + name)
|
||||
}
|
||||
|
||||
mount(dir, name)
|
||||
}
|
||||
|
||||
// New functions for qjs_io compatibility
|
||||
|
||||
function match(str, pattern) {
|
||||
return wildstar.match(pattern, str, wildstar.WM_PATHNAME | wildstar.WM_PERIOD | wildstar.WM_WILDSTAR)
|
||||
}
|
||||
|
||||
function rm(path) {
|
||||
var res = resolve(path, true)
|
||||
if (res.mount.type != 'fs') throw new Error("Cannot delete from non-fs mount")
|
||||
|
||||
var full_path = join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full_path)
|
||||
if (st.isDirectory) fd.rmdir(full_path)
|
||||
else fd.unlink(full_path)
|
||||
}
|
||||
|
||||
function mkdir(path) {
|
||||
var full = join_paths(writepath, path)
|
||||
fd.mkdir(full)
|
||||
}
|
||||
|
||||
function set_writepath(path) {
|
||||
writepath = path
|
||||
}
|
||||
|
||||
function basedir() {
|
||||
return fd.getcwd()
|
||||
}
|
||||
|
||||
function prefdir(org, app) {
|
||||
return "./"
|
||||
}
|
||||
|
||||
function realdir(path) {
|
||||
var res = resolve(path, false)
|
||||
if (!res) return null
|
||||
return join_paths(res.mount.source, res.path)
|
||||
}
|
||||
|
||||
function enumerate(path, recurse) {
|
||||
if (path == null) path = ""
|
||||
|
||||
var res = resolve(path, true)
|
||||
var results = []
|
||||
|
||||
function visit(curr_full, rel_prefix) {
|
||||
var list = fd.readdir(curr_full)
|
||||
if (!list) return
|
||||
|
||||
for (var item of list) {
|
||||
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
|
||||
results.push(item_rel)
|
||||
|
||||
if (recurse) {
|
||||
var st = fd.stat(join_paths(curr_full, item))
|
||||
if (st.isDirectory) {
|
||||
visit(join_paths(curr_full, item), item_rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res.mount.type == 'fs') {
|
||||
var full = join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full)
|
||||
if (st && st.isDirectory) {
|
||||
visit(full, "")
|
||||
}
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var all = res.mount.handle.list()
|
||||
var prefix = res.path ? res.path + "/" : ""
|
||||
var prefix_len = prefix.length
|
||||
|
||||
// Use a set to avoid duplicates if we are simulating directories
|
||||
var seen = {}
|
||||
|
||||
for (var p of all) {
|
||||
if (p.startsWith(prefix)) {
|
||||
var rel = p.substring(prefix_len)
|
||||
if (rel.length == 0) continue
|
||||
|
||||
if (!recurse) {
|
||||
var slash = rel.indexOf('/')
|
||||
if (slash != -1) {
|
||||
rel = rel.substring(0, slash)
|
||||
}
|
||||
}
|
||||
|
||||
if (!seen[rel]) {
|
||||
seen[rel] = true
|
||||
results.push(rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
function globfs(globs, dir) {
|
||||
if (dir == null) dir = ""
|
||||
var res = resolve(dir, true)
|
||||
var results = []
|
||||
|
||||
function check_neg(path) {
|
||||
for (var g of globs) {
|
||||
if (g.startsWith("!") && wildstar.match(g.substring(1), path, wildstar.WM_WILDSTAR)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function check_pos(path) {
|
||||
for (var g of globs) {
|
||||
if (!g.startsWith("!") && wildstar.match(g, path, wildstar.WM_WILDSTAR)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function visit(curr_full, rel_prefix) {
|
||||
if (rel_prefix && check_neg(rel_prefix)) return
|
||||
|
||||
var list = fd.readdir(curr_full)
|
||||
if (!list) return
|
||||
|
||||
for (var item of list) {
|
||||
var item_rel = rel_prefix ? rel_prefix + "/" + item : item
|
||||
|
||||
var child_full = join_paths(curr_full, item)
|
||||
var st = fd.stat(child_full)
|
||||
|
||||
if (st.isDirectory) {
|
||||
if (!check_neg(item_rel)) {
|
||||
visit(child_full, item_rel)
|
||||
}
|
||||
} else {
|
||||
if (!check_neg(item_rel) && check_pos(item_rel)) {
|
||||
results.push(item_rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res.mount.type == 'fs') {
|
||||
var full = join_paths(res.mount.source, res.path)
|
||||
var st = fd.stat(full)
|
||||
if (st && st.isDirectory) {
|
||||
visit(full, "")
|
||||
}
|
||||
} else if (res.mount.type == 'qop') {
|
||||
var all = res.mount.handle.list()
|
||||
var prefix = res.path ? res.path + "/" : ""
|
||||
var prefix_len = prefix.length
|
||||
|
||||
for (var p of all) {
|
||||
if (p.startsWith(prefix)) {
|
||||
var rel = p.substring(prefix_len)
|
||||
if (rel.length == 0) continue
|
||||
|
||||
if (!check_neg(rel) && check_pos(rel)) {
|
||||
results.push(rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Exports
|
||||
cellfs.mount = mount
|
||||
cellfs.mount_package = mount_package
|
||||
cellfs.unmount = unmount
|
||||
cellfs.slurp = slurp
|
||||
cellfs.slurpwrite = slurpwrite
|
||||
cellfs.exists = exists
|
||||
cellfs.is_directory = is_directory
|
||||
cellfs.stat = stat
|
||||
cellfs.searchpath = searchpath
|
||||
cellfs.match = match
|
||||
cellfs.enumerate = enumerate
|
||||
cellfs.globfs = globfs
|
||||
cellfs.rm = rm
|
||||
cellfs.mkdir = mkdir
|
||||
cellfs.writepath = set_writepath
|
||||
cellfs.basedir = basedir
|
||||
cellfs.prefdir = prefdir
|
||||
cellfs.realdir = realdir
|
||||
cellfs.slurp = slurp
|
||||
|
||||
cellfs.mount('.')
|
||||
|
||||
return cellfs
|
||||
Reference in New Issue
Block a user