507 lines
15 KiB
Lua
507 lines
15 KiB
Lua
local common = require "fs.rtfs"
|
|
local rtfs, proxy = {}, setmetatable({},{__index=common.proxy})
|
|
local ieformat = ">BI8I8c47" -- ftype, start, len, name
|
|
local iesize = string.packsize(ieformat)
|
|
local sbformat = ">c4I2I8c18" -- "rtfs" magic, version, index size in entries, label. label length subject to change.
|
|
local ftypes = {
|
|
unused = 0x00,
|
|
empty = 0x01,
|
|
file = 0x10,
|
|
tfile = 0x11,
|
|
dfile = 0x12,
|
|
directory = 0x20,
|
|
}
|
|
|
|
function proxy:log(message,level)
|
|
syslog(message,level or syslog.debug,"rtfs:"..self.label)
|
|
end
|
|
|
|
-- data mangling functions
|
|
local fnormalize = common.fnormalize
|
|
function proxy:cSOI(n)
|
|
n=n-1
|
|
local ms = self.capacity / self.blockSize
|
|
local ePS = self.blockSize // iesize
|
|
local s = n // ePS
|
|
local so = self.blockSize - iesize - ((n % ePS) * iesize)
|
|
local eo = so + iesize
|
|
return ms - s, so+1, eo
|
|
end
|
|
|
|
-- superblock stuff
|
|
function proxy:updateSB()
|
|
self:cachedWrite(1,string.pack(sbformat, "rtfs", 0, self.isize, self.label))
|
|
end
|
|
function proxy:setISize(n)
|
|
if self.isize ~= n then
|
|
self.isize = n
|
|
self:updateSB()
|
|
end
|
|
end
|
|
|
|
-- index I/O
|
|
function proxy:readIEntry(n)
|
|
local sector, sstart, send = self:cSOI(n)
|
|
local rt = {string.unpack(ieformat,self:cachedRead(sector):sub(sstart, send))}
|
|
rt[4] = rt[4]:gsub("\0","")
|
|
rt[5] = nil
|
|
return table.unpack(rt)
|
|
end
|
|
function proxy:writeIEntry(n,et,es,el,en)
|
|
local ne = string.pack(ieformat, et, es, el, en)
|
|
local sector, sstart, send = self:cSOI(n)
|
|
local fi, ft = self:findIEntry(nil,nil,nil,nil,sector)
|
|
if fi then
|
|
self:log("auto-compact triggered")
|
|
if not (ft[1] == ftypes.empty or ft[1] == ftypes.dfile or ftypes[1] == ftypes.tfile) then
|
|
self:log("automatic defragment triggered from:" .. debug.traceback())
|
|
assert(not self.runningDefragment, "recursive defragment triggered")
|
|
self:defragment()
|
|
end
|
|
n = self:compactIndex(n) or self.isize + 1
|
|
sector, sstart, send = self:cSOI(n)
|
|
end
|
|
local pes = self:cachedRead(sector)
|
|
local ns = pes:sub(1, sstart - 1) .. ne .. pes:sub(send + 1)
|
|
self:cachedWrite(sector, ns)
|
|
self:setISize(math.max(self.isize, n))
|
|
end
|
|
function proxy:allIEntries()
|
|
local i = 0
|
|
return function()
|
|
if i < self.isize then
|
|
i = i + 1
|
|
return i, self:readIEntry(i)
|
|
end
|
|
return nil
|
|
end
|
|
end
|
|
|
|
-- index maintenance
|
|
function proxy:compactIndex(ti) -- number -- number -- Compact the index, optionally returning the new index of *ti*.
|
|
while self.runningCompact do -- wait for any existing compact run to complete
|
|
coroutine.yield()
|
|
end
|
|
self.runningCompact = true
|
|
-- look for contingous free space
|
|
local ri
|
|
for i, tp, st, sl, n in self:allIEntries() do
|
|
if tp == ftypes.empty or tp == ftypes.dfile then
|
|
local ni, nt = self:findIEntry(nil,st+sl)
|
|
if ni and nt then
|
|
if nt[1] == ftypes.empty or nt[1] == ftypes.dfile then
|
|
self:writeIEntry(i,tp,st,sl+nt[3],n)
|
|
self:writeIEntry(ni,ftypes.unused,0,0,"")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- reverse walk to move any unused indexes
|
|
for i = self.isize, 1, -1 do
|
|
local ne = self:nextEntry() or self.isize
|
|
local ft = {self:readIEntry(i)}
|
|
if ne < i then
|
|
-- modify any open handles to point at the new index
|
|
for k,v in pairs(self.handles) do
|
|
if v.fi == i then
|
|
v.fi = ne
|
|
end
|
|
end
|
|
self:writeIEntry(ne, table.unpack(ft))
|
|
ri = (ti == i) and ne or ri -- track the requested index
|
|
elseif ft[1] ~= ftypes.unused then
|
|
self:setISize(i)
|
|
break
|
|
end
|
|
end
|
|
-- resize the free space / tentative file at the end of the FS to match the index size
|
|
local li, ls = 0, 0
|
|
local lt
|
|
for i, tp, st, sl, n in self:allIEntries() do
|
|
if st+sl > ls then
|
|
li, ls = i, st+sl
|
|
lt = {tp, st, sl, n}
|
|
end
|
|
end
|
|
if lt[1] == ftypes.empty or lt[1] == ftypes.dfile or lt[1] == ftypes.tfile then
|
|
local lastsec = (self.capacity / self.blockSize) - math.ceil(self.isize / (self.blockSize / iesize))
|
|
self:writeIEntry(li, lt[1], lt[2], lastsec - lt[2] - 1, lt[4])
|
|
-- modify the file handle in memory, if applicable
|
|
if lt[1] == ftypes.tfile then
|
|
for k,v in ipairs(self.handles) do
|
|
if v.fi == li then
|
|
v.ft[3] = lastsec - lt[2]
|
|
v.maxWrite = v.ft[3] * self.blockSize
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self.runningCompact = false
|
|
return ri
|
|
end
|
|
|
|
-- mostly just for debugging
|
|
function proxy:dumpIndex()
|
|
print(" # ty init len end name")
|
|
for i, tp, st, sl, n in self:allIEntries() do
|
|
print(string.format("%2i %2i %4i %4i %4i %s",i,tp,st,sl,st+sl-1, n))
|
|
end
|
|
end
|
|
|
|
-- index searching
|
|
function proxy:findIEntry(et,es,el,en,ef) -- number number number string number -- Find an entry matching all provided parameters and ignoring any not specified.
|
|
local ri, rt
|
|
for i, tp, st, sl, n in self:allIEntries() do
|
|
local sf = st + sl - 1
|
|
if (et or tp) == tp and (es or st) == st and (el or sl) == sl and (en or n) == n and (ef or sf) == sf then
|
|
ri, rt = i, {self:readIEntry(i)}
|
|
break
|
|
end
|
|
end
|
|
return ri, rt
|
|
end
|
|
function proxy:nextEntry()
|
|
local ni = self.isize+1
|
|
for i, tp, st, sl, n in self:allIEntries() do
|
|
if tp == ftypes.unused or tp == ftypes.dfile then
|
|
ni = i
|
|
break
|
|
end
|
|
end
|
|
return ni
|
|
end
|
|
|
|
-- file allocation
|
|
function proxy:bestFree()
|
|
local ri, sri
|
|
local bs, sbs = 0, 0
|
|
for i, tp, st, sl, n in self:allIEntries() do
|
|
if tp == ftypes.empty or tp == ftypes.dfile then
|
|
if sl > bs then
|
|
sri, sbs = ri, bs
|
|
ri, bs = i, sl
|
|
end
|
|
end
|
|
end
|
|
return bs/2 > sbs and ri or sri, bs/2 > sbs
|
|
end
|
|
function proxy:allocateNew(name)
|
|
self:compactIndex()
|
|
local lfi,lfis = self:bestFree()
|
|
local lf = {self:readIEntry(lfi)}
|
|
local new
|
|
if lfis then
|
|
new = {ftypes.tfile, lf[2] + math.floor(lf[3]/2), math.ceil(lf[3]/2), name}
|
|
lf[3] = math.floor(lf[3] / 2)
|
|
self:writeIEntry(lfi, table.unpack(lf))
|
|
local ni = self:nextEntry()
|
|
self:writeIEntry(ni, table.unpack(new))
|
|
self:setISize(math.max(ni,self.isize))
|
|
return self:findIEntry(ftypes.tfile,nil,nil,name)
|
|
-- return ni, new
|
|
else
|
|
new = {ftypes.tfile, lf[2], lf[3], name}
|
|
self:writeIEntry(lfi, table.unpack(new))
|
|
return lfi, new
|
|
end
|
|
return false
|
|
end
|
|
|
|
function proxy:relocateBlocks(start, len, dest, loud)
|
|
local ot = os.getTimeout()
|
|
os.setTimeout(0.00001) -- disable coroutine.yield's delay
|
|
io.write(loud and "\27[s" or "")
|
|
for i = 0, len-1 do
|
|
local buffer = self.d.readSector(start+i)
|
|
self.d.writeSector(dest+i, buffer)
|
|
io.write(loud and string.format("\27[u\27[2K%i/%i %i -> %i", i+1, len, start+i, dest+i) or "")
|
|
coroutine.yield()
|
|
end
|
|
io.write(loud and " done!\n" or "")
|
|
os.setTimeout(ot)
|
|
return true
|
|
end
|
|
function proxy:relocateFile(n,dest,loud)
|
|
local ft = {self:readIEntry(n)}
|
|
-- modify any writing handles to write to the destination before doing anything
|
|
local realLen
|
|
for k,v in pairs(self.handles) do
|
|
if v.ft[1] == n and (v.w or v.a) then
|
|
v.ft[2] = dest
|
|
realLen = v.currentSector
|
|
end
|
|
end
|
|
self:relocateBlocks(ft[2],realLen or ft[3],dest,loud)
|
|
local w, e = pcall(self.writeIEntry,self,n,ft[1],dest,ft[3],ft[4])
|
|
-- modify any reading handles to read from the destination after moving the data
|
|
for k,v in pairs(self.handles) do
|
|
if v.ft[1] == n and v.r then
|
|
v.ft[2] = dest
|
|
end
|
|
end
|
|
return w, {ft[1], dest, ft[3], ft[4]}
|
|
end
|
|
|
|
function proxy:defragment(loud)
|
|
while self.runningDefragment do
|
|
coroutine.yield()
|
|
end
|
|
self.runningDefragment = true
|
|
for i, tp, _, _, _ in self:allIEntries() do
|
|
if tp == ftypes.empty or tp == ftypes.dfile then
|
|
self:writeIEntry(i, ftypes.unused, 0, 0, "")
|
|
end
|
|
end
|
|
local ftab = {}
|
|
local fsec, lsec = 2, (self.capacity / self.blockSize) - math.ceil(self.isize / (self.blockSize / iesize))
|
|
for i, tp, st, sl, n in self:allIEntries() do
|
|
if tp ~= ftypes.empty and tp ~= ftypes.dfile and sl > 0 then
|
|
ftab[#ftab+1] = {i, tp, st, sl, n}
|
|
end
|
|
end
|
|
-- sort files in order of their start sector
|
|
table.sort(ftab, function(a,b)
|
|
return a[3] < b[3]
|
|
end)
|
|
-- move files closer to the start if applicable
|
|
local deferred = {}
|
|
for k, ft in ipairs(ftab) do
|
|
io.write(loud and string.format("%i/%i: %s\n",k,#ftab,ft[5]) or "")
|
|
if ft[3] ~= fsec then
|
|
local w, dt = self:relocateFile(ft[1], fsec, loud)
|
|
deferred[#deferred+1] = (not w and dt) or nil
|
|
end
|
|
fsec = fsec + ft[4]
|
|
end
|
|
self:writeIEntry(self:nextEntry(), ftypes.empty, fsec, lsec-fsec, "")
|
|
for k,v in ipairs(deferred) do
|
|
self:writeIEntry(self:nextEntry(), table.unpack(v))
|
|
end
|
|
self.runningDefragment = false
|
|
end
|
|
|
|
-- handle management
|
|
function proxy:openFile(name,mode)
|
|
name = fnormalize(name)
|
|
local handle = {}
|
|
for c in mode:gmatch(".") do
|
|
handle[c] = true
|
|
end
|
|
local fi,ft = self:findIEntry(ftypes.file, nil, nil, name)
|
|
if handle.r then
|
|
if not fi then
|
|
return false, "file not found"
|
|
end
|
|
handle.currentSector, handle.fi, handle.ft = 0, fi, ft
|
|
elseif handle.w or handle.a then
|
|
handle.writeBuffer, handle.writeCounter = "", 0
|
|
local oft = ft
|
|
fi, ft = self:allocateNew(name)
|
|
if oft and handle.a then
|
|
assert(ft[3] >= oft[3], "no extent large enough for relocated file")
|
|
handle.writeBuffer = self.d.readSector(oft[2] + oft[3] - 1):gsub("\0+$","")
|
|
handle.currentSector, handle.writeCounter = oft[3]-1, ((oft[3]-1) * 512) + #handle.writeBuffer
|
|
self:log(string.format("appending %s, CS: %i, WC: %i, WB: %i", name, handle.currentSector, handle.writeCounter, #handle.writeBuffer))
|
|
self:relocateBlocks(oft[2], oft[3], ft[2])
|
|
end
|
|
handle.fi, handle.ft, handle.maxWrite, handle.writeCounter, handle.currentSector = fi, ft, ft[3] * self.blockSize, handle.writeCounter or 0, handle.currentSector or 0
|
|
end
|
|
self.handles[#self.handles+1] = handle
|
|
return #self.handles
|
|
end
|
|
function proxy:closeHandle(h)
|
|
local handle = self.handles[h]
|
|
if handle.w or handle.a then
|
|
local ft = handle.ft
|
|
self.d.writeSector(ft[2] + handle.currentSector, handle.writeBuffer .. ("\0"):rep(self.blockSize - #handle.writeBuffer))
|
|
local fs = ft[3] - math.ceil(handle.writeCounter / self.blockSize)
|
|
local ni, nt = self:findIEntry(nil, ft[2] + ft[3])
|
|
if ni and (nt[1] == ftypes.empty or nt[1] == ftypes.dfile) then
|
|
fs = fs + nt[3]
|
|
else
|
|
ni = self:nextEntry()
|
|
end
|
|
while self:findIEntry(ftypes.file,nil,nil,ft[4]) do
|
|
local di, dt = self:findIEntry(ftypes.file, nil, nil, ft[4])
|
|
dt[1] = ftypes.dfile
|
|
self:writeIEntry(di, table.unpack(dt))
|
|
self:log(string.format("marking entry %i for %s as deleted", di, dt[4]))
|
|
end
|
|
ft[1], ft[3] = ftypes.file, math.ceil(handle.writeCounter / self.blockSize)
|
|
self:writeIEntry(handle.fi, table.unpack(ft))
|
|
self:writeIEntry(ni, ftypes.empty, ft[2] + ft[3], fs, "")
|
|
self:setISize(math.max(handle.fi,ni,self.isize))
|
|
end
|
|
self.handles[h] = nil
|
|
return true
|
|
end
|
|
|
|
-- handle I/O
|
|
function proxy:writeHandle(h,s)
|
|
local handle = self.handles[h]
|
|
if s:len() + handle.writeCounter > handle.maxWrite then
|
|
self:log("no space left in extent", syslog.error)
|
|
return false, "no space left in extent"
|
|
end
|
|
handle.writeCounter = handle.writeCounter + s:len()
|
|
handle.writeBuffer = handle.writeBuffer .. s
|
|
while #handle.writeBuffer > self.blockSize do
|
|
self.d.writeSector(handle.ft[2] + handle.currentSector, handle.writeBuffer:sub(1,self.blockSize))
|
|
handle.currentSector = handle.currentSector + 1
|
|
handle.writeBuffer = handle.writeBuffer:sub(self.blockSize+1)
|
|
end
|
|
return true
|
|
end
|
|
function proxy:readHandle(h,s)
|
|
local handle, rb = self.handles[h]
|
|
if handle.currentSector < handle.ft[3] - 1 then
|
|
local rb = self.d.readSector(handle.ft[2] + handle.currentSector)
|
|
elseif handle.currentSector == handle.ft[3] - 1 then
|
|
local rb = self.d.readSector(handle.ft[2] + handle.currentSector):gsub("\0+$","")
|
|
else
|
|
return nil
|
|
end
|
|
handle.currentSector = handle.currentSector + 1
|
|
return rb
|
|
end
|
|
|
|
-- primarily for debugging
|
|
function proxy:getHandle(h)
|
|
return self.handles[h]
|
|
end
|
|
|
|
----------
|
|
|
|
function rtfs.mount(d)
|
|
d=common.getProxy(d)
|
|
local p = setmetatable({}, {__index=proxy})
|
|
local magic, version, isize, label = string.unpack(sbformat,d.readSector(1))
|
|
assert(magic == "rtfs", "incorrect magic")
|
|
p.d = d
|
|
p.fstype = "rtfs"
|
|
p.cache, p.handles = {}, {}
|
|
p.cacheHits, p.cacheMisses = 0, 0
|
|
p.blockSize, p.capacity, p.label, p.isize = d.getSectorSize(), d.getCapacity(), label:gsub("\0",""), isize
|
|
|
|
-- FS proxy functions
|
|
function p.exists(name)
|
|
name = fnormalize(name)
|
|
for i, tp, st, sl, n in p:allIEntries() do
|
|
if (tp == ftypes.file or tp == ftypes.directory) and n == name then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
function p.isDirectory(name)
|
|
name = fnormalize(name)
|
|
return name == "" and true or p:findIEntry(ftypes.directory, nil, nil, name) ~= nil
|
|
end
|
|
function p.size(name)
|
|
name = fnormalize(name)
|
|
local fi, ft = p:findIEntry(ftypes.file, nil, nil, name)
|
|
if not fi and ft then return 0 end
|
|
return ft[3] * p.blockSize
|
|
end
|
|
function p.spaceUsed()
|
|
local c = 0
|
|
for i, tp, st, sl, n in p:allIEntries() do
|
|
if tp == ftypes.file then
|
|
c = c + sl
|
|
end
|
|
end
|
|
return c*p.blockSize
|
|
end
|
|
function p.spaceTotal()
|
|
return p.capacity - (math.ceil(p.isize / (p.blockSize / iesize))*p.blockSize) - p.blockSize
|
|
end
|
|
p.isReadOnly = p.d.isReadOnly
|
|
|
|
function p.list(name)
|
|
name = fnormalize(name)
|
|
local rt = {}
|
|
for i, tp, st, sl, n in p:allIEntries() do
|
|
local seg = fs.segments(n)
|
|
local pn = table.concat(seg,"/",1,#seg-1)
|
|
local fn = seg[#seg]
|
|
if (tp == ftypes.file or tp == ftypes.directory) and pn == name then
|
|
rt[#rt+1] = fn .. ((tp == ftypes.directory and "/") or "")
|
|
end
|
|
end
|
|
return rt
|
|
end
|
|
function p.makeDirectory(name)
|
|
name = fnormalize(name)
|
|
if #name < 1 or p:findIEntry(nil,nil,nil,name) then return false end
|
|
local seg = fs.segments(name)
|
|
for j = 1, #seg-1 do
|
|
p.makeDirectory(table.concat(seg, "/", 1, j))
|
|
end
|
|
if not p.isDirectory(table.concat(seg,"/",1,#seg-1)) then return false end
|
|
local ni = p:nextEntry()
|
|
p:writeIEntry(ni, ftypes.directory, 0, 0, name)
|
|
p:setISize(math.max(p.isize, ni))
|
|
return true
|
|
end
|
|
function p.remove(name)
|
|
name = fnormalize(name)
|
|
local fi, ft = p:findIEntry(ftypes.file, nil, nil, name)
|
|
if fi then
|
|
ft[1] = ftypes.dfile
|
|
p:writeIEntry(fi, table.unpack(ft))
|
|
end
|
|
local fi, ft = p:findIEntry(ftypes.directory, nil, nil, name)
|
|
if fi then
|
|
for _,f in ipairs(p.list(name)) do
|
|
p.remove(name.."/"..f)
|
|
end
|
|
p:writeIEntry(fi, ftypes.unused, 0, 0, "")
|
|
end
|
|
end
|
|
function p.rename(from, to)
|
|
from, to = fnormalize(from), fnormalize(to)
|
|
if p.exists(from) then
|
|
if p.exists(to) then
|
|
p.remove(to)
|
|
end
|
|
local fi, ft = p:findIEntry(p.isDirectory(name) and ftypes.directory or ftypes.file, nil, nil, to)
|
|
ft[4] = to
|
|
p:writeIEntry(fi, table.unpack(ft))
|
|
end
|
|
end
|
|
function p.lastModified()
|
|
return 0
|
|
end
|
|
|
|
function p.open(name, mode)
|
|
name, mode = fnormalize(name), mode or "r"
|
|
return p:openFile(name, mode)
|
|
end
|
|
function p.write(h,v)
|
|
return p:writeHandle(h,v)
|
|
end
|
|
function p.read(h,v)
|
|
return p:readHandle(h,v)
|
|
end
|
|
function p.close(h)
|
|
return p:closeHandle(h)
|
|
end
|
|
|
|
function p.getLabel()
|
|
return p.label
|
|
end
|
|
function p.setLabel(label)
|
|
p.label = label
|
|
p:updateSB()
|
|
end
|
|
return p
|
|
end
|
|
|
|
function rtfs.format(d,label)
|
|
d=common.getProxy(d)
|
|
print(d.address)
|
|
d.writeSector(1, string.pack(sbformat,"rtfs", 0, 1, label or ""))
|
|
rtfs.mount(d):writeIEntry(1, ftypes.empty, 2, (d.getCapacity() / d.getSectorSize()) - 2, "")
|
|
end
|
|
|
|
return rtfs
|