Skip to content

Instantly share code, notes, and snippets.

@boypt
Last active June 4, 2026 02:52
Show Gist options
  • Select an option

  • Save boypt/3a9ec68832f27df6d0d0efb913071595 to your computer and use it in GitHub Desktop.

Select an option

Save boypt/3a9ec68832f27df6d0d0efb913071595 to your computer and use it in GitHub Desktop.
Installation script to install assistant.koplugin in KOReader
#!./luajit
require("setupkoenv")
local socket = require("socket")
local ltn12 = require("ltn12")
local http = require("socket.http")
local Archiver = require("ffi/archiver")
local DataStorage = require("datastorage")
local lfs = require("libs/libkoreader-lfs")
local logger = require("logger")
local FFIUtil = require("ffi/util")
local koutil = require("frontend/util")
-- logger:setLevel(1) -- debug switch
-- --- Configuration ---
local PLUGIN_NAME = "assistant.koplugin"
-- Defaults to 'main' branch if no argument is given
local VERSION = arg[1] or "main"
-- Proxy mirror source
local GITHUB_BASE = "https://ghfast.top/https://github.com"
local GITHUB_REPO = "omer-faruq/assistant.koplugin"
-- --- URL and Path Construction ---
-- Determine if the version is a tag or a branch
local REPO_REF = VERSION:sub(1, 1) == "v" and "tags" or "heads"
local RELEASE_URL = string.format("%s/%s/archive/refs/%s/%s.tar.gz", GITHUB_BASE, GITHUB_REPO, REPO_REF, VERSION)
local KOREADER_DIR = DataStorage:getFullDataDir()
local SCRIPT_DIR = lfs.currentdir()
local PLUGIN_DIR = KOREADER_DIR .. "/plugins"
local ASSISTANT_DIR = PLUGIN_DIR .. "/" .. PLUGIN_NAME
local UPDATE_TMPDIR = KOREADER_DIR .. "/ota/" .. PLUGIN_NAME .. ".update"
local UPDATE_BAKDIR = UPDATE_TMPDIR .. "/backup"
local TARGET_PLUGIN_PATH = ASSISTANT_DIR
local BACKUP_PLUGIN_PATH = UPDATE_TMPDIR .. "/backup/" .. PLUGIN_NAME
local DL_TAR = string.format("%s/SOURCE-%s-%s.tar.gz", UPDATE_TMPDIR, PLUGIN_NAME, VERSION)
-- --- Helper Functions ---
-- HTTP download function using LuaSocket
local function download_file(url, output_file)
logger.info("--> Downloading from: ", url, "\n----> Saving to:", output_file)
local file_handle = io.open(output_file, "wb")
if not file_handle then
logger.warn("Error: Could not open file for writing: " .. output_file)
return false
end
local sink = ltn12.sink.file(file_handle)
local status_code = socket.skip(1, http.request{
url = url,
method = "GET",
sink = sink,
})
if status_code == 200 then
logger.info(string.format("--> Download successful. (HTTP %d) (FileSize %d)",
status_code, lfs.attributes(output_file, "size")))
return true
else
logger.warn(string.format("--> Download failed. Status: %s", tostring(status_code)))
if file_handle then file_handle:close() end
return false
end
end
-- Check if an extracted path matches any exclude pattern (equivalent to tar --exclude)
local function is_excluded(path)
if path:find("/%.") or path:sub(1,1) == "." then
return true
end
if path:find("LEXRANK_LANGUAGES%.md$")
or path:find("l10n/templates")
or path:find("l10n/AI_TRANSLATE%.sh$")
or path:find("l10n/Makefile$")
or path:find("l10n/README%.md$") then
return true
end
return false
end
-- Encapsulated archive extraction function
-- @param archive_path : Full absolute path to the tar.gz archive
-- @param dest_base_path: Target base directory for extraction (e.g., UPDATE_TMPDIR)
-- @return boolean, string : Returns true on success, or false and an error message on failure
local function extract_archive(archive_path, dest_base_path)
logger.info("--> Extracting archive: " .. archive_path .. " to " .. dest_base_path)
local arc = Archiver.Reader:new()
if not arc:open(archive_path) then
local err_msg = "Failed to open archive: " .. tostring(arc.err)
logger.warn("--> " .. err_msg)
return false, err_msg
end
local extract_success = true
local err_msg = nil
for entry in arc:iterate() do
if not is_excluded(entry.path) then
local dest_path = dest_base_path .. "/" .. entry.path
local parent_dir = dest_path:match("(.*)" .. package.config:sub(1,1))
if parent_dir and not koutil.pathExists(parent_dir) then
koutil.makePath(parent_dir)
end
if not arc:extractToPath(entry.path, dest_path) then
err_msg = "Failed to extract entry " .. entry.path .. ": " .. tostring(arc.err)
logger.warn("--> " .. err_msg)
extract_success = false
break
end
end
end
arc:close()
return extract_success, err_msg
end
-- --- Main Workflow ---
-- 1. Create a temporary directory for downloading and extraction
logger.info("--> Creating temporary directory...")
koutil.makePath(UPDATE_BAKDIR)
-- 2. Download the release archive
if not download_file(RELEASE_URL, DL_TAR) then
logger.warn("Error: Download failed, exiting.")
FFIUtil.purgeDir(UPDATE_TMPDIR)
os.exit(1)
end
-- 3. Extract files using the KOReader ffi/archiver library
logger.info("--> Extracting new plugin ...")
local success, err = extract_archive(DL_TAR, UPDATE_TMPDIR)
if not success then
FFIUtil.purgeDir(UPDATE_TMPDIR)
os.exit(1)
end
-- 4. Back up the existing plugin directory if it exists
if koutil.pathExists(TARGET_PLUGIN_PATH) then
logger.info("--> Backing up existing plugin directory...")
if koutil.pathExists(BACKUP_PLUGIN_PATH) then
FFIUtil.purgeDir(BACKUP_PLUGIN_PATH)
end
os.rename(TARGET_PLUGIN_PATH, BACKUP_PLUGIN_PATH)
end
-- 5. Locate and move the newly extracted directory to the destination
local found_extracted_dir = nil
for file in lfs.dir(UPDATE_TMPDIR) do
if file:sub(1, #PLUGIN_NAME) == PLUGIN_NAME then
if koutil.directoryExists(UPDATE_TMPDIR .. "/" .. file) then
found_extracted_dir = UPDATE_TMPDIR .. "/" .. file
break
end
end
end
if found_extracted_dir then
logger.info("--> Found Extracted dir:", found_extracted_dir)
os.rename(found_extracted_dir, TARGET_PLUGIN_PATH)
logger.info("--> New plugin directory is ready.")
else
logger.warn("Error: Could not find extracted plugin directory in temporary folder.")
if koutil.pathExists(BACKUP_PLUGIN_PATH) then
logger.info("--> Restoring backup due to failure...")
os.rename(BACKUP_PLUGIN_PATH, TARGET_PLUGIN_PATH)
end
FFIUtil.purgeDir(UPDATE_TMPDIR)
os.exit(1)
end
-- 6. Restore configuration and libraries from the old backup
if koutil.pathExists(BACKUP_PLUGIN_PATH) then
logger.info("--> Restoring configuration from backup...")
local restore_targets = {"configuration.lua", "lib"}
for _, filename in ipairs(restore_targets) do
local old_file = BACKUP_PLUGIN_PATH .. "/" .. filename
local new_file = TARGET_PLUGIN_PATH .. "/" .. filename
if koutil.pathExists(old_file) then
if koutil.pathExists(new_file) then FFIUtil.purgeDir(new_file) end
os.rename(old_file, new_file)
end
end
end
-- 7. Clean up temporary directories
FFIUtil.purgeDir(UPDATE_TMPDIR)
logger.info("--> Removed temporary download directory.")
-- 8. If an external configuration.lua exists alongside the script, relocate it
local script_config = SCRIPT_DIR .. "/configuration.lua"
if koutil.pathExists(script_config) then
logger.info("--> Found configuration.lua, moving to plugin directory...")
local dest_config = TARGET_PLUGIN_PATH .. "/configuration.lua"
if koutil.pathExists(dest_config) then
os.rename(dest_config, dest_config .. ".bak")
end
os.rename(script_config, dest_config)
end
logger.info("--> Update complete.")
os.exit(0)
#!/bin/sh
# This script downloads and installs/updates the assistant.koplugin for KOReader.
# Exit immediately if a command exits with a non-zero status.
set -e
# --- Configuration ---
# Set the version to download (e.g., a tag like 'v1.08' or a branch like 'main').
# Defaults to 'main' branch if no argument is given.
# Usage: ./assi.sh v1.08
VERSION=${1:-main}
# Base URL for GitHub. Can be changed to a mirror/proxy if needed.
GITHUB_BASE=https://github.com
#GITHUB_BASE=https://gh.llkk.cc/$GITHUB_BASE
#GITHUB_BASE=https://ghfast.top/$GITHUB_BASE
# The repository to download from.
RELEASE_REPO=omer-faruq/assistant.koplugin
#RELEASE_REPO=boypt/assistant.koplugin
# --- URL and Path Construction ---
# Determine the archive path based on the version format.
# If VERSION starts with 'v' (e.g., v1.0.8), assume it's a tag.
[[ "${VERSION#v}" != "$VERSION" ]] && REPO_ARCHIVE=archive/refs/tags/$VERSION.tar.gz
# Otherwise, assume it's a branch name (e.g., main).
[[ -z $REPO_ARCHIVE ]] && REPO_ARCHIVE=archive/refs/heads/$VERSION.tar.gz
# Construct the full download URL.
RELEASE_URL=$GITHUB_BASE/$RELEASE_REPO/$REPO_ARCHIVE
echo "--> Download From: $RELEASE_URL"
# Get the absolute path of the directory where the script is located.
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
# Determine KOReader's root directory. Default to the script's directory if not set.
[[ -z $KOREADER_DIR ]] && KOREADER_DIR=$SCRIPT_DIR
[[ ! -d $KOREADER_DIR ]] && exit 1
# Define plugin and temporary directory paths.
PLUGIN_NAME=assistant.koplugin
PLUGIN_DIR=$KOREADER_DIR/plugins/${PLUGIN_NAME}
UPDATE_TMPDIR=$KOREADER_DIR/ota/${PLUGIN_NAME}.update
OTA_EXTRACT_DIR=$UPDATE_TMPDIR/EXT
DL_TAR="$UPDATE_TMPDIR/SOURCE.tar.gz"
# Set the 'tar' command. Use koreader's bundled tar
TAR="$KOREADER_DIR/tar"
TAR_OPTIONS=" --exclude='*/.*' --exclude='*/LEXRANK_LANGUAGES.md' --exclude='*/l10n/templates' --exclude='*/l10n/AI_TRANSLATE.sh' --exclude='*/l10n/Makefile' --exclude='*/l10n/README.md'"
if [[ ! -x $TAR ]]; then
# if bundled tar not exists, fallback to system tar (does not support --exclude)
TAR=tar
TAR_OPTIONS=""
fi
# --- Helper Function ---
download_file() {
echo -e "--> Downloading from: $1\n--> Saving to $2"
# Try to download using curl, wget, or a fallback luajit script.
if command -v curl >/dev/null 2>&1; then
curl -L -o "$2" "$1" # -L to follow redirects
elif command -v wget >/dev/null 2>&1; then
wget -O "$2" "$1"
elif [[ -e $SCRIPT_DIR/luaget.lua ]]; then
env LUA_PATH="$SCRIPT_DIR/common/?.lua;;" LUA_CPATH="$SCRIPT_DIR/common/?.so;;" \
$SCRIPT_DIR/luajit $SCRIPT_DIR/luaget.lua "$1" "$2"
else
echo "Error: Neither curl, wget, nor luaget.lua found. Cannot download."
return 1
fi
}
# --- Main Script Logic ---
# 1. Create a temporary directory for the download and extraction process.
echo "--> Creating temporary directory..."
mkdir -p $UPDATE_TMPDIR $OTA_EXTRACT_DIR
# 2. Download the release archive.
download_file "$RELEASE_URL" "$DL_TAR"
# 3. Verify the integrity of the downloaded archive. Exit if it's corrupted.
if ! $TAR -tvf "$DL_TAR" >/dev/null; then
echo "--> Downloaded file is corrupted, exiting."
rm -rf "$UPDATE_TMPDIR"
exit 1
fi
# 4. If an old version of the plugin exists, back it up.
if [[ -e "$PLUGIN_DIR" ]]; then
echo "--> Backing up existing plugin directory..."
mv $PLUGIN_DIR $UPDATE_TMPDIR
fi
# 5. Extract the new version from the archive, excluding unnecessary files,
# and move it to the plugins directory.
echo "--> Extracting new plugin version..."
eval $TAR -xzf "$DL_TAR" -C "$OTA_EXTRACT_DIR" $TAR_OPTIONS
mv "$OTA_EXTRACT_DIR/$PLUGIN_NAME"* "$PLUGIN_DIR"
echo "--> New plugin directory is ready."
# 6. If a backup of the old plugin exists, restore user configuration and libraries,
# then remove the backup directory.
if [[ -d "$UPDATE_TMPDIR/$PLUGIN_NAME" ]]; then
echo "--> Restoring configuration from backup..."
for _OLDFILE in configuration.lua lib; do
if [[ -e "$UPDATE_TMPDIR/$PLUGIN_NAME/$_OLDFILE" ]]; then
mv -v "$UPDATE_TMPDIR/$PLUGIN_NAME/$_OLDFILE" "$PLUGIN_DIR/"
fi
done
fi
# 7. Clean up the temporary download directory.
rm -rf "$UPDATE_TMPDIR"
echo "--> Removed temporary ota directory."
# 8. If a `configuration.lua` file exists alongside the script, move it into the
# plugin directory. This is useful for initial setup.
if [[ -e "$SCRIPT_DIR/configuration.lua" ]]; then
echo "--> Found configuration.lua, moving to plugin directory..."
if [[ -e "$PLUGIN_DIR/configuration.lua" ]]; then
mv "$PLUGIN_DIR/configuration.lua" "$PLUGIN_DIR/configuration.lua.bak"
fi
mv "$SCRIPT_DIR/configuration.lua" "$PLUGIN_DIR/"
fi
echo "--> Update complete."
-- Set search path for `require()`.
package.path =
"common/?.lua;frontend/?.lua;plugins/exporter.koplugin/?.lua;" ..
package.path
package.cpath =
"common/?.so;common/?.dll;/usr/lib/lua/?.so;" ..
package.cpath
local socket = require("socket")
local ltn12 = require("ltn12")
local http = require("socket.http")
local url = arg[1]
local output_file = arg[2]
if not url then
io.stderr:write("Usage: lua luaget.lua <url> [output_file]\n")
print(" url: URL to download from")
print(" output_file: Optional filename to save to (default: stdout)")
os.exit(1)
end
print("Downloading from: " .. url)
if output_file then
print("Saving to file: " .. output_file)
end
local file_handle = nil
local success = false
local status_code = 0
local headers = {}
local sink
if output_file then
file_handle = io.open(output_file, "wb") -- "wb" for write binary
if not file_handle then
io.stderr:write("Error: Could not open file for writing: " .. output_file .. "\n")
os.exit(1)
end
sink = ltn12.sink.file(file_handle)
else
sink = ltn12.sink.file(io.stdout)
end
local success, status_code, headers = http.request{
url = url,
method = "GET",
sink = sink,
}
if success then
print(string.format("Download successful. \nHTTP Status: %d", status_code))
for k, v in pairs(headers) do
if type(v) == "string" and (k:lower() == "content-type" or k:lower() == "content-length") then
print(string.format(" %s: %s", k, v))
end
end
os.exit(0)
end
io.stderr:write("Error: " .. tostring(success) .. "\n")
os.exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment