diff --git a/libre_translate.sh b/libre_translate.sh new file mode 100755 index 0000000..502eef6 --- /dev/null +++ b/libre_translate.sh @@ -0,0 +1,381 @@ +#!/usr/bin/env bash +set -euo pipefail + +# LibreTranslate full setup script (Docker-based) +# Features: +# - Installs Docker if missing (optional --no-docker-install) +# - Pulls libretranslate image (tag configurable) +# - Creates persistent data + cache directories +# - Optionally pre-downloads language models +# - Generates or accepts an API key; can disable auth +# - (Removed) systemd service setup – now always ephemeral +# - Health check + sample translation +# - Uninstall mode removes container, image, service, and data (optional keep data) +# - Idempotent: safe to re-run for upgrades (will pull newer image) + +SCRIPT_NAME=$(basename "$0") +VERSION="1.0.0" + +# Defaults +IMAGE="libretranslate/libretranslate" +TAG="latest" +SERVICE_NAME="libretranslate" +DOCKER_INSTALL=1 +# Systemd removed – always run ephemeral container +API_KEY="" +GENERATE_API_KEY=1 +DISABLE_API_KEY=0 +PORT=5000 +HOST=0.0.0.0 +DATA_DIR="/var/lib/libretranslate" +CACHE_DIR="${DATA_DIR}/cache" +CONFIG_DIR="/etc/libretranslate" +ENV_FILE="${CONFIG_DIR}/libretranslate.env" +PULL_ONLY=0 +PRELOAD_LANGS="" +UNINSTALL=0 +KEEP_DATA=0 +HEALTH_TIMEOUT=15 +EXTRA_ENV=() +NO_COLOR=0 +KEEP_ALIVE=0 +RUN_COMMAND=() +DEBUG=0 + +# Colors +if [[ -t 1 && ${NO_COLOR} -eq 0 ]]; then + GREEN="\e[32m"; YELLOW="\e[33m"; RED="\e[31m"; BLUE="\e[34m"; BOLD="\e[1m"; RESET="\e[0m" +else + GREEN=""; YELLOW=""; RED=""; BLUE=""; BOLD=""; RESET="" +fi + +log() { echo -e "${BLUE}[INFO]${RESET} $*"; } +warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; } +err() { echo -e "${RED}[ERR ]${RESET} $*" >&2; } +success() { echo -e "${GREEN}[OK ]${RESET} $*"; } + +usage() { + cat </dev/null 2>&1; then + key=$(openssl rand -base64 48 | tr -dc 'A-Za-z0-9' | head -c 40 || true) + fi + fi + if [[ -z $key || ${#key} -lt 20 ]]; then + # Last resort static warning key (should not happen) + key="LT$(date +%s)$$RANDOM" + fi + printf '%s' "$key" +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || { err "Required command '$1' not found"; return 1; } +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --image) IMAGE="$2"; shift 2;; + --tag) TAG="$2"; shift 2;; + --port) PORT="$2"; shift 2;; + --host) HOST="$2"; shift 2;; + --data-dir) DATA_DIR="$2"; CACHE_DIR="${DATA_DIR}/cache"; shift 2;; + --cache-dir) CACHE_DIR="$2"; shift 2;; + --no-docker-install) DOCKER_INSTALL=0; shift;; + --keep-alive) KEEP_ALIVE=1; shift;; + --) shift; RUN_COMMAND=("$@"); break;; + --api-key) API_KEY="$2"; GENERATE_API_KEY=0; shift 2;; + --generate-api-key) GENERATE_API_KEY=1; shift;; + --disable-api-key) DISABLE_API_KEY=1; shift;; + --preload-langs) PRELOAD_LANGS="$2"; shift 2;; + --env) EXTRA_ENV+=("$2"); shift 2;; + --pull-only) PULL_ONLY=1; shift;; + --uninstall) UNINSTALL=1; shift;; + --purge) UNINSTALL=1; KEEP_DATA=0; shift;; + --keep-data) KEEP_DATA=1; shift;; + --health-timeout) HEALTH_TIMEOUT="$2"; shift 2;; + --no-color) NO_COLOR=1; shift;; + --debug) DEBUG=1; shift;; + -h|--help) usage; exit 0;; + -v|--version) echo "${VERSION}"; exit 0;; + *) err "Unknown argument: $1"; usage; exit 1;; + esac + done +} + +ensure_root() { + if [[ $EUID -ne 0 ]]; then + err "This script must run as root (or via sudo)."; exit 1 + fi +} + +install_docker() { + if command -v docker >/dev/null 2>&1; then + log "Docker already installed" + return 0 + fi + if [[ ${DOCKER_INSTALL} -eq 0 ]]; then + err "Docker is not installed and --no-docker-install specified."; exit 1 + fi + log "Installing Docker..." + if command -v apt-get >/dev/null 2>&1; then + apt-get update -y + apt-get install -y ca-certificates curl gnupg + install -d -m 0755 /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo \ +"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(. /etc/os-release; echo "$VERSION_CODENAME") stable" \ + > /etc/apt/sources.list.d/docker.list + apt-get update -y + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + else + err "Unsupported package manager. Please install Docker manually."; exit 1 + fi + # Attempt to start docker daemon if dockerd exists and systemctl available; otherwise rely on user + if command -v systemctl >/dev/null 2>&1; then + (systemctl enable --now docker 2>/dev/null && success "Docker installed and started") || warn "Docker installed; ensure dockerd is running" + else + warn "Docker installed; please ensure docker daemon is running" + fi +} + +pull_image() { + log "Pulling image ${IMAGE}:${TAG}" + docker pull "${IMAGE}:${TAG}" + success "Image pulled" +} + +detect_container_user() { + # Determine uid/gid of configured user inside image so host dirs can be chowned + if ! command -v docker >/dev/null 2>&1; then + return 0 + fi + local uid gid + uid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -u 2>/dev/null || echo "") + gid=$(docker run --rm --entrypoint /usr/bin/id "${IMAGE}:${TAG}" -g 2>/dev/null || echo "") + if [[ -n $uid && -n $gid ]]; then + CONTAINER_UID=$uid + CONTAINER_GID=$gid + fi +} + +write_env_file() { + mkdir -p "${CONFIG_DIR}" "${DATA_DIR}" "${CACHE_DIR}" + detect_container_user + if [[ -n ${CONTAINER_UID:-} && -n ${CONTAINER_GID:-} ]]; then + if command -v stat >/dev/null 2>&1; then + for d in "${DATA_DIR}" "${CACHE_DIR}"; do + if [[ -d $d ]]; then + CUR_UID=$(stat -c %u "$d" 2>/dev/null || echo -1) + if [[ ${CUR_UID} -ne ${CONTAINER_UID} ]]; then + chown ${CONTAINER_UID}:${CONTAINER_GID} "$d" 2>/dev/null || warn "Unable to chown $d to ${CONTAINER_UID}:${CONTAINER_GID}" + fi + fi + done + fi + fi + if [[ ${DISABLE_API_KEY} -eq 1 ]]; then + API_KEY_LINE="LT_NO_API_KEY=true" + else + if [[ -z ${API_KEY} && ${GENERATE_API_KEY} -eq 1 ]]; then + API_KEY=$(gen_api_key) + GENERATED=1 + else + GENERATED=0 + fi + API_KEY_LINE="LT_API_KEYS=${API_KEY}" + fi + + { echo "# LibreTranslate environment file"; echo "# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)"; echo "${API_KEY_LINE}"; + [[ -n ${PRELOAD_LANGS} ]] && echo "LT_PRELOAD_LANGS=${PRELOAD_LANGS}"; + for kv in "${EXTRA_ENV[@]:-}"; do echo "$kv"; done; } > "${ENV_FILE}.tmp" + mv "${ENV_FILE}.tmp" "${ENV_FILE}" + chmod 600 "${ENV_FILE}" + success "Environment file written: ${ENV_FILE}" +} + +start_container_ephemeral() { + log "Starting ephemeral container..." + docker rm -f "${SERVICE_NAME}" >/dev/null 2>&1 || true + docker run -d --name "${SERVICE_NAME}" \ + --env-file "${ENV_FILE}" \ + -v "${DATA_DIR}:/home/libretranslate/.local/share/argos-translate" \ + -v "${CACHE_DIR}:/app/cache" \ + -p "${PORT}:${PORT}" \ + "${IMAGE}:${TAG}" \ + --host 0.0.0.0 --port ${PORT} + success "Container started (ephemeral)" + echo + echo "Endpoint (pending readiness): http://$(hostname -I | awk '{print $1}'):${PORT}" + echo "Waiting for health..." +} + +health_check() { + local start=$(date +%s) + local url="http://127.0.0.1:${PORT}/languages" + local attempt=0 + while true; do + attempt=$((attempt+1)) + if curl ${DEBUG:+-v} -fsS "$url" >/dev/null 2>&1; then + success "Service healthy (attempt $attempt)" + return 0 + else + [[ $DEBUG -eq 1 ]] && log "Health attempt $attempt failed" + fi + if (( $(date +%s) - start > HEALTH_TIMEOUT )); then + err "Health check failed after ${HEALTH_TIMEOUT}s (attempts: $attempt)" + docker logs --tail 200 "${SERVICE_NAME}" || true + return 1 + fi + sleep 0.5 + done +} + +sample_request() { + if [[ ${DISABLE_API_KEY} -eq 0 ]]; then + local key="${API_KEY}" + else + local key="" + fi + log "Performing sample translation (en->es)..." + local DATA='{"q":"Hello world","source":"en","target":"es","format":"text"}' + if [[ -n $key ]]; then + curl -fsS -H "Content-Type: application/json" -H "Authorization: ${key}" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" + else + curl -fsS -H "Content-Type: application/json" -d "$DATA" "http://127.0.0.1:${PORT}/translate" || warn "Sample request failed" + fi + echo +} + +uninstall_all() { + log "Uninstalling LibreTranslate (ephemeral mode)..." + docker rm -f "${SERVICE_NAME}" 2>/dev/null || true + docker rmi "${IMAGE}:${TAG}" 2>/dev/null || true + if [[ ${KEEP_DATA} -eq 0 ]]; then + rm -rf "${DATA_DIR}" "${CONFIG_DIR}" || true + success "Data directories removed" + else + log "Data kept in ${DATA_DIR} and ${CONFIG_DIR}" + fi + success "Uninstall complete" + exit 0 +} + +main() { + parse_args "$@" + ensure_root + + if [[ ${UNINSTALL} -eq 1 ]]; then + uninstall_all + fi + + install_docker + pull_image + if [[ ${PULL_ONLY} -eq 1 ]]; then + log "Pull-only requested, exiting." + exit 0 + fi + + write_env_file + + # Always ephemeral now + start_container_ephemeral + + health_check + sample_request || true + + # If a command is provided, run it and then shutdown container + if [[ ${#RUN_COMMAND[@]} -gt 0 ]]; then + log "Running user command: ${RUN_COMMAND[*]}" + set +e + "${RUN_COMMAND[@]}" + CMD_STATUS=$? + set -e + log "Command exited with status ${CMD_STATUS}; stopping container" + docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true + exit ${CMD_STATUS} + fi + + if [[ ${KEEP_ALIVE} -eq 1 ]]; then + log "Tailing logs (Ctrl-C to stop and remove container)" + trap 'log "Stopping container"; docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true; exit 0' INT TERM + docker logs -f "${SERVICE_NAME}" + log "Logs ended; stopping container" + docker stop "${SERVICE_NAME}" >/dev/null 2>&1 || true + else + log "Ephemeral container left running in background (id: $(docker inspect --format '{{.Id}}' ${SERVICE_NAME} 2>/dev/null || echo unknown))" + log "Stop manually with: docker stop ${SERVICE_NAME}" + fi + + echo + echo "${BOLD}LibreTranslate is ready.${RESET}" + echo "Endpoint: http://$(hostname -I | awk '{print $1}'):${PORT}" + if [[ ${DISABLE_API_KEY} -eq 0 ]]; then + if [[ ${GENERATED:-0} -eq 1 ]]; then + echo "Generated API key: ${API_KEY}" + else + echo "API key: ${API_KEY}" + fi + echo "Use header: Authorization: " + else + echo "API key authentication DISABLED (public instance)." + fi + [[ -n ${PRELOAD_LANGS} ]] && echo "Preloaded languages requested: ${PRELOAD_LANGS}" || true + echo "Environment file: ${ENV_FILE}" + echo "Manage: docker logs -f ${SERVICE_NAME} | docker stop ${SERVICE_NAME}" + echo "Uninstall: sudo ${SCRIPT_NAME} --uninstall" + echo +} + +main "$@" +