diff options
| author | Mike Vink <mike@pionative.com> | 2025-02-03 21:29:42 +0100 |
|---|---|---|
| committer | Mike Vink <mike@pionative.com> | 2025-02-03 21:29:42 +0100 |
| commit | 5155816b7b925dec5d5feb1568b1d7ceb00938b9 (patch) | |
| tree | deca28ea15e79f6f804c3d90d2ba757881638af5 /src/luarocks/fs/lua.lua | |
Diffstat (limited to 'src/luarocks/fs/lua.lua')
| -rw-r--r-- | src/luarocks/fs/lua.lua | 1307 |
1 files changed, 1307 insertions, 0 deletions
diff --git a/src/luarocks/fs/lua.lua b/src/luarocks/fs/lua.lua new file mode 100644 index 0000000..4016ddc --- /dev/null +++ b/src/luarocks/fs/lua.lua @@ -0,0 +1,1307 @@ + +--- Native Lua implementation of filesystem and platform abstractions, +-- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5. +-- module("luarocks.fs.lua") +local fs_lua = {} + +local fs = require("luarocks.fs") + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") + +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local socket_ok, zip_ok, lfs_ok, md5_ok, posix_ok, bz2_ok, _ +local http, ftp, zip, lfs, md5, posix, bz2 + +if cfg.fs_use_modules then + socket_ok, http = pcall(require, "socket.http") + _, ftp = pcall(require, "socket.ftp") + zip_ok, zip = pcall(require, "luarocks.tools.zip") + bz2_ok, bz2 = pcall(require, "bz2") + lfs_ok, lfs = pcall(require, "lfs") + md5_ok, md5 = pcall(require, "md5") + posix_ok, posix = pcall(require, "posix") +end + +local patch = require("luarocks.tools.patch") +local tar = require("luarocks.tools.tar") + +local dir_sep = package.config:sub(1, 1) + +local dir_stack = {} + +--- 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 fs_lua.is_writable(file) + assert(file) + file = dir.normalize(file) + local result + if fs.is_dir(file) then + local file2 = dir.path(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, 'r+b') + result = fh ~= nil + if fh then fh:close() end + end + return result +end + +function fs_lua.quote_args(command, ...) + local out = { command } + local args = pack(...) + for i=1, args.n do + local arg = args[i] + assert(type(arg) == "string") + out[#out+1] = fs.Q(arg) + end + return table.concat(out, " ") +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 fs_lua.execute(command, ...) + assert(type(command) == "string") + return fs.execute_string(fs.quote_args(command, ...)) +end + +--- Run the given command, quoting its arguments, silencing its output. +-- The command is executed in the current directory in the dir stack. +-- Silencing is omitted if 'verbose' mode is enabled. +-- @param command string: The command to be executed. No quoting/escaping +-- is applied. +-- @param ... Strings containing additional arguments, which will be quoted. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function fs_lua.execute_quiet(command, ...) + assert(type(command) == "string") + if cfg.verbose then -- omit silencing output + return fs.execute_string(fs.quote_args(command, ...)) + else + return fs.execute_string(fs.quiet(fs.quote_args(command, ...))) + end +end + +function fs_lua.execute_env(env, command, ...) + assert(type(command) == "string") + local envstr = {} + for var, val in pairs(env) do + table.insert(envstr, fs.export_cmd(var, val)) + end + return fs.execute_string(table.concat(envstr, "\n") .. "\n" .. fs.quote_args(command, ...)) +end + +local tool_available_cache = {} + +function fs_lua.set_tool_available(tool_name, value) + assert(type(value) == "boolean") + tool_available_cache[tool_name] = value +end + +--- Checks if the given tool is available. +-- The tool is executed using a flag, usually just to ask its version. +-- @param tool_cmd string: The command to be used to check the tool's presence (e.g. hg in case of Mercurial) +-- @param tool_name string: The actual name of the tool (e.g. Mercurial) +function fs_lua.is_tool_available(tool_cmd, tool_name) + assert(type(tool_cmd) == "string") + assert(type(tool_name) == "string") + + local ok + if tool_available_cache[tool_name] ~= nil then + ok = tool_available_cache[tool_name] + else + local tool_cmd_no_args = tool_cmd:gsub(" [^\"]*$", "") + + -- if it looks like the tool has a pathname, try that first + if tool_cmd_no_args:match("[/\\]") then + local tool_cmd_no_args_normalized = dir.path(tool_cmd_no_args) + local fd = io.open(tool_cmd_no_args_normalized, "r") + if fd then + fd:close() + ok = true + end + end + + if not ok then + ok = fs.search_in_path(tool_cmd_no_args) + end + + tool_available_cache[tool_name] = (ok == true) + end + + if ok then + return true + else + local msg = "'%s' program not found. Make sure %s is installed and is available in your PATH " .. + "(or you may want to edit the 'variables.%s' value in file '%s')" + return nil, msg:format(tool_cmd, tool_name, tool_name:upper(), cfg.config_files.nearest) + end +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 + msg if not +-- or if it could not perform the check for any reason. +function fs_lua.check_md5(file, md5sum) + file = dir.normalize(file) + local computed, msg = fs.get_md5(file) + if not computed then + return false, msg + end + if computed:match("^"..md5sum) then + return true + else + return false, "Mismatch MD5 hash for file "..file + end +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 fs_lua.list_dir(at) + local result = {} + for file in fs.dir(at) do + result[#result+1] = file + end + return result +end + +--- Iterate over the contents of a directory. +-- @param at string or nil: directory to list (will be the current +-- directory if none is given). +-- @return function: an iterator function suitable for use with +-- the for statement. +function fs_lua.dir(at) + if not at then + at = fs.current_dir() + end + at = dir.normalize(at) + if not fs.is_dir(at) then + return function() end + end + return coroutine.wrap(function() fs.dir_iterator(at) end) +end + +--- List the Lua modules at a specific require path. +-- eg. `modules("luarocks.cmd")` would return a list of all LuaRocks command +-- modules, in the current Lua path. +function fs_lua.modules(at) + at = at or "" + if #at > 0 then + -- turn require path into file path + at = at:gsub("%.", package.config:sub(1,1)) .. package.config:sub(1,1) + end + + local path = package.path:sub(-1, -1) == ";" and package.path or package.path .. ";" + local paths = {} + for location in path:gmatch("(.-);") do + if location:lower() == "?.lua" then + location = "./?.lua" + end + local _, q_count = location:gsub("%?", "") -- only use the ones with a single '?' + if location:match("%?%.[lL][uU][aA]$") and q_count == 1 then -- only use when ending with "?.lua" + location = location:gsub("%?%.[lL][uU][aA]$", at) + table.insert(paths, location) + end + end + + if #paths == 0 then + return {} + end + + local modules = {} + local is_duplicate = {} + for _, path in ipairs(paths) do -- luacheck: ignore 421 + local files = fs.list_dir(path) + for _, filename in ipairs(files or {}) do + if filename:match("%.[lL][uU][aA]$") then + filename = filename:sub(1,-5) -- drop the extension + if not is_duplicate[filename] then + is_duplicate[filename] = true + table.insert(modules, filename) + end + end + end + end + + return modules +end + +function fs_lua.filter_file(fn, input_filename, output_filename) + local fd, err = io.open(input_filename, "rb") + if not fd then + return nil, err + end + + local input, err = fd:read("*a") + fd:close() + if not input then + return nil, err + end + + local output, err = fn(input) + if not output then + return nil, err + end + + fd, err = io.open(output_filename, "wb") + if not fd then + return nil, err + end + + local ok, err = fd:write(output) + fd:close() + if not ok then + return nil, err + end + + return true +end + +function fs_lua.system_temp_dir() + return os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp" +end + +local function temp_dir_pattern(name_pattern) + return dir.path(fs.system_temp_dir(), + "luarocks_" .. dir.normalize(name_pattern):gsub("[/\\]", "_") .. "-") +end + +--------------------------------------------------------------------- +-- LuaFileSystem functions +--------------------------------------------------------------------- + +if lfs_ok then + +function fs_lua.file_age(filename) + local attr = lfs.attributes(filename) + if attr and attr.change then + return os.difftime(os.time(), attr.change) + end + return math.huge +end + +function fs_lua.lock_access(dirname, force) + fs.make_dir(dirname) + local lockfile = dir.path(dirname, "lockfile.lfs") + + -- drop stale lock, older than 1 hour + local age = fs.file_age(lockfile) + if age > 3600 and age < math.huge then + force = true + end + + if force then + os.remove(lockfile) + end + return lfs.lock_dir(dirname) +end + +function fs_lua.unlock_access(lock) + return lock:free() +end + +--- 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 fs_lua.execute_string(cmd) + local code = os.execute(cmd) + return (code == 0 or code == true) +end + +--- Obtain current directory. +-- Uses the module's internal dir stack. +-- @return string: the absolute pathname of the current directory. +function fs_lua.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 fs_lua.change_dir(d) + table.insert(dir_stack, lfs.currentdir()) + d = dir.normalize(d) + return lfs.chdir(d) +end + +--- Change directory to root. +-- Allows leaving a directory (e.g. for deleting it) in +-- a crossplatform way. +function fs_lua.change_dir_to_root() + local current = lfs.currentdir() + if not current or current == "" then + return false + end + table.insert(dir_stack, current) + lfs.chdir("/") -- works on Windows too + return true +end + +--- Change working directory to the previous in the dir stack. +-- @return true if a pop occurred, false if the stack was empty. +function fs_lua.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 do not exist +-- too, they are created as well. +-- @param directory string: pathname of directory to create. +-- @return boolean or (boolean, string): true on success or (false, error message) on failure. +function fs_lua.make_dir(directory) + assert(type(directory) == "string") + directory = dir.normalize(directory) + local path = nil + if directory:sub(2, 2) == ":" then + path = directory:sub(1, 2) + directory = directory:sub(4) + else + if directory:match("^" .. dir_sep) then + path = "" + end + end + for d in directory:gmatch("([^" .. dir_sep .. "]+)" .. dir_sep .. "*") do + path = path and path .. dir_sep .. d or d + local mode = lfs.attributes(path, "mode") + if not mode then + local ok, err = lfs.mkdir(path) + if not ok then + return false, err + end + if cfg.is_platform("unix") then + ok, err = fs.set_permissions(path, "exec", "all") + if not ok then + return false, err + end + end + elseif mode ~= "directory" then + return false, path.." is not a directory" + 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 fs_lua.remove_dir_if_empty(d) + assert(d) + d = dir.normalize(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 fs_lua.remove_dir_tree_if_empty(d) + assert(d) + d = dir.normalize(d) + for i=1,10 do + lfs.rmdir(d) + d = dir.dir_name(d) + end +end + +local function are_the_same_file(f1, f2) + if f1 == f2 then + return true + end + if cfg.is_platform("unix") then + local i1 = lfs.attributes(f1, "ino") + local i2 = lfs.attributes(f2, "ino") + if i1 ~= nil and i1 == i2 then + return true + end + end + return false +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source file permissions +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.copy(src, dest, perms) + assert(src and dest) + src = dir.normalize(src) + dest = dir.normalize(dest) + local destmode = lfs.attributes(dest, "mode") + if destmode == "directory" then + dest = dir.path(dest, dir.base_name(src)) + end + if are_the_same_file(src, dest) then + return nil, "The source and destination are the same files" + 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, "w+b") + 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 + local ok, err = dest_h:write(block) + if not ok then return nil, err end + end + src_h:close() + dest_h:close() + + local fullattrs + if not perms then + fullattrs = lfs.attributes(src, "permissions") + end + if fullattrs and posix_ok then + return posix.chmod(dest, fullattrs) + else + if cfg.is_platform("unix") then + if not perms then + perms = fullattrs:match("x") and "exec" or "read" + end + return fs.set_permissions(dest, perms, "all") + else + return true + end + end +end + +--- Implementation function for recursive copy of directory contents. +-- Assumes paths are normalized. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Optional permissions. +-- If not given, permissions of the source are copied over to the destination. +-- @return boolean or (boolean, string): true on success, false on failure +local function recursive_copy(src, dest, perms) + local srcmode = lfs.attributes(src, "mode") + + if srcmode == "file" then + local ok = fs.copy(src, dest, perms) + if not ok then return false end + elseif srcmode == "directory" then + local subdir = dir.path(dest, dir.base_name(src)) + local ok, err = fs.make_dir(subdir) + if not ok then return nil, err end + if pcall(lfs.dir, src) == false then + return false + end + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok = recursive_copy(dir.path(src, file), subdir, perms) + 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 +-- @param perms string ("read" or "exec") or nil: Optional permissions. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.copy_contents(src, dest, perms) + assert(src) + assert(dest) + src = dir.normalize(src) + dest = dir.normalize(dest) + if not fs.is_dir(src) then + return false, src .. " is not a directory" + end + if pcall(lfs.dir, src) == false then + return false, "Permission denied" + end + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok = recursive_copy(dir.path(src, file), dest, perms) + if not ok then + return false, "Failed copying "..src.." to "..dest + end + end + end + return true +end + +--- Implementation function for recursive removal of directories. +-- Assumes paths are normalized. +-- @param name string: Pathname of file +-- @return boolean or (boolean, string): true on success, +-- or nil and an error message on failure. +local function recursive_delete(name) + local ok = os.remove(name) + if ok then return true end + local pok, ok, err = pcall(function() + for file in lfs.dir(name) do + if file ~= "." and file ~= ".." then + local ok, err = recursive_delete(dir.path(name, file)) + if not ok then return nil, err end + end + end + local ok, err = lfs.rmdir(name) + return ok, (not ok) and err + end) + if pok then + return ok, err + else + return pok, ok + end +end + +--- Delete a file or a directory and all its contents. +-- @param name string: Pathname of source +-- @return nil +function fs_lua.delete(name) + name = dir.normalize(name) + recursive_delete(name) +end + +--- Internal implementation function for fs.dir. +-- Yields a filename on each iteration. +-- @param at string: directory to list +-- @return nil or (nil and string): an error message on failure +function fs_lua.dir_iterator(at) + local pok, iter, arg = pcall(lfs.dir, at) + if not pok then + return nil, iter + end + for file in iter, arg do + if file ~= "." and file ~= ".." then + coroutine.yield(file) + end + end +end + +--- Implementation function for recursive find. +-- Assumes paths are normalized. +-- @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) + local pok, iter, arg = pcall(lfs.dir, cwd) + if not pok then + return nil + end + for file in iter, arg 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_sep, 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 fs_lua.find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs.current_dir() + end + at = dir.normalize(at) + local result = {} + recursive_find(at, "", result) + return result +end + +--- Test for existence of a file. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function fs_lua.exists(file) + assert(file) + file = dir.normalize(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 fs_lua.is_dir(file) + assert(file) + file = dir.normalize(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 fs_lua.is_file(file) + assert(file) + file = dir.normalize(file) + return lfs.attributes(file, "mode") == "file" +end + +-- Set access and modification times for a file. +-- @param filename File to set access and modification times for. +-- @param time may be a number containing the format returned +-- by os.time, or a table ready to be processed via os.time; if +-- nil, current time is assumed. +function fs_lua.set_time(file, time) + assert(time == nil or type(time) == "table" or type(time) == "number") + file = dir.normalize(file) + if type(time) == "table" then + time = os.time(time) + end + return lfs.touch(file, time) +end + +else -- if not lfs_ok + +function fs_lua.exists(file) + assert(file) + -- check if file exists by attempting to open it + return util.exists(fs.absolute_name(file)) +end + +function fs_lua.file_age(_) + return math.huge +end + +end + +--------------------------------------------------------------------- +-- lua-bz2 functions +--------------------------------------------------------------------- + +if bz2_ok then + +local function bunzip2_string(data) + local decompressor = bz2.initDecompress() + local output, err = decompressor:update(data) + if not output then + return nil, err + end + decompressor:close() + return output +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function fs_lua.bunzip2(infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + if not outfile then + outfile = infile:gsub("%.bz2$", "") + end + + return fs.filter_file(bunzip2_string, infile, outfile) +end + +end + +--------------------------------------------------------------------- +-- luarocks.tools.zip functions +--------------------------------------------------------------------- + +if zip_ok then + +function fs_lua.zip(zipfile, ...) + return zip.zip(zipfile, ...) +end + +function fs_lua.unzip(zipfile) + return zip.unzip(zipfile) +end + +function fs_lua.gunzip(infile, outfile) + return zip.gunzip(infile, outfile) +end + +end + +--------------------------------------------------------------------- +-- LuaSocket functions +--------------------------------------------------------------------- + +if socket_ok then + +local ltn12 = require("ltn12") +local luasec_ok, https = pcall(require, "ssl.https") + +if luasec_ok and not vers.compare_versions(https._VERSION, "1.0.3") then + luasec_ok = false + https = nil +end + +local redirect_protocols = { + http = http, + https = luasec_ok and https, +} + +local function request(url, method, http, loop_control) -- luacheck: ignore 431 + local result = {} + + if cfg.verbose then + print(method, url) + end + + local proxy = os.getenv("http_proxy") + if type(proxy) ~= "string" then proxy = nil end + -- LuaSocket's http.request crashes when given URLs missing the scheme part. + if proxy and not proxy:find("://") then + proxy = "http://" .. proxy + end + + if cfg.show_downloads then + io.write(method.." "..url.." ...\n") + end + local dots = 0 + if cfg.connection_timeout and cfg.connection_timeout > 0 then + http.TIMEOUT = cfg.connection_timeout + end + local res, status, headers, err = http.request { + url = url, + proxy = proxy, + method = method, + redirect = false, + sink = ltn12.sink.table(result), + step = cfg.show_downloads and function(...) + io.write(".") + io.flush() + dots = dots + 1 + if dots == 70 then + io.write("\n") + dots = 0 + end + return ltn12.pump.step(...) + end, + headers = { + ["user-agent"] = cfg.user_agent.." via LuaSocket" + }, + } + if cfg.show_downloads then + io.write("\n") + end + if not res then + return nil, status + elseif status == 301 or status == 302 then + local location = headers.location + if location then + local protocol, rest = dir.split_url(location) + if redirect_protocols[protocol] then + if not loop_control then + loop_control = {} + elseif loop_control[location] then + return nil, "Redirection loop -- broken URL?" + end + loop_control[url] = true + return request(location, method, redirect_protocols[protocol], loop_control) + else + return nil, "URL redirected to unsupported protocol - install luasec >= 1.1 to get HTTPS support.", "https" + end + end + return nil, err + elseif status ~= 200 then + return nil, err + else + return result, status, headers, err + end +end + +local function write_timestamp(filename, data) + local fd = io.open(filename, "w") + if fd then + fd:write(data) + fd:close() + end +end + +local function read_timestamp(filename) + local fd = io.open(filename, "r") + if fd then + local data = fd:read("*a") + fd:close() + return data + end +end + +local function fail_with_status(filename, status, headers) + write_timestamp(filename .. ".unixtime", os.time()) + write_timestamp(filename .. ".status", status) + return nil, status, headers +end + +-- @param url string: URL to fetch. +-- @param filename string: local filename of the file to fetch. +-- @param http table: The library to use (http from LuaSocket or LuaSec) +-- @param cache boolean: Whether to use a `.timestamp` file to check +-- via the HTTP Last-Modified header if the full download is needed. +-- @return (boolean | (nil, string, string?)): True if successful, or +-- nil, error message and optionally HTTPS error in case of errors. +local function http_request(url, filename, http, cache) -- luacheck: ignore 431 + if cache then + local status = read_timestamp(filename..".status") + local timestamp = read_timestamp(filename..".timestamp") + if status or timestamp then + local unixtime = read_timestamp(filename..".unixtime") + if tonumber(unixtime) then + local diff = os.time() - tonumber(unixtime) + if status then + if diff < cfg.cache_fail_timeout then + return nil, status, {} + end + else + if diff < cfg.cache_timeout then + return true, nil, nil, true + end + end + end + + local result, status, headers, err = request(url, "HEAD", http) -- luacheck: ignore 421 + if not result then + return fail_with_status(filename, status, headers) + end + if status == 200 and headers["last-modified"] == timestamp then + write_timestamp(filename .. ".unixtime", os.time()) + return true, nil, nil, true + end + end + end + local result, status, headers, err = request(url, "GET", http) + if not result then + if status then + return fail_with_status(filename, status, headers) + end + end + if cache and headers["last-modified"] then + write_timestamp(filename .. ".timestamp", headers["last-modified"]) + write_timestamp(filename .. ".unixtime", os.time()) + end + local file = io.open(filename, "wb") + if not file then return nil, 0, {} end + for _, data in ipairs(result) do + file:write(data) + end + file:close() + return true +end + +local function ftp_request(url, filename) + local content, err = ftp.get(url) + if not content then + return false, err + end + local file = io.open(filename, "wb") + if not file then return false, err end + file:write(content) + file:close() + return true +end + +local downloader_warning = false + +--- 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, string, boolean): +-- In case of success: +-- * true +-- * a string with the filename +-- * true if the file was retrieved from local cache +-- In case of failure: +-- * false +-- * error message +function fs_lua.download(url, filename, cache) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + filename = fs.absolute_name(filename or dir.base_name(url)) + + -- delegate to the configured downloader so we don't have to deal with whitelists + if os.getenv("no_proxy") then + return fs.use_downloader(url, filename, cache) + end + + local ok, err, https_err, from_cache + if util.starts_with(url, "http:") then + ok, err, https_err, from_cache = http_request(url, filename, http, cache) + elseif util.starts_with(url, "ftp:") then + ok, err = ftp_request(url, filename) + elseif util.starts_with(url, "https:") then + -- skip LuaSec when proxy is enabled since it is not supported + if luasec_ok and not os.getenv("https_proxy") then + local _ + ok, err, _, from_cache = http_request(url, filename, https, cache) + else + https_err = true + end + else + err = "Unsupported protocol" + end + if https_err then + local downloader, err = fs.which_tool("downloader") + if not downloader then + return nil, err + end + if not downloader_warning then + util.warning("falling back to "..downloader.." - install luasec >= 1.1 to get native HTTPS support") + downloader_warning = true + end + return fs.use_downloader(url, filename, cache) + elseif not ok then + return nil, err, "network" + end + return true, filename, from_cache +end + +else --...if socket_ok == false then + +function fs_lua.download(url, filename, cache) + return fs.use_downloader(url, filename, cache) +end + +end +--------------------------------------------------------------------- +-- MD5 functions +--------------------------------------------------------------------- + +if md5_ok then + +-- Support the interface of lmd5 by lhf in addition to md5 by Roberto +-- and the keplerproject. +if not md5.sumhexa and md5.digest then + md5.sumhexa = function(msg) + return md5.digest(msg) + end +end + +if md5.sumhexa then + +--- Get the MD5 checksum for a file. +-- @param file string: The file to be computed. +-- @return string: The MD5 checksum or nil + error +function fs_lua.get_md5(file) + file = fs.absolute_name(file) + local file_handler = io.open(file, "rb") + if not file_handler then return nil, "Failed to open file for reading: "..file end + local computed = md5.sumhexa(file_handler:read("*a")) + file_handler:close() + if computed then return computed end + return nil, "Failed to compute MD5 hash for file "..file +end + +end +end + +--------------------------------------------------------------------- +-- POSIX functions +--------------------------------------------------------------------- + +function fs_lua._unix_rwx_to_number(rwx, neg) + local num = 0 + neg = neg or false + for i = 1, 9 do + local c = rwx:sub(10 - i, 10 - i) == "-" + if neg == c then + num = num + 2^(i-1) + end + end + return math.floor(num) +end + +if posix_ok then + +local octal_to_rwx = { + ["0"] = "---", + ["1"] = "--x", + ["2"] = "-w-", + ["3"] = "-wx", + ["4"] = "r--", + ["5"] = "r-x", + ["6"] = "rw-", + ["7"] = "rwx", +} + +do + local umask_cache + function fs_lua._unix_umask() + if umask_cache then + return umask_cache + end + -- LuaPosix (as of 34.0.4) only returns the umask as rwx + local rwx = posix.umask() + local num = fs_lua._unix_rwx_to_number(rwx, true) + umask_cache = ("%03o"):format(num) + return umask_cache + end +end + +function fs_lua.set_permissions(filename, mode, scope) + local perms, err = fs._unix_mode_scope_to_perms(mode, scope) + if err then + return false, err + end + + -- LuaPosix (as of 5.1.15) does not support octal notation... + local new_perms = {} + for c in perms:sub(-3):gmatch(".") do + table.insert(new_perms, octal_to_rwx[c]) + end + perms = table.concat(new_perms) + local err = posix.chmod(filename, perms) + return err == 0 +end + +function fs_lua.current_user() + return posix.getpwuid(posix.geteuid()).pw_name +end + +function fs_lua.is_superuser() + return posix.geteuid() == 0 +end + +-- This call is not available on all systems, see #677 +if posix.mkdtemp then + +--- Create a temporary directory. +-- @param name_pattern string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure. +function fs_lua.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + + return posix.mkdtemp(temp_dir_pattern(name_pattern) .. "-XXXXXX") +end + +end -- if posix.mkdtemp + +end + +--------------------------------------------------------------------- +-- Other functions +--------------------------------------------------------------------- + +if not fs_lua.make_temp_dir then + +function fs_lua.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + + local ok, err + for _ = 1, 3 do + local name = temp_dir_pattern(name_pattern) .. tostring(math.random(10000000)) + ok, err = fs.make_dir(name) + if ok then + return name + end + end + + return nil, err +end + +end + +--- Apply a patch. +-- @param patchname string: The filename of the patch. +-- @param patchdata string or nil: The actual patch as a string. +-- @param create_delete boolean: Support creating and deleting files in a patch. +function fs_lua.apply_patch(patchname, patchdata, create_delete) + 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, create_delete) + end +end + +--- Move a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source file permissions. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.move(src, dest, perms) + 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, perms) + if not ok then + return false, err + end + fs.delete(src) + if fs.exists(src) then + return false, "Failed move: could not delete "..src.." after copy." + end + return true +end + +local function get_local_tree() + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "table" and tree.name == "user" then + return fs.absolute_name(tree.root) + end + end +end + +local function is_local_tree_in_env(local_tree) + local lua_path + if _VERSION == "Lua 5.1" then + lua_path = os.getenv("LUA_PATH") + else + lua_path = os.getenv("LUA_PATH_" .. _VERSION:sub(5):gsub("%.", "_")) + or os.getenv("LUA_PATH") + end + if lua_path and lua_path:match(local_tree, 1, true) then + return true + end +end + +--- Check if user has write permissions for the command. +-- Assumes the configuration variables under cfg have been previously set up. +-- @param args table: the args table passed to run() drivers. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.check_command_permissions(args) + local ok = true + local err = "" + if args._command_permissions_checked then + return true + end + for _, directory in ipairs { cfg.rocks_dir, cfg.deploy_lua_dir, cfg.deploy_bin_dir, cfg.deploy_lua_dir } do + if fs.exists(directory) then + if not fs.is_writable(directory) then + ok = false + err = "Your user does not have write permissions in " .. directory + break + end + else + local root = fs.root_of(directory) + local parent = directory + repeat + parent = dir.dir_name(parent) + if parent == "" then + parent = root + end + until parent == root or fs.exists(parent) + if not fs.is_writable(parent) then + ok = false + err = directory.." does not exist\nand your user does not have write permissions in " .. parent + break + end + end + end + if ok then + args._command_permissions_checked = true + return true + else + if args["local"] or cfg.local_by_default then + err = err .. "\n\nPlease check your permissions.\n" + else + local local_tree = get_local_tree() + if local_tree then + err = err .. "\n\nYou may want to run as a privileged user," + .. "\nor use --local to install into your local tree at " .. local_tree + .. "\nor run 'luarocks config local_by_default true' to make --local the default.\n" + + if not is_local_tree_in_env(local_tree) then + err = err .. "\n(You may need to configure your Lua package paths\nto use the local tree, see 'luarocks path --help')\n" + end + else + err = err .. "\n\nYou may want to run as a privileged user.\n" + end + end + return nil, err + end +end + +--- Check whether a file is a Lua script +-- When the file can be successfully compiled by the configured +-- Lua interpreter, it's considered to be a valid Lua file. +-- @param filename filename of file to check +-- @return boolean true, if it is a Lua script, false otherwise +function fs_lua.is_lua(filename) + filename = filename:gsub([[%\]],"/") -- normalize on fw slash to prevent escaping issues + local lua = fs.Q(cfg.variables.LUA) -- get lua interpreter configured + -- execute on configured interpreter, might not be the same as the interpreter LR is run on + local result = fs.execute_string(lua..[[ -e "if loadfile(']]..filename..[[') then os.exit(0) else os.exit(1) end"]]) + return (result == true) +end + +--- Unpack an archive. +-- Extract the contents of an archive, detecting its format by +-- filename extension. +-- @param archive string: Filename of archive. +-- @return boolean or (boolean, string): true on success, false and an error message on failure. +function fs_lua.unpack_archive(archive) + assert(type(archive) == "string") + + local ok, err + archive = fs.absolute_name(archive) + if archive:match("%.tar%.gz$") then + local tar_filename = archive:gsub("%.gz$", "") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tgz$") then + local tar_filename = archive:gsub("%.tgz$", ".tar") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tar%.bz2$") then + local tar_filename = archive:gsub("%.bz2$", "") + ok, err = fs.bunzip2(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.zip$") then + ok, err = fs.unzip(archive) + elseif archive:match("%.lua$") or archive:match("%.c$") then + -- Ignore .lua and .c files; they don't need to be extracted. + return true + else + return false, "Couldn't extract archive "..archive..": unrecognized filename extension" + end + if not ok then + return false, "Failed extracting "..archive..": "..err + end + return true +end + +return fs_lua |
