310 lines
8.7 KiB
Lua
310 lines
8.7 KiB
Lua
--[[
|
|
KOReader Fetch Plugin
|
|
|
|
Fetches files from a remote server endpoint and saves them to a configured directory.
|
|
|
|
Installation:
|
|
1. Copy this file to: koreader/plugins/fetch.koplugin/main.lua
|
|
2. Create config file at: koreader/plugins/fetch.koplugin/config.lua with:
|
|
return {
|
|
server_url = "http://example.com/get",
|
|
auth_token = "your-secret-token-here-change-me"
|
|
}
|
|
3. Restart KOReader
|
|
]]
|
|
|
|
local DataStorage = require("datastorage")
|
|
local Dispatcher = require("dispatcher")
|
|
local FFIUtil = require("ffi/util")
|
|
local InfoMessage = require("ui/widget/infomessage")
|
|
local PathChooser = require("ui/widget/pathchooser")
|
|
local UIManager = require("ui/uimanager")
|
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
|
local http = require("socket.http")
|
|
local ltn12 = require("ltn12")
|
|
local logger = require("logger")
|
|
local _ = require("gettext")
|
|
local T = FFIUtil.template
|
|
local ffi = require("ffi")
|
|
local C = ffi.C
|
|
|
|
-- Load configuration
|
|
local function loadConfig()
|
|
local config_path = FFIUtil.joinPath(
|
|
FFIUtil.joinPath(DataStorage:getDataDir(), "plugins"),
|
|
"fetch.koplugin/config.lua"
|
|
)
|
|
|
|
local ok, config = pcall(dofile, config_path)
|
|
if ok and config then
|
|
return config
|
|
else
|
|
logger.warn("Fetch plugin: Could not load config file, using defaults")
|
|
return {
|
|
server_url = "http://192.168.1.100:8000/get",
|
|
auth_token = "your-secret-token-here"
|
|
}
|
|
end
|
|
end
|
|
|
|
local Fetch = WidgetContainer:extend{
|
|
name = "fetch",
|
|
is_doc_only = false,
|
|
}
|
|
|
|
function Fetch:init()
|
|
-- Load server configuration
|
|
self.config = loadConfig()
|
|
|
|
-- Load or set default home directory
|
|
self.settings = G_reader_settings:readSetting("fetch_plugin") or {}
|
|
self.home_directory = self.settings.home_directory or "/mnt/onboard/fetched"
|
|
|
|
-- Add menu items
|
|
self.ui.menu:registerToMainMenu(self)
|
|
|
|
logger.info("Fetch plugin initialized")
|
|
end
|
|
|
|
function Fetch:addToMainMenu(menu_items)
|
|
menu_items.fetch = {
|
|
text = _("Fetch Files"),
|
|
sub_item_table = {
|
|
{
|
|
text = _("Fetch Now"),
|
|
callback = function()
|
|
self:fetchFiles()
|
|
end,
|
|
},
|
|
{
|
|
text = _("Configure Home Directory"),
|
|
keep_menu_open = true,
|
|
callback = function()
|
|
self:showDirectoryDialog()
|
|
end,
|
|
},
|
|
{
|
|
text = _("View Settings"),
|
|
keep_menu_open = true,
|
|
callback = function()
|
|
self:showSettings()
|
|
end,
|
|
},
|
|
},
|
|
}
|
|
end
|
|
|
|
function Fetch:showSettings()
|
|
local text = T(_(
|
|
"Server URL: %1\n" ..
|
|
"Auth Token: %2\n" ..
|
|
"Home Directory: %3\n\n" ..
|
|
"To change server settings, edit:\n" ..
|
|
"koreader/plugins/fetch.koplugin/config.lua"
|
|
),
|
|
self.config.server_url,
|
|
string.sub(self.config.auth_token, 1, 10) .. "...",
|
|
self.home_directory
|
|
)
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
text = text,
|
|
timeout = 5,
|
|
})
|
|
end
|
|
|
|
function Fetch:showDirectoryDialog()
|
|
local path_chooser = PathChooser:new{
|
|
title = _("Choose Home Directory"),
|
|
path = self.home_directory,
|
|
show_files = false,
|
|
onConfirm = function(new_dir)
|
|
self.home_directory = new_dir
|
|
self.settings.home_directory = new_dir
|
|
G_reader_settings:saveSetting("fetch_plugin", self.settings)
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Home directory set to:\n%1"), new_dir),
|
|
timeout = 2,
|
|
})
|
|
end,
|
|
}
|
|
UIManager:show(path_chooser)
|
|
end
|
|
|
|
function Fetch:fetchFiles()
|
|
logger.info("Fetch: Starting file fetch from " .. self.config.server_url)
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Fetching files from server..."),
|
|
timeout = 1,
|
|
})
|
|
|
|
-- Ensure home directory exists
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
local attributes = lfs.attributes(self.home_directory)
|
|
if not attributes or attributes.mode ~= "directory" then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Home directory does not exist:\n%1"), self.home_directory),
|
|
timeout = 3,
|
|
})
|
|
return
|
|
end
|
|
|
|
-- Generate temporary file path for downloaded zip
|
|
local temp_zip = FFIUtil.joinPath(
|
|
DataStorage:getDataDir(),
|
|
"fetch_temp_" .. os.time() .. ".zip"
|
|
)
|
|
|
|
-- Download file using socket.http
|
|
local response_body = {}
|
|
local request_body = ""
|
|
|
|
local result, status_code, headers = http.request{
|
|
url = self.config.server_url,
|
|
method = "POST",
|
|
headers = {
|
|
["Authorization"] = "Bearer " .. self.config.auth_token,
|
|
["Content-Length"] = "0",
|
|
},
|
|
sink = ltn12.sink.table(response_body)
|
|
}
|
|
|
|
logger.info("Fetch: HTTP result=" .. tostring(result) .. ", status=" .. tostring(status_code))
|
|
|
|
if not result or status_code ~= 200 then
|
|
local error_msg = _("Failed to fetch files from server")
|
|
if status_code == 401 then
|
|
error_msg = _("Authentication failed. Check your token.")
|
|
elseif status_code == 404 then
|
|
error_msg = _("No files available on server")
|
|
elseif not result then
|
|
error_msg = _("Could not connect to server")
|
|
end
|
|
|
|
UIManager:show(InfoMessage:new{
|
|
text = error_msg,
|
|
timeout = 3,
|
|
})
|
|
return
|
|
end
|
|
|
|
-- Concatenate response body
|
|
local zip_data = table.concat(response_body)
|
|
|
|
if #zip_data == 0 then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("No files received from server"),
|
|
timeout = 3,
|
|
})
|
|
return
|
|
end
|
|
|
|
logger.info("Fetch: Downloaded " .. #zip_data .. " bytes")
|
|
|
|
-- Write zip data to temp file
|
|
local file = io.open(temp_zip, "wb")
|
|
if not file then
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Failed to save downloaded file"),
|
|
timeout = 3,
|
|
})
|
|
return
|
|
end
|
|
file:write(zip_data)
|
|
file:close()
|
|
|
|
-- Extract zip file
|
|
local extract_success, file_count = self:extractZip(temp_zip, self.home_directory)
|
|
|
|
-- Cleanup temp file
|
|
self:cleanup(temp_zip)
|
|
|
|
if extract_success then
|
|
UIManager:show(InfoMessage:new{
|
|
text = T(_("Successfully fetched %1 file(s) to:\n%2"), file_count, self.home_directory),
|
|
timeout = 3,
|
|
})
|
|
logger.info("Fetch: Successfully extracted " .. file_count .. " files")
|
|
else
|
|
UIManager:show(InfoMessage:new{
|
|
text = _("Failed to extract downloaded files"),
|
|
timeout = 3,
|
|
})
|
|
end
|
|
end
|
|
|
|
function Fetch:extractZip(zip_path, destination)
|
|
logger.info("Fetch: Extracting zip to " .. destination)
|
|
|
|
-- Use system unzip command
|
|
local unzip_cmd = string.format(
|
|
'unzip -o "%s" -d "%s" 2>&1',
|
|
zip_path,
|
|
destination
|
|
)
|
|
|
|
logger.dbg("Fetch: Running unzip command")
|
|
local handle = io.popen(unzip_cmd)
|
|
if not handle then
|
|
logger.err("Fetch: Failed to execute unzip command")
|
|
return false, 0
|
|
end
|
|
|
|
local output = handle:read("*a")
|
|
local success = handle:close()
|
|
|
|
logger.info("Fetch: Unzip output: " .. output)
|
|
|
|
if not success then
|
|
logger.err("Fetch: Failed to extract zip file")
|
|
return false, 0
|
|
end
|
|
|
|
-- Count extracted files from output
|
|
local file_count = 0
|
|
for line in output:gmatch("[^\r\n]+") do
|
|
if line:match("inflating:") or line:match("extracting:") then
|
|
file_count = file_count + 1
|
|
end
|
|
end
|
|
|
|
-- If count is 0, try counting files in directory
|
|
if file_count == 0 then
|
|
local lfs = require("libs/libkoreader-lfs")
|
|
for file in lfs.dir(destination) do
|
|
if file ~= "." and file ~= ".." then
|
|
local attr = lfs.attributes(FFIUtil.joinPath(destination, file))
|
|
if attr and attr.mode == "file" then
|
|
file_count = file_count + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return true, file_count
|
|
end
|
|
|
|
function Fetch:cleanup(file_path)
|
|
if file_path then
|
|
os.remove(file_path)
|
|
logger.dbg("Fetch: Cleaned up temporary file")
|
|
end
|
|
end
|
|
|
|
function Fetch:onDispatcherRegisterActions()
|
|
Dispatcher:registerAction("fetch_files", {
|
|
category = "none",
|
|
event = "FetchFiles",
|
|
title = _("Fetch files from server"),
|
|
general = true,
|
|
})
|
|
end
|
|
|
|
function Fetch:onFetchFiles()
|
|
self:fetchFiles()
|
|
end
|
|
|
|
return Fetch
|