-- gitano.util -- -- Low level utility routines for Gitano -- -- Copyright 2012 Daniel Silverstone -- -- local luxio = require 'luxio' local sio = require 'luxio.simple' local log = require 'gitano.log' local scrypt = require 'scrypt' local tconcat = table.concat local check_password = scrypt.verify_password local function run_command(cmd, cmdline, parsed_cmdline, user, config, env, repo) log.debug("Welcome to " .. config.global.site_name) log.debug("Running:") for i = 1, #parsed_cmdline do log.debug(" => " .. parsed_cmdline[i]) end log.debug("") log.debug("On behalf of " .. user .. " using key " .. env["GITANO_KEYTAG"]) local how, why = cmd.run(config, repo, parsed_cmdline, env) if how ~= "exit" or why ~= 0 then log.critical("Error running " .. parsed_cmdline[1] .. ": " .. how) log.critical("Unable to continue") return why else log.syslog.info(cmdline, "completed successfully") return 0 end end local function hash_password(password) -- For the moment we are using scrypt, -- we may decide to use other hash functions in the future -- so it's useful to provide some way to identify which hash -- function was used return 'scrypt', scrypt.hash_password(password, 2^14, 8, 1) end local function _deep_copy(t, memo) if not memo then memo = {} end if memo[t] then return memo[t] end local ret = {} local kk, vv for k, v in pairs(t) do kk, vv = k, v if type(k) == "table" then kk = _deep_copy(k, memo) end if type(v) == "table" then vv = _deep_copy(v, memo) end ret[kk] = vv end return ret end local function _parse_cmdline(cmdline) local r = {} local acc = "" local c local escaping = false local quoting = false while #cmdline > 0 do c, cmdline = cmdline:match("^(.)(.*)$") if escaping then if c == "n" then acc = acc .. "\n" elseif c == "t" then acc = acc .. "\t" else acc = acc .. c end escaping = false else if c == "'" and quoting == false then -- Start single quotes quoting = c elseif c == '"' and quoting == false then -- Start double quotes quoting = c elseif c == "'" and quoting == c then -- End single quotes quoting = false elseif c == '"' and quoting == c then -- End double quotes quoting = false elseif c == "\\" then -- A backslash, entering escaping mode escaping = true elseif quoting then -- Within quotes, so accumulate acc = acc .. c elseif c == " " then -- A space and not quoting, so clear the accumulator if acc ~= "" then r[#r+1] = acc end acc = "" else acc = acc .. c end end end if acc ~= "" then r[#r+1] = acc end local warnings = {} if quoting then warnings[#warnings+1] = "Un-terminated quoted string" end if escaping then warnings[#warnings+1] = "Un-used escape at end" end if #r == 0 then warnings[#warnings+1] = "No command found?" end return r, warnings end local function patesc(s) return ((s:gsub("[%%%*%-%?%.%+%[%]%(%)]", "%%%0"):gsub("%z", "%%z"))) end local function path_components(path) local ret = {} for elem in path:gmatch("([^/]*)/*") do ret[#ret+1] = elem end ret[#ret] = nil return ret end local function path_join(...) return tconcat({...}, "/") end local function dirname(path) local t = path_components(path) t[#t] = nil return table.concat(t, "/") end local function basename(path, ext) local t = path_components(path) local ret = t[#t] if ext then local pat = patesc(ext) .. "$" if ret:find(pat) then ret = ret:sub(1, -(#ext+1)) end end return ret end local function mkdir_p(path, mode) if not mode then mode = sio.tomode("0755") end if path == "" or path == "/" then return true end if path:find("/") then local ok, msg = mkdir_p(dirname(path)) if not ok then return ok, msg end end local r, err = luxio.mkdir(path, mode) if r < 0 then if err == luxio.EEXIST then return true end return nil, "mkdir(" .. path .. "): " .. luxio.strerror(err) end return true end local function rm_rf(path) local ret, err, dirp dirp, err = luxio.opendir(path) if not dirp then return false, luxio.strerror(err) end local e, i repeat e, i = luxio.readdir(dirp) if e == 0 then if i.d_name ~= "." and i.d_name ~= ".." then local elem = path .. "/" .. i.d_name ret, err = luxio.unlink(elem) if ret ~= 0 and err == luxio.EISDIR then ret, err = rm_rf(elem) if not ret then return ret, err end elseif ret ~= 0 then return false, luxio.strerror(err) end end end until not e -- explicitly close the dir so we can remove it luxio.closedir(dirp) ret, err = luxio.rmdir(path) return (ret == 0), luxio.strerror(err) end local function _write_all(file, data) local towrite = #data local written = 0 while written < towrite do local write_count, emsg = file:write(data, written) if not write_count then return false, written, emsg end written = written + write_count end return true, written end local function copy_file(from, to, buffer_size) -- Default buffer size is 4M, but can be changed buffer_size = buffer_size or 4 * 1024 * 1024 local fromfile, emsg = sio.open(from, "r") if not fromfile then return false, emsg end local tofile, emsg = sio.open(to, "wce") if not tofile then return false, emsg end local write_count repeat local ok local bytes, emsg = fromfile:read(buffer_size) if not bytes then fromfile:close() tofile:close() return false, emsg end ok, write_count, emsg = _write_all(tofile, bytes) if not ok then fromfile:close() tofile:close() return false, emsg end until write_count == 0 return true end -- Adapter function, so hardlink follows the same return convention -- as the copy_file function local function hardlink_file(from, to) local ret, err = luxio.link(from, to) return ret == 0, luxio.strerror(err) end -- TODO: optionally re-base absolute paths when target is moved -- outside its base directory local function copy_symlink(from, to) local link_target, ret, err ret, link_target = luxio.readlink(from) if ret == -1 then return false, luxio.strerror(link_target) end ret, err = luxio.symlink(link_target, to) if ret ~= 0 then return false, luxio.strerror(err) end return true end local function copy_pathname_filter(exclude_set) return function(parent_path, filename, fileinfo) return exclude_set[filename] end end local _exclude_builtin = { ["."] = true, [".."] = true } local copy_dir_filter_base = copy_pathname_filter(_exclude_builtin) local copy_dir_copy_callbacks -- filter_cb is a function, which takes (parent_path, filename, fileinfo) -- and returns true if the component should not be copied -- parent_path is required, since filter_cb is passed on to subdirectories -- copy_cbs is an optional table of callbacks local function copy_dir(from, to, copy_cbs, filter_cb) filter_cb = filter_cb or copy_dir_filter_base copy_cbs = copy_cbs or copy_dir_copy_callbacks local ret, err, dirp ret, err = mkdir_p(to) if not ret then return ret, err end dirp, err, ret = sio.opendir(from) if not dirp then return ret, err end for filename, fileinfo in dirp:iterate() do local copycb = copy_cbs[fileinfo.d_type] local filefrom = path_join(from, filename) local fileto = path_join(to, filename) if filter_cb(from, filename, fileinfo) then log.ddebug("Skipping file", filename) elseif fileinfo.d_type == luxio.DT_REG then log.ddebug("Copying file", filefrom, "to", fileto) ret, err = copycb(filefrom, fileto) if not ret then log.critical("Copy file", filefrom, "to", fileto, "failed:", err) return false, err end elseif fileinfo.d_type == luxio.DT_LNK then log.ddebug("Copying symlink", filefrom, "to", fileto) ret, err = copycb(filefrom, fileto) if not ret then log.critical("Copy symlink", filefrom, "to", fileto, "failed:", err) return false, err end elseif fileinfo.d_type == luxio.DT_DIR then log.ddebug("Copying dir", filefrom, "to", fileto) ret, err = copycb(filefrom, fileto, copy_cbs, filter_cb) if not ret then log.critical("Copy dir", filefrom, "to", fileto, "failed:", err) return ret, err end else return false, ("Unsupported file type %d"):format(fileinfo.d_type) end end return true end copy_dir_copy_callbacks = { [luxio.DT_DIR] = copy_dir, [luxio.DT_REG] = copy_file, [luxio.DT_LNK] = copy_symlink, } local function html_escape(s) return (s:gsub("&", "&"): gsub("<", "<"): gsub(">", ">"): gsub('"', """)) end local tagname_pattern = "^[a-z0-9_%-/]*[a-z0-9_%-]*$" local cached_expansions = {} local function prep_expansion(str) -- Parse 'str' and return a table representing a sequence of -- operations required to evaluate the expansion of the string. -- in the simple case, it's merely the string in a table -- if the entry in ret is a string, it's copied. If it's a table -- then that table's [1] is a string which is a tag name to expand. if cached_expansions[str] then return cached_expansions[str] end local ret = {} local acc = "" local c local seen = false while #str > 0 do c, str = str:match("^(.)(.*)$") if seen == false then if c == "$" then seen = c else acc = acc .. c end elseif seen == "$" then if c == "{" then seen = c if acc ~= "" then ret[#ret+1] = acc acc = "" end else acc = acc .. c seen = false end elseif seen == "{" then if c == "}" then seen = false assert(acc:match(tagname_pattern), "Expected tag name in string expansion of ${...}") ret[#ret+1] = { acc } acc = "" else acc = acc .. c end end end if seen == "$" then acc = acc .. seen elseif seen ~= false then error("Unterminated tag expansion in string.") end if acc ~= "" then ret[#ret+1] = acc end cached_expansions[str] = ret return ret end local function process_expansion(tags, expn, tagsactive) -- Expand expn with tags and return a single string if type(expn) == "string" then return expn end if not tagsactive then tagsactive = {} end local r = {} for i = 1, #expn do local elem = expn[i] if type(elem) == "string" then r[#r+1] = elem else elem = elem[1] if tagsactive[elem] then return do_deny(tags, "Loop detected in tag expansion") end local tag = tags[elem] if type(tag) == "function" then tags[elem] = tag(tags) tag = tags[elem] end if type(tag) == "string" then tagsactive[elem] = true tag = process_expansion(tags, tag, tagsactive) tagsactive[elem] = nil else -- Can't implicitly expand lists etc. tag = "" end r[#r+1] = tag end end return tconcat(r) end local function set(t) local ret = {} for i, v in ipairs(t) do ret[i] = v ret[v] = i end return ret end local function add_splitable(context, key, value, splitter, prefix_name, suffix_name) if not value or value == "" then return end local function _(k, v) context[k] = v end _(key, value) local prefix, suffix = value:match("^(.*%" .. splitter .. ")" .. "([^%" .. splitter .. "]+)$") if prefix then _(key .. "/" .. prefix_name, prefix:sub(1, -2)) _(key .. "/" .. suffix_name, suffix) else _(key .. "/" .. suffix_name, value) end local i = 1 for section in value:gmatch("([^%" .. splitter .. "]+)") do _(key .. "/" .. tostring(i), section) i = i + 1 end end return { parse_cmdline = _parse_cmdline, patesc = patesc, path_components = path_components, path_join = path_join, dirname = dirname, basename = basename, copy_symlink = copy_symlink, hardlink_file = hardlink_file, copy_file = copy_file, mkdir_p = mkdir_p, rm_rf = rm_rf, copy_pathname_filter = copy_pathname_filter, copy_dir_filter_base = copy_dir_filter_base, copy_dir_copy_callbacks = copy_dir_copy_callbacks, copy_dir = copy_dir, html_escape = html_escape, deep_copy = _deep_copy, prep_expansion = prep_expansion, add_splitable = add_splitable, process_expansion = process_expansion, set = set, hash_password = hash_password, check_password = check_password, run_command = run_command, }