fix(sync): link fresh books via read-only SHA-256 resolve before first pull
Downloading a book from OPDS and opening it for the first time left the plugin without a cached bookhoard UUID, so it bootstrapped the book's identity by pushing once. That push carried the device's first-page position, which the server stored as a real progress update for a book that could be mid-read from another source — tripping the cross-source conflict detector and clobbering genuine progress before the first pull could deliver it. Until the conflict was resolved on the web, the device could not pull the correct progress at all. - onReaderReady now always pulls first; a pull no longer delegates to a push to learn the book's identity - new _linkBookThenPull: resolves the UUID with a read-only GET /api/sync/koreader/resolve?sha256= call (BookhoardAPI:resolveBook), caches it in the document sidecar, then performs the pull — the device's position is never transmitted - the legacy push-bootstrap survives only as a background fallback for books the server does not know by hash (404, sideloaded); interactive pulls are told to push explicitly instead Requires the server-side resolve endpoint (bookhoard feat(sync): add read-only KOReader book resolve endpoint).
This commit is contained in:
@@ -126,6 +126,13 @@ function BookhoardAPI:syncProgress(book_data, sync_mode)
|
|||||||
}, PROGRESS_TIMEOUTS)
|
}, PROGRESS_TIMEOUTS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Read-only SHA-256 → book UUID lookup. Used to link a freshly downloaded
|
||||||
|
-- book WITHOUT pushing progress (a push would transmit the device's
|
||||||
|
-- first-page position and clobber/conflict with real server progress).
|
||||||
|
function BookhoardAPI:resolveBook(sha256)
|
||||||
|
return self:_request("GET", "/api/sync/koreader/resolve?sha256=" .. sha256, nil, PROGRESS_TIMEOUTS)
|
||||||
|
end
|
||||||
|
|
||||||
function BookhoardAPI:getMetadata(uuid)
|
function BookhoardAPI:getMetadata(uuid)
|
||||||
return self:_request("GET", "/api/sync/koreader/metadata/" .. uuid, nil, PROGRESS_TIMEOUTS)
|
return self:_request("GET", "/api/sync/koreader/metadata/" .. uuid, nil, PROGRESS_TIMEOUTS)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -121,17 +121,13 @@ end
|
|||||||
function Bookhoard:onReaderReady()
|
function Bookhoard:onReaderReady()
|
||||||
if self.settings.auto_sync then
|
if self.settings.auto_sync then
|
||||||
UIManager:nextTick(function()
|
UIManager:nextTick(function()
|
||||||
-- On the first open of a freshly downloaded book there is no cached
|
-- Pull-first on every open. A freshly downloaded book has no cached
|
||||||
-- bookhoard UUID yet (it is only learned from a successful push
|
-- bookhoard UUID yet; getProgress resolves it by SHA-256 with a
|
||||||
-- response). Pulling now would just fail with "Push progress first".
|
-- read-only lookup (no progress push). The previous push-to-
|
||||||
-- Instead, push once to bootstrap identity: the server resolves the
|
-- bootstrap strategy transmitted the device's first-page position
|
||||||
-- book by SHA-256 (format-aware) and returns the UUID, which we then
|
-- to the server on books already mid-read elsewhere, creating a
|
||||||
-- cache. After that, push and pull both work without ordering.
|
-- sync conflict before the first pull could ever happen.
|
||||||
if not self:getBookhoardUUID() then
|
self:getProgress(true, false)
|
||||||
self:updateProgress(true, false)
|
|
||||||
else
|
|
||||||
self:getProgress(true, false)
|
|
||||||
end
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
self:registerEvents()
|
self:registerEvents()
|
||||||
@@ -1116,9 +1112,70 @@ function Bookhoard:getProgress(ensure_networking, interactive)
|
|||||||
self.pull_timestamp = now
|
self.pull_timestamp = now
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Establish the bookhoard UUID for the current document by pushing once, then
|
-- Link the current document to Bookhoard WITHOUT pushing progress, then run
|
||||||
-- (once cached) perform the originally-requested pull. Used when a pull is
|
-- the pull. The UUID is learned via a read-only SHA-256 resolve; only when
|
||||||
-- requested before any push has run on a newly downloaded book.
|
-- the server does not know the hash at all (sideloaded book) do we fall back
|
||||||
|
-- to the legacy push-bootstrap, and only for background (non-interactive)
|
||||||
|
-- flows — a user-initiated pull must never push this device's position.
|
||||||
|
function Bookhoard:_linkBookThenPull(interactive)
|
||||||
|
local file_sha256 = self:getFileSHA256()
|
||||||
|
if not file_sha256 then
|
||||||
|
if interactive then
|
||||||
|
UIManager:show(InfoMessage:new{
|
||||||
|
text = _("Could not read this file to link it with Bookhoard."),
|
||||||
|
timeout = 3,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
UIManager:scheduleIn(0.5, function()
|
||||||
|
local api = self:getAPI()
|
||||||
|
local ok, result = api:resolveBook(file_sha256)
|
||||||
|
|
||||||
|
UIManager:nextTick(function()
|
||||||
|
if not self.ui.doc_settings then return end
|
||||||
|
|
||||||
|
if ok and type(result) == "table" and result.book_uuid then
|
||||||
|
self.ui.doc_settings:saveSetting("bookhoard_uuid", result.book_uuid)
|
||||||
|
self.ui.doc_settings:flush()
|
||||||
|
logger.dbg("Bookhoard: linked book by SHA-256 to", result.book_uuid)
|
||||||
|
self:_doGetProgress(interactive)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if result and result.status == 404 then
|
||||||
|
-- Genuinely unknown to the server (sideloaded book). The
|
||||||
|
-- legacy push-bootstrap only ever linked such books via fuzzy
|
||||||
|
-- title/author matching; keep that as a background attempt,
|
||||||
|
-- but tell interactive users to push explicitly.
|
||||||
|
if interactive then
|
||||||
|
UIManager:show(InfoMessage:new{
|
||||||
|
text = _("This book is not in your Bookhoard library yet. Push progress to link it."),
|
||||||
|
timeout = 4,
|
||||||
|
})
|
||||||
|
else
|
||||||
|
self:_bootstrapUUIDThenPull(interactive)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
logger.warn("Bookhoard: failed to resolve book by SHA-256")
|
||||||
|
if interactive then
|
||||||
|
UIManager:show(InfoMessage:new{
|
||||||
|
text = _("Could not reach Bookhoard. Check your network connection and try again."),
|
||||||
|
timeout = 4,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Legacy fallback: establish the bookhoard UUID for the current document by
|
||||||
|
-- pushing once, then (once cached) perform the originally-requested pull.
|
||||||
|
-- Only reached from background flows when the SHA-256 resolve reported the
|
||||||
|
-- book as unknown to the server; the push mainly serves fuzzy title/author
|
||||||
|
-- linking and device alias registration for sideloaded books.
|
||||||
function Bookhoard:_bootstrapUUIDThenPull(interactive)
|
function Bookhoard:_bootstrapUUIDThenPull(interactive)
|
||||||
self:updateProgress(false, false)
|
self:updateProgress(false, false)
|
||||||
UIManager:scheduleIn(2, function()
|
UIManager:scheduleIn(2, function()
|
||||||
|
|||||||
Reference in New Issue
Block a user