koreader-plugin/bookorbit.koplugin/bookorbit_transfer_progress.lua
Rama main · réplica de lectura del repositorio original.
--[[--
Progress channel between a transfer subprocess and its parent.
A forked child cannot call back into KOReader's UI, so a background transfer
publishes bounded progress snapshots to a generation-scoped file instead. The
parent polls that file from short scheduled callbacks.
Each snapshot is one short line published with an atomic rename, so the parent
never reads a half-written value, malformed content decodes to nothing, and a
snapshot stamped with another generation is ignored rather than displayed.
]]
local MAGIC = "bookorbit-progress"
local VERSION = 1
local MAX_SNAPSHOT_BYTES = 128
local MIN_INTERVAL = 1
local MIN_BYTES = 128 * 1024
local TransferProgress = {}
TransferProgress.MAX_SNAPSHOT_BYTES = MAX_SNAPSHOT_BYTES
function TransferProgress.encode(snapshot)
snapshot = snapshot or {}
return string.format("%s %d %d %d %d %d\n",
MAGIC,
VERSION,
math.max(0, math.floor(tonumber(snapshot.generation) or 0)),
math.max(0, math.floor(tonumber(snapshot.received) or 0)),
math.max(0, math.floor(tonumber(snapshot.total) or 0)),
snapshot.done and 1 or 0)
end
function TransferProgress.decode(text)
if type(text) ~= "string" or text == "" or #text > MAX_SNAPSHOT_BYTES then return nil end
local magic, version, generation, received, total, done =
text:match("^(%S+) (%d+) (%d+) (%d+) (%d+) (%d+)%s*$")
if magic ~= MAGIC or tonumber(version) ~= VERSION then return nil end
return {
generation = tonumber(generation),
received = tonumber(received),
total = tonumber(total),
done = done == "1",
}
end
local function defaultWrite(content, path)
local handle, err = io.open(path, "w")
if not handle then return nil, tostring(err or "open_failed") end
handle:write(content)
handle:close()
return true
end
local function defaultRead(path)
local handle = io.open(path, "r")
if not handle then return nil end
local content = handle:read(MAX_SNAPSHOT_BYTES + 1)
handle:close()
return content
end
-- Returns writer(received, done). Snapshots are throttled so a fast transfer
-- cannot turn into a write-per-chunk storm on slow device storage; completion
-- always publishes.
function TransferProgress.writer(path, opts)
opts = opts or {}
local generation = opts.generation or 0
local total = opts.total or 0
local now = opts.now or os.time
local write = opts.write or defaultWrite
local rename = opts.rename or os.rename
local remove = opts.remove or os.remove
local min_interval = opts.min_interval or MIN_INTERVAL
local min_bytes = opts.min_bytes or MIN_BYTES
local temporary = path .. ".tmp"
local last_at, last_bytes
return function(received, done)
received = received or 0
local at = now()
if not done and last_at ~= nil
and received - last_bytes < min_bytes
and at - last_at < min_interval then
return false
end
last_at, last_bytes = at, received
local content = TransferProgress.encode({
generation = generation,
received = received,
total = total,
done = done,
})
remove(temporary)
if not write(content, temporary) then return false end
if not rename(temporary, path) then
remove(temporary)
return false
end
return true
end
end
-- Returns the current snapshot, or nil when the file is absent, malformed or
-- stamped with a generation the caller no longer cares about.
function TransferProgress.read(path, opts)
opts = opts or {}
local snapshot = TransferProgress.decode((opts.read or defaultRead)(path))
if not snapshot then return nil end
if opts.generation ~= nil and snapshot.generation ~= opts.generation then return nil end
return snapshot
end
function TransferProgress.cleanup(path, opts)
opts = opts or {}
local remove = opts.remove or os.remove
remove(path .. ".tmp")
remove(path)
end
return TransferProgress