lzwfs
This commit is contained in:
6
lzwfs/.package
Normal file
6
lzwfs/.package
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
title = 'Disk compression',
|
||||||
|
repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/lzwfs',
|
||||||
|
description = [[ Disk compression ]],
|
||||||
|
license = 'MIT',
|
||||||
|
}
|
||||||
27
lzwfs/autorun/install.lua
Normal file
27
lzwfs/autorun/install.lua
Normal file
@@ -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
|
||||||
1
lzwfs/etc/fstab
Normal file
1
lzwfs/etc/fstab
Normal file
@@ -0,0 +1 @@
|
|||||||
|
sys/apps/system/lzwfs.lua linkfs packages/lzwfs/system/lzwfs.lua
|
||||||
248
lzwfs/lzwfs.lua
Normal file
248
lzwfs/lzwfs.lua
Normal file
@@ -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')
|
||||||
18
lzwfs/startup.lua
Normal file
18
lzwfs/startup.lua
Normal file
@@ -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)
|
||||||
150
lzwfs/system/lzwfs.lua
Normal file
150
lzwfs/system/lzwfs.lua
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user