660 lines
14 KiB
Lua

local fs = {}
local is_windows = package.config:sub(1,1) == '\\'
local path_sep = is_windows and '\\' or '/'
-- ======================================================================
-- FILE OPERATIONS
-- ======================================================================
function fs.exists(path)
local file = io.open(path, "r")
if file then
file:close()
return true
end
return false
end
function fs.size(path)
local file = io.open(path, "r")
if not file then return nil end
local size = file:seek("end")
file:close()
return size
end
function fs.is_dir(path)
if not fs.exists(path) then return false end
-- Check if we can list directory (platform specific)
if is_windows then
local handle = io.popen('dir "' .. path .. '" 2>nul')
if handle then
local result = handle:read("*a")
handle:close()
return result and result:match("Directory of") ~= nil
end
else
local handle = io.popen('test -d "' .. path .. '" 2>/dev/null && echo "dir"')
if handle then
local result = handle:read("*l")
handle:close()
return result == "dir"
end
end
return false
end
function fs.is_file(path)
return fs.exists(path) and not fs.is_dir(path)
end
function fs.read(path)
local file = io.open(path, "r")
if not file then
error("Failed to read file '" .. path .. "': file not found or permission denied")
end
local content = file:read("*all")
file:close()
if not content then
error("Failed to read file '" .. path .. "': read error")
end
return content
end
function fs.write(path, content)
-- Check for obviously invalid paths
if path == "" or path:find("\0") then
error("Failed to write file '" .. path .. "': invalid path")
end
local file = io.open(path, "w")
if not file then
error("Failed to write file '" .. path .. "': permission denied or invalid path")
end
local success = file:write(content)
file:close()
if not success then
error("Failed to write file '" .. path .. "': write error")
end
return true
end
function fs.append(path, content)
local file = io.open(path, "a")
if not file then
error("Failed to append to file '" .. path .. "': permission denied or invalid path")
end
local success = file:write(content)
file:close()
if not success then
error("Failed to append to file '" .. path .. "': write error")
end
return true
end
function fs.copy(src, dst)
local src_file = io.open(src, "rb")
if not src_file then
error("Failed to copy '" .. src .. "' to '" .. dst .. "': source file not found")
end
local dst_file = io.open(dst, "wb")
if not dst_file then
src_file:close()
error("Failed to copy '" .. src .. "' to '" .. dst .. "': cannot create destination")
end
local chunk_size = 8192
while true do
local chunk = src_file:read(chunk_size)
if not chunk then break end
if not dst_file:write(chunk) then
src_file:close()
dst_file:close()
error("Failed to copy '" .. src .. "' to '" .. dst .. "': write error")
end
end
src_file:close()
dst_file:close()
return true
end
function fs.move(src, dst)
-- Try native rename first
local success = os.rename(src, dst)
if success then return true end
-- Fallback to copy + delete
fs.copy(src, dst)
fs.remove(src)
return true
end
function fs.remove(path)
local success = os.remove(path)
if not success then
error("Failed to remove '" .. path .. "': file not found or permission denied")
end
return true
end
function fs.mtime(path)
-- Use os.execute with stat command
if is_windows then
local cmd = 'for %i in ("' .. path .. '") do @echo %~ti'
local handle = io.popen(cmd)
if handle then
local result = handle:read("*line")
handle:close()
if result then
-- Parse Windows timestamp (basic implementation)
return os.time() -- Fallback to current time
end
end
else
local cmd = 'stat -c %Y "' .. path .. '" 2>/dev/null'
local handle = io.popen(cmd)
if handle then
local result = handle:read("*line")
handle:close()
if result and result:match("^%d+$") then
return tonumber(result)
end
end
end
return nil
end
function fs.touch(path)
if fs.exists(path) then
-- Update timestamp - basic implementation
local content = fs.read(path)
fs.write(path, content)
else
-- Create empty file
fs.write(path, "")
end
return true
end
function fs.lines(path)
local lines = {}
local file = io.open(path, "r")
if not file then
error("Failed to read lines from '" .. path .. "': file not found")
end
for line in file:lines() do
lines[#lines + 1] = line
end
file:close()
return lines
end
-- ======================================================================
-- DIRECTORY OPERATIONS
-- ======================================================================
function fs.mkdir(path)
local cmd
if is_windows then
cmd = 'mkdir "' .. path .. '" 2>nul'
else
cmd = 'mkdir -p "' .. path .. '"'
end
local success = os.execute(cmd)
if not success then
error("Failed to create directory '" .. path .. "'")
end
return true
end
function fs.rmdir(path)
local cmd
if is_windows then
cmd = 'rmdir /s /q "' .. path .. '"'
else
cmd = 'rm -rf "' .. path .. '"'
end
local success = os.execute(cmd)
if not success then
error("Failed to remove directory '" .. path .. "'")
end
return true
end
function fs.list(path)
if not fs.exists(path) then
error("Failed to list directory '" .. path .. "': directory not found")
end
if not fs.is_dir(path) then
error("Failed to list directory '" .. path .. "': not a directory")
end
local entries = {}
local cmd
if is_windows then
cmd = 'dir /b "' .. path .. '" 2>nul'
else
cmd = 'ls -1 "' .. path .. '" 2>/dev/null'
end
local handle = io.popen(cmd)
if not handle then
return nil
end
for name in handle:lines() do
local full_path = fs.join(path, name)
entries[#entries + 1] = {
name = name,
is_dir = fs.is_dir(full_path),
size = fs.is_file(full_path) and fs.size(full_path) or 0,
mtime = fs.mtime(full_path)
}
end
handle:close()
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
files[#files + 1] = 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
dirs[#dirs + 1] = entry
end
end
return dirs
end
function fs.list_names(path)
local entries = fs.list(path)
local names = {}
for _, entry in ipairs(entries) do
names[#names + 1] = entry.name
end
return names
end
-- ======================================================================
-- PATH OPERATIONS (Optimized pure Lua)
-- ======================================================================
function fs.join(...)
local parts = {...}
if #parts == 0 then return "" end
if #parts == 1 then return parts[1] end
local result = parts[1]
for i = 2, #parts do
local part = parts[i]
if part ~= "" then
if result:sub(-1) == path_sep or result == "" then
result = result .. part
else
result = result .. path_sep .. part
end
end
end
return result
end
function fs.dirname(path)
local pos = path:find("[/\\][^/\\]*$")
if pos then
return path:sub(1, pos - 1)
end
return "."
end
function fs.basename(path)
return path:match("[^/\\]*$") or ""
end
function fs.ext(path)
local base = fs.basename(path)
local dot_pos = base:find("%.[^%.]*$")
return dot_pos and base:sub(dot_pos) or ""
end
function fs.abs(path)
if is_windows then
if path:match("^[A-Za-z]:") or path:match("^\\\\") then
return path
end
else
if path:sub(1, 1) == "/" then
return path
end
end
local cwd = fs.getcwd()
return fs.join(cwd, path)
end
function fs.clean(path)
-- Normalize path separators
path = path:gsub("[/\\]+", path_sep)
-- Handle . and .. components
local parts = {}
for part in path:gmatch("[^" .. path_sep .. "]+") do
if part == ".." and #parts > 0 and parts[#parts] ~= ".." then
parts[#parts] = nil
elseif part ~= "." then
parts[#parts + 1] = part
end
end
local result = table.concat(parts, path_sep)
if path:sub(1, 1) == path_sep then
result = path_sep .. result
end
return result ~= "" and result or "."
end
function fs.split(path)
local pos = path:find("[/\\][^/\\]*$")
if pos then
return path:sub(1, pos), path:sub(pos + 1)
end
return "", path
end
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)
prefix = prefix or "tmp"
local temp_name = prefix .. "_" .. os.time() .. "_" .. math.random(10000)
local temp_path
if is_windows then
local temp_dir = os.getenv("TEMP") or os.getenv("TMP") or "C:\\temp"
temp_path = fs.join(temp_dir, temp_name)
else
temp_path = fs.join("/tmp", temp_name)
end
-- Create the file
fs.write(temp_path, "")
return temp_path
end
function fs.tempdir(prefix)
prefix = prefix or "tmp"
local temp_name = prefix .. "_" .. os.time() .. "_" .. math.random(10000)
local temp_path
if is_windows then
local temp_dir = os.getenv("TEMP") or os.getenv("TMP") or "C:\\temp"
temp_path = fs.join(temp_dir, temp_name)
else
temp_path = fs.join("/tmp", temp_name)
end
fs.mkdir(temp_path)
return temp_path
end
-- ======================================================================
-- PATTERN MATCHING
-- ======================================================================
function fs.glob(pattern)
local cmd
if is_windows then
cmd = 'for %f in ("' .. pattern .. '") do @echo %f'
else
cmd = 'ls -1d ' .. pattern .. ' 2>/dev/null'
end
local matches = {}
local handle = io.popen(cmd)
if handle then
for match in handle:lines() do
matches[#matches + 1] = match
end
handle:close()
end
return matches
end
function fs.walk(root)
local files = {}
local function walk_recursive(path)
if not fs.exists(path) then return end
files[#files + 1] = path
if fs.is_dir(path) then
local entries = fs.list(path)
for _, entry in ipairs(entries) do
walk_recursive(fs.join(path, entry.name))
end
end
end
walk_recursive(root)
return files
end
-- ======================================================================
-- UTILITY FUNCTIONS (using optimized implementations)
-- ======================================================================
function fs.extension(path)
local ext = fs.ext(path)
return ext:sub(2) -- Remove leading dot
end
function fs.change_ext(path, new_ext)
local dir, name, _ = fs.splitext(path)
if not new_ext:match("^%.") then
new_ext = "." .. new_ext
end
if dir == "." then
return name .. new_ext
end
return fs.join(dir, name .. new_ext)
end
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
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 = 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
function fs.is_safe_path(path)
path = fs.clean(path)
if path:match("%.%.") then return false end
if path:match("^[/\\]") then return false end
if path:match("^~") then return false end
return true
end
-- Aliases for convenience
fs.makedirs = fs.mkdir
fs.removedirs = fs.rmdir
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
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
results[#results + 1] = 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
node.children[#node.children + 1] = child
end
end
else
node.size = fs.size(path)
node.mtime = fs.mtime(path)
end
return node
end
return build_tree(root, 1)
end
return fs