446 lines
10 KiB
Lua
446 lines
10 KiB
Lua
-- modules/fs.lua - Comprehensive filesystem utilities
|
|
|
|
local fs = {}
|
|
|
|
-- ======================================================================
|
|
-- FILE OPERATIONS
|
|
-- ======================================================================
|
|
|
|
function fs.exists(path)
|
|
return moonshark.file_exists(path)
|
|
end
|
|
|
|
function fs.size(path)
|
|
local size = moonshark.file_size(path)
|
|
return size >= 0 and size or nil
|
|
end
|
|
|
|
function fs.is_dir(path)
|
|
return moonshark.file_is_dir(path)
|
|
end
|
|
|
|
function fs.is_file(path)
|
|
return fs.exists(path) and not fs.is_dir(path)
|
|
end
|
|
|
|
function fs.read(path)
|
|
local content, err = moonshark.file_read(path)
|
|
if not content then
|
|
error("Failed to read file '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return content
|
|
end
|
|
|
|
function fs.write(path, content)
|
|
local success, err = moonshark.file_write(path, content)
|
|
if not success then
|
|
error("Failed to write file '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.append(path, content)
|
|
local success, err = moonshark.file_append(path, content)
|
|
if not success then
|
|
error("Failed to append to file '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.copy(src, dst)
|
|
local success, err = moonshark.file_copy(src, dst)
|
|
if not success then
|
|
error("Failed to copy '" .. src .. "' to '" .. dst .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.move(src, dst)
|
|
local success, err = moonshark.file_move(src, dst)
|
|
if not success then
|
|
error("Failed to move '" .. src .. "' to '" .. dst .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.remove(path)
|
|
local success, err = moonshark.file_delete(path)
|
|
if not success then
|
|
error("Failed to remove '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.mtime(path)
|
|
local timestamp = moonshark.file_mtime(path)
|
|
return timestamp >= 0 and timestamp or nil
|
|
end
|
|
|
|
function fs.touch(path)
|
|
local success, err = moonshark.touch(path)
|
|
if not success then
|
|
error("Failed to touch '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.lines(path)
|
|
local lines, err = moonshark.file_lines(path)
|
|
if not lines then
|
|
error("Failed to read lines from '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return lines
|
|
end
|
|
|
|
-- ======================================================================
|
|
-- DIRECTORY OPERATIONS
|
|
-- ======================================================================
|
|
|
|
function fs.mkdir(path)
|
|
local success, err = moonshark.dir_create(path)
|
|
if not success then
|
|
error("Failed to create directory '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.rmdir(path)
|
|
local success, err = moonshark.dir_remove(path)
|
|
if not success then
|
|
error("Failed to remove directory '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function fs.list(path)
|
|
local entries, err = moonshark.dir_list(path)
|
|
if not entries then
|
|
error("Failed to list directory '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return entries
|
|
end
|
|
|
|
function fs.list_files(path)
|
|
local entries = fs.list(path)
|
|
local files = {}
|
|
for _, entry in ipairs(entries) do
|
|
if not entry.is_dir then
|
|
table.insert(files, entry)
|
|
end
|
|
end
|
|
return files
|
|
end
|
|
|
|
function fs.list_dirs(path)
|
|
local entries = fs.list(path)
|
|
local dirs = {}
|
|
for _, entry in ipairs(entries) do
|
|
if entry.is_dir then
|
|
table.insert(dirs, entry)
|
|
end
|
|
end
|
|
return dirs
|
|
end
|
|
|
|
function fs.list_names(path)
|
|
local entries = fs.list(path)
|
|
local names = {}
|
|
for _, entry in ipairs(entries) do
|
|
table.insert(names, entry.name)
|
|
end
|
|
return names
|
|
end
|
|
|
|
-- ======================================================================
|
|
-- PATH OPERATIONS
|
|
-- ======================================================================
|
|
|
|
function fs.join(...)
|
|
return moonshark.path_join(...)
|
|
end
|
|
|
|
function fs.dirname(path)
|
|
return moonshark.path_dir(path)
|
|
end
|
|
|
|
function fs.basename(path)
|
|
return moonshark.path_basename(path)
|
|
end
|
|
|
|
function fs.ext(path)
|
|
return moonshark.path_ext(path)
|
|
end
|
|
|
|
function fs.abs(path)
|
|
local abs_path, err = moonshark.path_abs(path)
|
|
if not abs_path then
|
|
error("Failed to get absolute path for '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return abs_path
|
|
end
|
|
|
|
function fs.clean(path)
|
|
return moonshark.path_clean(path)
|
|
end
|
|
|
|
function fs.split(path)
|
|
return moonshark.path_split(path)
|
|
end
|
|
|
|
-- Split path into directory, name, and extension
|
|
function fs.splitext(path)
|
|
local dir = fs.dirname(path)
|
|
local base = fs.basename(path)
|
|
local ext = fs.ext(path)
|
|
local name = base
|
|
|
|
if ext ~= "" then
|
|
name = base:sub(1, -(#ext + 1))
|
|
end
|
|
|
|
return dir, name, ext
|
|
end
|
|
|
|
-- ======================================================================
|
|
-- WORKING DIRECTORY
|
|
-- ======================================================================
|
|
|
|
function fs.getcwd()
|
|
local cwd, err = moonshark.getcwd()
|
|
if not cwd then
|
|
error("Failed to get current directory: " .. (err or "unknown error"))
|
|
end
|
|
return cwd
|
|
end
|
|
|
|
function fs.chdir(path)
|
|
local success, err = moonshark.chdir(path)
|
|
if not success then
|
|
error("Failed to change directory to '" .. path .. "': " .. (err or "unknown error"))
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- ======================================================================
|
|
-- TEMPORARY FILES
|
|
-- ======================================================================
|
|
|
|
function fs.tempfile(prefix)
|
|
local path, err = moonshark.temp_file(prefix)
|
|
if not path then
|
|
error("Failed to create temporary file: " .. (err or "unknown error"))
|
|
end
|
|
return path
|
|
end
|
|
|
|
function fs.tempdir(prefix)
|
|
local path, err = moonshark.temp_dir(prefix)
|
|
if not path then
|
|
error("Failed to create temporary directory: " .. (err or "unknown error"))
|
|
end
|
|
return path
|
|
end
|
|
|
|
-- ======================================================================
|
|
-- PATTERN MATCHING
|
|
-- ======================================================================
|
|
|
|
function fs.glob(pattern)
|
|
local matches, err = moonshark.glob(pattern)
|
|
if not matches then
|
|
error("Failed to glob pattern '" .. pattern .. "': " .. (err or "unknown error"))
|
|
end
|
|
return matches
|
|
end
|
|
|
|
function fs.walk(root)
|
|
local files, err = moonshark.walk(root)
|
|
if not files then
|
|
error("Failed to walk directory '" .. root .. "': " .. (err or "unknown error"))
|
|
end
|
|
return files
|
|
end
|
|
|
|
-- ======================================================================
|
|
-- UTILITY FUNCTIONS
|
|
-- ======================================================================
|
|
|
|
-- Get file extension without dot
|
|
function fs.extension(path)
|
|
local ext = fs.ext(path)
|
|
return ext:sub(2) -- Remove leading dot
|
|
end
|
|
|
|
-- Change file extension
|
|
function fs.change_ext(path, new_ext)
|
|
local dir, name, _ = fs.splitext(path)
|
|
if not new_ext:match("^%.") then
|
|
new_ext = "." .. new_ext
|
|
end
|
|
return fs.join(dir, name .. new_ext)
|
|
end
|
|
|
|
-- Ensure directory exists
|
|
function fs.ensure_dir(path)
|
|
if not fs.exists(path) then
|
|
fs.mkdir(path)
|
|
elseif not fs.is_dir(path) then
|
|
error("Path exists but is not a directory: " .. path)
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Get file size in human readable format
|
|
function fs.size_human(path)
|
|
local size = fs.size(path)
|
|
if not size then return nil end
|
|
|
|
local units = {"B", "KB", "MB", "GB", "TB"}
|
|
local unit_index = 1
|
|
local size_float = tonumber(size)
|
|
|
|
while size_float >= 1024 and unit_index < #units do
|
|
size_float = size_float / 1024
|
|
unit_index = unit_index + 1
|
|
end
|
|
|
|
if unit_index == 1 then
|
|
return string.format("%d %s", size_float, units[unit_index])
|
|
else
|
|
return string.format("%.1f %s", size_float, units[unit_index])
|
|
end
|
|
end
|
|
|
|
-- Check if path is safe (doesn't contain .. or other dangerous patterns)
|
|
function fs.is_safe_path(path)
|
|
-- Normalize path
|
|
path = fs.clean(path)
|
|
|
|
-- Check for dangerous patterns
|
|
if path:match("%.%.") then return false end
|
|
if path:match("^/") then return false end -- Absolute paths might be dangerous
|
|
if path:match("^~") then return false end -- Home directory references
|
|
|
|
return true
|
|
end
|
|
|
|
-- Create directory tree
|
|
function fs.makedirs(path)
|
|
return fs.mkdir(path) -- mkdir already creates parent directories
|
|
end
|
|
|
|
-- Remove directory tree
|
|
function fs.removedirs(path)
|
|
return fs.rmdir(path) -- rmdir already removes recursively
|
|
end
|
|
|
|
-- Copy directory tree
|
|
function fs.copytree(src, dst)
|
|
if not fs.exists(src) then
|
|
error("Source directory does not exist: " .. src)
|
|
end
|
|
|
|
if not fs.is_dir(src) then
|
|
error("Source is not a directory: " .. src)
|
|
end
|
|
|
|
fs.mkdir(dst)
|
|
|
|
local entries = fs.list(src)
|
|
for _, entry in ipairs(entries) do
|
|
local src_path = fs.join(src, entry.name)
|
|
local dst_path = fs.join(dst, entry.name)
|
|
|
|
if entry.is_dir then
|
|
fs.copytree(src_path, dst_path)
|
|
else
|
|
fs.copy(src_path, dst_path)
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Find files by pattern
|
|
function fs.find(root, pattern, recursive)
|
|
recursive = recursive ~= false
|
|
local results = {}
|
|
|
|
local function search(dir)
|
|
local entries = fs.list(dir)
|
|
for _, entry in ipairs(entries) do
|
|
local full_path = fs.join(dir, entry.name)
|
|
|
|
if not entry.is_dir and entry.name:match(pattern) then
|
|
table.insert(results, full_path)
|
|
elseif entry.is_dir and recursive then
|
|
search(full_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
search(root)
|
|
return results
|
|
end
|
|
|
|
-- Get directory tree as nested table
|
|
function fs.tree(root, max_depth)
|
|
max_depth = max_depth or 10
|
|
|
|
local function build_tree(path, depth)
|
|
if depth > max_depth then return nil end
|
|
|
|
if not fs.exists(path) then return nil end
|
|
|
|
local node = {
|
|
name = fs.basename(path),
|
|
path = path,
|
|
is_dir = fs.is_dir(path)
|
|
}
|
|
|
|
if node.is_dir then
|
|
node.children = {}
|
|
local entries = fs.list(path)
|
|
for _, entry in ipairs(entries) do
|
|
local child_path = fs.join(path, entry.name)
|
|
local child = build_tree(child_path, depth + 1)
|
|
if child then
|
|
table.insert(node.children, child)
|
|
end
|
|
end
|
|
else
|
|
node.size = fs.size(path)
|
|
node.mtime = fs.mtime(path)
|
|
end
|
|
|
|
return node
|
|
end
|
|
|
|
return build_tree(root, 1)
|
|
end
|
|
|
|
-- Monitor file changes (simplified version)
|
|
function fs.watch(path, callback, interval)
|
|
interval = interval or 1
|
|
|
|
if not fs.exists(path) then
|
|
error("Path does not exist: " .. path)
|
|
end
|
|
|
|
local last_mtime = fs.mtime(path)
|
|
|
|
while true do
|
|
local current_mtime = fs.mtime(path)
|
|
if current_mtime and current_mtime ~= last_mtime then
|
|
callback(path, "modified")
|
|
last_mtime = current_mtime
|
|
end
|
|
|
|
-- Sleep for interval (this would need a proper sleep function)
|
|
-- This is a placeholder - real implementation would need proper timing
|
|
local start = os.clock()
|
|
while os.clock() - start < interval do end
|
|
end
|
|
end
|
|
|
|
return fs |