Compare commits
2
Commits
656505eda7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1c0fba4e | ||
|
|
957ae80836 |
@@ -849,6 +849,118 @@ function Bookhoard:getAnnotationStore()
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
-- ============================================================================
|
||||
-- Deletion propagation
|
||||
--
|
||||
-- The push is upsert-only: absence from the annotation arrays must never be
|
||||
-- read as a delete (a category toggled off in "What to sync" would otherwise
|
||||
-- wipe the server). Instead the sidecar remembers the dedup keys of every
|
||||
-- annotation the server has served us (bookhoard_known_keys). When one of
|
||||
-- those keys is no longer present in the local store, the user deleted it on
|
||||
-- this device, and the next push reports it via deleted_highlights /
|
||||
-- deleted_bookmarks so the server tombstones it (restorable from the web
|
||||
-- history). Keys are only ever learned from server pulls, so a device-native
|
||||
-- annotation can never be mis-flagged.
|
||||
-- ============================================================================
|
||||
|
||||
function Bookhoard:getKnownAnnotationKeys()
|
||||
if not self.ui.doc_settings then return nil end
|
||||
local known = self.ui.doc_settings:readSetting("bookhoard_known_keys")
|
||||
if type(known) ~= "table" then return nil end
|
||||
return known
|
||||
end
|
||||
|
||||
-- Record one dedup key of an annotation that exists locally (applied now or
|
||||
-- already present), so a later local absence can be reported as a deliberate
|
||||
-- deletion. kind: "highlight" or "bookmark" (device notes are highlights-
|
||||
-- with-notes server-side). Returns true when the sidecar changed.
|
||||
function Bookhoard:rememberAnnotationKey(kind, key)
|
||||
if not key or key == "" or not self.ui.doc_settings then return false end
|
||||
local known = self:getKnownAnnotationKeys() or {}
|
||||
if known[key] == kind then return false end
|
||||
known[key] = kind
|
||||
self.ui.doc_settings:saveSetting("bookhoard_known_keys", known)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Drop keys the server tombstoned: the deletion already happened server-side,
|
||||
-- so this device must never report it again.
|
||||
function Bookhoard:forgetAnnotationKeys(tombstones)
|
||||
if not tombstones or not self.ui.doc_settings then return false end
|
||||
local known = self:getKnownAnnotationKeys()
|
||||
if not known then return false end
|
||||
local changed = false
|
||||
for _, del in ipairs(tombstones) do
|
||||
local key = del.dedup_key
|
||||
if key and key ~= "" and known[key] ~= nil then
|
||||
known[key] = nil
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
self.ui.doc_settings:saveSetting("bookhoard_known_keys", known)
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
-- Diff the remembered keys against the local annotation store. Returns
|
||||
-- arrays of { dedup_key = key } entries for highlights (incl. notes) and
|
||||
-- bookmarks whose annotations vanished locally, or nil when there is nothing
|
||||
-- to report. Requires a readable local store: with none, local absence is
|
||||
-- meaningless and nothing may be flagged.
|
||||
function Bookhoard:collectDeletions()
|
||||
local model, entries = self:getAnnotationStore()
|
||||
if not model then return nil, nil end
|
||||
local known = self:getKnownAnnotationKeys()
|
||||
if not known or next(known) == nil then return nil, nil end
|
||||
|
||||
local present = {}
|
||||
for _, bm in ipairs(entries) do
|
||||
if bm.bookhoard_dedup_key then
|
||||
present[bm.bookhoard_dedup_key] = true
|
||||
end
|
||||
end
|
||||
|
||||
local deleted_highlights, deleted_bookmarks
|
||||
for key, kind in pairs(known) do
|
||||
if not present[key] then
|
||||
if kind == "bookmark" then
|
||||
deleted_bookmarks = deleted_bookmarks or {}
|
||||
table.insert(deleted_bookmarks, { dedup_key = key })
|
||||
else
|
||||
deleted_highlights = deleted_highlights or {}
|
||||
table.insert(deleted_highlights, { dedup_key = key })
|
||||
end
|
||||
end
|
||||
end
|
||||
return deleted_highlights, deleted_bookmarks
|
||||
end
|
||||
|
||||
-- After a successful push the flagged deletions are now tombstones on the
|
||||
-- server; drop their keys so they are not re-reported forever (the tombstone
|
||||
-- echo on the next pull would clear them too, this just avoids the wait).
|
||||
function Bookhoard:pruneKnownAnnotationKeys(deleted_highlights, deleted_bookmarks)
|
||||
if not self.ui.doc_settings then return end
|
||||
if not deleted_highlights and not deleted_bookmarks then return end
|
||||
local known = self:getKnownAnnotationKeys()
|
||||
if not known then return end
|
||||
local changed = false
|
||||
for _, list in ipairs({ deleted_highlights, deleted_bookmarks }) do
|
||||
if list then
|
||||
for _, entry in ipairs(list) do
|
||||
if known[entry.dedup_key] ~= nil then
|
||||
known[entry.dedup_key] = nil
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if changed then
|
||||
self.ui.doc_settings:saveSetting("bookhoard_known_keys", known)
|
||||
self.ui.doc_settings:flush()
|
||||
end
|
||||
end
|
||||
|
||||
function Bookhoard:collectAnnotations()
|
||||
local bookmarks = json_util.InitArray({})
|
||||
local highlights = json_util.InitArray({})
|
||||
@@ -1052,6 +1164,25 @@ function Bookhoard:_doUpdateProgress(interactive)
|
||||
book_data.notes = {}
|
||||
end
|
||||
|
||||
-- Report annotations deleted locally since the last pull. Gated per
|
||||
-- category exactly like the arrays above: a disabled category sends
|
||||
-- neither its annotations nor its deletions.
|
||||
local deleted_hl, deleted_bm = self:collectDeletions()
|
||||
if deleted_hl then
|
||||
if self.settings.sync_highlights then
|
||||
book_data.deleted_highlights = deleted_hl
|
||||
else
|
||||
deleted_hl = nil -- not sent: must not be pruned below
|
||||
end
|
||||
end
|
||||
if deleted_bm then
|
||||
if self.settings.sync_bookmarks then
|
||||
book_data.deleted_bookmarks = deleted_bm
|
||||
else
|
||||
deleted_bm = nil
|
||||
end
|
||||
end
|
||||
|
||||
local api = self:getAPI()
|
||||
local ok, result = api:syncProgress(book_data, self.settings.sync_mode)
|
||||
|
||||
@@ -1067,6 +1198,7 @@ function Bookhoard:_doUpdateProgress(interactive)
|
||||
end
|
||||
end
|
||||
end
|
||||
self:pruneKnownAnnotationKeys(deleted_hl, deleted_bm)
|
||||
if interactive then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Progress has been pushed."),
|
||||
@@ -1186,17 +1318,36 @@ function Bookhoard:_linkBookThenPull(interactive)
|
||||
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,
|
||||
})
|
||||
-- Two very different failures share this status. The server's
|
||||
-- resolve handler answers a book miss with JSON
|
||||
-- {"error": "book not found"}; anything else behind a 404
|
||||
-- (an HTML error page) means the route itself is missing —
|
||||
-- a server image predating the resolve endpoint, a reverse
|
||||
-- proxy, or a wrong server URL. Only the former may advise
|
||||
-- pushing to link: acting on a stale-server 404 would push
|
||||
-- this device's first-page position over the server's real
|
||||
-- progress — the very conflict this flow exists to prevent.
|
||||
if result.error == "book not found" 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
|
||||
else
|
||||
self:_bootstrapUUIDThenPull(interactive)
|
||||
logger.warn("Bookhoard: resolve endpoint returned 404 without a book-not-found body (stale server?)")
|
||||
if interactive then
|
||||
UIManager:show(InfoMessage:new{
|
||||
text = _("Your Bookhoard server does not support this request and may be outdated. Update the server and try again."),
|
||||
timeout = 5,
|
||||
})
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -1416,9 +1567,22 @@ function Bookhoard:applyServerAnnotations(annotations)
|
||||
if not isValidPos0(pos0) then return end
|
||||
local srv_text = server_entry.text or ""
|
||||
local srv_notes = server_entry.notes or ""
|
||||
local srv_key = server_entry.dedup_key
|
||||
-- Annotations that exist locally (after this call) get their key
|
||||
-- remembered so a later deletion can be reported. Unplaceable
|
||||
-- locators return above and are never tracked — absence of an
|
||||
-- annotation this device never held is not a deletion.
|
||||
local kind = has_text and "highlight" or "bookmark"
|
||||
local idx = findLocal(server_entry)
|
||||
if idx then
|
||||
local bm = entries[idx]
|
||||
-- Legacy pos0 match on a device-native entry: stamp the key so
|
||||
-- identity matching and deletion tracking work from now on.
|
||||
if srv_key and srv_key ~= "" and not bm.bookhoard_dedup_key then
|
||||
bm.bookhoard_dedup_key = srv_key
|
||||
changed = true
|
||||
end
|
||||
changed = self:rememberAnnotationKey(kind, srv_key) or changed
|
||||
local cur_text, cur_note = getFields(bm)
|
||||
if cur_text ~= srv_text or cur_note ~= srv_notes then
|
||||
setFields(bm, srv_text, srv_notes)
|
||||
@@ -1464,6 +1628,7 @@ function Bookhoard:applyServerAnnotations(annotations)
|
||||
end
|
||||
local index = self.ui.annotation:addItem(entry)
|
||||
notify({ entry, index_modified = index })
|
||||
changed = self:rememberAnnotationKey(kind, srv_key) or changed
|
||||
changed = true
|
||||
else
|
||||
local entry = {
|
||||
@@ -1485,6 +1650,7 @@ function Bookhoard:applyServerAnnotations(annotations)
|
||||
end
|
||||
if server_entry.chapter and server_entry.chapter ~= "" then entry.chapter = server_entry.chapter end
|
||||
table.insert(entries, entry)
|
||||
changed = self:rememberAnnotationKey(kind, srv_key) or changed
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
@@ -1533,6 +1699,7 @@ function Bookhoard:applyServerAnnotations(annotations)
|
||||
end
|
||||
|
||||
if annotations.deleted_highlights then
|
||||
changed = self:forgetAnnotationKeys(annotations.deleted_highlights) or changed
|
||||
for _, del in ipairs(annotations.deleted_highlights) do
|
||||
local idx = findLocalByTombstone(del, true)
|
||||
local bm = idx and entries[idx]
|
||||
@@ -1546,6 +1713,7 @@ function Bookhoard:applyServerAnnotations(annotations)
|
||||
end
|
||||
|
||||
if annotations.deleted_bookmarks then
|
||||
changed = self:forgetAnnotationKeys(annotations.deleted_bookmarks) or changed
|
||||
for _, del in ipairs(annotations.deleted_bookmarks) do
|
||||
local idx = findLocalByTombstone(del, false)
|
||||
if idx then
|
||||
|
||||
Reference in New Issue
Block a user