local util = { } util.fileExist = function(filename) local file, err = io.open(filename, mode or "r") local exist = false if file then exist = true file:close() end return exist end util.readfile = function(filename, mode, silent) local file, err = io.open(filename, mode or "r") if not file then if not silent then print("Error opening file:", filename, err) end return end local content = file:read("*a") file:close() return content end util.writefile = function(filename, content, mode) local file, err = io.open(filename, mode or "w") -- Default write mode if not file then return print("Error opening file:", filename, err) end file:write(content) file:close() end util.clone = function(orig) local orig_type = type(orig) local copy if orig_type == 'table' then copy = {} for orig_key, orig_value in next, orig, nil do copy[util.clone(orig_key)] = util.clone(orig_value) end setmetatable(copy, util.clone(getmetatable(orig))) else -- number, string, boolean, etc copy = orig end return copy end util.flatten = function(tbl, prefix, result) result = result or {} for k, v in pairs(tbl) do local new_key = (prefix or "") .. "." .. k if type(v) == "table" then util.flatten(v, new_key, result) else result[new_key] = v end end return result end util.pagerender = function(file,data,serve) return function(req,res,next) data = data or {opts = app.opts} data.page = file:gsub(".*","") res.status(200) res.header('content-type','text/html; charset=utf-8') res.body( util.template( LoadAsset(file),data or app) ) next() end end util.templateFile = function(file,data) trace('templateFile(' .. file .. ')') return util.template( LoadAsset(file), data) end util.baseURLify = function(url) if url:match("^" .. app.opts.baseurl) then return url end -- only do once if url:match("://") then return url end return url:gsub("^[/]?", app.opts.baseurl) end util.getURL = function(url,opts) opts = opts or {} local port = '' if opts.server then url = url or '/' return 'http://localhost:' .. app.port .. url else port = ':' .. GetPort() if port == ':80' then port = '' end if arg[1] == 'generate' and port == ':8080' then port = '' end -- if hostname is not localhost or ip then we can assume live server (reverse proxy e.g.) if port and not GetHost():match('localhost') and not GetHost():match('^[0-9]') then port = '' end local host = #app.opts.hostname > 0 and app.opts.hostname or GetHost() return GetScheme() .. '://' .. host .. port .. (url or GetPath()) end end util.template = function(content,data) local mdata = util.merge(data or app,{}) mdata = util.merge(mdata, {opts = app.opts}) -- set fallback vars mdata.urlpath = util.baseURLify( GetPath() ) mdata.url = mdata.url or mdata.urlpath mdata.janusurl = mdata.janusurl or mdata.urlpath mdata.title = mdata.title or app.opts.subtitle mdata.header_include = mdata.header_include or '' mdata.footerlinks = {} app.pub('util.template', nil, mdata) mdata.header = app.tpl( LoadAsset('page/header.html'), mdata ) if not mdata.janusxr then mdata.janusxr = app.tpl( LoadAsset('page/janusxr.html'), mdata ) end mdata.footer = app.tpl( LoadAsset('page/footer.html'), mdata ) return app.tpl( content, mdata ) end util.contains = function(tab,k_or_v) local result = false foreach( tab, function(k,v) if k == k_or_v or v == k_or_v then result = true end end) return result end util.execute = function(cmd) local handle = io.popen(cmd) print("running : "..cmd) local result = handle:read("*a") handle:close() return result end util.memoize = function(fn, ttl_seconds) local cached_results = nil local last_executed = 0 return function(...) local now = os.time() -- Check if cache is missing or if the TTL has passed print("now = "..now .. " last_executed = " ..last_executed) if not cached_results or (now - last_executed) >= ttl_seconds then -- Pass all input arguments and pack all return values into a table cached_results = { fn(...) } last_executed = now end -- Unpack and return the cached results return table.unpack(cached_results) end end util.cmdExist = function(cmd) cmd = cmd .. " 1>/dev/null 2>/dev/null" local stdout, retstr, retcode = os.execute(cmd) return retcode ~= 127 end function util.merge(t1,t2) local t3 = {} foreach( t1, function(k,v) t3[k] = v end) foreach( t2, function(k,v) t3[k] = v end) return t3 end function util.dump(value, indent, seen) indent = indent or "" seen = seen or {} local t = type(value) if t == "string" then return string.format("%q", value) elseif t == "boolean" or t == "number" or t == "nil" then return tostring(value) elseif t == "function" or t == "userdata" or t == "thread" then return string.format("<%s: %s>", t, tostring(value)) elseif t == "table" then if seen[value] then return string.format("", tostring(value)) end seen[value] = true local result = "{\n" local next_indent = indent .. " " local seen_keys = {} for i, v in ipairs(value) do seen_keys[i] = true result = result .. next_indent .. util.dump(v, next_indent, seen) .. ",\n" end for k, v in pairs(value) do if not seen_keys[k] then local key_str if type(k) == "string" and k:match("^[%a_][%w_]*$") then key_str = k else key_str = "[" .. util.dump(k, next_indent, seen) .. "]" end result = result .. next_indent .. key_str .. " = " .. util.dump(v, next_indent, seen) .. ",\n" end end if result:sub(-2) == ",\n" then result = result:sub(1, -3) .. "\n" end return result .. indent .. "}" end end function util.count(self) local i = 0 if self == nil then return i end if type(self) == 'table' then for _, _ in pairs(self) do i = i + 1 end return i else return #self end end util.call = function(fn,a,b,c,d,e,f) return function(k,v) if v ~= nil and v[fn] then v[fn](a,b,c,d,e,f) end end end function foreach(t, f, alwayscontinue) if t ~= nil then for k, v in pairs(t) do if alwayscontinue then local success, err = pcall(f, k, v) else f(k, v) end end end end function trace(o) if os.getenv('DEBUG') then if type(o) == 'table' then print_r(o) else print(o) end end end -- usage: result = filter({ {foo=1},{foo=2} }, function(k,v) return v.foo > 1 end) -- {{foo=2}} -- result = filter({ {foo=1},{foo=2} }, {foo=1}) -- {{foo=1}} function util.filter(t,t_or_func) local result = {} foreach(t, function(k,v) if type(t_or_func) == "function" then if t_or_func(k,v) then result[k] = v end else local match = 0 foreach( t_or_func, function(kk,vv) if v[kk] == vv then result[k] = v end end) if match == util.count(t_or_func) then result[k] = v end end end) return result end function util.countChar(char, str) local _, nchars = str:gsub(char,"") return nchars end function print_r(t) print( util.dump(t) ) end function util.split(inputstr, sep) if sep == nil then sep = "%s" end local t = {} for str in string.gmatch(inputstr, "([^"..sep.."]+)") do table.insert(t, str) end return t end function util.pluck(path, tbl, default) default = default or '' if type(tbl) ~= "table" or type(path) ~= "string" then return default end local current = tbl -- Match non-dot segments in the path key string for key in string.gmatch(path, "[^.]+") do if type(current) ~= "table" then return default end current = current[key] if current == nil then return default end end return current end function util.fetchInstall(app) app.cache.expectedreq = {} app.post('/bus',function(req,res,next) local reqid = GetParam('id') local cb = app.cache.expectedreq[reqid] if cb then cb(res,req,next) -- important: flipped req,res app.cache.expectedreq[reqid] = nil else trace("util.fetch: reqid not found " .. reqid .. " pid=" .. unix.getpid()) end end) end function util.fetchServer(url, opts, cb) util.fetch( util.getURL(url, {server=true}), opts, cb ) end function util.fetch(url, opts, cb) local reqid = tostring( math.abs(Rdrand()) ) print("reqid = " .. reqid .. " pid:" .. unix.getpid() ) app.cache.expectedreq[reqid] = cb local urlret = util.getURL('/bus?id=' .. reqid, {server=true} ) if assert(unix.fork()) == 0 then print('🤞 fetch ' .. url) local ok, headers, body = Fetch(url, opts) if ok then Fetch(urlret, body) -- post back to ourselves else print(headers) end unix.exit(0) end end function util.stripPath(str) return str:gsub("^/zip/",""):gsub("^./",""):gsub("^/",""):gsub("/$","") end function util.truncate(str,maxchar,filler) filler = filler or '..' str = str:gsub(".*://",""):gsub("/$",""):gsub("/.*/","/.../") if maxchar ~= nil and #str > maxchar then str = str:sub(0,maxchar) .. filler end return str end -- WARNING: this is a destructive function which renders the redbean server inactive -- only to be used in tests or staticwebgen plugin util.queueServerRequest = function(method, path, params, host, headers, body, scheme, port) GetMethod = function() return method end GetPath = function() return path end GetParams = function() return params or {} end -- note: { {"key","value"} } format GetHost = function() return host or 'localhost' end GetHeaders = function() return headers or {} end GetScheme = function() return scheme or 'https' end GetBody = function() return body or '' end GetPort = function() return port or 80 end SetStatus = function() end Write = function() end SetHeader = function() end Route = function() end end function util.traverse(arr, cb, key, parent) if key == nil then key = 'children' end foreach( arr, function(k,child) if type(child) == 'table' then cb(child,parent) if type(child[key]) == 'table' then if child[key][1] then util.traverse( child[key], cb, key, child ) end end end end) end function util.traverseXML( parsedXml, cb) util.traverse( parsedXml, function(node, parent) local struct = { tag = '?', prop = {} } if node['name'] ~= nil then struct['tag'] = node:name():lower() end foreach( node['___props'], function(k,v) -- move to props array local val = node[ '@' .. v['name'] ] if type(val) == "table" then foreach( val, function(k,v) val = v end) -- pick last end struct['prop'][ v['name'] ] = val end) if parent ~= nil then struct['parent'] = (function(parent) return function() return parent end end)(parent) end cb(struct,node) end, '___children' ) end util.hyphenate = function(str) local str,match = str:lower() :gsub("[^%w_]", "-") :gsub("%-+", "-") :gsub("^%-", "") :gsub("%-$", "") return str end util.dehyphenate = function(str) local str,match = str:gsub("^_[a-zA-Z0-9]+","") -- remove ids starting with _ :gsub("%-%-","-") -- remove double dash :gsub("-"," ") -- convert to spaces :gsub("^%s*(.-)%s*$", "%1") -- trim return str end util.stripId = function(path) return path:gsub("_[a-zA-Z0-9]+",""), path:match("_[a-zA-Z0-9]+") end util.parseIsoDate = function(iso_str) --local iso_str = "2017-11-03T08:04:45.076Z" local year, month, day, hour, min, sec, ms = iso_str:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)%.(%d+)Z") year, month, day = tonumber(year), tonumber(month), tonumber(day) hour, min, sec = tonumber(hour), tonumber(min), tonumber(sec) ms = tonumber("0." .. ms) -- Optional: keep as decimal seconds fraction local local_time = os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec}) local utc_offset = os.difftime(local_time, os.time(os.date("!*t", local_time))) local timestamp = local_time + utc_offset return timestamp, ms end return util