From 636deed29f3e27be5e951dfe3ccd21396b76d089 Mon Sep 17 00:00:00 2001 From: "kepler155c@gmail.com" Date: Tue, 5 Nov 2019 11:18:49 -0700 Subject: [PATCH] lzwfs --- lzwfs/.package | 6 + lzwfs/autorun/install.lua | 27 +++++ lzwfs/etc/fstab | 1 + lzwfs/lzwfs.lua | 248 ++++++++++++++++++++++++++++++++++++++ lzwfs/startup.lua | 18 +++ lzwfs/system/lzwfs.lua | 150 +++++++++++++++++++++++ 6 files changed, 450 insertions(+) create mode 100644 lzwfs/.package create mode 100644 lzwfs/autorun/install.lua create mode 100644 lzwfs/etc/fstab create mode 100644 lzwfs/lzwfs.lua create mode 100644 lzwfs/startup.lua create mode 100644 lzwfs/system/lzwfs.lua diff --git a/lzwfs/.package b/lzwfs/.package new file mode 100644 index 0000000..3df8991 --- /dev/null +++ b/lzwfs/.package @@ -0,0 +1,6 @@ +{ + title = 'Disk compression', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/lzwfs', + description = [[ Disk compression ]], + license = 'MIT', +} diff --git a/lzwfs/autorun/install.lua b/lzwfs/autorun/install.lua new file mode 100644 index 0000000..1ced079 --- /dev/null +++ b/lzwfs/autorun/install.lua @@ -0,0 +1,27 @@ +local Config = require('opus.config') +local Util = require('opus.util') + +local config = Config.load('lzwfs', { + enabled = false, + installed = false, + filters = { + '/packages', + '/sys', + '/usr/config', + } +}) + +if not config.installed then + -- insert lzwfs into boot startup + local boot = Util.readTable('.startup.boot') + table.insert(boot.preload, 1, '/packages/lzwfs/startup.lua') + Util.writeTable('.startup.boot', boot) + + -- update config + config.installed = true + Config.update('lzwfs', config) + + print('Installing lzwfs - rebooting in 3 seconds') + os.sleep(3) + os.reboot() +end diff --git a/lzwfs/etc/fstab b/lzwfs/etc/fstab new file mode 100644 index 0000000..3fade6c --- /dev/null +++ b/lzwfs/etc/fstab @@ -0,0 +1 @@ +sys/apps/system/lzwfs.lua linkfs packages/lzwfs/system/lzwfs.lua \ No newline at end of file diff --git a/lzwfs/lzwfs.lua b/lzwfs/lzwfs.lua new file mode 100644 index 0000000..3c2a7ee --- /dev/null +++ b/lzwfs/lzwfs.lua @@ -0,0 +1,248 @@ +-- see: https://github.com/Rochet2/lualzw +-- MIT License - Copyright (c) 2016 Rochet2 + +-- Transparent file system compression for non-binary files using lzw + +-- Files that are compressed will have the first bytes in file set to 'LZWC'. +-- If a file does not benefit from compression, the contents will not be altered. + +local char = string.char +local type = type +local sub = string.sub +local tconcat = table.concat +local tinsert = table.insert + +local fs = _G.fs + +local SIGC = 'LZWC' + +local basedictcompress = {} +local basedictdecompress = {} +for i = 0, 255 do + local ic, iic = char(i), char(i, 0) + basedictcompress[ic] = iic + basedictdecompress[iic] = ic +end + +local native = { open = fs.open } +local enabled = false +local filters = { } + +local function dictAddA(str, dict, a, b) + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[str] = char(a,b) + a = a+1 + return dict, a, b +end + +local function compress(input) + if type(input) ~= "string" then + error ("string expected, got "..type(input)) + end + local len = #input + if len <= 1 then + return input + end + + local dict = {} + local a, b = 0, 1 + + local result = { SIGC } + local resultlen = 1 + local n = 2 + local word = "" + for i = 1, len do + local c = sub(input, i, i) + local wc = word..c + if not (basedictcompress[wc] or dict[wc]) then + local write = basedictcompress[word] or dict[word] + if not write then + error "algorithm error, could not fetch word" + end + result[n] = write + resultlen = resultlen + #write + n = n+1 + if len <= resultlen then + return input + end + dict, a, b = dictAddA(wc, dict, a, b) + word = c + else + word = wc + end + end + result[n] = basedictcompress[word] or dict[word] + resultlen = resultlen+#result[n] + if len <= resultlen then + return input + end + return tconcat(result) +end + +local function dictAddB(str, dict, a, b) + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[char(a,b)] = str + a = a+1 + return dict, a, b +end + +local function decompress(input) + if type(input) ~= "string" then + error( "string expected, got "..type(input)) + end + + if #input <= 1 then + return input + end + + local control = sub(input, 1, 4) + if control ~= SIGC then + return input + end + input = sub(input, 5) + local len = #input + + if len < 2 then + error("invalid input - not a compressed string") + end + + local dict = {} + local a, b = 0, 1 + + local result = {} + local n = 1 + local last = sub(input, 1, 2) + result[n] = basedictdecompress[last] or dict[last] + n = n+1 + for i = 3, len, 2 do + local code = sub(input, i, i+1) + local lastStr = basedictdecompress[last] or dict[last] + if not lastStr then + error( "could not find last from dict. Invalid input?") + end + local toAdd = basedictdecompress[code] or dict[code] + if toAdd then + result[n] = toAdd + n = n+1 + dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b) + else + local tmp = lastStr..sub(lastStr, 1, 1) + result[n] = tmp + n = n+1 + dict, a, b = dictAddB(tmp, dict, a, b) + end + last = code + end + return tconcat(result) +end + +local function split(str, pattern) + pattern = pattern or "(.-)\n" + local t = {} + local function helper(line) tinsert(t, line) return "" end + helper((str:gsub(pattern, helper))) + return t +end + +local function matchesFilter(fname) + for _, filter in pairs(filters) do + if fname:match(filter) then + return true + end + end +end + +function fs.open(fname, flags) + if not enabled then + return native.open(fname, flags) + end + + if flags == 'r' then + local f, err = native.open(fname, 'rb') + if not f then + return f, err + end + + local ctr = 0 + local lines + return { + readLine = function() + if not lines then + lines = split(decompress(f.readAll())) + end + ctr = ctr + 1 + return lines[ctr] + end, + readAll = function() + return decompress(f.readAll()) + end, + close = function() + f.close() + end, + } + elseif flags == 'w' or flags == 'a' then + if not matchesFilter(fs.combine(fname, '')) or true then + return native.open(fname, flags) + end + + local c = { } + + if flags == 'a' then + local f = fs.open(fname, 'r') + if f then + tinsert(c, f.readAll()) + f.close() + end + end + + local f, err = native.open(fname, 'wb') + if not f then + return f, err + end + + return { + write = function(str) + tinsert(c, str) + end, + writeLine = function(str) + tinsert(c, str) + tinsert(c, '\n') + end, + flush = function() + -- this isn't gonna work... + -- f.write(compress(tconcat(c))) + f.flush(); + end, + close = function() + f.write(compress(tconcat(c))) + f.close() + end, + } + end + + return native.open(fname, flags) +end + +function fs.option(category, action, option) + if category == 'compression' then + if action == 'enabled' then + enabled = option + elseif action == 'filters' then + filters = option + end + end +end + +print('lzwfs started') diff --git a/lzwfs/startup.lua b/lzwfs/startup.lua new file mode 100644 index 0000000..b12b64d --- /dev/null +++ b/lzwfs/startup.lua @@ -0,0 +1,18 @@ +local fs = _G.fs +local textutils = _G.textutils + +local CONFIG = 'usr/config/lzwfs' + +local config = { } + +if fs.exists(CONFIG) then + local f = fs.open(CONFIG, 'r') + if f then + config = textutils.unserialize(f.readAll()) + f.close() + end +end + +os.run(_ENV, '/packages/lzwfs/lzwfs.lua') +fs.option('compression', 'filters', config.filters) +fs.option('compression', 'enable', config.enabled) diff --git a/lzwfs/system/lzwfs.lua b/lzwfs/system/lzwfs.lua new file mode 100644 index 0000000..f22335c --- /dev/null +++ b/lzwfs/system/lzwfs.lua @@ -0,0 +1,150 @@ +local Config = require('opus.config') +local UI = require('opus.ui') +local Util = require('opus.util') + +local fs = _G.fs + +local config = Config.load('lzwfs', { + enabled = false, + filters = { + '/packages', + '/sys', + '/usr/config', + } +}) + +local tab = UI.Tab { + tabTitle = 'Compression', + description = 'Disk compression', + label1 = UI.Text { + x = 2, y = 2, + value = 'Enable compression', + }, + checkbox = UI.Checkbox { + x = 20, y = 2, + value = config.enabled + }, + entry = UI.TextEntry { + x = 2, y = 4, ex = -2, + limit = 256, + shadowText = 'enter new path', + accelerators = { + enter = 'add_path', + }, + help = 'add a new path', + }, + grid = UI.Grid { + x = 2, ex = -2, y = 6, ey = -5, + disableHeader = true, + columns = { { key = 'value' } }, + autospace = true, + sortColumn = 'index', + help = 'double-click to remove', + accelerators = { + delete = 'remove', + }, + }, + button = UI.Button { + x = -9, ex = -2, y = -3, + text = 'Apply', + event = 'apply', + }, + statusBar = UI.StatusBar { }, +} + +function tab:enable() + self.grid.values = { } + for k,v in ipairs(config.filters or { }) do + table.insert(self.grid.values, { index = k, value = v }) + end + self.grid:update() + UI.Tab.enable(self) +end + +local function rewriteFiles(p) + if type(p) == 'table' then + for _, path in pairs(p) do + rewriteFiles(path) + end + else + local function recurse(path) + _G._syslog('rewriting: ' .. path) + if fs.isDir(path) then + for _, v in pairs(fs.list(path)) do + recurse(fs.combine(path, v)) + end + else + local c = Util.readFile(path) + Util.writeFile(path, c) + end + end + + recurse(fs.combine(p, '')) + end +end + +function tab:eventHandler(event) + if event.type == 'add_path' then + table.insert(self.grid.values, { + index = #self.grid.values + 1, + value = self.entry.value, + }) + self.entry:reset() + self.entry:draw() + self.grid:update() + self.grid:draw() + return true + + elseif event.type == 'grid_select' or event.type == 'remove' then + local selected = self.grid:getSelected() + if selected then + table.remove(self.grid.values, selected.index) + self.grid:update() + self.grid:draw() + end + + elseif event.type == 'focus_change' then + self.statusBar:setStatus(event.focused.help) + + elseif event.type == 'apply' then + local filters = { } + + for _, v in pairs(self.grid.values) do + table.insert(filters, v.value) + end + + if self.checkbox.value ~= config.enabled then + if not self.checkbox.value then + fs.option('compression', 'filters', { }) + self:rewriteFiles(config.filters) + fs.option('compression', 'enabled', false) + else + fs.option('compression', 'enabled', true) + fs.option('compression', 'filters', filters) + rewriteFiles(filters) + end + elseif self.checkbox.value then + fs.option('compression', 'filters', filters) + for _,v in pairs(filters) do + if not Util.find(config.filters, v) then + rewriteFiles(v) -- uncompress paths not in current filter + end + end + + for _,v in pairs(config.filters) do + if not Util.find(filters, v) then + rewriteFiles(v) -- compress new filters + end + end + end + config.filters = filters + config.enabled = self.checkbox.value + Config.update('lzwfs', config) + + self:emit({ type = 'success_message', message = 'Settings updated' }) + end + + return UI.Tab.eventHandler(self, event) +end + +return tab