add simple sync service as a plugin

The 'KOSync' plugin will synchronize furthest reading progress
across different koreader devices after users registering their
devices.

The synchronizing service is open-sourced as the project
[koreader/koreader-sync-server](https://github.com/koreader/koreader-sync-server).
pull/1448/head
chrox 9 years ago
parent 9ab6224963
commit d08e22ec2e

@ -22,6 +22,6 @@ indent_size = 8
indent_style = tab
indent_size = 8
[*.{js,json,css,scss,sass,html,handlebars,tpl}]
[*.{js,css,scss,sass,html,handlebars,tpl}]
indent_style = space
indent_size = 2

@ -131,10 +131,20 @@ end
function ReaderPaging:onSaveSettings()
self.ui.doc_settings:saveSetting("page_positions", self.page_positions)
self.ui.doc_settings:saveSetting("last_page", self:getTopPage())
self.ui.doc_settings:saveSetting("percent_finished", self.current_page/self.number_of_pages)
self.ui.doc_settings:saveSetting("percent_finished", self:getLastPercent())
self.ui.doc_settings:saveSetting("show_overlap_enable", self.show_overlap_enable)
end
function ReaderPaging:getLastProgress()
return self:getTopPage()
end
function ReaderPaging:getLastPercent()
if self.current_page > 0 and self.number_of_pages > 0 then
return self.current_page/self.number_of_pages
end
end
function ReaderPaging:addToMainMenu(tab_item_table)
-- FIXME: repeated code with page overlap menu for readerrolling
-- needs to keep only one copy of the logic as for the DRY principle.

@ -208,6 +208,10 @@ function ReaderRolling:onSaveSettings()
self.ui.doc_settings:saveSetting("show_overlap_enable", self.show_overlap_enable)
end
function ReaderRolling:getLastProgress()
return self.xpointer
end
function ReaderRolling:addToMainMenu(tab_item_table)
-- FIXME: repeated code with page overlap menu for readerpaging
-- needs to keep only one copy of the logic as for the DRY principle.

@ -368,7 +368,8 @@ function ReaderUI:closeDocument()
self.document = nil
end
function ReaderUI:onCloseDocument()
function ReaderUI:notifyCloseDocument()
self:handleEvent(Event:new("CloseDocument"))
if self.document:isEdited() then
UIManager:show(ConfirmBox:new{
text = _("Do you want to save this document?"),
@ -392,7 +393,7 @@ function ReaderUI:onClose()
self:saveSettings()
if self.document ~= nil then
DEBUG("closing document")
self:onCloseDocument()
self:notifyCloseDocument()
end
UIManager:close(self.dialog, "full")
-- serialize last used items for later launch

@ -2,7 +2,6 @@ local UIManager = require("ui/uimanager")
local DEBUG = require("dbg")
local HTTPClient = {
headers = {},
input_timeouts = 0,
INPUT_TIMEOUT = 100*1000,
}
@ -14,20 +13,7 @@ function HTTPClient:new()
return o
end
function HTTPClient:addHeader(header, value)
self.headers[header] = value
end
function HTTPClient:removeHeader(header)
self.headers[header] = nil
end
function HTTPClient:request(request, response_callback)
request.on_headers = function(headers)
for header, value in pairs(self.headers) do
headers[header] = value
end
end
request.connect_timeout = 10
request.request_timeout = 20
UIManager:initLooper()

@ -0,0 +1,150 @@
local UIManager = require("ui/uimanager")
local DEBUG = require("dbg")
local KOSyncClient = {
service_spec = nil,
}
function KOSyncClient:new(o)
local o = o or {}
setmetatable(o, self)
self.__index = self
if o.init then o:init() end
return o
end
function KOSyncClient:init()
local Spore = require("Spore")
self.client = Spore.new_from_spec(self.service_spec)
package.loaded['Spore.Middleware.GinClient'] = {}
require('Spore.Middleware.GinClient').call = function(self, req)
req.headers['accept'] = "application/vnd.koreader.v1+json"
end
package.loaded['Spore.Middleware.KOSyncAuth'] = {}
require('Spore.Middleware.KOSyncAuth').call = function(args, req)
req.headers['x-auth-user'] = args.username
req.headers['x-auth-key'] = args.userkey
end
local HTTPClient = require("httpclient")
local async_http_client = HTTPClient:new()
package.loaded['Spore.Middleware.AsyncHTTP'] = {}
require('Spore.Middleware.AsyncHTTP').call = function(args, req)
req:finalize()
local result
async_http_client:request({
url = req.url,
method = req.method,
body = req.env.spore.payload,
on_headers = function(headers)
for header, value in pairs(req.headers) do
if type(header) == 'string' then
headers:add(header, value)
end
end
end,
}, function(res)
result = res
-- Turbo HTTP client uses code instead of status
-- change to status so that Spore can understand
result.status = res.code
coroutine.resume(args.thread)
end)
return coroutine.create(function() coroutine.yield(result) end)
end
end
function KOSyncClient:register(username, password)
self.client:reset_middlewares()
self.client:enable('Format.JSON')
self.client:enable("GinClient")
local ok, res = pcall(function()
return self.client:register({
username = username,
password = password,
})
end)
if ok then
return res.status == 201, res.body
else
DEBUG(ok, res)
return false, res.body
end
end
function KOSyncClient:authorize(username, password)
self.client:reset_middlewares()
self.client:enable('Format.JSON')
self.client:enable("GinClient")
self.client:enable("KOSyncAuth", {
username = username,
userkey = password,
})
local ok, res = pcall(function()
return self.client:authorize()
end)
if ok then
return res.status == 200, res.body
else
DEBUG("err:", res)
return false, res
end
end
function KOSyncClient:update_progress(username, password,
document, progress, percentage, device, callback)
self.client:reset_middlewares()
self.client:enable('Format.JSON')
self.client:enable("GinClient")
self.client:enable("KOSyncAuth", {
username = username,
userkey = password,
})
local co = coroutine.create(function()
local ok, res = pcall(function()
return self.client:update_progress({
document = document,
progress = progress,
percentage = percentage,
device = device,
})
end)
if ok then
callback(res.status == 200, res.body)
else
DEBUG("err:", res)
callback(false, res)
end
end)
self.client:enable("AsyncHTTP", {thread = co})
coroutine.resume(co)
UIManager.INPUT_TIMEOUT = 100
end
function KOSyncClient:get_progress(username, password,
document, callback)
self.client:reset_middlewares()
self.client:enable('Format.JSON')
self.client:enable("GinClient")
self.client:enable("KOSyncAuth", {
username = username,
userkey = password,
})
local co = coroutine.create(function()
local ok, res = pcall(function()
return self.client:get_progress({
document = document,
})
end)
if ok then
callback(res.status == 200, res.body)
else
DEBUG("err:", res)
callback(false, res)
end
end)
self.client:enable("AsyncHTTP", {thread = co})
coroutine.resume(co)
UIManager.INPUT_TIMEOUT = 100
end
return KOSyncClient

@ -0,0 +1,49 @@
{
"base_url" : "https://vislab.bjmu.edu.cn:7200/",
"name" : "koreader-sync-api",
"methods" : {
"register" : {
"path" : "/users/create",
"method" : "POST",
"required_params" : [
"username",
"password",
],
"payload" : [
"username",
"password",
],
"expected_status" : [201, 402]
},
"authorize" : {
"path" : "/users/auth",
"method" : "GET",
"expected_status" : [200, 401]
},
"update_progress" : {
"path" : "/syncs/progress",
"method" : "PUT",
"required_params" : [
"document",
"progress",
"percentage",
"device",
],
"payload" : [
"document",
"progress",
"percentage",
"device",
],
"expected_status" : [200, 202, 401]
},
"get_progress" : {
"path" : "/syncs/progress/:document",
"method" : "GET",
"required_params" : [
"document",
],
"expected_status" : [200, 401]
},
}
}

@ -0,0 +1,272 @@
local InputContainer = require("ui/widget/container/inputcontainer")
local LoginDialog = require("ui/widget/logindialog")
local InfoMessage = require("ui/widget/infomessage")
local ConfirmBox = require("ui/widget/confirmbox")
local DocSettings = require("docsettings")
local NetworkMgr = require("ui/networkmgr")
local UIManager = require("ui/uimanager")
local Screen = require("device").screen
local Device = require("device")
local Event = require("ui/event")
local DEBUG = require("dbg")
local T = require("ffi/util").template
local _ = require("gettext")
local md5 = require("MD5")
local KOSync = InputContainer:new{
name = "kosync",
register_title = _("Register an account in Koreader server"),
login_title = _("Login to Koreader server"),
}
function KOSync:init()
local settings = G_reader_settings:readSetting("kosync") or {}
self.kosync_username = settings.username or ""
self.kosync_userkey = settings.userkey
self.ui:registerPostInitCallback(function()
UIManager:scheduleIn(1, function() self:getProgress() end)
end)
self.ui.menu:registerToMainMenu(self)
end
function KOSync:addToMainMenu(tab_item_table)
table.insert(tab_item_table.plugins, {
text = _("Progress Sync"),
sub_item_table = {
{
text_func = function()
return self.kosync_userkey and (_("Logout"))
or _("Register") .. " / " .. _("Login")
end,
callback_func = function()
return self.kosync_userkey and
function() self:logout() end or
function() self:login() end
end,
},
}
})
end
function KOSync:login()
if NetworkMgr:getWifiStatus() == false then
NetworkMgr:promptWifiOn()
end
self.login_dialog = LoginDialog:new{
title = self.kosync_username and self.login_title or self.register_title,
username = self.kosync_username or "",
buttons = {
{
{
text = _("Cancel"),
enabled = true,
callback = function()
self:closeDialog()
end,
},
{
text = _("Login"),
enabled = true,
callback = function()
local username, password = self:getCredential()
self:closeDialog()
UIManager:scheduleIn(0.5, function()
self:doLogin(username, password)
end)
UIManager:show(InfoMessage:new{
text = _("Logging in. Please wait..."),
timeout = 1,
})
end,
},
{
text = _("Register"),
enabled = not self.kosync and true or false,
callback = function()
local username, password = self:getCredential()
self:closeDialog()
UIManager:scheduleIn(0.5, function()
self:doRegister(username, password)
end)
UIManager:show(InfoMessage:new{
text = _("Registering. Please wait..."),
timeout = 1,
})
end,
},
},
},
width = Screen:getWidth() * 0.8,
height = Screen:getHeight() * 0.4,
}
self.login_dialog:onShowKeyboard()
UIManager:show(self.login_dialog)
end
function KOSync:closeDialog()
self.login_dialog:onClose()
UIManager:close(self.login_dialog)
end
function KOSync:getCredential()
return self.login_dialog:getCredential()
end
function KOSync:doRegister(username, password)
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
service_spec = self.path .. "/api.json"
}
local userkey = md5:sum(password)
local ok, status, body = pcall(client.register, client, username, userkey)
if not ok and status then
UIManager:show(InfoMessage:new{
text = _("An error occurred while registering:") ..
"\n" .. status,
})
elseif ok then
if status then
self.kosync_username = username
self.kosync_userkey = userkey
UIManager:show(InfoMessage:new{
text = _("Registered to Koreader server successfully."),
})
else
UIManager:show(InfoMessage:new{
text = _(body.message or "Unknown server error"),
})
end
end
self:onSaveSettings()
end
function KOSync:doLogin(username, password)
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
service_spec = self.path .. "/api.json"
}
local userkey = md5:sum(password)
local ok, status, body = pcall(client.authorize, client, username, userkey)
if not ok and status then
UIManager:show(InfoMessage:new{
text = _("An error occurred while logging in:") ..
"\n" .. status,
})
elseif ok then
if status then
self.kosync_username = username
self.kosync_userkey = userkey
UIManager:show(InfoMessage:new{
text = _("Logged in to Koreader server successfully."),
})
else
UIManager:show(InfoMessage:new{
text = _(body.message or "Unknown server error"),
})
end
end
self:onSaveSettings()
end
function KOSync:logout()
self.kosync_username = nil
self.kosync_userkey = nil
self:onSaveSettings()
end
function KOSync:getLastPercent()
if self.ui.document.info.has_pages then
return self.ui.paging:getLastPercent()
else
return self.ui.rolling:getLastPercent()
end
end
function KOSync:getLastProgress()
if self.ui.document.info.has_pages then
return self.ui.paging:getLastProgress()
else
return self.ui.rolling:getLastProgress()
end
end
function KOSync:syncToProgress(progress)
DEBUG("sync to", progress)
if self.ui.document.info.has_pages then
self.ui:handleEvent(Event:new("GotoPage", tonumber(progress)))
else
self.ui:handleEvent(Event:new("GotoXPointer", progress))
end
end
function KOSync:updateProgress()
if self.kosync_username and self.kosync_userkey then
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
service_spec = self.path .. "/api.json"
}
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,
function(ok, body)
DEBUG("update progress for", self.view.document.file, ok)
end)
if not ok and err then
DEBUG("err:", err)
end
end
end
function KOSync:getProgress()
if self.kosync_username and self.kosync_userkey then
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
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)
DEBUG("get progress for", self.view.document.file, ok, body)
if body and body.percentage then
local percentage = self:getLastPercent()
DEBUG("current progress", percentage)
if (body.percentage - percentage) > 0.0001 then
UIManager:show(ConfirmBox:new{
text = T(_("Sync to furthest location from '%1'?"),
body.device),
ok_callback = function()
self:syncToProgress(body.progress)
end,
})
end
end
end)
if not ok and err then
DEBUG("err:", err)
end
end
end
function KOSync:onSaveSettings()
local settings = {
username = self.kosync_username,
userkey = self.kosync_userkey,
}
G_reader_settings:saveSetting("kosync", settings)
end
function KOSync:onCloseDocument()
DEBUG("on close document")
self:updateProgress()
end
return KOSync

@ -5,8 +5,8 @@ require "defaults"
pcall(dofile, "defaults.persistent.lua")
-- set search path for 'require()'
package.path = "common/?.lua;frontend/?.lua;" .. package.path
package.cpath = "common/?.so;common/?.dll;/usr/lib/lua/?.so;" .. package.cpath
package.path = "common/?.lua;frontend/?.lua;rocks/share/lua/5.1/?.lua;" .. package.path
package.cpath = "common/?.so;common/?.dll;/usr/lib/lua/?.so;rocks/lib/lua/5.1/?.so;" .. package.cpath
-- set search path for 'ffi.load()'
local ffi = require("ffi")

@ -0,0 +1,178 @@
package.path = "rocks/share/lua/5.1/?.lua;" .. package.path
package.cpath = "rocks/lib/lua/5.1/?.so;" .. package.cpath
require("commonrequire")
local UIManager = require("ui/uimanager")
local HTTPClient = require("httpclient")
local DEBUG = require("dbg")
local md5 = require("MD5")
DEBUG:turnOn()
local service = [[
{
"base_url" : "https://192.168.1.101:7200",
"name" : "api",
"methods" : {
"register" : {
"path" : "/users/create",
"method" : "POST",
"required_params" : [
"username",
"password",
],
"payload" : [
"username",
"password",
],
"expected_status" : [201, 402]
},
"authorize" : {
"path" : "/users/auth",
"method" : "GET",
"expected_status" : [200, 401]
},
"update_progress" : {
"path" : "/syncs/progress",
"method" : "PUT",
"required_params" : [
"document",
"progress",
"percentage",
"device",
],
"payload" : [
"document",
"progress",
"percentage",
"device",
],
"expected_status" : [200, 202, 401]
},
"get_progress" : {
"path" : "/syncs/progress/:document",
"method" : "GET",
"required_params" : [
"document",
],
"expected_status" : [200, 401]
},
}
}
]]
describe("KOSync modules #notest #nocov", function()
local Spore = require("Spore")
local client = Spore.new_from_string(service)
package.loaded['Spore.Middleware.GinClient'] = {}
require('Spore.Middleware.GinClient').call = function(self, req)
req.headers['accept'] = "application/vnd.koreader.v1+json"
end
package.loaded['Spore.Middleware.KOSyncAuth'] = {}
require('Spore.Middleware.KOSyncAuth').call = function(args, req)
req.headers['x-auth-user'] = args.username
req.headers['x-auth-key'] = args.userkey
end
-- password should be hashed before submitting to server
local username, password = "koreader", md5:sum("koreader")
-- fake progress data
local doc, percentage, progress, device =
"41cce710f34e5ec21315e19c99821415", -- fast digest of the document
0.356, -- percentage of the progress
"69", -- page number or xpointer
"my kpw" -- device name
it("should create new user", function()
client:reset_middlewares()
client:enable('Format.JSON')
client:enable("GinClient")
local ok, res = pcall(function()
return client:register({
username = username,
password = password,
})
end)
if ok then
if res.status == 200 then
DEBUG("register successful to ", res.body.username)
elseif res.status == 402 then
DEBUG("register unsuccessful: ", res.body.message)
end
else
DEBUG("Please retry later", res)
end
end)
it("should authorize user", function()
client:reset_middlewares()
client:enable('Format.JSON')
client:enable("GinClient")
client:enable("KOSyncAuth", {
username = username,
userkey = password,
})
local ok, res = pcall(function()
return client:authorize()
end)
if ok then
if res.status == 200 then
assert.are.same("OK", res.body.authorized)
else
DEBUG(res.body)
end
else
DEBUG("Please retry later", res)
end
end)
it("should update progress", function()
client:reset_middlewares()
client:enable('Format.JSON')
client:enable("GinClient")
client:enable("KOSyncAuth", {
username = username,
userkey = password,
})
local ok, res = pcall(function()
return client:update_progress({
document = doc,
progress = progress,
percentage = percentage,
device = device,
})
end)
if ok then
if res.status == 200 then
local result = res.body
assert.are.same(progress, result.progress)
assert.are.same(percentage, result.percentage)
assert.are.same(device, result.device)
else
DEBUG(res.body.message)
end
else
DEBUG("Please retry later", res)
end
end)
it("should get progress", function()
client:reset_middlewares()
client:enable('Format.JSON')
client:enable("GinClient")
client:enable("KOSyncAuth", {
username = username,
userkey = password,
})
local ok, res = pcall(function()
return client:get_progress({
document = doc,
})
end)
if ok then
if res.status == 200 then
local result = res.body
assert.are.same(progress, result.progress)
assert.are.same(percentage, result.percentage)
assert.are.same(device, result.device)
else
DEBUG(res.body.message)
end
else
DEBUG("Please retry later", res)
end
end)
end)

