Add Library Catalog addon, lib/docker.sh and tests
- lib/docker.sh: shared Docker/Compose tooling (on-demand install via apt or get.docker.com, compose wrapper, container state/health, HTTP wait) - menus/addon_library.sh: install/update/uninstall the Koha + Quick Add stack from library/ to /opt/kiosk-library, add the catalogue as a kiosk page (duration 0), choose kiosk-only vs LAN access, show addresses and login - wire into install.sh menu, complete_uninstall and clone_settings - tests/addon_library.test.sh: 41 checks against a stub sudo/docker and a fixture stack (no root, Docker or network needed) Note: the 'network access' choice relies on a BIND_ADDR setting in the library stack, added in a follow-up commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
This commit is contained in:
+11
-3
@@ -38,7 +38,11 @@
|
|||||||
# Display/Lockout, the webui/ Node app installed as a systemd
|
# Display/Lockout, the webui/ Node app installed as a systemd
|
||||||
# service; no login of its own, put it behind your own reverse proxy
|
# service; no login of its own, put it behind your own reverse proxy
|
||||||
# with Authelia forward-auth if it needs to be reachable beyond a
|
# with Authelia forward-auth if it needs to be reachable beyond a
|
||||||
# trusted LAN - see the file header).
|
# trusted LAN - see the file header), Library Catalog (menus/
|
||||||
|
# addon_library.sh - a self-hosted Koha library catalogue in Docker with
|
||||||
|
# Z39.50 lookup, label printing and a scan-to-add page, shown on the
|
||||||
|
# kiosk as a public search terminal; installs Docker on demand via
|
||||||
|
# lib/docker.sh).
|
||||||
# Advanced: Diagnostics (menus/diagnostics.sh - system status/logs/
|
# Advanced: Diagnostics (menus/diagnostics.sh - system status/logs/
|
||||||
# audio/network), Electron Maintenance (menus/advanced_electron.sh -
|
# audio/network), Electron Maintenance (menus/advanced_electron.sh -
|
||||||
# manual update, fix blank screen), Factory Reset
|
# manual update, fix blank screen), Factory Reset
|
||||||
@@ -67,6 +71,8 @@ source "$SCRIPT_DIR/lib/menu.sh"
|
|||||||
source "$SCRIPT_DIR/lib/config.sh"
|
source "$SCRIPT_DIR/lib/config.sh"
|
||||||
# shellcheck source=lib/electron.sh
|
# shellcheck source=lib/electron.sh
|
||||||
source "$SCRIPT_DIR/lib/electron.sh"
|
source "$SCRIPT_DIR/lib/electron.sh"
|
||||||
|
# shellcheck source=lib/docker.sh
|
||||||
|
source "$SCRIPT_DIR/lib/docker.sh"
|
||||||
# shellcheck source=menus/sites.sh
|
# shellcheck source=menus/sites.sh
|
||||||
source "$SCRIPT_DIR/menus/sites.sh"
|
source "$SCRIPT_DIR/menus/sites.sh"
|
||||||
# shellcheck source=menus/display.sh
|
# shellcheck source=menus/display.sh
|
||||||
@@ -97,6 +103,8 @@ source "$SCRIPT_DIR/menus/addon_lms_squeezelite.sh"
|
|||||||
source "$SCRIPT_DIR/menus/addon_asterisk_intercom.sh"
|
source "$SCRIPT_DIR/menus/addon_asterisk_intercom.sh"
|
||||||
# shellcheck source=menus/addon_webui.sh
|
# shellcheck source=menus/addon_webui.sh
|
||||||
source "$SCRIPT_DIR/menus/addon_webui.sh"
|
source "$SCRIPT_DIR/menus/addon_webui.sh"
|
||||||
|
# shellcheck source=menus/addon_library.sh
|
||||||
|
source "$SCRIPT_DIR/menus/addon_library.sh"
|
||||||
# shellcheck source=menus/advanced_electron.sh
|
# shellcheck source=menus/advanced_electron.sh
|
||||||
source "$SCRIPT_DIR/menus/advanced_electron.sh"
|
source "$SCRIPT_DIR/menus/advanced_electron.sh"
|
||||||
# shellcheck source=menus/advanced_upgrade.sh
|
# shellcheck source=menus/advanced_upgrade.sh
|
||||||
@@ -179,8 +187,8 @@ core_settings_menu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addons_menu_builder() {
|
addons_menu_builder() {
|
||||||
MENU_LABELS=("CUPS Printing" "Authelia Auto-Login" "Remote Access" "LMS Server / Squeezelite Player" "Asterisk Intercom (SIP Extension)" "Web UI")
|
MENU_LABELS=("CUPS Printing" "Authelia Auto-Login" "Remote Access" "LMS Server / Squeezelite Player" "Asterisk Intercom (SIP Extension)" "Web UI" "Library Catalog (Koha)")
|
||||||
MENU_HANDLERS=(addon_cups_menu addon_authelia_menu remote_access_menu addon_lms_squeezelite_menu addon_asterisk_intercom_menu addon_webui_menu)
|
MENU_HANDLERS=(addon_cups_menu addon_authelia_menu remote_access_menu addon_lms_squeezelite_menu addon_asterisk_intercom_menu addon_webui_menu addon_library_menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
addons_menu() {
|
addons_menu() {
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
# other addon's own scripts.
|
# other addon's own scripts.
|
||||||
: "${WEBUI_HELPER_PATH:=$BIN_DIR/kiosk-webui-helper}"
|
: "${WEBUI_HELPER_PATH:=$BIN_DIR/kiosk-webui-helper}"
|
||||||
|
|
||||||
|
# Library Catalog addon (menus/addon_library.sh): the Koha + Quick Add Docker
|
||||||
|
# stack is deployed here from this checkout's library/ directory. Root-owned
|
||||||
|
# on purpose - it holds the generated database/admin passwords in .env.
|
||||||
|
: "${LIBRARY_DIR:=/opt/kiosk-library}"
|
||||||
|
|
||||||
# Offline install bundle - only present on systems installed from an ISO
|
# Offline install bundle - only present on systems installed from an ISO
|
||||||
# built with `iso/build-iso.sh --offline`: a local apt repository (every
|
# built with `iso/build-iso.sh --offline`: a local apt repository (every
|
||||||
# package provisioning and the CUPS addon install) plus prebuilt
|
# package provisioning and the CUPS addon install) plus prebuilt
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# lib/docker.sh - Docker / Compose tooling shared by every addon that runs a
|
||||||
|
# containerised service (the Library Catalog addon is the first).
|
||||||
|
#
|
||||||
|
# Everything here goes through `sudo docker ...`: the kiosk admin runs
|
||||||
|
# install.sh as a regular user and is deliberately NOT put in the `docker`
|
||||||
|
# group (docker-group membership is root-equivalent).
|
||||||
|
#
|
||||||
|
# Depends on: lib/menu.sh (log_* helpers) being sourced first.
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# Overridable so tests can substitute stubs for the real binaries.
|
||||||
|
: "${DOCKER_BIN:=docker}"
|
||||||
|
|
||||||
|
docker_is_installed() {
|
||||||
|
command -v "$DOCKER_BIN" >/dev/null 2>&1 \
|
||||||
|
&& sudo "$DOCKER_BIN" compose version >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
docker_daemon_running() {
|
||||||
|
sudo "$DOCKER_BIN" info >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install Docker Engine + the Compose plugin if they are missing.
|
||||||
|
#
|
||||||
|
# Ubuntu 24.04+ ships both in its own archive (docker.io, docker-compose-v2):
|
||||||
|
# preferred because it also works through the offline apt bundle's mirror and
|
||||||
|
# gets security updates with everything else. On releases without
|
||||||
|
# docker-compose-v2 (22.04), fall back to Docker's official install script.
|
||||||
|
docker_ensure_installed() {
|
||||||
|
if docker_is_installed; then
|
||||||
|
docker_daemon_running || sudo systemctl enable --now docker >/dev/null 2>&1 || true
|
||||||
|
log_success "Docker $(sudo "$DOCKER_BIN" --version 2>/dev/null | awk '{print $3}' | tr -d ,) is installed"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Installing Docker..."
|
||||||
|
if ! sudo apt-get update -qq; then
|
||||||
|
log_error "apt update failed - check the network/package sources and try again"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if apt-cache show docker-compose-v2 >/dev/null 2>&1; then
|
||||||
|
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io docker-compose-v2 \
|
||||||
|
|| { log_error "Installing docker.io / docker-compose-v2 failed"; return 1; }
|
||||||
|
else
|
||||||
|
log_info "This Ubuntu release has no docker-compose-v2 package - using Docker's install script"
|
||||||
|
have_internet 2>/dev/null || { log_error "No internet connection - can't download Docker"; return 1; }
|
||||||
|
local script
|
||||||
|
script=$(mktemp)
|
||||||
|
if ! curl -fsSL https://get.docker.com -o "$script"; then
|
||||||
|
rm -f "$script"
|
||||||
|
log_error "Could not download Docker's install script"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sudo sh "$script" || { rm -f "$script"; log_error "Docker's install script failed"; return 1; }
|
||||||
|
rm -f "$script"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo systemctl enable --now docker >/dev/null 2>&1 || true
|
||||||
|
if ! docker_is_installed || ! docker_daemon_running; then
|
||||||
|
log_error "Docker was installed but is not working - check: sudo systemctl status docker"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
log_success "Docker installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# docker_compose DIR args... -> sudo docker compose --project-directory DIR args...
|
||||||
|
docker_compose() {
|
||||||
|
local dir="$1"; shift
|
||||||
|
sudo "$DOCKER_BIN" compose --project-directory "$dir" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# docker_container_state NAME -> running | exited | restarting | ... | missing
|
||||||
|
docker_container_state() {
|
||||||
|
sudo "$DOCKER_BIN" inspect -f '{{.State.Status}}' "$1" 2>/dev/null || echo "missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
# docker_container_health NAME -> healthy | unhealthy | starting | none | missing
|
||||||
|
docker_container_health() {
|
||||||
|
sudo "$DOCKER_BIN" inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$1" 2>/dev/null \
|
||||||
|
|| echo "missing"
|
||||||
|
}
|
||||||
|
|
||||||
|
# docker_wait_http URL [TIMEOUT_SECONDS]
|
||||||
|
# Polls until URL answers 2xx/3xx; returns 1 on timeout.
|
||||||
|
docker_wait_http() {
|
||||||
|
local url="$1" timeout="${2:-300}" waited=0
|
||||||
|
while (( waited < timeout )); do
|
||||||
|
if curl -fsS -o /dev/null -m 5 "$url" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
waited=$((waited + 5))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# docker_free_port START -> prints the first TCP port >= START nothing listens on.
|
||||||
|
docker_free_port() {
|
||||||
|
local port="$1"
|
||||||
|
while ss -tlnH "sport = :${port}" 2>/dev/null | grep -q .; do
|
||||||
|
port=$((port + 1))
|
||||||
|
done
|
||||||
|
echo "$port"
|
||||||
|
}
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# menus/addon_library.sh - "Library Catalog" addon: a self-hosted library
|
||||||
|
# catalogue (Koha) running in Docker, shown on the kiosk as a public
|
||||||
|
# search terminal.
|
||||||
|
#
|
||||||
|
# What gets installed (all from this checkout's library/ directory, deployed to
|
||||||
|
# $LIBRARY_DIR - see library/README.md for the stack itself):
|
||||||
|
# - Koha: the catalogue ("OPAC", public search, no login) and the staff
|
||||||
|
# interface (cataloguing, Z39.50 lookup, label printing).
|
||||||
|
# - Quick Add: a one-box "scan an ISBN, the book is added" page.
|
||||||
|
# - MariaDB.
|
||||||
|
# Docker itself is installed on demand (lib/docker.sh).
|
||||||
|
#
|
||||||
|
# On the kiosk the catalogue is added as one of the Sites (tabs) with
|
||||||
|
# duration 0 (stay put - it's a search terminal, not a rotating display).
|
||||||
|
# Staff and Quick Add are meant for the librarian's own device; whether they
|
||||||
|
# are reachable from the LAN or only from this kiosk is the "Network access"
|
||||||
|
# choice below. NOTE: Docker publishes ports straight through iptables and
|
||||||
|
# bypasses ufw, so "this kiosk only" is enforced by binding the published
|
||||||
|
# ports to 127.0.0.1 (BIND_ADDR in library/.env), not by firewall rules.
|
||||||
|
#
|
||||||
|
# Depends on: lib/menu.sh, lib/config.sh, lib/docker.sh being sourced first.
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
LIBRARY_CATALOGUE_TAB_NAME="Library catalogue"
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Detection / state
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
library_env_get() {
|
||||||
|
sudo grep -m1 "^$1=" "$LIBRARY_DIR/.env" 2>/dev/null | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//'
|
||||||
|
}
|
||||||
|
|
||||||
|
library_is_installed() {
|
||||||
|
[[ -f "$LIBRARY_DIR/docker-compose.yml" ]] || return 1
|
||||||
|
[[ "$(docker_container_state library-koha)" != "missing" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
library_is_running() {
|
||||||
|
[[ "$(docker_container_state library-koha)" == "running" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
library_catalogue_url() {
|
||||||
|
echo "http://localhost:$(library_env_get OPAC_PORT)/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# True when the kiosk's page list already shows the catalogue.
|
||||||
|
library_tab_exists() {
|
||||||
|
local url idx
|
||||||
|
url="$(library_catalogue_url)"
|
||||||
|
for idx in "${!URLS[@]}"; do
|
||||||
|
[[ "${URLS[$idx]}" == "$url" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
addon_library_status() {
|
||||||
|
if ! library_is_installed; then
|
||||||
|
echo "Library Catalog: not installed"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local state health ip
|
||||||
|
state="$(docker_container_state library-koha)"
|
||||||
|
health="$(docker_container_health library-koha)"
|
||||||
|
ip="$(get_ip_address)"
|
||||||
|
echo "Library Catalog: ${state}${health:+ ($health)}"
|
||||||
|
echo " Catalogue: http://localhost:$(library_env_get OPAC_PORT)/ (shown on the kiosk)"
|
||||||
|
if [[ "$(library_env_get BIND_ADDR)" == "127.0.0.1" ]]; then
|
||||||
|
echo " Network: this kiosk only"
|
||||||
|
else
|
||||||
|
echo " Network: other devices on the LAN can reach it at http://${ip}:$(library_env_get OPAC_PORT)/"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
addon_library_menu_builder() {
|
||||||
|
if library_is_installed; then
|
||||||
|
load_existing_config
|
||||||
|
local tab_label="Show the catalogue on the kiosk"
|
||||||
|
library_tab_exists && tab_label="Stop showing the catalogue on the kiosk"
|
||||||
|
MENU_LABELS=("Addresses and staff login" "$tab_label" "Network access (this kiosk only / LAN)" "Start" "Stop" "Update" "Uninstall")
|
||||||
|
MENU_HANDLERS=(action_library_show_access action_library_toggle_tab action_library_network action_library_start action_library_stop action_library_update action_library_uninstall)
|
||||||
|
else
|
||||||
|
MENU_LABELS=("Install Library Catalog")
|
||||||
|
MENU_HANDLERS=(action_library_install)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
addon_library_menu() {
|
||||||
|
run_menu "LIBRARY CATALOG" addon_library_menu_builder addon_library_status
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Helpers
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# Copy library/ from this checkout to $LIBRARY_DIR. Never overwrites the
|
||||||
|
# deployed .env (passwords) even if a stray one exists in the checkout.
|
||||||
|
library_sync_files() {
|
||||||
|
local src="${SCRIPT_DIR}/library"
|
||||||
|
if [[ ! -f "$src/docker-compose.yml" ]]; then
|
||||||
|
log_error "library/ not found in this checkout ($src) - re-clone or git pull"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
sudo mkdir -p "$LIBRARY_DIR" || return 1
|
||||||
|
tar -C "$src" --exclude=./.env --exclude='*.crt' -cf - . | sudo tar -C "$LIBRARY_DIR" -xf - || return 1
|
||||||
|
sudo chmod 755 "$LIBRARY_DIR/install.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
library_add_tab() {
|
||||||
|
load_existing_config
|
||||||
|
if library_tab_exists; then
|
||||||
|
log_info "The catalogue is already one of the kiosk's pages."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
URLS+=("$(library_catalogue_url)")
|
||||||
|
DURS+=("0")
|
||||||
|
USERS+=("")
|
||||||
|
PASSES+=("")
|
||||||
|
NAMES+=("$LIBRARY_CATALOGUE_TAB_NAME")
|
||||||
|
save_config
|
||||||
|
}
|
||||||
|
|
||||||
|
# Drop every page that points at the catalogue's local address.
|
||||||
|
library_remove_tab() {
|
||||||
|
load_existing_config
|
||||||
|
local url idx
|
||||||
|
url="$(library_catalogue_url)"
|
||||||
|
local -a nu=() nd=() nus=() np=() nn=()
|
||||||
|
for idx in "${!URLS[@]}"; do
|
||||||
|
[[ "${URLS[$idx]}" == "$url" ]] && continue
|
||||||
|
nu+=("${URLS[$idx]}"); nd+=("${DURS[$idx]:-0}"); nus+=("${USERS[$idx]:-}")
|
||||||
|
np+=("${PASSES[$idx]:-}"); nn+=("${NAMES[$idx]:-}")
|
||||||
|
done
|
||||||
|
if [[ "${#nu[@]}" -eq "${#URLS[@]}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
URLS=("${nu[@]}"); DURS=("${nd[@]}"); USERS=("${nus[@]}"); PASSES=("${np[@]}"); NAMES=("${nn[@]}")
|
||||||
|
HOME_TAB_INDEX=-1
|
||||||
|
save_config
|
||||||
|
}
|
||||||
|
|
||||||
|
# library_set_bind ADDR - persist BIND_ADDR in .env and recreate the containers.
|
||||||
|
library_set_bind() {
|
||||||
|
local addr="$1"
|
||||||
|
if sudo grep -q '^BIND_ADDR=' "$LIBRARY_DIR/.env" 2>/dev/null; then
|
||||||
|
sudo sed -i "s|^BIND_ADDR=.*|BIND_ADDR=${addr}|" "$LIBRARY_DIR/.env"
|
||||||
|
else
|
||||||
|
echo "BIND_ADDR=${addr}" | sudo tee -a "$LIBRARY_DIR/.env" >/dev/null
|
||||||
|
fi
|
||||||
|
docker_compose "$LIBRARY_DIR" up -d
|
||||||
|
}
|
||||||
|
|
||||||
|
library_warn_if_low_memory() {
|
||||||
|
local total_mb
|
||||||
|
total_mb=$(awk '/MemTotal/ {printf "%d", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)
|
||||||
|
if [[ "$total_mb" -gt 0 && "$total_mb" -lt 3000 ]]; then
|
||||||
|
log_warning "This machine has about ${total_mb} MB of RAM. The catalogue needs roughly 2 GB on top of the kiosk itself - it may be slow."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Actions
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
action_library_install() {
|
||||||
|
echo
|
||||||
|
echo "This installs a library catalogue (Koha) with Z39.50 book lookup, label"
|
||||||
|
echo "printing and a scan-to-add page. It needs Docker (installed for you if"
|
||||||
|
echo "missing), about 3 GB of disk space and ~2 GB of RAM. The first build takes"
|
||||||
|
echo "5-10 minutes."
|
||||||
|
echo
|
||||||
|
library_warn_if_low_memory
|
||||||
|
|
||||||
|
local library_name
|
||||||
|
library_name=$(ask_text "Library name (shown on the catalogue)" "My Library")
|
||||||
|
[[ -z "$library_name" ]] && library_name="My Library"
|
||||||
|
|
||||||
|
local bind="0.0.0.0"
|
||||||
|
echo
|
||||||
|
echo "Who should be able to open the catalogue, staff interface and Quick Add"
|
||||||
|
echo "page besides this kiosk?"
|
||||||
|
echo " - Yes: any device on your network (use it from your own laptop/phone;"
|
||||||
|
echo " the staff pages are protected by a generated password)."
|
||||||
|
echo " - No: only this kiosk. Safest, but you can only manage the library here."
|
||||||
|
if ! ask_yes_no "Allow other devices on the network?" "y"; then
|
||||||
|
bind="127.0.0.1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker_ensure_installed || { pause; return 1; }
|
||||||
|
library_sync_files || { pause; return 1; }
|
||||||
|
|
||||||
|
echo
|
||||||
|
log_info "Building and starting the stack - this takes a while on the first run..."
|
||||||
|
if ! sudo env LIBRARY_NAME="$library_name" BIND_ADDR="$bind" "$LIBRARY_DIR/install.sh"; then
|
||||||
|
log_error "The library stack failed to start - see: sudo docker logs library-koha"
|
||||||
|
pause
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ask_yes_no "Show the catalogue on the kiosk screen?" "y"; then
|
||||||
|
library_add_tab
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
log_success "Library Catalog installed"
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_library_show_access() {
|
||||||
|
local ip user pass
|
||||||
|
ip="$(get_ip_address)"
|
||||||
|
user="$(library_env_get ADMIN_USER)"
|
||||||
|
pass="$(library_env_get ADMIN_PASSWORD)"
|
||||||
|
echo
|
||||||
|
if [[ "$(library_env_get BIND_ADDR)" == "127.0.0.1" ]]; then
|
||||||
|
ip="localhost"
|
||||||
|
echo "(Network access is limited to this kiosk.)"
|
||||||
|
fi
|
||||||
|
echo " Catalogue (public search): http://${ip}:$(library_env_get OPAC_PORT)/"
|
||||||
|
echo " Staff interface: http://${ip}:$(library_env_get STAFF_PORT)/"
|
||||||
|
echo " Add books (scan ISBNs): http://${ip}:$(library_env_get QUICKADD_PORT)/"
|
||||||
|
echo " Login (staff and add books): ${user} / ${pass}"
|
||||||
|
echo
|
||||||
|
echo " Passwords live in $LIBRARY_DIR/.env (root only)."
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_library_toggle_tab() {
|
||||||
|
load_existing_config
|
||||||
|
if library_tab_exists; then
|
||||||
|
library_remove_tab
|
||||||
|
else
|
||||||
|
library_add_tab
|
||||||
|
fi
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_library_network() {
|
||||||
|
local current
|
||||||
|
current="$(library_env_get BIND_ADDR)"
|
||||||
|
echo
|
||||||
|
echo "Currently: $([[ "$current" == "127.0.0.1" ]] && echo "this kiosk only" || echo "other devices on the network can connect")"
|
||||||
|
echo
|
||||||
|
if ask_yes_no "Allow other devices on the network?" "$([[ "$current" == "127.0.0.1" ]] && echo n || echo y)"; then
|
||||||
|
library_set_bind "0.0.0.0" && log_success "Other devices on the network can now connect"
|
||||||
|
else
|
||||||
|
library_set_bind "127.0.0.1" && log_success "Only this kiosk can connect now"
|
||||||
|
fi
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_library_start() {
|
||||||
|
docker_compose "$LIBRARY_DIR" start && log_success "Started"
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_library_stop() {
|
||||||
|
docker_compose "$LIBRARY_DIR" stop && log_success "Stopped (the kiosk page will show an error until you start it again)"
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
action_library_update() {
|
||||||
|
echo
|
||||||
|
echo "This rebuilds the images with the latest Koha release. Your catalogue data is kept."
|
||||||
|
if ! ask_yes_no "Update now?" "y"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
docker_ensure_installed || { pause; return 1; }
|
||||||
|
library_sync_files || { pause; return 1; }
|
||||||
|
if sudo "$LIBRARY_DIR/install.sh" --update; then
|
||||||
|
log_success "Updated"
|
||||||
|
else
|
||||||
|
log_error "Update failed - see: sudo docker logs library-koha"
|
||||||
|
fi
|
||||||
|
pause
|
||||||
|
}
|
||||||
|
|
||||||
|
# library_do_uninstall [purge]
|
||||||
|
# no argument: stop and remove the containers but KEEP the data volumes and
|
||||||
|
# $LIBRARY_DIR/.env - the database password is baked into the
|
||||||
|
# database volume, so a later reinstall needs the same .env.
|
||||||
|
# purge: also delete the data (all books!), the images and $LIBRARY_DIR.
|
||||||
|
library_do_uninstall() {
|
||||||
|
local mode="${1:-}"
|
||||||
|
library_remove_tab
|
||||||
|
if [[ -f "$LIBRARY_DIR/docker-compose.yml" ]]; then
|
||||||
|
if [[ "$mode" == "purge" ]]; then
|
||||||
|
docker_compose "$LIBRARY_DIR" down -v --rmi local 2>/dev/null || true
|
||||||
|
else
|
||||||
|
docker_compose "$LIBRARY_DIR" down 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ "$mode" == "purge" ]]; then
|
||||||
|
sudo rm -rf "$LIBRARY_DIR"
|
||||||
|
log_success "Library Catalog and all its data removed"
|
||||||
|
else
|
||||||
|
log_success "Library Catalog stopped and removed (your books are kept)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
action_library_uninstall() {
|
||||||
|
echo
|
||||||
|
echo "This stops and removes the catalogue containers."
|
||||||
|
if ask_yes_no "Also DELETE all catalogue data (every book you have added)?" "n"; then
|
||||||
|
local confirm
|
||||||
|
confirm=$(ask_text "Type DELETE to confirm" "")
|
||||||
|
if [[ "$confirm" != "DELETE" ]]; then
|
||||||
|
echo "Cancelled"
|
||||||
|
pause
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
library_do_uninstall purge
|
||||||
|
else
|
||||||
|
library_do_uninstall
|
||||||
|
echo "Your books are kept; install again from this menu to bring them back."
|
||||||
|
fi
|
||||||
|
pause
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ clone_detect_addons() {
|
|||||||
command -v netbird &>/dev/null && addons+=("netbird")
|
command -v netbird &>/dev/null && addons+=("netbird")
|
||||||
baresip_is_installed 2>/dev/null && addons+=("asterisk_intercom")
|
baresip_is_installed 2>/dev/null && addons+=("asterisk_intercom")
|
||||||
webui_is_installed 2>/dev/null && addons+=("webui")
|
webui_is_installed 2>/dev/null && addons+=("webui")
|
||||||
|
library_is_installed 2>/dev/null && addons+=("library")
|
||||||
|
|
||||||
if sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then
|
if sudo -u "$KIOSK_USER" test -f "$CONFIG_PATH" 2>/dev/null; then
|
||||||
local authelia_url
|
local authelia_url
|
||||||
@@ -243,6 +244,13 @@ action_apply_clone_settings() {
|
|||||||
fi ;;
|
fi ;;
|
||||||
asterisk_intercom)
|
asterisk_intercom)
|
||||||
echo " [!] Asterisk Intercom - needs its own SIP extension on this device" ;;
|
echo " [!] Asterisk Intercom - needs its own SIP extension on this device" ;;
|
||||||
|
library)
|
||||||
|
if library_is_installed; then
|
||||||
|
echo " [x] Library Catalog - already installed (the books themselves are not cloned)"
|
||||||
|
else
|
||||||
|
echo " [ ] Library Catalog - not installed, install via Addons (the catalogue page is cloned, the books are not)"
|
||||||
|
missing_any=true
|
||||||
|
fi ;;
|
||||||
authelia)
|
authelia)
|
||||||
echo " [!] Authelia - needs a fresh login on this device" ;;
|
echo " [!] Authelia - needs a fresh login on this device" ;;
|
||||||
*)
|
*)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# (cups_do_uninstall, vnc_do_uninstall, wireguard_do_uninstall,
|
# (cups_do_uninstall, vnc_do_uninstall, wireguard_do_uninstall,
|
||||||
# tailscale_do_uninstall, netbird_do_uninstall, lms_do_uninstall,
|
# tailscale_do_uninstall, netbird_do_uninstall, lms_do_uninstall,
|
||||||
# squeezelite_do_uninstall, asterisk_intercom_do_uninstall,
|
# squeezelite_do_uninstall, asterisk_intercom_do_uninstall,
|
||||||
# webui_do_uninstall, power_schedule_do_remove_all,
|
# webui_do_uninstall, library_do_uninstall, power_schedule_do_remove_all,
|
||||||
# emergency_hotspot_do_disable, time_sync_do_reset) instead of
|
# emergency_hotspot_do_disable, time_sync_do_reset) instead of
|
||||||
# re-implementing removal logic for each addon a second time here - if an
|
# re-implementing removal logic for each addon a second time here - if an
|
||||||
# addon's uninstall logic changes, this picks it up automatically. Only
|
# addon's uninstall logic changes, this picks it up automatically. Only
|
||||||
@@ -59,6 +59,7 @@ action_complete_uninstall() {
|
|||||||
echo " • Remote access (VNC, WireGuard, Tailscale, Netbird)"
|
echo " • Remote access (VNC, WireGuard, Tailscale, Netbird)"
|
||||||
echo " • Asterisk Intercom (Baresip)"
|
echo " • Asterisk Intercom (Baresip)"
|
||||||
echo " • Web UI"
|
echo " • Web UI"
|
||||||
|
echo " • Library Catalog (Koha) and ALL the books in it"
|
||||||
echo " • LightDM and Openbox"
|
echo " • LightDM and Openbox"
|
||||||
echo " • All kiosk schedules and services"
|
echo " • All kiosk schedules and services"
|
||||||
echo " • Emergency hotspot configuration"
|
echo " • Emergency hotspot configuration"
|
||||||
@@ -90,6 +91,7 @@ action_complete_uninstall() {
|
|||||||
squeezelite_do_uninstall
|
squeezelite_do_uninstall
|
||||||
asterisk_intercom_do_uninstall purge
|
asterisk_intercom_do_uninstall purge
|
||||||
webui_do_uninstall
|
webui_do_uninstall
|
||||||
|
library_do_uninstall purge
|
||||||
|
|
||||||
echo "[3/12] Removing schedules, emergency hotspot and time server setting..."
|
echo "[3/12] Removing schedules, emergency hotspot and time server setting..."
|
||||||
power_schedule_do_remove_all
|
power_schedule_do_remove_all
|
||||||
|
|||||||
Executable
+203
@@ -0,0 +1,203 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
################################################################################
|
||||||
|
# tests/addon_library.test.sh - tests for menus/addon_library.sh + lib/docker.sh
|
||||||
|
#
|
||||||
|
# Runs entirely in a scratch directory with a stub `sudo`, a stub `docker` and
|
||||||
|
# a fixture stack standing in for library/, so it needs no root, no Docker and
|
||||||
|
# no network - and cannot touch a real kiosk. (The real stack is exercised by
|
||||||
|
# library/install.sh itself.)
|
||||||
|
#
|
||||||
|
# bash tests/addon_library.test.sh
|
||||||
|
################################################################################
|
||||||
|
set -u
|
||||||
|
|
||||||
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
T="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$T"' EXIT
|
||||||
|
|
||||||
|
PASS=0 FAIL=0
|
||||||
|
ok() { PASS=$((PASS + 1)); echo " ok - $1"; }
|
||||||
|
bad() { FAIL=$((FAIL + 1)); echo " FAIL - $1"; }
|
||||||
|
check() { # check "description" command...
|
||||||
|
local d="$1"; shift
|
||||||
|
if "$@" >/dev/null 2>&1; then ok "$d"; else bad "$d"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- stubs --------------------------------------------------------------------
|
||||||
|
mkdir -p "$T/bin"
|
||||||
|
cat > "$T/bin/sudo" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Stub sudo: drop "-u USER" and run the command as-is.
|
||||||
|
if [[ "$1" == "-u" ]]; then shift 2; fi
|
||||||
|
exec "$@"
|
||||||
|
EOF
|
||||||
|
cat > "$T/bin/docker" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Stub docker: logs every call; container state comes from $STUB_STATE.
|
||||||
|
echo "$*" >> "$STUB_LOG"
|
||||||
|
case "$1" in
|
||||||
|
--version) echo "Docker version 99.0.0, build stub"; exit 0 ;;
|
||||||
|
info) exit 0 ;;
|
||||||
|
compose)
|
||||||
|
[[ "$2" == "version" ]] && { echo "Docker Compose version v9"; exit 0; }
|
||||||
|
exit 0 ;;
|
||||||
|
inspect)
|
||||||
|
st="$(cat "$STUB_STATE" 2>/dev/null || echo missing)"
|
||||||
|
[[ "$st" == "missing" ]] && exit 1
|
||||||
|
case "$*" in *Health*) echo healthy ;; *) echo "$st" ;; esac
|
||||||
|
exit 0 ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$T/bin/sudo" "$T/bin/docker"
|
||||||
|
export PATH="$T/bin:$PATH"
|
||||||
|
export STUB_LOG="$T/docker.log" STUB_STATE="$T/state"
|
||||||
|
echo missing > "$STUB_STATE"; : > "$STUB_LOG"
|
||||||
|
|
||||||
|
# ---- fixture "checkout" with a stub library stack ----------------------------
|
||||||
|
FIX="$T/checkout"
|
||||||
|
mkdir -p "$FIX/library/koha/certs"
|
||||||
|
echo "name: library" > "$FIX/library/docker-compose.yml"
|
||||||
|
echo "SHOULD-NOT-BE-COPIED" > "$FIX/library/.env"
|
||||||
|
echo "cert" > "$FIX/library/koha/certs/proxy.crt"
|
||||||
|
echo "koha-file" > "$FIX/library/koha/Dockerfile"
|
||||||
|
cat > "$FIX/library/install.sh" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Stub of library/install.sh: records how it was called and writes a .env.
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
echo "install.sh $* LIBRARY_NAME=${LIBRARY_NAME:-} BIND_ADDR=${BIND_ADDR:-}" >> "$STUB_LOG"
|
||||||
|
[[ "${1:-}" == "--update" ]] && exit 0
|
||||||
|
cat > "$HERE/.env" << ENV
|
||||||
|
LIBRARY_NAME="${LIBRARY_NAME:-}"
|
||||||
|
OPAC_PORT=8080
|
||||||
|
STAFF_PORT=8081
|
||||||
|
QUICKADD_PORT=8082
|
||||||
|
ADMIN_USER=admin
|
||||||
|
ADMIN_PASSWORD=s3cretPASS
|
||||||
|
BIND_ADDR=${BIND_ADDR:-0.0.0.0}
|
||||||
|
ENV
|
||||||
|
echo running > "$STUB_STATE"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ---- environment the addon expects -------------------------------------------
|
||||||
|
export KIOSK_USER="$(id -un)"
|
||||||
|
export KIOSK_HOME="$T/home" KIOSK_DIR="$T/home/kiosk-app" CONFIG_PATH="$T/home/kiosk-app/config.json"
|
||||||
|
export LIBRARY_DIR="$T/opt-library"
|
||||||
|
SCRIPT_DIR="$FIX"
|
||||||
|
mkdir -p "$KIOSK_DIR"
|
||||||
|
cat > "$CONFIG_PATH" << 'EOF'
|
||||||
|
{"tabs":[{"url":"https://example.com","duration":60,"username":"","password":"","name":"Example"}],
|
||||||
|
"homeTabIndex":-1,"inactivityTimeout":120,"swipeMode":"dual","allowNavigation":"same-origin","somethingUntracked":"keep-me"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$REPO/lib/menu.sh"
|
||||||
|
source "$REPO/lib/config.sh"
|
||||||
|
source "$REPO/lib/docker.sh"
|
||||||
|
source "$REPO/menus/addon_library.sh"
|
||||||
|
export KIOSK_UNATTENDED=0
|
||||||
|
docker_ensure_installed() { return 0; } # the stub stands in for a working Docker
|
||||||
|
|
||||||
|
tabs() { jq -r '.tabs[] | "\(.url)|\(.duration)|\(.name)"' "$CONFIG_PATH"; }
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== lib/docker.sh"
|
||||||
|
check "docker_is_installed sees the stub" docker_is_installed
|
||||||
|
check "container state: missing" test "$(docker_container_state library-koha)" = missing
|
||||||
|
echo running > "$STUB_STATE"
|
||||||
|
check "container state: running" test "$(docker_container_state library-koha)" = running
|
||||||
|
check "container health: healthy" test "$(docker_container_health library-koha)" = healthy
|
||||||
|
echo missing > "$STUB_STATE"
|
||||||
|
check "docker_free_port returns a number" test -n "$(docker_free_port 20000)"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== sync files"
|
||||||
|
mkdir -p "$LIBRARY_DIR"; echo "DEPLOYED-ENV" > "$LIBRARY_DIR/.env"
|
||||||
|
library_sync_files
|
||||||
|
check "copies the stack files" test -f "$LIBRARY_DIR/koha/Dockerfile"
|
||||||
|
check "never overwrites a deployed .env" test "$(cat "$LIBRARY_DIR/.env")" = DEPLOYED-ENV
|
||||||
|
check "does not copy certificates" test ! -e "$LIBRARY_DIR/koha/certs/proxy.crt"
|
||||||
|
rm -rf "$LIBRARY_DIR"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== not installed"
|
||||||
|
if library_is_installed; then bad "library_is_installed false before install"; else ok "library_is_installed false before install"; fi
|
||||||
|
addon_library_menu_builder
|
||||||
|
check "offers only Install" test "${MENU_LABELS[*]}" = "Install Library Catalog"
|
||||||
|
check "status says not installed" test "$(addon_library_status)" = "Library Catalog: not installed"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== install action (kiosk-only network, show on kiosk)"
|
||||||
|
# answers: library name, allow-network=n, show-on-kiosk=y, <Enter> for pause
|
||||||
|
printf 'Smith Family Library\nn\ny\n\n' | action_library_install >"$T/install.out" 2>&1
|
||||||
|
check "install.sh got the library name" grep -q 'LIBRARY_NAME=Smith Family Library' "$STUB_LOG"
|
||||||
|
check "install.sh got BIND_ADDR=127.0.0.1" grep -q 'BIND_ADDR=127.0.0.1' "$STUB_LOG"
|
||||||
|
check "is_installed is now true" library_is_installed
|
||||||
|
check "catalogue tab added, duration 0" grep -qx 'http://localhost:8080/|0|Library catalogue' <(tabs)
|
||||||
|
check "existing tab preserved" grep -qx 'https://example.com|60|Example' <(tabs)
|
||||||
|
check "untracked config keys preserved" test "$(jq -r .somethingUntracked "$CONFIG_PATH")" = keep-me
|
||||||
|
addon_library_status > "$T/status.out"
|
||||||
|
check "status shows 'this kiosk only'" grep -q 'this kiosk only' "$T/status.out"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== menu while installed"
|
||||||
|
addon_library_menu_builder
|
||||||
|
check "offers 'stop showing' (tab exists)" grep -q 'Stop showing the catalogue' <<< "${MENU_LABELS[*]}"
|
||||||
|
check "has Update and Uninstall" grep -q 'Update.*Uninstall' <<< "${MENU_LABELS[*]}"
|
||||||
|
|
||||||
|
echo "== tab toggling is idempotent"
|
||||||
|
library_add_tab >/dev/null 2>&1
|
||||||
|
check "adding again does not duplicate" test "$(tabs | grep -c 'localhost:8080')" = 1
|
||||||
|
printf '\n' | action_library_toggle_tab >/dev/null 2>&1 # now removes it
|
||||||
|
check "toggle removes the catalogue tab" test "$(tabs | grep -c 'localhost:8080')" = 0
|
||||||
|
check "toggle keeps the other tab" grep -qx 'https://example.com|60|Example' <(tabs)
|
||||||
|
addon_library_menu_builder
|
||||||
|
check "menu now offers 'Show'" grep -q 'Show the catalogue on the kiosk' <<< "${MENU_LABELS[*]}"
|
||||||
|
printf '\n' | action_library_toggle_tab >/dev/null 2>&1 # and adds it back
|
||||||
|
check "toggle adds it back" test "$(tabs | grep -c 'localhost:8080')" = 1
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== network access"
|
||||||
|
: > "$STUB_LOG"
|
||||||
|
printf 'y\n\n' | action_library_network >/dev/null 2>&1 # allow network
|
||||||
|
check ".env BIND_ADDR is now 0.0.0.0" test "$(library_env_get BIND_ADDR)" = 0.0.0.0
|
||||||
|
check "containers were recreated" grep -q 'compose --project-directory .* up -d' "$STUB_LOG"
|
||||||
|
printf 'n\n\n' | action_library_network >/dev/null 2>&1 # back to kiosk-only
|
||||||
|
check ".env BIND_ADDR back to 127.0.0.1" test "$(library_env_get BIND_ADDR)" = 127.0.0.1
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== access screen"
|
||||||
|
printf '\n' | action_library_show_access > "$T/access.out" 2>&1
|
||||||
|
check "shows the staff login" grep -q 'admin / s3cretPASS' "$T/access.out"
|
||||||
|
check "shows quick-add address" grep -q 'Add books.*:8082' "$T/access.out"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== update"
|
||||||
|
: > "$STUB_LOG"
|
||||||
|
printf 'y\n\n' | action_library_update >/dev/null 2>&1
|
||||||
|
check "runs install.sh --update" grep -q 'install.sh --update' "$STUB_LOG"
|
||||||
|
check "update keeps the .env" test "$(library_env_get ADMIN_PASSWORD)" = s3cretPASS
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
echo "== uninstall keeps the books by default"
|
||||||
|
: > "$STUB_LOG"
|
||||||
|
printf 'n\n\n' | action_library_uninstall >/dev/null 2>&1
|
||||||
|
check "compose down without -v" grep -qE 'compose .* down$' "$STUB_LOG"
|
||||||
|
check "no volume deletion" bash -c "! grep -q -- ' -v' '$STUB_LOG'"
|
||||||
|
check "install dir and .env kept" test -f "$LIBRARY_DIR/.env"
|
||||||
|
check "catalogue tab removed" test "$(tabs | grep -c 'localhost:8080')" = 0
|
||||||
|
|
||||||
|
echo "== purge needs the typed confirmation"
|
||||||
|
printf 'y\nnope\n\n' | action_library_uninstall >/dev/null 2>&1
|
||||||
|
check "wrong confirmation cancels" test -f "$LIBRARY_DIR/.env"
|
||||||
|
: > "$STUB_LOG"
|
||||||
|
library_add_tab >/dev/null 2>&1
|
||||||
|
printf 'y\nDELETE\n\n' | action_library_uninstall >/dev/null 2>&1
|
||||||
|
check "compose down -v --rmi local" grep -q 'down -v --rmi local' "$STUB_LOG"
|
||||||
|
check "install dir removed" test ! -e "$LIBRARY_DIR"
|
||||||
|
check "tab removed again" test "$(tabs | grep -c 'localhost:8080')" = 0
|
||||||
|
check "other tab still there" grep -qx 'https://example.com|60|Example' <(tabs)
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "passed: $PASS failed: $FAIL"
|
||||||
|
[[ "$FAIL" -eq 0 ]]
|
||||||
Reference in New Issue
Block a user