--[[ 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