@ -55,12 +55,35 @@ describe("Lua Spore modules #nocov", function()
end)
end)
describe("Lua Spore modules with async request #nocov", function()
describe("Lua Spore modules with async http request #nocov", function()
local Spore = require("Spore")
local client = Spore.new_from_string(service)
client:enable("Format.JSON")
package.loaded['Spore.Middleware.Async'] = {}
local async_http_client = HTTPClient:new()
package.loaded['Spore.Middleware.AsyncHTTP'] = {}
require('Spore.Middleware.AsyncHTTP').call = function(args, req)
req:finalize()
local result
async_http_client:request({
url = req.url,
method = req.method,
body = req.env.spore.payload,
on_headers = function(headers)
for header, value in pairs(req.headers) do
if type(header) == 'string' then
headers:add(header, value)
end
end
end,
}, function(res)
result = res
-- Turbo HTTP client uses code instead of status
-- change to status so that Spore can understand
result.status = res.code
coroutine.resume(args.thread)
UIManager.INPUT_TIMEOUT = 100 -- no need in production
end)
return coroutine.create(function() coroutine.yield(result) end)
end
it("should complete GET request", function()
UIManager:quit()
local co = coroutine.create(function()
@ -69,22 +92,10 @@ describe("Lua Spore modules with async request #nocov", function()
UIManager:quit()
assert.are.same(res.body.args, info)
end)
require('Spore.Middleware.Async').call = function(self, req)
req:finalize()
local result
async_http_client:request({
url = req.url,
method = req.method,
}, function(res)
result = res
coroutine.resume(co)
UIManager.INPUT_TIMEOUT = 100 -- no need in production
end)
return coroutine.create(function() coroutine.yield(result) end)
end
client:enable("Async")
client:reset_middlewares()
client:enable("Format.JSON")
client:enable("AsyncHTTP", {thread = co})
coroutine.resume(co)
UIManager.INPUT_TIMEOUT = 100
UIManager:runForever()
end)
it("should complete POST request", function()
@ -95,22 +106,10 @@ describe("Lua Spore modules with async request #nocov", function()
UIManager:quit()
assert.are.same(res.body.json, info)
end)
require('Spore.Middleware.Async').call = function(self, req)
req:finalize()
local result
async_http_client:request({
url = req.url,
method = req.method,
}, function(res)
result = res
coroutine.resume(co)
UIManager.INPUT_TIMEOUT = 100 -- no need in production
end)
return coroutine.create(function() coroutine.yield(result) end)
end
client:enable("Async")
client:reset_middlewares()
client:enable("Format.JSON")
client:enable("AsyncHTTP", {thread = co})
coroutine.resume(co)
UIManager.INPUT_TIMEOUT = 100
UIManager:runForever()
end)
end)

Loading…
Cancel
Save