module (..., package.seeall) -- Load libraries require("modelfunctions") require("fs") require("format") require("validator") -- Set variables local configfile = "/etc/lbu/lbu.conf" local includefile = "/etc/lbu/include" local excludefile = "/etc/lbu/exclude" -- ################################################################################ -- LOCAL FUNCTIONS local function get_version () local f,error = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin lbu 2>&1") local programversion = f:read("*l") f:close() return programversion end local function getLbuStatus() local ret = {} local f = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin lbu status -v 2>&1", "r") if not (f) then return ret end for line in f:lines() do if (string.match(line, "^Include files")) then break end if (string.match(line, "^Exclude files")) then break end local status, name = string.match(line, "^(%S+)%s+(.+)$") if (status) and (name) then ret[string.gsub('/' .. name, "/+", "/")] = status end end f:close() return ret end local function availablemedias() local medias = {} local fstab = fs.read_file("/etc/fstab") or "" for media in string.gmatch(fstab, "/media/(%w+)%s") do if not string.find(media, "^cdrom") and not string.find(media, "^dvd") then medias[#medias+1] = media end end return medias end local function getLbuCommit(flag) local err = {} local ret = "" local f = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin lbu commit " .. flag .. " 2>&1", "r") for line in f:lines() do ret = ret .. line .. "\n" --Look for error messages in output local searchrow, search = string.match(line, "^(lbu.*(%-%a).*)") if (search) then err[search] = searchrow end end f:close() return ret, err end local function getciphers() local opensslciphers = {} local watchdog = nil local f = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin openssl -v 2>&1", "r") if not (f) then return ciphers end for line in f:lines() do if (watchdog) then for cipher in string.gmatch(line, "(%S+)") do table.insert(opensslciphers,tostring(cipher)) end end if (string.match(line, "^Cipher commands")) then watchdog="yes" end end f:close() return opensslciphers end local function getincexl(state) local incexl = {} if (string.lower(state) == "include") or (string.lower(state) == "exclude") then local f = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin lbu " .. string.lower(state) .. " -l 2>&1", "r") if not (f) then return incexl end for line in f:lines() do table.insert(incexl,tostring(line)) end f:close() end return incexl end local function validateconfig(config) -- Set errormessages when configs are wrong config.value.LBU_MEDIA.errtxt = "Invalid Media" for i,media in ipairs(config.value.LBU_MEDIA.option) do if media == config.value.LBU_MEDIA.value then config.value.LBU_MEDIA.errtxt = nil break end end config.value.DEFAULT_CIPHER.errtxt = "Invalid Cipher" for i,cipher in ipairs(config.value.DEFAULT_CIPHER.option) do if cipher == config.value.DEFAULT_CIPHER.value then config.value.DEFAULT_CIPHER.errtxt = nil break end end if config.value.PASSWORD.value == "" and config.value.ENCRYPTION.value then config.value.PASSWORD.errtxt = "Encryption without password is not allowed!\nDeactivate password protection or configure a password!" end if not validator.is_integer(config.value.BACKUP_LIMIT.value) then config.value.BACKUP_LIMIT.errtxt = "Must be an integer" end for name,value in pairs(config.value) do if value.errtxt then config.errtxt = "Invalid Config" break end end return config end local function replacestring (string, find, replace) local count string,count = string.gsub(string, find, replace) if 0 == count then string = string .. replace end return string end local function validatefilelist(filelist) local errors = {} local files = {} for line in string.gmatch(format.dostounix(filelist.value).."\n", "([^\n]*)\n") do if not string.match(line, "^%s*$") then local glob = posix.glob("/"..line) if not glob then errors[#errors+1] = '"' .. tostring(line) .. '" not a valid file/directory' files[#files+1] = line else for i,value in pairs(glob) do files[#files+1] = string.gsub(value, "^/+", "") end end end end filelist.value = table.concat(files, "\n") if #errors ~= 0 then filelist.errtxt = table.concat(errors, "\n") end return filelist end local function validatefilecontent (filedetails) local success = true local config = getconfig(filedetails.value.filecontent.value) local errors = {} for name,value in pairs(config.value) do if value.errtxt then errors[#errors+1] = value.errtxt end end if #errors > 0 then success = false filedetails.value.filecontent.errtxt = table.concat(errors, "\n") end return success, filedetails end --[[ local was_mounted local mnt local function mount() local configopts = format.parse_ini_file(fs.read_file(configfile), "") or {} mnt = "/media/"..configopts.LBU_MEDIA local f = io.popen("grep "..mnt.." /proc/mounts") local cmdresult = f:read("*a") f:close() if cmdresult ~= "" then was_mounted = true else local g = io.popen("mount "..mnt) g:close() was_mounted = false end end local function unmount() if not was_mounted then local g = io.popen("umount "..mnt) g:close() end end --]] -- ################################################################################ -- PUBLIC FUNCTIONS function getstatus () local status = {} status["version"] = cfe({ value=get_version(), label="Program version", }) status["committed"] = cfe({ type="boolean", value=(#list().value==0), descr=descrtxt, label="Program status", }) local config = getconfig() status["LBU_MEDIA"] = config.value.LBU_MEDIA if (config.value.ENCRYPTION.value) then status["DEFAULT_CIPHER"] = config.value.DEFAULT_CIPHER end return cfe({ type="group", value=status, label="LBU Status" }) end function list() local ret = {} local lbuStatus = getLbuStatus() for k,v in pairs(lbuStatus) do ret[#ret + 1] = { name=k, status=v } end table.sort(ret, function(a,b) return (a.name < b.name) end) return cfe({ type="structure", value=ret, label="List of changes" }) end function getconfig (configcontents) local config = {} local configopts = {} if configcontents then configopts = format.parse_ini_file(configcontents, "") or {} elseif (fs.is_file(configfile)) then configopts = format.parse_ini_file(fs.read_file(configfile), "") or {} end config["LBU_MEDIA"] = cfe({ value=configopts.LBU_MEDIA or "", label="Media for commit", type="select", option=availablemedias() or {}, }) config["ENCRYPTION"] = cfe({ value=(configopts.ENCRYPTION ~= nil), label="Password protected commits", type="boolean", }) config["DEFAULT_CIPHER"] = cfe({ value=configopts.DEFAULT_CIPHER or "", label="Cipher to use for encryption", option=getciphers() or {}, type="select", }) config["PASSWORD"] = cfe({ value=configopts.PASSWORD or "", label="Password when encrypting", }) config["BACKUP_LIMIT"] = cfe({ value=configopts.BACKUP_LIMIT or "0", label="Backup archive limit", }) retval = cfe({ type="group", value=config, label="LBU Config" }) validateconfig(retval) return retval end function setconfig (config) validateconfig(config) if not config.errtxt then local content = (fs.read_file(configfile) or "").."\n" -- LBU_MEDIA, ENCRYPTION, DEFAULT_CIPHER, PASSWORD content = replacestring(content, "[^\n%w]*LBU_MEDIA=[^\n]*\n", "LBU_MEDIA="..config.value.LBU_MEDIA.value.."\n") local newstring = "ENCRYPTION=$DEFAULT_CIPHER\n" if not config.value.ENCRYPTION.value then newstring = "#" .. newstring end content = replacestring(content, "[^\n%w]*ENCRYPTION=[^\n]*\n", newstring) content = replacestring(content, "[^\n%w]*DEFAULT_CIPHER=[^\n]*\n", "DEFAULT_CIPHER="..config.value.DEFAULT_CIPHER.value.."\n") content = replacestring(content, "[^\n%w]*PASSWORD=[^\n]*\n", "PASSWORD="..config.value.PASSWORD.value.."\n") content = replacestring(content, "[^\n%w]*BACKUP_LIMIT=[^\n]*\n", "BACKUP_LIMIT="..config.value.BACKUP_LIMIT.value.."\n") -- Write changes to file fs.write_file(configfile,content) end return config end function getincluded () local included = cfe({ value=fs.read_file(includefile) or "", label="Included item(s)", type="longtext", descr="List one file/directory per line\nAssumes root directory, no leading '/' necessary", }) validatefilelist(included) return cfe({ type="group", value={included = included} }) end function setincluded (included) validatefilelist(included.value.included) if not included.value.included.errtxt then fs.write_file(includefile, included.value.included.value) else included.errtxt = "Failed to set included" end return included end function getexcluded () local excluded = cfe({ value=fs.read_file(excludefile) or "", label="Excluded item(s)", type="longtext", descr="List one file/directory per line\nAssumes root directory, no leading '/' necessary", }) validatefilelist(excluded) return cfe({ type="group", value={excluded = excluded} }) end function setexcluded (excluded) validatefilelist(excluded.value.excluded) if not excluded.value.excluded.errtxt then fs.write_file(excludefile, excluded.value.excluded.value) else excluded.errtxt = "Failed to set excluded" end return excluded end function get_filedetails() return modelfunctions.getfiledetails(configfile) end function set_filedetails(filedetails) return modelfunctions.setfiledetails(filedetails, {configfile}, validatefilecontent) end function getcommit() local simulate = cfe({ type="boolean", value=false, label="Simulate a commit" }) local delete = cfe({ type="boolean", value=false, label="Remove existing overlays on commit" }) return cfe({ type="group", value={simulate=simulate, delete=delete}, label="Commit changes" }) end function commit(input) if input.value.simulate.value and input.value.delete.value then input.errtxt = "Cannot delete overlays when simulating" else local flag = "-v" if input.value.simulate.value then flag=flag.." -n" end if input.value.delete.value then flag=flag.." -d" end input.descr = getLbuCommit(flag) end return input end --[[ function getbackupfiles() mount() local files = {} local selected local f = io.popen("ls "..mnt.."/*.[0-9]*[0-9].tar.gz") for line in f:lines() do files[#files + 1] = line end f:close() if #files then f = io.popen("date -u -r "..mnt.."/*.apkovl.tar.gz +%Y%m%d%H%m%S") selected = string.match(files[1], "^[^.]*.") .. f:read("*l") .. ".tar.gz" end unmount() return cfe({ type="list", value=files, label="Backup archive list", selected = selected }) end function selectbackupfile(file) mount() if string.find(file, "^"..mnt) and fs.is_file(file) then local f = io.popen("cp -p "..file.." "..string.match(file, "^[^.]*.").."apkovl.tar.gz") f:close() end unmount() end function deletebackupfile(file) mount() if string.find(file, "^"..mnt) and fs.is_file(file) then local f = io.popen("rm "..file) f:close() end unmount() end --]] function getbackupfiles() local files = {} local f = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin lbu lb 2>&1", "r") for line in f:lines() do files[#files + 1] = line end f:close() return cfe({ type="list", value=files, label="Backup archive list"}) end function selectbackupfile(selectfile) local cmdresult = cfe({ value="Failed to select backup", errtxt="Backup not found", label="Select Backup result"}) local files = getbackupfiles() for i,file in ipairs(files.value) do if file == selectfile then local f = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin lbu revert "..file.." 2>&1", "r") cmdresult.errtxt = f:read("*a") if "" == cmdresult.errtxt then cmdresult.errtxt = nil cmdresult.value = "Selected backup "..file end f:close() break end end return cmdresult end function getpackage() -- create a temporary directory to store the file require("session") local tmp = "/tmp/"..session.random_hash(10) while posix.stat( tmp ) do tmp = "/tmp/"..session.random_hash(10) end posix.mkdir(tmp) local f = io.popen("PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin lbu package "..tmp.." 2>&1") local cmdresult = f:read("*a") f:close() -- find and read the file local package = cfe({ type="raw", label="error", option="application/x-gzip" }) for name in fs.find(".*tar%.gz", tmp) do package.label = basename(name) package.value = fs.read_file(name) break end -- delete the temp directory and file f = io.popen("rm -r "..tmp) f:close() return package end