fs module improvements
This commit is contained in:
parent
e5df8a5b8a
commit
25a44660a4
@ -3,6 +3,20 @@ local fs = {}
|
|||||||
local is_windows = package.config:sub(1,1) == '\\'
|
local is_windows = package.config:sub(1,1) == '\\'
|
||||||
local path_sep = is_windows and '\\' or '/'
|
local path_sep = is_windows and '\\' or '/'
|
||||||
|
|
||||||
|
-- ======================================================================
|
||||||
|
-- UTILITY FUNCTIONS
|
||||||
|
-- ======================================================================
|
||||||
|
|
||||||
|
local function shell_escape(str)
|
||||||
|
if is_windows then
|
||||||
|
-- Windows: escape quotes and wrap in quotes
|
||||||
|
return '"' .. str:gsub('"', '""') .. '"'
|
||||||
|
else
|
||||||
|
-- Unix: escape shell metacharacters
|
||||||
|
return "'" .. str:gsub("'", "'\"'\"'") .. "'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- FILE OPERATIONS
|
-- FILE OPERATIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
@ -28,22 +42,19 @@ end
|
|||||||
function fs.is_dir(path)
|
function fs.is_dir(path)
|
||||||
if not fs.exists(path) then return false end
|
if not fs.exists(path) then return false end
|
||||||
|
|
||||||
-- Check if we can list directory (platform specific)
|
local cmd
|
||||||
if is_windows then
|
if is_windows then
|
||||||
local handle = io.popen('dir "' .. path .. '" 2>nul')
|
cmd = 'dir ' .. shell_escape(path) .. ' >nul 2>&1 && echo dir'
|
||||||
if handle then
|
|
||||||
local result = handle:read("*a")
|
|
||||||
handle:close()
|
|
||||||
return result and result:match("Directory of") ~= nil
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
local handle = io.popen('test -d "' .. path .. '" 2>/dev/null && echo "dir"')
|
cmd = 'test -d ' .. shell_escape(path) .. ' && echo dir'
|
||||||
|
end
|
||||||
|
|
||||||
|
local handle = io.popen(cmd)
|
||||||
if handle then
|
if handle then
|
||||||
local result = handle:read("*l")
|
local result = handle:read("*l")
|
||||||
handle:close()
|
handle:close()
|
||||||
return result == "dir"
|
return result == "dir"
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@ -69,7 +80,6 @@ function fs.read(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function fs.write(path, content)
|
function fs.write(path, content)
|
||||||
-- Check for obviously invalid paths
|
|
||||||
if path == "" or path:find("\0") then
|
if path == "" or path:find("\0") then
|
||||||
error("Failed to write file '" .. path .. "': invalid path")
|
error("Failed to write file '" .. path .. "': invalid path")
|
||||||
end
|
end
|
||||||
@ -135,7 +145,6 @@ function fs.copy(src, dst)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function fs.move(src, dst)
|
function fs.move(src, dst)
|
||||||
-- Try native rename first
|
|
||||||
local success = os.rename(src, dst)
|
local success = os.rename(src, dst)
|
||||||
if success then return true end
|
if success then return true end
|
||||||
|
|
||||||
@ -154,20 +163,20 @@ function fs.remove(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function fs.mtime(path)
|
function fs.mtime(path)
|
||||||
-- Use os.execute with stat command
|
local cmd
|
||||||
if is_windows then
|
if is_windows then
|
||||||
local cmd = 'for %i in ("' .. path .. '") do @echo %~ti'
|
cmd = 'forfiles /p . /m ' .. shell_escape(fs.basename(path)) .. ' /c "cmd /c echo @fdate @ftime" 2>nul'
|
||||||
local handle = io.popen(cmd)
|
local handle = io.popen(cmd)
|
||||||
if handle then
|
if handle then
|
||||||
local result = handle:read("*line")
|
local result = handle:read("*line")
|
||||||
handle:close()
|
handle:close()
|
||||||
if result then
|
if result then
|
||||||
-- Parse Windows timestamp (basic implementation)
|
-- Basic Windows timestamp parsing - fallback to file existence check
|
||||||
return os.time() -- Fallback to current time
|
return fs.exists(path) and os.time() or nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
local cmd = 'stat -c %Y "' .. path .. '" 2>/dev/null'
|
cmd = 'stat -c %Y ' .. shell_escape(path) .. ' 2>/dev/null'
|
||||||
local handle = io.popen(cmd)
|
local handle = io.popen(cmd)
|
||||||
if handle then
|
if handle then
|
||||||
local result = handle:read("*line")
|
local result = handle:read("*line")
|
||||||
@ -183,7 +192,7 @@ end
|
|||||||
|
|
||||||
function fs.touch(path)
|
function fs.touch(path)
|
||||||
if fs.exists(path) then
|
if fs.exists(path) then
|
||||||
-- Update timestamp - basic implementation
|
-- Update timestamp
|
||||||
local content = fs.read(path)
|
local content = fs.read(path)
|
||||||
fs.write(path, content)
|
fs.write(path, content)
|
||||||
else
|
else
|
||||||
@ -215,12 +224,14 @@ end
|
|||||||
function fs.mkdir(path)
|
function fs.mkdir(path)
|
||||||
local cmd
|
local cmd
|
||||||
if is_windows then
|
if is_windows then
|
||||||
cmd = 'mkdir "' .. path .. '" 2>nul'
|
cmd = 'mkdir ' .. shell_escape(path) .. ' >nul 2>&1'
|
||||||
else
|
else
|
||||||
cmd = 'mkdir -p "' .. path .. '"'
|
cmd = 'mkdir -p ' .. shell_escape(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
local success = os.execute(cmd)
|
local result = os.execute(cmd)
|
||||||
|
-- Handle different Lua version return values
|
||||||
|
local success = (result == 0) or (result == true)
|
||||||
if not success then
|
if not success then
|
||||||
error("Failed to create directory '" .. path .. "'")
|
error("Failed to create directory '" .. path .. "'")
|
||||||
end
|
end
|
||||||
@ -230,12 +241,13 @@ end
|
|||||||
function fs.rmdir(path)
|
function fs.rmdir(path)
|
||||||
local cmd
|
local cmd
|
||||||
if is_windows then
|
if is_windows then
|
||||||
cmd = 'rmdir /s /q "' .. path .. '"'
|
cmd = 'rmdir /s /q ' .. shell_escape(path) .. ' >nul 2>&1'
|
||||||
else
|
else
|
||||||
cmd = 'rm -rf "' .. path .. '"'
|
cmd = 'rm -rf ' .. shell_escape(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
local success = os.execute(cmd)
|
local result = os.execute(cmd)
|
||||||
|
local success = (result == 0) or (result == true)
|
||||||
if not success then
|
if not success then
|
||||||
error("Failed to remove directory '" .. path .. "'")
|
error("Failed to remove directory '" .. path .. "'")
|
||||||
end
|
end
|
||||||
@ -255,14 +267,14 @@ function fs.list(path)
|
|||||||
local cmd
|
local cmd
|
||||||
|
|
||||||
if is_windows then
|
if is_windows then
|
||||||
cmd = 'dir /b "' .. path .. '" 2>nul'
|
cmd = 'dir /b ' .. shell_escape(path) .. ' 2>nul'
|
||||||
else
|
else
|
||||||
cmd = 'ls -1 "' .. path .. '" 2>/dev/null'
|
cmd = 'ls -1 ' .. shell_escape(path) .. ' 2>/dev/null'
|
||||||
end
|
end
|
||||||
|
|
||||||
local handle = io.popen(cmd)
|
local handle = io.popen(cmd)
|
||||||
if not handle then
|
if not handle then
|
||||||
return nil
|
error("Failed to list directory '" .. path .. "': command failed")
|
||||||
end
|
end
|
||||||
|
|
||||||
for name in handle:lines() do
|
for name in handle:lines() do
|
||||||
@ -311,19 +323,21 @@ function fs.list_names(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- PATH OPERATIONS (Optimized pure Lua)
|
-- PATH OPERATIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
function fs.join(...)
|
function fs.join(...)
|
||||||
local parts = {...}
|
local parts = {...}
|
||||||
if #parts == 0 then return "" end
|
if #parts == 0 then return "" end
|
||||||
if #parts == 1 then return parts[1] end
|
if #parts == 1 then return parts[1] or "" end
|
||||||
|
|
||||||
local result = parts[1]
|
local result = parts[1] or ""
|
||||||
for i = 2, #parts do
|
for i = 2, #parts do
|
||||||
local part = parts[i]
|
local part = parts[i]
|
||||||
if part ~= "" then
|
if part and part ~= "" then
|
||||||
if result:sub(-1) == path_sep or result == "" then
|
if result == "" then
|
||||||
|
result = part
|
||||||
|
elseif result:sub(-1) == path_sep then
|
||||||
result = result .. part
|
result = result .. part
|
||||||
else
|
else
|
||||||
result = result .. path_sep .. part
|
result = result .. path_sep .. part
|
||||||
@ -335,14 +349,25 @@ function fs.join(...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function fs.dirname(path)
|
function fs.dirname(path)
|
||||||
|
if path == "" then return "." end
|
||||||
|
|
||||||
|
-- Remove trailing separators
|
||||||
|
path = path:gsub("[/\\]+$", "")
|
||||||
|
|
||||||
local pos = path:find("[/\\][^/\\]*$")
|
local pos = path:find("[/\\][^/\\]*$")
|
||||||
if pos then
|
if pos then
|
||||||
return path:sub(1, pos - 1)
|
local dir = path:sub(1, pos - 1)
|
||||||
|
return dir ~= "" and dir or path_sep
|
||||||
end
|
end
|
||||||
return "."
|
return "."
|
||||||
end
|
end
|
||||||
|
|
||||||
function fs.basename(path)
|
function fs.basename(path)
|
||||||
|
if path == "" then return "" end
|
||||||
|
|
||||||
|
-- Remove trailing separators
|
||||||
|
path = path:gsub("[/\\]+$", "")
|
||||||
|
|
||||||
return path:match("[^/\\]*$") or ""
|
return path:match("[^/\\]*$") or ""
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -353,40 +378,69 @@ function fs.ext(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function fs.abs(path)
|
function fs.abs(path)
|
||||||
|
if path == "" then path = "." end
|
||||||
|
|
||||||
if is_windows then
|
if is_windows then
|
||||||
if path:match("^[A-Za-z]:") or path:match("^\\\\") then
|
if path:match("^[A-Za-z]:") or path:match("^\\\\") then
|
||||||
return path
|
return fs.clean(path)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if path:sub(1, 1) == "/" then
|
if path:sub(1, 1) == "/" then
|
||||||
return path
|
return fs.clean(path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local cwd = fs.getcwd()
|
local cwd = fs.getcwd()
|
||||||
return fs.join(cwd, path)
|
return fs.clean(fs.join(cwd, path))
|
||||||
end
|
end
|
||||||
|
|
||||||
function fs.clean(path)
|
function fs.clean(path)
|
||||||
|
if path == "" then return "." end
|
||||||
|
|
||||||
-- Normalize path separators
|
-- Normalize path separators
|
||||||
path = path:gsub("[/\\]+", path_sep)
|
path = path:gsub("[/\\]+", path_sep)
|
||||||
|
|
||||||
-- Handle . and .. components
|
-- Track if path was absolute
|
||||||
|
local is_absolute = false
|
||||||
|
if is_windows then
|
||||||
|
is_absolute = path:match("^[A-Za-z]:") or path:match("^\\\\")
|
||||||
|
else
|
||||||
|
is_absolute = path:sub(1, 1) == "/"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Split into components
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for part in path:gmatch("[^" .. path_sep .. "]+") do
|
for part in path:gmatch("[^" .. path_sep:gsub("\\", "\\\\") .. "]+") do
|
||||||
if part == ".." and #parts > 0 and parts[#parts] ~= ".." then
|
if part == ".." then
|
||||||
|
if #parts > 0 and parts[#parts] ~= ".." then
|
||||||
|
if not is_absolute or #parts > 1 then
|
||||||
parts[#parts] = nil
|
parts[#parts] = nil
|
||||||
elseif part ~= "." then
|
end
|
||||||
|
elseif not is_absolute then
|
||||||
|
parts[#parts + 1] = ".."
|
||||||
|
end
|
||||||
|
elseif part ~= "." and part ~= "" then
|
||||||
parts[#parts + 1] = part
|
parts[#parts + 1] = part
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local result = table.concat(parts, path_sep)
|
local result = table.concat(parts, path_sep)
|
||||||
if path:sub(1, 1) == path_sep then
|
|
||||||
|
if is_absolute then
|
||||||
|
if is_windows and path:match("^[A-Za-z]:") then
|
||||||
|
-- Windows drive letter
|
||||||
|
local drive = path:match("^[A-Za-z]:")
|
||||||
|
result = drive .. path_sep .. result
|
||||||
|
elseif is_windows and path:match("^\\\\") then
|
||||||
|
-- UNC path
|
||||||
|
result = "\\\\" .. result
|
||||||
|
else
|
||||||
|
-- Unix absolute
|
||||||
result = path_sep .. result
|
result = path_sep .. result
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return result ~= "" and result or "."
|
return result ~= "" and result or (is_absolute and path_sep or ".")
|
||||||
end
|
end
|
||||||
|
|
||||||
function fs.split(path)
|
function fs.split(path)
|
||||||
@ -446,7 +500,6 @@ function fs.tempfile(prefix)
|
|||||||
temp_path = fs.join("/tmp", temp_name)
|
temp_path = fs.join("/tmp", temp_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create the file
|
|
||||||
fs.write(temp_path, "")
|
fs.write(temp_path, "")
|
||||||
return temp_path
|
return temp_path
|
||||||
end
|
end
|
||||||
@ -472,9 +525,14 @@ end
|
|||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
function fs.glob(pattern)
|
function fs.glob(pattern)
|
||||||
|
-- Simple validation to prevent obvious shell injection
|
||||||
|
if pattern:find("[;&|`$(){}]") then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
local cmd
|
local cmd
|
||||||
if is_windows then
|
if is_windows then
|
||||||
cmd = 'for %f in ("' .. pattern .. '") do @echo %f'
|
cmd = 'for %f in (' .. pattern .. ') do @echo %f 2>nul'
|
||||||
else
|
else
|
||||||
cmd = 'ls -1d ' .. pattern .. ' 2>/dev/null'
|
cmd = 'ls -1d ' .. pattern .. ' 2>/dev/null'
|
||||||
end
|
end
|
||||||
@ -500,19 +558,21 @@ function fs.walk(root)
|
|||||||
files[#files + 1] = path
|
files[#files + 1] = path
|
||||||
|
|
||||||
if fs.is_dir(path) then
|
if fs.is_dir(path) then
|
||||||
local entries = fs.list(path)
|
local success, entries = pcall(fs.list, path)
|
||||||
|
if success then
|
||||||
for _, entry in ipairs(entries) do
|
for _, entry in ipairs(entries) do
|
||||||
walk_recursive(fs.join(path, entry.name))
|
walk_recursive(fs.join(path, entry.name))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
walk_recursive(root)
|
walk_recursive(root)
|
||||||
return files
|
return files
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
-- UTILITY FUNCTIONS (using optimized implementations)
|
-- UTILITY FUNCTIONS
|
||||||
-- ======================================================================
|
-- ======================================================================
|
||||||
|
|
||||||
function fs.extension(path)
|
function fs.extension(path)
|
||||||
@ -605,7 +665,9 @@ function fs.find(root, pattern, recursive)
|
|||||||
local results = {}
|
local results = {}
|
||||||
|
|
||||||
local function search(dir)
|
local function search(dir)
|
||||||
local entries = fs.list(dir)
|
local success, entries = pcall(fs.list, dir)
|
||||||
|
if not success then return end
|
||||||
|
|
||||||
for _, entry in ipairs(entries) do
|
for _, entry in ipairs(entries) do
|
||||||
local full_path = fs.join(dir, entry.name)
|
local full_path = fs.join(dir, entry.name)
|
||||||
|
|
||||||
@ -621,7 +683,6 @@ function fs.find(root, pattern, recursive)
|
|||||||
return results
|
return results
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Get directory tree as nested table
|
|
||||||
function fs.tree(root, max_depth)
|
function fs.tree(root, max_depth)
|
||||||
max_depth = max_depth or 10
|
max_depth = max_depth or 10
|
||||||
|
|
||||||
@ -638,7 +699,8 @@ function fs.tree(root, max_depth)
|
|||||||
|
|
||||||
if node.is_dir then
|
if node.is_dir then
|
||||||
node.children = {}
|
node.children = {}
|
||||||
local entries = fs.list(path)
|
local success, entries = pcall(fs.list, path)
|
||||||
|
if success then
|
||||||
for _, entry in ipairs(entries) do
|
for _, entry in ipairs(entries) do
|
||||||
local child_path = fs.join(path, entry.name)
|
local child_path = fs.join(path, entry.name)
|
||||||
local child = build_tree(child_path, depth + 1)
|
local child = build_tree(child_path, depth + 1)
|
||||||
@ -646,6 +708,7 @@ function fs.tree(root, max_depth)
|
|||||||
node.children[#node.children + 1] = child
|
node.children[#node.children + 1] = child
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
else
|
else
|
||||||
node.size = fs.size(path)
|
node.size = fs.size(path)
|
||||||
node.mtime = fs.mtime(path)
|
node.mtime = fs.mtime(path)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user