mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
a72ca1155a
Add the KOReader catalog surfaces, plugin package metadata, and device progress handling needed for the plugin workflow. Keep progress status updates non-blocking so secondary status failures do not reject successful sync writes.
112 lines
4.2 KiB
Lua
112 lines
4.2 KiB
Lua
--[[--
|
|
Self-update support for the BookOrbit KOReader plugin.
|
|
|
|
Compares semver strings and, when an update is confirmed by the user, downloads
|
|
the plugin zip from the BookOrbit server and atomically replaces the current
|
|
plugin directory. KOReader must be restarted for the new files to load.
|
|
]]
|
|
|
|
local Device = require("device")
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
|
|
local BookOrbitUpdater = {}
|
|
|
|
-- Returns true if `candidate` is strictly newer than `current` (semver, optional "v" prefix).
|
|
function BookOrbitUpdater.isNewer(candidate, current)
|
|
local function parse(v)
|
|
if type(v) ~= "string" then return nil end
|
|
local a, b, c = v:match("^v?(%d+)%.(%d+)%.(%d+)")
|
|
if not a then return nil end
|
|
return { tonumber(a), tonumber(b), tonumber(c) }
|
|
end
|
|
local c = parse(candidate)
|
|
local cur = parse(current)
|
|
if not c or not cur then return false end
|
|
if c[1] ~= cur[1] then return c[1] > cur[1] end
|
|
if c[2] ~= cur[2] then return c[2] > cur[2] end
|
|
return c[3] > cur[3]
|
|
end
|
|
|
|
-- Wraps a path in POSIX single quotes, escaping any embedded single quotes.
|
|
local function sq(path)
|
|
return "'" .. path:gsub("'", "'\\''") .. "'"
|
|
end
|
|
|
|
-- Downloads the plugin zip from the server and atomically replaces `plugin_dir`.
|
|
--
|
|
-- Strategy: extract into a staging directory, backup the current plugin dir,
|
|
-- rename the new one into place, then clean up. On any failure after the backup
|
|
-- the original is restored so the user always has a working plugin.
|
|
--
|
|
-- `api` BookOrbitApi instance (must be logged in)
|
|
-- `plugin_dir` Absolute path of the running plugin directory
|
|
-- `progress_cb` Optional function(bytes_received) called during download
|
|
--
|
|
-- Returns true on success, or nil + error string on failure.
|
|
function BookOrbitUpdater.apply(api, plugin_dir, progress_cb)
|
|
local dir = plugin_dir:gsub("/+$", "")
|
|
local parent_dir = dir:match("^(.*)/[^/]+$")
|
|
local plugin_name = dir:match("([^/]+)$")
|
|
|
|
if not parent_dir or parent_dir == "" or not plugin_name then
|
|
return nil, "cannot determine plugin parent directory"
|
|
end
|
|
|
|
local tmp_zip = parent_dir .. "/bookorbit-update.zip"
|
|
local staging = parent_dir .. "/" .. plugin_name .. ".update"
|
|
local backup = parent_dir .. "/" .. plugin_name .. ".bak"
|
|
|
|
-- Remove any leftover staging dir from a previous failed attempt.
|
|
os.execute("rm -rf " .. sq(staging))
|
|
|
|
local ok, err = api:downloadPluginUpdate(tmp_zip, progress_cb)
|
|
if not ok then
|
|
return nil, tostring(err or "download failed")
|
|
end
|
|
|
|
if not lfs.mkdir(staging) and lfs.attributes(staging, "mode") ~= "directory" then
|
|
os.remove(tmp_zip)
|
|
return nil, "could not create update staging directory"
|
|
end
|
|
|
|
-- Extract into a staging directory so a partial unpack never touches the
|
|
-- live plugin directory. Use KOReader's archive helper instead of the
|
|
-- platform unzip command: unzip warning exit codes vary by platform.
|
|
local ok_unpack, unpack_err = Device:unpackArchive(tmp_zip, staging, true)
|
|
os.remove(tmp_zip)
|
|
|
|
if not ok_unpack then
|
|
os.execute("rm -rf " .. sq(staging))
|
|
return nil, tostring(unpack_err or "archive extraction failed")
|
|
end
|
|
|
|
-- The zip must contain the plugin folder as its top-level entry. We strip
|
|
-- that root during extraction, so the staging directory should now look
|
|
-- like the plugin directory itself.
|
|
if lfs.attributes(staging .. "/main.lua", "mode") ~= "file" then
|
|
os.execute("rm -rf " .. sq(staging))
|
|
return nil, "update zip does not contain expected plugin files: " .. plugin_name
|
|
end
|
|
|
|
-- Atomic-ish swap (all paths share the same filesystem):
|
|
-- 1. backup current plugin
|
|
-- 2. move new plugin into place
|
|
-- 3. clean up backup and staging
|
|
os.execute("rm -rf " .. sq(backup))
|
|
if not os.rename(dir, backup) then
|
|
os.execute("rm -rf " .. sq(staging))
|
|
return nil, "could not create plugin backup"
|
|
end
|
|
if not os.rename(staging, dir) then
|
|
-- Restore backup so the plugin remains usable.
|
|
os.rename(backup, dir)
|
|
os.execute("rm -rf " .. sq(staging))
|
|
return nil, "could not install updated plugin"
|
|
end
|
|
|
|
os.execute("rm -rf " .. sq(backup) .. " " .. sq(staging))
|
|
return true
|
|
end
|
|
|
|
return BookOrbitUpdater
|