local mymodule = {} -- Load libraries posix = require("posix") modelfunctions = require("modelfunctions") fs = require("acf.fs") format = require("acf.format") validator = require("acf.validator") -- Set variables local configfile = "/etc/lbu/lbu.conf" -- ################################################################################ -- LOCAL FUNCTIONS local function get_version () local programversion = modelfunctions.run_executable({"lbu"}, true) return string.match(programversion, "^[^\n]*") end local function getLbuStatus() local ret = {} local f = modelfunctions.run_executable({"lbu", "status", "-v"}, true) for line in string.gmatch(f, "[^\n]+") 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 return ret end local function availablemedias() local medias = {} local found = {} -- First, look in fstab 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 found[media] = true end end -- Then, look in result of 'mount' local mount = modelfunctions.run_executable({"mount"}) for media in string.gmatch(mount, "/media/(%w+)%s") do if not string.find(media, "^cdrom") and not string.find(media, "^dvd") and not found[media] then medias[#medias+1] = media end end table.sort(medias) return medias end local function getciphers() local opensslciphers = {} local watchdog = nil local f = modelfunctions.run_executable({"openssl", "-v"}, true) for line in string.gmatch(f, "[^\n]+") 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 return opensslciphers 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 files end local function validatefilecontent (filedetails) local success = true local clientdata = clientdata or self.clientdata or {} clientdata.configcontents = filedetails.value.filecontent.value local config = mymodule.getconfig(nil, clientdata) 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 listbackupfiles = function() local files = {} local content = modelfunctions.run_executable({"lbu", "lb"}) if not string.match(content, "usage: lbu") then for line in string.gmatch(content, "([^\n]+)\n?") do files[#files + 1] = line end end return files end -- ################################################################################ -- PUBLIC FUNCTIONS function mymodule.getstatus () local status = {} status["version"] = cfe({ value=get_version(), label="Program version", seq=2, }) status["committed"] = cfe({ type="boolean", value=(#mymodule.list().value==0), descr=descrtxt, label="Program status", seq=1, }) local config = mymodule.getconfig() status["LBU_MEDIA"] = config.value.LBU_MEDIA status["LBU_MEDIA"].seq=3 if (config.value.ENCRYPTION.value) then status["DEFAULT_CIPHER"] = config.value.DEFAULT_CIPHER status["DEFAULT_CIPHER"].seq=4 end return cfe({ type="group", value=status, label="LBU Status" }) end function mymodule.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="Changes Since Last Commit" }) end function mymodule.getconfig (self, clientdata) local clientdata = clientdata or {} local config = {} local configopts = {} if clientdata.configcontents then configopts = format.parse_ini_file(clientdata.configcontents, "") or {} elseif (fs.is_file(configfile)) then configopts = format.parse_ini_file(fs.read_file(configfile) or "", "") or {} end config["LBU_MEDIA"] = cfe({ value=configopts.LBU_MEDIA or "", label="Media for commit", type="select", option=availablemedias() or {}, seq=1, }) config["ENCRYPTION"] = cfe({ value=(configopts.ENCRYPTION ~= nil), label="Password protected commits", type="boolean", seq=2, }) config["DEFAULT_CIPHER"] = cfe({ value=configopts.DEFAULT_CIPHER or "", label="Cipher to use for encryption", option=getciphers() or {}, type="select", seq=3, }) config["PASSWORD"] = cfe({ value=configopts.PASSWORD or "", label="Password when encrypting", seq=4, }) config["BACKUP_LIMIT"] = cfe({ value=configopts.BACKUP_LIMIT or "0", label="Backup archive limit", seq=5, }) retval = cfe({ type="group", value=config, label="LBU Config" }) validateconfig(retval) return retval end function mymodule.setconfig (self, 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 mymodule.getincluded () local included = cfe({ label="Included item(s)", type="longtext", descr="List one file/directory per line\nAssumes root directory, no leading '/' necessary", }) -- Read the list included.value = modelfunctions.run_executable({"lbu", "include", "-l"}) validatefilelist(included) return cfe({ type="group", value={included = included} }) end function mymodule.setincluded (self, included) local files = validatefilelist(included.value.included) if not included.value.included.errtxt then local current = format.string_to_table(modelfunctions.run_executable({"lbu", "include", "-l"}), "\n") table.insert(current, 1, "-r") table.insert(current, 1, "include") table.insert(current, 1, "lbu") modelfunctions.run_executable(current) table.insert(files, 1, "include") table.insert(files, 1, "lbu") included.descr, included.errtxt = modelfunctions.run_executable(files) else included.errtxt = "Failed to set included" end return included end function mymodule.getexcluded () local excluded = cfe({ label="Excluded item(s)", type="longtext", descr="List one file/directory per line\nAssumes root directory, no leading '/' necessary", }) -- Read the list excluded.value = modelfunctions.run_executable({"lbu", "exclude", "-l"}) validatefilelist(excluded) return cfe({ type="group", value={excluded = excluded} }) end function mymodule.setexcluded (self, excluded) local files = validatefilelist(excluded.value.excluded) if not excluded.value.excluded.errtxt then local current = format.string_to_table(modelfunctions.run_executable({"lbu", "exclude", "-l"}), "\n") table.insert(current, 1, "-r") table.insert(current, 1, "exclude") table.insert(current, 1, "lbu") modelfunctions.run_executable(current) table.insert(files, 1, "exclude") table.insert(files, 1, "lbu") excluded.descr, excluded.errtxt = modelfunctions.run_executable(files) else excluded.errtxt = "Failed to set excluded" end return excluded end function mymodule.get_filedetails() return modelfunctions.getfiledetails(configfile) end function mymodule.set_filedetails(self, filedetails) return modelfunctions.setfiledetails(self, filedetails, {configfile}, validatefilecontent) end function mymodule.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 mymodule.commit(self, 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, input.errtxt = modelfunctions.run_executable({"lbu", "commit", flag}, true) end return input end function mymodule.getbackupfiles() local files = {} for i,file in ipairs(listbackupfiles()) do local filedetails = posix.stat(file) filedetails.filename = file file[#files+1] = filedetails end return cfe({ type="structure", value=files, label="Backup Archives"}) end function mymodule.get_selectbackup(self, clientdata) local result = {} result.backup = cfe({ type="select", value=clientdata.backup or "", label="Backup" }) result.backup.option = listbackupfiles() return cfe({ type="group", value=result, label="Revert to Backup" }) end function mymodule.selectbackupfile(self, backuprequest) local success = modelfunctions.validate_select(backuprequest.value.backup) if success then backuprequest.descr, backuprequest.errtxt = modelfunctions.run_executable({"lbu", "revert", backuprequest.value.backup.value}) if not backuprequest.errtxt and backuprequest.descr == "" then backuprequest.descr = "Reverted to backup "..file end else backuprequest.errtxt = "Failed to revert to backup" end return backuprequest end function mymodule.getpackage() -- create a temporary directory to store the file session = 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 cmdresult, errtxt = modelfunctions.run_executable({"lbu", "package", tmp}, true) -- find and read the file local package = cfe({ type="raw", label="error", option="application/x-gzip", errtxt=errtxt }) for name in fs.find(".*tar%.gz", tmp) do package.label = posix.basename(name) package.value = fs.read_file(name) or "" break end -- delete the temp directory and file fs.remove_directory(tmp) return package end return mymodule