606 lines
18 KiB
Lua

--- Native Lua implementation of filesystem and platform abstractions,
-- using LuaFileSystem, LZLib, MD5 and LuaCurl.
module("luarocks.fs.lua", package.seeall)
local fs = require("luarocks.fs")
local cfg = require("luarocks.cfg")
local dir = require("luarocks.dir")
local util = require("luarocks.util")
local socket_ok, http = pcall(require, "socket.http")
local _, ftp = pcall(require, "socket.ftp")
local zip_ok, lrzip = pcall(require, "luarocks.tools.zip")
local unzip_ok, luazip = pcall(require, "zip"); _G.zip = nil
local lfs_ok, lfs = pcall(require, "lfs")
--local curl_ok, curl = pcall(require, "luacurl")
local md5_ok, md5 = pcall(require, "md5")
local posix_ok, posix = pcall(require, "posix")
local tar = require("luarocks.tools.tar")
local patch = require("luarocks.tools.patch")
local dir_stack = {}
math.randomseed(os.time())
dir_separator = "/"
--- Quote argument for shell processing.
-- Adds single quotes and escapes.
-- @param arg string: Unquoted argument.
-- @return string: Quoted argument.
function Q(arg)
assert(type(arg) == "string")
-- FIXME Unix-specific
return "'" .. arg:gsub("\\", "\\\\"):gsub("'", "'\\''") .. "'"
end
--- Test is file/dir is writable.
-- Warning: testing if a file/dir is writable does not guarantee
-- that it will remain writable and therefore it is no replacement
-- for checking the result of subsequent operations.
-- @param file string: filename to test
-- @return boolean: true if file exists, false otherwise.
function is_writable(file)
assert(file)
local result
if fs.is_dir(file) then
local file2 = file .. '/.tmpluarockstestwritable'
local fh = io.open(file2, 'wb')
result = fh ~= nil
if fh then fh:close() end
os.remove(file2)
else
local fh = io.open(file, 'rb+')
result = fh ~= nil
if fh then fh:close() end
end
return result
end
--- Create a temporary directory.
-- @param name string: name pattern to use for avoiding conflicts
-- when creating temporary directory.
-- @return string or nil: name of temporary directory or nil on failure.
function make_temp_dir(name)
assert(type(name) == "string")
name = name:gsub("\\", "/")
local temp_dir = (os.getenv("TMP") or "/tmp") .. "/luarocks_" .. name:gsub(dir.separator, "_") .. "-" .. tostring(math.floor(math.random() * 10000))
if fs.make_dir(temp_dir) then
return temp_dir
else
return nil
end
end
--- Run the given command, quoting its arguments.
-- The command is executed in the current directory in the dir stack.
-- @param command string: The command to be executed. No quoting/escaping
-- is applied.
-- @param ... Strings containing additional arguments, which are quoted.
-- @return boolean: true if command succeeds (status code 0), false
-- otherwise.
function execute(command, ...)
assert(type(command) == "string")
for _, arg in ipairs({...}) do
assert(type(arg) == "string")
command = command .. " " .. fs.Q(arg)
end
return fs.execute_string(command)
end
--- Check the MD5 checksum for a file.
-- @param file string: The file to be checked.
-- @param md5sum string: The string with the expected MD5 checksum.
-- @return boolean: true if the MD5 checksum for 'file' equals 'md5sum', false if not
-- or if it could not perform the check for any reason.
function check_md5(file, md5sum)
local computed = fs.get_md5(file)
if not computed then
return false
end
if computed:match("^"..md5sum) then
return true
else
return false
end
end
---------------------------------------------------------------------
-- LuaFileSystem functions
---------------------------------------------------------------------
if lfs_ok then
--- Run the given command.
-- The command is executed in the current directory in the dir stack.
-- @param cmd string: No quoting/escaping is applied to the command.
-- @return boolean: true if command succeeds (status code 0), false
-- otherwise.
function execute_string(cmd)
if os.execute(cmd) == 0 then
return true
else
return false
end
end
--- Obtain current directory.
-- Uses the module's internal dir stack.
-- @return string: the absolute pathname of the current directory.
function current_dir()
return lfs.currentdir()
end
--- Change the current directory.
-- Uses the module's internal dir stack. This does not have exact
-- semantics of chdir, as it does not handle errors the same way,
-- but works well for our purposes for now.
-- @param d string: The directory to switch to.
function change_dir(d)
table.insert(dir_stack, lfs.currentdir())
lfs.chdir(d)
end
--- Change directory to root.
-- Allows leaving a directory (e.g. for deleting it) in
-- a crossplatform way.
function change_dir_to_root()
table.insert(dir_stack, lfs.currentdir())
-- TODO Does this work on Windows?
lfs.chdir("/")
end
--- Change working directory to the previous in the dir stack.
-- @return true if a pop ocurred, false if the stack was empty.
function pop_dir()
local d = table.remove(dir_stack)
if d then
lfs.chdir(d)
return true
else
return false
end
end
--- Create a directory if it does not already exist.
-- If any of the higher levels in the path name does not exist
-- too, they are created as well.
-- @param directory string: pathname of directory to create.
-- @return boolean: true on success, false on failure.
function make_dir(directory)
assert(type(directory) == "string")
directory = directory:gsub("\\", "/")
local path = nil
if directory:sub(2, 2) == ":" then
path = directory:sub(1, 2)
directory = directory:sub(4)
else
if directory:match("^/") then
path = ""
end
end
for d in directory:gmatch("([^"..dir.separator.."]+)"..dir.separator.."*") do
path = path and path .. dir.separator .. d or d
local mode = lfs.attributes(path, "mode")
if not mode then
if not lfs.mkdir(path) then
return false
end
elseif mode ~= "directory" then
return false
end
end
return true
end
--- Remove a directory if it is empty.
-- Does not return errors (for example, if directory is not empty or
-- if already does not exist)
-- @param d string: pathname of directory to remove.
function remove_dir_if_empty(d)
assert(d)
lfs.rmdir(d)
end
--- Remove a directory if it is empty.
-- Does not return errors (for example, if directory is not empty or
-- if already does not exist)
-- @param d string: pathname of directory to remove.
function remove_dir_tree_if_empty(d)
assert(d)
for i=1,10 do
lfs.rmdir(d)
d = dir.dir_name(d)
end
end
--- Copy a file.
-- @param src string: Pathname of source
-- @param dest string: Pathname of destination
-- @return boolean or (boolean, string): true on success, false on failure,
-- plus an error message.
function copy(src, dest)
assert(src and dest)
local destmode = lfs.attributes(dest, "mode")
if destmode == "directory" then
dest = dir.path(dest, dir.base_name(src))
end
local src_h, err = io.open(src, "rb")
if not src_h then return nil, err end
local dest_h, err = io.open(dest, "wb+")
if not dest_h then src_h:close() return nil, err end
while true do
local block = src_h:read(8192)
if not block then break end
dest_h:write(block)
end
src_h:close()
dest_h:close()
return true
end
--- Implementation function for recursive copy of directory contents.
-- @param src string: Pathname of source
-- @param dest string: Pathname of destination
-- @return boolean or (boolean, string): true on success, false on failure
local function recursive_copy(src, dest)
local srcmode = lfs.attributes(src, "mode")
if srcmode == "file" then
local ok = fs.copy(src, dest)
if not ok then return false end
elseif srcmode == "directory" then
local subdir = dir.path(dest, dir.base_name(src))
fs.make_dir(subdir)
for file in lfs.dir(src) do
if file ~= "." and file ~= ".." then
local ok = recursive_copy(dir.path(src, file), subdir)
if not ok then return false end
end
end
end
return true
end
--- Recursively copy the contents of a directory.
-- @param src string: Pathname of source
-- @param dest string: Pathname of destination
-- @return boolean or (boolean, string): true on success, false on failure,
-- plus an error message.
function copy_contents(src, dest)
assert(src and dest)
assert(lfs.attributes(src, "mode") == "directory")
for file in lfs.dir(src) do
if file ~= "." and file ~= ".." then
local ok = recursive_copy(dir.path(src, file), dest)
if not ok then
return false, "Failed copying "..src.." to "..dest
end
end
end
return true
end
--- Implementation function for recursive removal of directories.
-- @param src string: Pathname of source
-- @param dest string: Pathname of destination
-- @return boolean or (boolean, string): true on success,
-- or nil and an error message on failure.
local function recursive_delete(src)
local srcmode = lfs.attributes(src, "mode")
if srcmode == "file" then
return os.remove(src)
elseif srcmode == "directory" then
for file in lfs.dir(src) do
if file ~= "." and file ~= ".." then
local ok, err = recursive_delete(dir.path(src, file))
if not ok then return nil, err end
end
end
local ok, err = lfs.rmdir(src)
if not ok then return nil, err end
end
return true
end
--- Delete a file or a directory and all its contents.
-- For safety, this only accepts absolute paths.
-- @param arg string: Pathname of source
-- @return boolean: true on success, false on failure.
function delete(arg)
assert(arg)
return recursive_delete(arg) or false
end
--- List the contents of a directory.
-- @param at string or nil: directory to list (will be the current
-- directory if none is given).
-- @return table: an array of strings with the filenames representing
-- the contents of a directory.
function list_dir(at)
assert(type(at) == "string" or not at)
if not at then
at = fs.current_dir()
end
if not fs.is_dir(at) then
return {}
end
local result = {}
for file in lfs.dir(at) do
if file ~= "." and file ~= ".." then
table.insert(result, file)
end
end
return result
end
--- Implementation function for recursive find.
-- @param cwd string: Current working directory in recursion.
-- @param prefix string: Auxiliary prefix string to form pathname.
-- @param result table: Array of strings where results are collected.
local function recursive_find(cwd, prefix, result)
for file in lfs.dir(cwd) do
if file ~= "." and file ~= ".." then
local item = prefix .. file
table.insert(result, item)
local pathname = dir.path(cwd, file)
if lfs.attributes(pathname, "mode") == "directory" then
recursive_find(pathname, item..dir_separator, result)
end
end
end
end
--- Recursively scan the contents of a directory.
-- @param at string or nil: directory to scan (will be the current
-- directory if none is given).
-- @return table: an array of strings with the filenames representing
-- the contents of a directory.
function find(at)
assert(type(at) == "string" or not at)
if not at then
at = fs.current_dir()
end
if not fs.is_dir(at) then
return {}
end
local result = {}
recursive_find(at, "", result)
return result
end
--- Test for existance of a file.
-- @param file string: filename to test
-- @return boolean: true if file exists, false otherwise.
function exists(file)
assert(file)
return type(lfs.attributes(file)) == "table"
end
--- Test is pathname is a directory.
-- @param file string: pathname to test
-- @return boolean: true if it is a directory, false otherwise.
function is_dir(file)
assert(file)
return lfs.attributes(file, "mode") == "directory"
end
--- Test is pathname is a regular file.
-- @param file string: pathname to test
-- @return boolean: true if it is a file, false otherwise.
function is_file(file)
assert(file)
return lfs.attributes(file, "mode") == "file"
end
function set_time(file, time)
return lfs.touch(file, time)
end
end
---------------------------------------------------------------------
-- LuaZip functions
---------------------------------------------------------------------
if zip_ok then
function zip(zipfile, ...)
return lrzip.zip(zipfile, ...)
end
end
if unzip_ok then
--- Uncompress files from a .zip archive.
-- @param zipfile string: pathname of .zip archive to be extracted.
-- @return boolean: true on success, false on failure.
function unzip(zipfile)
local zipfile, err = luazip.open(zipfile)
if not zipfile then return nil, err end
local files = zipfile:files()
local file = files()
repeat
if file.filename:sub(#file.filename) == "/" then
fs.make_dir(dir.path(fs.current_dir(), file.filename))
else
local rf, err = zipfile:open(file.filename)
if not rf then zipfile:close(); return nil, err end
local contents = rf:read("*a")
rf:close()
local wf, err = io.open(dir.path(fs.current_dir(), file.filename), "wb")
if not wf then zipfile:close(); return nil, err end
wf:write(contents)
wf:close()
end
file = files()
until not file
zipfile:close()
return true
end
end
---------------------------------------------------------------------
-- LuaCurl functions
---------------------------------------------------------------------
if curl_ok then
--- Download a remote file.
-- @param url string: URL to be fetched.
-- @param filename string or nil: this function attempts to detect the
-- resulting local filename of the remote file as the basename of the URL;
-- if that is not correct (due to a redirection, for example), the local
-- filename can be given explicitly as this second argument.
-- @return boolean: true on success, false on failure.
function download(url, filename)
assert(type(url) == "string")
assert(type(filename) == "string" or not filename)
filename = dir.path(fs.current_dir(), filename or dir.base_name(url))
local c = curl.new()
if not c then return false end
local file = io.open(filename, "wb")
if not file then return false end
local ok = c:setopt(curl.OPT_WRITEFUNCTION, function (stream, buffer)
stream:write(buffer)
return string.len(buffer)
end)
ok = ok and c:setopt(curl.OPT_WRITEDATA, file)
ok = ok and c:setopt(curl.OPT_BUFFERSIZE, 5000)
ok = ok and c:setopt(curl.OPT_HTTPHEADER, "Connection: Keep-Alive")
ok = ok and c:setopt(curl.OPT_URL, url)
ok = ok and c:setopt(curl.OPT_CONNECTTIMEOUT, 15)
ok = ok and c:setopt(curl.OPT_USERAGENT, cfg.user_agent)
ok = ok and c:perform()
ok = ok and c:close()
file:close()
return ok
end
end
---------------------------------------------------------------------
-- LuaSocket functions
---------------------------------------------------------------------
if socket_ok then
--- Download a remote file.
-- @param url string: URL to be fetched.
-- @param filename string or nil: this function attempts to detect the
-- resulting local filename of the remote file as the basename of the URL;
-- if that is not correct (due to a redirection, for example), the local
-- filename can be given explicitly as this second argument.
-- @return boolean: true on success, false on failure.
function download(url, filename)
assert(type(url) == "string")
assert(type(filename) == "string" or not filename)
filename = dir.path(fs.current_dir(), filename or dir.base_name(url))
local content, err
if util.starts_with(url, "http:") then
local res, status, headers, line = http.request(url)
if not res then
err = status
elseif status ~= 200 then
err = line
else
content = res
end
elseif util.starts_with(url, "ftp:") then
content, err = ftp.get(url)
end
if not content then
return false, "Failed downloading: " .. err
end
local file = io.open(filename, "wb")
if not file then return false end
file:write(content)
file:close()
return true
end
end
---------------------------------------------------------------------
-- MD5 functions
---------------------------------------------------------------------
if md5_ok then
--- Get the MD5 checksum for a file.
-- @param file string: The file to be computed.
-- @return string: The MD5 checksum
function get_md5(file)
file = fs.absolute_name(file)
local file = io.open(file, "rb")
if not file then return false end
local computed = md5.sumhexa(file:read("*a"))
file:close()
return computed
end
end
---------------------------------------------------------------------
-- POSIX functions
---------------------------------------------------------------------
if posix_ok then
function chmod(file, mode)
local err = posix.chmod(file, mode)
return err == 0
end
end
---------------------------------------------------------------------
-- Other functions
---------------------------------------------------------------------
--- Apply a patch.
-- @param patchname string: The filename of the patch.
function apply_patch(patchname, patchdata)
local p, all_ok = patch.read_patch(patchname, patchdata)
if not all_ok then
return nil, "Failed reading patch "..patchname
end
if p then
return patch.apply_patch(p, 1)
end
end
--- Move a file.
-- @param src string: Pathname of source
-- @param dest string: Pathname of destination
-- @return boolean or (boolean, string): true on success, false on failure,
-- plus an error message.
function move(src, dest)
assert(src and dest)
if fs.exists(dest) and not fs.is_dir(dest) then
return false, "File already exists: "..dest
end
local ok, err = fs.copy(src, dest)
if not ok then
return false, err
end
ok = fs.delete(src)
if not ok then
return false, "Failed move: could not delete "..src.." after copy."
end
return true
end