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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user