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