From 820a39c8f73e109314f33f63e384302dd72dcabb Mon Sep 17 00:00:00 2001 From: Zijie He Date: Fri, 29 Jul 2016 17:38:02 -0700 Subject: [PATCH] Update KOSyncClient --- frontend/document/document.lua | 37 ++++-- frontend/random.lua | 33 +++++ plugins/kosync.koplugin/KOSyncClient.lua | 19 ++- plugins/kosync.koplugin/api.json | 2 + plugins/kosync.koplugin/main.lua | 153 +++++++++++++++++------ reader.lua | 5 + spec/unit/random_spec.lua | 33 +++++ 7 files changed, 227 insertions(+), 55 deletions(-) create mode 100644 frontend/random.lua create mode 100644 spec/unit/random_spec.lua diff --git a/frontend/document/document.lua b/frontend/document/document.lua index 2577bad3e..793e0a3f2 100644 --- a/frontend/document/document.lua +++ b/frontend/document/document.lua @@ -99,7 +99,10 @@ function Document:discardChange() self.is_edited = false end --- calculate partial digest of the document +-- calculate partial digest of the document and store in its docsettings to avoid document saving +-- feature to change its checksum. +-- +-- To the calculating mechanism itself. -- since only PDF documents could be modified by KOReader by appending data -- at the end of the files when highlighting, we use a non-even sampling -- algorithm which samples with larger weight at file head and much smaller @@ -109,23 +112,31 @@ end -- 1048576, 4194304, 16777216, 67108864, 268435456 or 1073741824, appending data -- by highlighting in KOReader may change the digest value. function Document:fastDigest() - local md5 = require("ffi/MD5") - local lshift = bit.lshift + if not self.file then return end local file = io.open(self.file, 'rb') if file then - local step, size = 1024, 1024 - local m = md5.new() - for i = -1, 10 do - file:seek("set", lshift(step, 2*i)) - local sample = file:read(size) - if sample then - m:update(sample) - else - break + local docsettings = require("docsettings"):open(self.file) + local result = docsettings:readSetting("partial_md5_checksum") + if not result then + local md5 = require("ffi/MD5") + local lshift = bit.lshift + local step, size = 1024, 1024 + local m = md5.new() + for i = -1, 10 do + file:seek("set", lshift(step, 2*i)) + local sample = file:read(size) + if sample then + m:update(sample) + else + break + end end + result = m:sum() + docsettings:saveSetting("partial_md5_checksum", result) end + docsettings:close() file:close() - return m:sum() + return result end end diff --git a/frontend/random.lua b/frontend/random.lua new file mode 100644 index 000000000..954a3214c --- /dev/null +++ b/frontend/random.lua @@ -0,0 +1,33 @@ +-- A set of functions to extend math.random and math.randomseed. + +local random = {} + +-- Use current time as seed to randomlize. +function random.seed() + math.randomseed(os.time()) +end + +random.seed() + +-- Return a UUID (v4, random). +function random.uuid(with_dash) + local array = {} + for i = 1, 16 do + table.insert(array, math.random(256) - 1) + end + -- The 13th character should be 4. + array[7] = bit.band(array[7], 79) + array[7] = bit.bor(array[7], 64) + -- The 17th character should be 8 / 9 / a / b. + array[9] = bit.band(array[9], 191) + array[9] = bit.bor(array[9], 128) + if with_dash then + return string.format("%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X", + unpack(array)) + else + return string.format("%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", + unpack(array)) + end +end + +return random diff --git a/plugins/kosync.koplugin/KOSyncClient.lua b/plugins/kosync.koplugin/KOSyncClient.lua index 5b6afc93d..67e9ac8e9 100644 --- a/plugins/kosync.koplugin/KOSyncClient.lua +++ b/plugins/kosync.koplugin/KOSyncClient.lua @@ -93,8 +93,15 @@ function KOSyncClient:authorize(username, password) end end -function KOSyncClient:update_progress(username, password, - document, progress, percentage, device, callback) +function KOSyncClient:update_progress( + username, + password, + document, + progress, + percentage, + device, + device_id, + callback) self.client:reset_middlewares() self.client:enable('Format.JSON') self.client:enable("GinClient") @@ -109,6 +116,7 @@ function KOSyncClient:update_progress(username, password, progress = progress, percentage = percentage, device = device, + device_id = device_id, }) end) if ok then @@ -123,8 +131,11 @@ function KOSyncClient:update_progress(username, password, if UIManager.looper then UIManager:setInputTimeout() end end -function KOSyncClient:get_progress(username, password, - document, callback) +function KOSyncClient:get_progress( + username, + password, + document, + callback) self.client:reset_middlewares() self.client:enable('Format.JSON') self.client:enable("GinClient") diff --git a/plugins/kosync.koplugin/api.json b/plugins/kosync.koplugin/api.json index 3db2673cb..dfe9cce12 100644 --- a/plugins/kosync.koplugin/api.json +++ b/plugins/kosync.koplugin/api.json @@ -28,12 +28,14 @@ "progress", "percentage", "device", + "device_id", ], "payload" : [ "document", "progress", "percentage", "device", + "device_id", ], "expected_status" : [200, 202, 401] }, diff --git a/plugins/kosync.koplugin/main.lua b/plugins/kosync.koplugin/main.lua index c697c8115..1d3345a4c 100644 --- a/plugins/kosync.koplugin/main.lua +++ b/plugins/kosync.koplugin/main.lua @@ -6,7 +6,7 @@ local DocSettings = require("docsettings") local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") local Screen = require("device").screen -local Device = require("device") +local DeviceModel = require("device").model local Event = require("ui/event") local Math = require("optmath") local DEBUG = require("dbg") @@ -14,12 +14,6 @@ local T = require("ffi/util").template local _ = require("gettext") local md5 = require("ffi/MD5") -local l10n = { - _("Unknown server error."), - _("Unauthorized"), - _("Username is already registered."), -} - local KOSync = InputContainer:new{ name = "kosync", title = _("Register/login to KOReader server"), @@ -31,12 +25,18 @@ function KOSync:init() self.kosync_username = settings.username self.kosync_userkey = settings.userkey self.kosync_auto_sync = not (settings.auto_sync == false) + self.kosync_device_id = G_reader_settings:readSetting("device_id") + assert(self.kosync_device_id) self.ui:registerPostInitCallback(function() if self.kosync_auto_sync then UIManager:scheduleIn(1, function() self:getProgress() end) end end) self.ui.menu:registerToMainMenu(self) + -- Make sure checksum has been calculated at the very first time a document has been opened, to + -- avoid document saving feature to impact the checksum, and eventually impact the document + -- identity in the progress sync feature. + self.view.document:fastDigest() end function KOSync:addToMainMenu(tab_item_table) @@ -55,19 +55,36 @@ function KOSync:addToMainMenu(tab_item_table) end, }, { - text = _("Auto sync"), + text = _("Auto sync now and future"), checked_func = function() return self.kosync_auto_sync end, callback = function() self.kosync_auto_sync = not self.kosync_auto_sync + if self.kosync_auto_sync then + -- since we will update the progress when closing document, we should pull + -- current progress now to avoid to overwrite it silently. + self:getProgress(true) + else + -- since we won't update the progress when closing document, we should push + -- current progress now to avoid to lose it silently. + self:updateProgress(true) + end end, }, { - text = _("Sync now"), + text = _("Push progress from this device"), + enabled_func = function() + return self.kosync_userkey ~= nil + end, + callback = function() + self:updateProgress(true) + end, + }, + { + text = _("Pull progress from other devices"), enabled_func = function() return self.kosync_userkey ~= nil end, callback = function() - self:updateProgress() self:getProgress(true) end, }, @@ -225,11 +242,15 @@ function KOSync:logout() self:onSaveSettings() end +local function roundPercent(percent) + return math.floor(percent * 10000) / 10000 +end + function KOSync:getLastPercent() if self.ui.document.info.has_pages then - return self.ui.paging:getLastPercent() + return roundPercent(self.ui.paging:getLastPercent()) else - return self.ui.rolling:getLastPercent() + return roundPercent(self.ui.rolling:getLastPercent()) end end @@ -250,7 +271,21 @@ function KOSync:syncToProgress(progress) end end -function KOSync:updateProgress() +local function promptLogin() + UIManager:show(InfoMessage:new{ + text = _("Please register / login before using progress synchronization feature."), + timeout = 3, + }) +end + +local function showSyncError() + UIManager:show(InfoMessage:new{ + text = _("Something went wrong when syncing progress, please check your network connection and try again later."), + timeout = 3, + }) +end + +function KOSync:updateProgress(manual) if self.kosync_username and self.kosync_userkey then local KOSyncClient = require("KOSyncClient") local client = KOSyncClient:new{ @@ -260,15 +295,34 @@ function KOSync:updateProgress() local doc_digest = self.view.document:fastDigest() local progress = self:getLastProgress() local percentage = self:getLastPercent() - local ok, err = pcall(client.update_progress, client, - self.kosync_username, self.kosync_userkey, - doc_digest, progress, percentage, Device.model, + local ok, err = pcall(client.update_progress, + client, + self.kosync_username, + self.kosync_userkey, + doc_digest, + progress, + percentage, + DeviceModel, + self.kosync_device_id, function(ok, body) DEBUG("update progress for", self.view.document.file, ok) + if manual then + if ok then + UIManager:show(InfoMessage:new{ + text = _("Progress has been pushed."), + timeout = 3, + }) + else + showSyncError() + end + end end) - if not ok and err then - DEBUG("err:", err) + if not ok then + if manual then showSyncError() end + if err then DEBUG("err:", err) end end + elseif manual then + promptLogin() end end @@ -280,34 +334,57 @@ function KOSync:getProgress(manual) service_spec = self.path .. "/api.json" } local doc_digest = self.view.document:fastDigest() - local ok, err = pcall(client.get_progress, client, - self.kosync_username, self.kosync_userkey, - doc_digest, function(ok, body) + local ok, err = pcall(client.get_progress, + client, + self.kosync_username, + self.kosync_userkey, + doc_digest, + function(ok, body) DEBUG("get progress for", self.view.document.file, ok, body) - if body and body.percentage then - local progress = self:getLastProgress() - local percentage = self:getLastPercent() - DEBUG("current progress", percentage) - if body.percentage > percentage - and tostring(body.progress) ~= tostring(progress) then - UIManager:show(ConfirmBox:new{ - text = T(_("Sync to furthest location %1% from device '%2'?"), - Math.round(body.percentage*100), body.device), - ok_callback = function() - self:syncToProgress(body.progress) - end, - }) - elseif manual and body.progress == progress then + if body then + if body.percentage then + if body.device ~= DeviceModel + or body.device_id ~= self.kosync_device_id then + body.percentage = roundPercent(body.percentage) + local progress = self:getLastProgress() + local percentage = self:getLastPercent() + DEBUG("current progress", percentage) + if body.percentage > percentage and body.progress ~= progress then + UIManager:show(ConfirmBox:new{ + text = T(_("Sync to furthest location %1% from device '%2'?"), + Math.round(body.percentage*100), body.device), + ok_callback = function() + self:syncToProgress(body.progress) + end, + }) + elseif manual then + UIManager:show(InfoMessage:new{ + text = _("Already synchronized."), + timeout = 3, + }) + end + elseif manual then + UIManager:show(InfoMessage:new{ + text = _("Latest progress is coming from this device."), + timeout = 3, + }) + end + elseif manual then UIManager:show(InfoMessage:new{ - text = _("Already synchronized."), + text = _("No progress found for this document."), timeout = 3, }) end + elseif manual then + showSyncError() end end) - if not ok and err then - DEBUG("err:", err) + if not ok then + if manual then showSyncError() end + if err then DEBUG("err:", err) end end + elseif manual then + promptLogin() end end diff --git a/reader.lua b/reader.lua index ffe5adb82..eb3177475 100755 --- a/reader.lua +++ b/reader.lua @@ -96,6 +96,11 @@ local lfs = require("libs/libkoreader-lfs") local UIManager = require("ui/uimanager") local Device = require("device") local Font = require("ui/font") +local random = require("random") + +if not G_reader_settings:readSetting("device_id") then + G_reader_settings:saveSetting("device_id", random.uuid()) +end -- read some global reader setting here: -- font diff --git a/spec/unit/random_spec.lua b/spec/unit/random_spec.lua new file mode 100644 index 000000000..617bebf2d --- /dev/null +++ b/spec/unit/random_spec.lua @@ -0,0 +1,33 @@ +describe("random package tests", function() + local random + + local function is_magic_char(c) + return c == "8" or c == "9" or c == "A" or c == "B" + end + + setup(function() + random = require("frontend/random") + end) + + it("should generate uuid without dash", function() + for i = 1, 10000 do + local uuid = random.uuid() + assert.Equals(uuid:len(), 32) + assert.Equals(uuid:sub(13, 13), "4") + assert.is_true(is_magic_char(uuid:sub(17, 17))) + end + end) + + it("should generate uuid with dash", function() + for i = 1, 10000 do + local uuid = random.uuid(true) + assert.Equals(uuid:len(), 36) + assert.Equals(uuid:sub(9, 9), "-") + assert.Equals(uuid:sub(14, 14), "-") + assert.Equals(uuid:sub(19, 19), "-") + assert.Equals(uuid:sub(24, 24), "-") + assert.Equals(uuid:sub(15, 15), "4") + assert.is_true(is_magic_char(uuid:sub(20, 20))) + end + end) +end)