#!/usr/bin/env bash set -Eeuo pipefail DEFAULT_INSTALL_DIR="/srv/Archivyn" POSTGRES_IMAGE="postgres:17" # Allows prompts when run through: # # curl -fsSL https://.../install.sh | sudo bash # exec 3&2 exit 1 } prompt_absolute_path() { local prompt_text="$1" local default_value="$2" local entered_value while true; do read -r -u 3 -p \ "$prompt_text [$default_value]: " \ entered_value entered_value="${entered_value:-$default_value}" if [[ "$entered_value" != /* ]]; then echo \ "Enter an absolute path beginning with /." \ >&2 continue fi # Remove trailing slashes. while [[ "$entered_value" != "/" && "$entered_value" == */ ]] do entered_value="${entered_value%/}" done # Prevent obviously dangerous installation paths. case "$entered_value" in /|\ /bin|\ /boot|\ /dev|\ /etc|\ /home|\ /lib|\ /lib64|\ /media|\ /mnt|\ /opt|\ /proc|\ /root|\ /run|\ /sbin|\ /srv|\ /sys|\ /tmp|\ /usr|\ /var) echo \ "Choose a dedicated Archivyn directory, such as /srv/Archivyn." \ >&2 continue ;; esac printf '%s' "$entered_value" return done } prompt_port() { local default_value="$1" local entered_value while true; do read -r -u 3 -p \ "Archivyn web port [$default_value]: " \ entered_value entered_value="${entered_value:-$default_value}" if [[ "$entered_value" =~ ^[0-9]+$ ]] && (( entered_value >= 1 && entered_value <= 65535 )) then printf '%s' "$entered_value" return fi echo \ "Enter a port between 1 and 65535." \ >&2 done } prompt_database_identifier() { local prompt_text="$1" local default_value="$2" local entered_value while true; do read -r -u 3 -p \ "$prompt_text [$default_value]: " \ entered_value entered_value="${entered_value:-$default_value}" if [[ "$entered_value" =~ ^[a-z_][a-z0-9_]*$ ]]; then printf '%s' "$entered_value" return fi echo \ "Use lowercase letters, numbers, and underscores only." \ >&2 done } create_password() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 32 else head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' fi } container_exists() { docker container inspect "$1" >/dev/null 2>&1 } directory_has_files() { local directory="$1" [[ -d "$directory" ]] || return 1 [[ -n "$( find \ "$directory" \ -mindepth 1 \ -print \ -quit \ 2>/dev/null )" ]] } if [[ "$EUID" -ne 0 ]]; then fail "run this installer with sudo." fi if [[ ! -r /dev/tty ]]; then fail "an interactive terminal is required." fi if ! command -v docker >/dev/null 2>&1; then fail "Docker is not installed." fi if ! docker compose version >/dev/null 2>&1; then fail "Docker Compose v2 is not installed." fi INSTALL_USER="${SUDO_USER:-root}" INSTALL_GROUP="$(id -gn "$INSTALL_USER")" echo echo "Archivyn Installer" echo INSTALL_DIR="$( prompt_absolute_path \ "Archivyn installation directory" \ "$DEFAULT_INSTALL_DIR" )" COMPOSE_FILE="$INSTALL_DIR/compose.yml" # Filename generated by previous installer versions. OLD_COMPOSE_FILE="$INSTALL_DIR/compose.yaml" ENV_FILE="$INSTALL_DIR/.env" DATA_DIR="$INSTALL_DIR/data" DOCUMENTS_DIR="$INSTALL_DIR/documents" EXISTING_INSTALL=false if [[ -e "$COMPOSE_FILE" || -e "$OLD_COMPOSE_FILE" || -e "$ENV_FILE" ]] || container_exists archivyn || container_exists archivyn-postgres then EXISTING_INSTALL=true fi if [[ "$EXISTING_INSTALL" == true ]]; then echo echo "An existing Archivyn installation was found." echo echo "A fresh reinstall will permanently delete:" echo echo " - The existing Archivyn SQL database" echo " - All documents in:" echo " $DOCUMENTS_DIR" echo " - The existing .env file" echo " - The existing Compose configuration" echo read -r -u 3 -p \ "Continue with a fresh reinstall? [y/N]: " \ CONFIRM_REINSTALL case "${CONFIRM_REINSTALL,,}" in y|yes) ;; *) echo "Reinstall cancelled." exit 0 ;; esac echo echo "Stopping the existing Archivyn containers..." docker rm \ -f \ archivyn \ archivyn-postgres \ >/dev/null 2>&1 || true echo "Removing volumes created by older installers..." docker volume rm \ archivyn_archivyn_postgres_data \ archivyn_archivyn_data_protection \ archivyn_postgres_data \ archivyn_data_protection \ >/dev/null 2>&1 || true echo "Removing the existing local database and documents..." rm -rf -- \ "$DATA_DIR" \ "$DOCUMENTS_DIR" rm -f -- \ "$COMPOSE_FILE" \ "$OLD_COMPOSE_FILE" \ "$ENV_FILE" else # Protect existing folders that are not associated with a detected # Archivyn installation. if directory_has_files "$DATA_DIR"; then fail \ "$DATA_DIR already contains files, but no existing Archivyn configuration was found." fi if directory_has_files "$DOCUMENTS_DIR"; then fail \ "$DOCUMENTS_DIR already contains files, but no existing Archivyn configuration was found." fi fi echo echo "New installation settings" echo ARCHIVYN_PORT="$( prompt_port "7421" )" POSTGRES_DB="$( prompt_database_identifier \ "Database name" \ "archivyn" )" POSTGRES_USER="$( prompt_database_identifier \ "Database user" \ "archivyn" )" POSTGRES_PASSWORD="$( create_password )" echo echo "Creating the Archivyn directories..." install \ -d \ -m 0750 \ -o "$INSTALL_USER" \ -g "$INSTALL_GROUP" \ "$INSTALL_DIR" install \ -d \ -m 0700 \ "$DATA_DIR" install \ -d \ -m 0750 \ "$DOCUMENTS_DIR" umask 077 cat > "$ENV_FILE" < "$COMPOSE_FILE" <<'COMPOSE' name: archivyn services: archivyn: image: ghcr.io/brentopc/archivyn:${ARCHIVYN_VERSION:-latest} container_name: archivyn restart: unless-stopped ports: - "${ARCHIVYN_PORT:-7421}:8080" environment: ASPNETCORE_ENVIRONMENT: Production # This path is inside the Archivyn container. # It maps to ./documents beside this compose.yml file. DocumentStorage__Path: /documents ConnectionStrings__Archivyn: >- Host=postgres; Port=5432; Database=${POSTGRES_DB}; Username=${POSTGRES_USER}; Password=${POSTGRES_PASSWORD}; volumes: - ./documents:/documents depends_on: postgres: condition: service_healthy postgres: image: postgres:17 container_name: archivyn-postgres restart: unless-stopped environment: POSTGRES_DB: "${POSTGRES_DB}" POSTGRES_USER: "${POSTGRES_USER}" POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" volumes: - ./data:/var/lib/postgresql/data healthcheck: test: - CMD-SHELL - pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB} interval: 5s timeout: 5s retries: 20 COMPOSE chown \ "$INSTALL_USER:$INSTALL_GROUP" \ "$COMPOSE_FILE" chmod 0644 "$COMPOSE_FILE" # The password is now stored in the protected .env file. unset POSTGRES_PASSWORD echo echo "Validating the generated configuration..." cd "$INSTALL_DIR" # Docker Compose automatically reads .env because it is beside compose.yml. docker compose config >/dev/null echo "Pulling container images..." docker compose pull echo "Preparing the PostgreSQL data directory..." POSTGRES_UID="$( docker run \ --rm \ --entrypoint sh \ "$POSTGRES_IMAGE" \ -c 'id -u postgres' )" POSTGRES_GID="$( docker run \ --rm \ --entrypoint sh \ "$POSTGRES_IMAGE" \ -c 'id -g postgres' )" chown \ -R \ "$POSTGRES_UID:$POSTGRES_GID" \ "$DATA_DIR" chmod 0700 "$DATA_DIR" # Archivyn currently runs as root inside its container, so this directory # remains private while still being writable by the application. chown \ -R \ root:root \ "$DOCUMENTS_DIR" chmod 0750 "$DOCUMENTS_DIR" echo "Starting Archivyn..." docker compose \ up \ -d \ --remove-orphans echo "Checking Archivyn..." STARTED=false for _ in {1..30}; do if [[ "$( docker inspect \ -f '{{.State.Running}}' \ archivyn \ 2>/dev/null || true )" == "true" ]] then STARTED=true break fi sleep 2 done if [[ "$STARTED" != true ]]; then echo echo "Archivyn failed to start." echo echo "Archivyn logs:" echo docker logs \ --tail=150 \ archivyn \ 2>/dev/null || true echo echo "PostgreSQL logs:" echo docker logs \ --tail=150 \ archivyn-postgres \ 2>/dev/null || true exit 1 fi # Give Archivyn time to run its database migrations. If the application # exits during migration, report the failure instead of claiming success. sleep 5 if [[ "$( docker inspect \ -f '{{.State.Running}}' \ archivyn \ 2>/dev/null || true )" != "true" ]] then echo echo "Archivyn stopped during startup." echo docker logs \ --tail=150 \ archivyn \ 2>/dev/null || true exit 1 fi docker compose ps SERVER_IP="$( hostname -I 2>/dev/null | awk '{print $1}' )" SERVER_IP="${SERVER_IP:-localhost}" echo echo "Archivyn installation complete." echo echo "Open: http://${SERVER_IP}:${ARCHIVYN_PORT}" echo echo "Installation directory: $INSTALL_DIR" echo "Compose file: $COMPOSE_FILE" echo "Environment file: $ENV_FILE" echo "SQL data: $DATA_DIR" echo "Documents: $DOCUMENTS_DIR" echo echo "To manage Archivyn later:" echo echo " cd $INSTALL_DIR" echo " docker compose ps" echo " docker compose logs -f"