#!/bin/sh # ============================================================================= # install-seafile.sh - install Seafile Server (Community Edition) on (almost) # any Linux distribution and register it as a service that starts on boot. # ============================================================================= # Seafile 13.x Community Edition is deployed with Docker, which is also the one # method that behaves identically on every distribution. This script: # # 1. installs Docker Engine + the Compose v2 plugin if they are missing, # using whatever fits your distro (apt, dnf, yum, zypper, pacman, apk, # xbps; static-binary fallback for the Compose plugin), # 2. downloads Seafile's official compose files for the chosen release and # generates /.env with random secrets, # 3. installs a service called "seafile" (systemd unit, or OpenRC script) # so the stack starts at boot and is managed with systemctl/rc-service, # 4. starts everything and waits until the web UI is up. # # Usage: sudo sh install-seafile.sh [install|upgrade|uninstall] [options] # sudo sh install-seafile.sh --help # # POSIX sh only (works with dash, ash/busybox, bash) - no bashisms. # ============================================================================= set -eu umask 022 PROG=${0##*/} # ------------------------------- defaults ------------------------------------ CMD=install RELEASE=${SEAFILE_RELEASE:-13.0} INSTALL_DIR=${SEAFILE_DIR:-/opt/seafile} DATA_DIR=${SEAFILE_DATA_DIR:-/opt} SF_HOST=${SEAFILE_HOSTNAME:-} PROTOCOL=${SEAFILE_PROTOCOL:-} ADMIN_EMAIL=${SEAFILE_ADMIN_EMAIL:-} ADMIN_PASSWORD=${SEAFILE_ADMIN_PASSWORD:-} TZ_NAME=${SEAFILE_TIMEZONE:-} WAIT_TIMEOUT=${SEAFILE_WAIT_TIMEOUT:-900} WITH_SEADOC=1 START_NOW=1 AUTO_DOCKER=1 ASSUME_YES=0 SKIP_CHECKS=0 SERVICE=seafile UNIT_DIR=${SEAFILE_UNIT_DIR:-/etc/systemd/system} INITD_DIR=${SEAFILE_INITD_DIR:-/etc/init.d} NL=' ' PM= INIT=none ARCH= OS_ID=unknown OS_NAME=Linux INTERACTIVE=0 FRESH=1 PASSWORD_GENERATED=0 ENV_FINAL= ENV_FILE= DOCKER_BIN= # ------------------------------- helpers ------------------------------------- if [ -t 1 ]; then C_RED=$(printf '\033[31m'); C_YEL=$(printf '\033[33m') C_GRN=$(printf '\033[32m'); C_OFF=$(printf '\033[0m') else C_RED=; C_YEL=; C_GRN=; C_OFF= fi log() { printf '%s==>%s %s\n' "$C_GRN" "$C_OFF" "$*"; } warn() { printf '%sWARNING:%s %s\n' "$C_YEL" "$C_OFF" "$*" >&2; } die() { printf '%sERROR:%s %s\n' "$C_RED" "$C_OFF" "$*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } try() { "$@" >/dev/null 2>&1; } usage() { cat < or admin@seafile.local) --admin-password PASS First admin password (default: random). Prefer the SEAFILE_ADMIN_PASSWORD variable - arguments show up in 'ps'. --release X.Y Seafile release line to deploy (default: $RELEASE) --dir PATH Where the compose files and .env live (default: $INSTALL_DIR) --data-dir PATH Base directory for persistent data (default: $DATA_DIR); creates seafile-data, seafile-mysql, seafile-caddy, seadoc-data --timezone TZ e.g. Europe/Berlin (default: this machine's time zone) --no-seadoc Do not deploy the SeaDoc collaborative editor --no-start Prepare everything but do not start the stack --no-docker-install Fail instead of installing Docker automatically --skip-checks Skip the port / RAM / container-name preflight checks -y, --yes Never prompt; use defaults for whatever is not given -h, --help Show this help Environment variables: SEAFILE_HOSTNAME, SEAFILE_PROTOCOL, SEAFILE_ADMIN_EMAIL, SEAFILE_ADMIN_PASSWORD, SEAFILE_RELEASE, SEAFILE_DIR, SEAFILE_DATA_DIR, SEAFILE_TIMEZONE (same meaning as the options above). EOF } set_opt() { # option value case $1 in --hostname) SF_HOST=$2 ;; --protocol) PROTOCOL=$2 ;; --admin-email) ADMIN_EMAIL=$2 ;; --admin-password) ADMIN_PASSWORD=$2 ;; --release) RELEASE=$2 ;; --dir) INSTALL_DIR=$2 ;; --data-dir) DATA_DIR=$2 ;; --timezone) TZ_NAME=$2 ;; esac } parse_args() { while [ $# -gt 0 ]; do case $1 in install|upgrade|uninstall) CMD=$1 ;; --hostname|--protocol|--admin-email|--admin-password|--release|--dir|--data-dir|--timezone) [ $# -ge 2 ] || die "Option $1 needs a value." set_opt "$1" "$2" shift ;; --hostname=*|--protocol=*|--admin-email=*|--admin-password=*|--release=*|--dir=*|--data-dir=*|--timezone=*) set_opt "${1%%=*}" "${1#*=}" ;; --no-seadoc) WITH_SEADOC=0 ;; --no-start) START_NOW=0 ;; --no-docker-install) AUTO_DOCKER=0 ;; --skip-checks) SKIP_CHECKS=1 ;; -y|--yes) ASSUME_YES=1 ;; -h|--help) usage; exit 0 ;; *) die "Unknown argument: $1 (see --help)" ;; esac shift done } validate_inputs() { case $RELEASE in latest) ;; *) printf '%s' "$RELEASE" | grep -Eq '^[0-9]+\.[0-9]+$' \ || die "--release must look like 13.0 (or be 'latest')." ;; esac case $PROTOCOL in ''|http|https) ;; *) die "--protocol must be http or https." ;; esac INSTALL_DIR=${INSTALL_DIR%/} DATA_DIR=${DATA_DIR%/} for _p in "$INSTALL_DIR" "$DATA_DIR"; do case $_p in /*) ;; *) die "Paths must be absolute: '$_p'" ;; esac case $_p in *[!A-Za-z0-9_./-]*) die "Unsupported characters in path '$_p' (allowed: letters, digits, _ . / -)." ;; esac done if [ -n "$SF_HOST" ]; then printf '%s' "$SF_HOST" | grep -Eq '^[A-Za-z0-9.:-]+$' || die "Invalid --hostname '$SF_HOST'." fi if [ -n "$ADMIN_EMAIL" ]; then printf '%s' "$ADMIN_EMAIL" | grep -Eq '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$' \ || die "Invalid --admin-email '$ADMIN_EMAIL'." fi case $ADMIN_PASSWORD in *"'"*|*"$NL"*) die "The admin password must not contain a single quote or a newline." ;; esac ENV_FINAL=$INSTALL_DIR/.env ENV_FILE=$ENV_FINAL } require_root() { if [ "$(id -u)" -eq 0 ]; then return 0; fi if have sudo && [ -f "$0" ]; then log "Not running as root - re-executing with sudo." exec sudo -E sh "$0" "$@" fi die "Run this script as root (for example: sudo sh $PROG)." } # ------------------------- small utility functions --------------------------- fetch() { # url destination if have curl; then curl -fsSL --retry 3 --connect-timeout 20 -o "$2" "$1" elif have wget; then wget -q -O "$2" "$1" else return 1 fi } rand() { # N random alphanumeric characters LC_ALL=C tr -dc 'A-Za-z0-9' /dev/null \ | awk '{ for (i = 1; i < NF; i++) if ($i == "src") { print $(i + 1); exit } }') fi if [ -z "$_ip" ] && have hostname; then _ip=$(hostname -I 2>/dev/null | awk '{ print $1 }') fi printf '%s' "${_ip:-localhost}" } detect_tz() { _tz= if [ -r /etc/timezone ]; then _tz=$(head -n 1 /etc/timezone | tr -d '[:space:]') fi if [ -z "$_tz" ] && [ -L /etc/localtime ]; then _tz=$(readlink /etc/localtime | sed 's|.*/zoneinfo/||') fi if [ -z "$_tz" ] && have timedatectl; then _tz=$(timedatectl show -p Timezone --value 2>/dev/null || true) fi case $_tz in ''|*[!A-Za-z0-9_/+-]*) _tz=Etc/UTC ;; esac if [ -d /usr/share/zoneinfo ] && [ ! -e "/usr/share/zoneinfo/$_tz" ]; then _tz=Etc/UTC fi printf '%s' "$_tz" } ask() { # prompt default -> answer on stdout (prompt goes to stderr) printf '%s [%s]: ' "$1" "$2" >&2 _a= read -r _a || _a= printf '%s' "${_a:-$2}" } confirm() { printf '%s [Y/n]: ' "$1" >&2 _a= read -r _a || _a= case $_a in n|N|no|NO|No) return 1 ;; esac return 0 } port_in_use() { # true if something listens on TCP port $1 (reads /proc: no ss/netstat needed) _hex=$(printf '%04X' "$1") for _f in /proc/net/tcp /proc/net/tcp6; do [ -r "$_f" ] || continue if awk -v h="$_hex" 'NR > 1 && $4 == "0A" { n = split($2, a, ":"); if (toupper(a[n]) == h) f = 1 } END { exit !f }' "$_f"; then return 0 fi done return 1 } # --- .env editing (values are single-quoted only when they need to be) --- env_quote() { case $1 in *[!A-Za-z0-9_.,:@/+=-]*) printf "'%s'" "$1" ;; *) printf '%s' "$1" ;; esac } set_env() { # key value : replace KEY=... in $ENV_FILE, or append it _v=$(env_quote "$2") _tmp=$(mktemp "$ENV_FILE.XXXXXX") SE_KEY=$1 SE_VAL=$_v awk ' BEGIN { k = ENVIRON["SE_KEY"]; v = ENVIRON["SE_VAL"]; done = 0 } { i = index($0, "=") if (!done && i > 1 && substr($0, 1, i - 1) == k) { print k "=" v; done = 1 } else print } END { if (!done) print k "=" v } ' "$ENV_FILE" > "$_tmp" cat "$_tmp" > "$ENV_FILE" rm -f "$_tmp" } get_env() { # key -> value (quotes stripped) from $ENV_FILE sed -n "s/^$1=//p" "$ENV_FILE" | head -n 1 | tr -d "'\"" } # ------------------------- platform detection -------------------------------- osr() { sed -n "s/^$1=//p" /etc/os-release 2>/dev/null | head -n 1 | tr -d "\"'"; } detect_init() { case ${SEAFILE_INIT_SYSTEM:-} in systemd|openrc|none) INIT=$SEAFILE_INIT_SYSTEM; return 0 ;; esac if [ -d /run/systemd/system ] && have systemctl; then INIT=systemd elif have rc-service && have rc-update; then INIT=openrc else INIT=none fi } detect_platform() { [ "$(uname -s)" = Linux ] || die "This installer supports Linux only." case $(uname -m) in x86_64|amd64) ARCH=x86_64 ;; aarch64|arm64) ARCH=aarch64 ;; *) die "Unsupported CPU architecture '$(uname -m)': Seafile's images exist for x86_64 and arm64 only." ;; esac if [ -r /etc/os-release ]; then OS_ID=$(osr ID); OS_NAME=$(osr PRETTY_NAME) fi if [ -z "$OS_ID" ]; then OS_ID=unknown; fi if [ -z "$OS_NAME" ]; then OS_NAME=Linux; fi for _p in apt-get dnf yum zypper pacman apk xbps-install; do if have "$_p"; then PM=$_p; break; fi done detect_init } # --------------------------- package manager glue ---------------------------- pm_refresh() { case $PM in apt-get) apt-get update ;; zypper) zypper --non-interactive refresh ;; *) return 0 ;; esac } pm_install() { # packages... case $PM in apt-get) DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" ;; dnf) dnf install -y "$@" ;; yum) yum install -y "$@" ;; zypper) zypper --non-interactive install --no-recommends "$@" ;; pacman) pacman -S --needed --noconfirm "$@" ;; apk) apk add --no-cache "$@" ;; xbps-install) xbps-install -Sy "$@" ;; *) return 1 ;; esac } ensure_downloader() { if have curl || have wget; then return 0; fi [ -n "$PM" ] || die "Neither curl nor wget is installed, and no supported package manager was found." log "Installing curl..." pm_refresh || true pm_install curl ca-certificates || die "Could not install curl; please install curl or wget and re-run." } # ------------------------------- Docker -------------------------------------- docker_up() { have docker && docker info >/dev/null 2>&1; } compose_ok() { have docker && docker compose version >/dev/null 2>&1; } docker_convenience_script() { log "Running Docker's official convenience script (https://get.docker.com)..." _s=$(mktemp) if fetch https://get.docker.com "$_s" && sh "$_s"; then rm -f "$_s" return 0 fi rm -f "$_s" return 1 } docker_distro_packages() { warn "Docker's own installer did not work on $OS_NAME; trying the distribution's Docker package instead." case $PM in apt-get) pm_refresh && pm_install docker.io ;; dnf|yum) pm_install docker || pm_install moby-engine ;; *) return 1 ;; esac } install_docker_engine() { case $PM in pacman) pm_install docker docker-compose || pm_install docker ;; apk) pm_install docker docker-cli-compose || pm_install docker ;; xbps-install) pm_install docker docker-compose || pm_install docker ;; zypper) pm_refresh || true pm_install docker docker-compose || pm_install docker || docker_convenience_script ;; apt-get|dnf|yum) if [ "$OS_ID" = amzn ]; then pm_install docker else docker_convenience_script || docker_distro_packages fi ;; *) return 1 ;; esac } start_docker_service() { case $INIT in systemd) try systemctl enable --now docker || try systemctl start docker || true ;; openrc) try rc-update add docker default || true try rc-service docker start || true ;; *) if [ -d /etc/sv/docker ] && [ -d /var/service ]; then ln -sf /etc/sv/docker /var/service/docker elif have service; then try service docker start || true elif [ -x /etc/init.d/docker ]; then try /etc/init.d/docker start || true fi ;; esac _i=0 while [ "$_i" -lt 60 ]; do if docker info >/dev/null 2>&1; then return 0; fi sleep 1 _i=$((_i + 1)) done return 1 } install_compose_plugin() { log "Docker Compose v2 plugin not found - installing it..." case $PM in apt-get) try pm_install docker-compose-v2 || try pm_install docker-compose-plugin || true ;; dnf|yum) try pm_install docker-compose-plugin || true ;; zypper) try pm_install docker-compose || true ;; esac if compose_ok; then return 0; fi # Fallback that works on every distro: the official static binary from GitHub. _dest=/usr/local/lib/docker/cli-plugins _url=https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$ARCH _tmp=$(mktemp) mkdir -p "$_dest" if ! fetch "$_url" "$_tmp"; then rm -f "$_tmp" die "Could not download the Compose plugin from $_url" fi if have sha256sum && fetch "$_url.sha256" "$_tmp.sha256" 2>/dev/null; then _want=$(awk '{ print $1; exit }' "$_tmp.sha256") _got=$(sha256sum "$_tmp" | awk '{ print $1 }') if printf '%s' "$_want" | grep -Eq '^[0-9a-f]{64}$' && [ "$_want" != "$_got" ]; then rm -f "$_tmp" "$_tmp.sha256" die "Checksum mismatch for the downloaded Compose plugin - refusing to install it." fi fi rm -f "$_tmp.sha256" cp "$_tmp" "$_dest/docker-compose" chmod 755 "$_dest/docker-compose" rm -f "$_tmp" } ensure_docker() { if docker_up && compose_ok; then log "Docker and Compose are already available: $(docker --version)" return 0 fi if ! have docker; then [ "$AUTO_DOCKER" -eq 1 ] || die "Docker is not installed (re-run without --no-docker-install to install it automatically)." [ -n "$PM" ] || die "No supported package manager found. Install Docker Engine + Compose v2 manually (https://docs.docker.com/engine/install/) and re-run." ensure_downloader log "Installing Docker Engine on $OS_NAME (package manager: $PM)..." install_docker_engine || die "Automatic Docker installation failed on $OS_NAME (on Arch, run 'pacman -Syu' and reboot first if packages are not found). Install Docker Engine + the Compose v2 plugin manually and re-run." fi start_docker_service || die "The Docker daemon is not running and could not be started." if ! compose_ok; then [ "$AUTO_DOCKER" -eq 1 ] || die "The Docker Compose v2 plugin is missing (re-run without --no-docker-install)." ensure_downloader install_compose_plugin fi compose_ok || die "'docker compose' still does not work. Please install the Docker Compose v2 plugin manually." log "Docker is ready: $(docker --version)" } # ------------------------- preflight checks ---------------------------------- preflight() { if [ "$SKIP_CHECKS" -eq 1 ]; then return 0; fi _mem=$(awk '/^MemTotal:/ { print int($2 / 1024) }' /proc/meminfo 2>/dev/null || true) if [ -n "$_mem" ] && [ "$_mem" -lt 1800 ]; then warn "Only ${_mem} MB of RAM detected. MariaDB, Redis, Seafile, Caddy and SeaDoc together may be slow or get OOM-killed. Consider more RAM/swap, or --no-seadoc." fi if [ "$FRESH" -eq 1 ]; then for _port in 80 443; do if port_in_use "$_port"; then die "TCP port $_port is already in use (Seafile's Caddy proxy needs ports 80 and 443). Stop the other service first, or use --skip-checks." fi done fi if [ "$ARCH" != x86_64 ]; then warn "Running on $ARCH: Seafile publishes arm64 images, but 'docker compose pull' will tell you if one is missing." fi } check_name_conflicts() { if [ "$SKIP_CHECKS" -eq 1 ] || [ "$FRESH" -eq 0 ]; then return 0; fi _existing=$(docker ps -a --format '{{.Names}}' 2>/dev/null || true) for _n in seafile seafile-mysql seafile-redis seafile-caddy seadoc; do if printf '%s\n' "$_existing" | grep -qx "$_n"; then die "A Docker container named '$_n' already exists. Remove/rename it first (docker rm -f $_n) or use --skip-checks." fi done } # ------------------------- settings & configuration -------------------------- resolve_settings() { if [ -z "$SF_HOST" ]; then _def=$(detect_ip) if [ "$INTERACTIVE" -eq 1 ]; then SF_HOST=$(ask "Domain name or IP address for Seafile" "$_def") else SF_HOST=$_def fi fi printf '%s' "$SF_HOST" | grep -Eq '^[A-Za-z0-9.:-]+$' || die "Invalid hostname '$SF_HOST'." if [ -z "$PROTOCOL" ]; then if wants_https "$SF_HOST"; then PROTOCOL=https; else PROTOCOL=http; fi fi if [ -z "$ADMIN_EMAIL" ]; then if [ "$PROTOCOL" = https ] && ! is_ip "$SF_HOST"; then _def=admin@$SF_HOST else _def=admin@seafile.local fi if [ "$INTERACTIVE" -eq 1 ]; then ADMIN_EMAIL=$(ask "Admin e-mail address (used as login name)" "$_def") else ADMIN_EMAIL=$_def fi printf '%s' "$ADMIN_EMAIL" | grep -Eq '^[^@[:space:]]+@[^@[:space:]]+\.[^@[:space:]]+$' \ || die "Invalid admin e-mail '$ADMIN_EMAIL'." fi if [ -z "$ADMIN_PASSWORD" ]; then ADMIN_PASSWORD=$(rand 20) PASSWORD_GENERATED=1 elif [ "${#ADMIN_PASSWORD}" -lt 8 ]; then warn "The admin password is shorter than 8 characters - please change it after the first login." fi if [ -z "$TZ_NAME" ]; then TZ_NAME=$(detect_tz); fi } DOC_BASE= GH_BASE= get_remote() { # path-relative-to-docker-dir destination fetch "$DOC_BASE/$1" "$2" 2>/dev/null && return 0 fetch "$GH_BASE/$1" "$2" 2>/dev/null && return 0 return 1 } download_files() { _ref=$RELEASE if [ "$RELEASE" = latest ]; then _ref=master; fi DOC_BASE=https://manual.seafile.com/$RELEASE/repo/docker GH_BASE=https://raw.githubusercontent.com/haiwen/seafile-admin-docs/$_ref/manual/repo/docker log "Downloading Seafile $RELEASE compose files..." get_remote ce/env "$ENV_FILE" \ || die "Could not download the .env template for Seafile $RELEASE (is --release valid? is the network up?)." get_remote ce/seafile-server.yml "$INSTALL_DIR/seafile-server.yml" \ || die "Could not download seafile-server.yml for Seafile $RELEASE." get_remote caddy.yml "$INSTALL_DIR/caddy.yml" \ || die "Could not download caddy.yml for Seafile $RELEASE." if [ "$WITH_SEADOC" -eq 1 ]; then get_remote seadoc.yml "$INSTALL_DIR/seadoc.yml" \ || die "Could not download seadoc.yml for Seafile $RELEASE." fi grep -q '^JWT_PRIVATE_KEY=' "$ENV_FILE" || die "The downloaded .env template looks wrong (no JWT_PRIVATE_KEY)." grep -q '^services:' "$INSTALL_DIR/seafile-server.yml" || die "The downloaded seafile-server.yml looks wrong." } write_env() { chmod 600 "$ENV_FILE" set_env SEAFILE_SERVER_HOSTNAME "$SF_HOST" set_env SEAFILE_SERVER_PROTOCOL "$PROTOCOL" set_env TIME_ZONE "$TZ_NAME" set_env BASIC_STORAGE_PATH "$DATA_DIR" set_env JWT_PRIVATE_KEY "$(rand 48)" set_env SEAFILE_MYSQL_DB_PASSWORD "$(rand 24)" set_env INIT_SEAFILE_MYSQL_ROOT_PASSWORD "$(rand 24)" set_env INIT_SEAFILE_ADMIN_EMAIL "$ADMIN_EMAIL" set_env INIT_SEAFILE_ADMIN_PASSWORD "$ADMIN_PASSWORD" if [ "$WITH_SEADOC" -eq 0 ]; then set_env COMPOSE_FILE "seafile-server.yml,caddy.yml" set_env ENABLE_SEADOC false fi } make_dirs() { mkdir -p "$INSTALL_DIR" "$DATA_DIR/seafile-data" "$DATA_DIR/seafile-mysql/db" "$DATA_DIR/seafile-caddy" if [ "$WITH_SEADOC" -eq 1 ]; then mkdir -p "$DATA_DIR/seadoc-data"; fi } # ------------------------- the service (the "daemon") ------------------------ install_service() { DOCKER_BIN=$(command -v docker) || die "Cannot locate the docker binary." case $INIT in systemd) mkdir -p "$UNIT_DIR" _dep= if try systemctl cat docker.service; then _dep="Requires=docker.service"; fi cat > "$UNIT_DIR/$SERVICE.service" < "$INITD_DIR/$SERVICE" </dev/null || true) if [ "$_h" = healthy ]; then return 0; fi if docker logs --tail 300 seafile 2>&1 | grep -q 'Seahub is started'; then return 0; fi sleep 5 _t=$((_t + 5)) if [ $((_t % 30)) -eq 0 ]; then log " ...still starting (${_t}s)"; fi done return 1 } row() { printf ' %-46s %s\n' "$1" "$2"; } print_manage_help() { printf '\nUseful commands:\n' case $INIT in systemd) row "systemctl status|start|stop|restart $SERVICE" "(control the stack)" row "journalctl -u $SERVICE" "(service log)" ;; openrc) row "rc-service $SERVICE status|start|stop|restart" "(control the stack)" ;; *) row "cd $INSTALL_DIR && docker compose ps|stop|up -d" "(control the stack)" ;; esac row "docker logs -f seafile" "(Seafile application log)" row "sudo sh $PROG upgrade" "(pull newer images of this release line)" } print_summary() { _url=$(get_env SEAFILE_SERVER_PROTOCOL)://$(get_env SEAFILE_SERVER_HOSTNAME) _data=$(get_env BASIC_STORAGE_PATH) printf '\n URL %s\n' "$_url" printf ' Admin %s\n' "$(get_env INIT_SEAFILE_ADMIN_EMAIL)" if [ "$PASSWORD_GENERATED" -eq 1 ]; then printf ' Password %s (generated - change it after logging in)\n' "$ADMIN_PASSWORD" elif [ "$FRESH" -eq 1 ]; then printf ' Password (the one you supplied)\n' else printf ' Password see INIT_SEAFILE_ADMIN_PASSWORD in %s (valid only until you change it in the web UI)\n' "$ENV_FINAL" fi printf ' Settings %s (contains secrets, mode 600)\n' "$ENV_FINAL" printf ' Data %s/seafile-data, %s/seafile-mysql\n' "${_data:-$DATA_DIR}" "${_data:-$DATA_DIR}" if [ "$(get_env SEAFILE_SERVER_PROTOCOL)" = https ]; then printf '\nHTTPS: Caddy requests a Let'"'"'s Encrypt certificate on first start, so the DNS record\nfor %s must point to this machine and ports 80/443 must be reachable.\n' "$(get_env SEAFILE_SERVER_HOSTNAME)" fi print_manage_help } # ------------------------------ commands ------------------------------------- do_install() { if [ -f "$ENV_FINAL" ]; then FRESH=0; fi if [ "$FRESH" -eq 1 ]; then resolve_settings; fi log "System: $OS_NAME ($ARCH) | package manager: ${PM:-none} | init system: $INIT" if [ "$FRESH" -eq 1 ] && [ "$INTERACTIVE" -eq 1 ]; then printf '\nSeafile %s will be installed with:\n address %s://%s\n admin %s\n files %s (compose + .env) and %s (data)\n SeaDoc %s\n\n' \ "$RELEASE" "$PROTOCOL" "$SF_HOST" "$ADMIN_EMAIL" "$INSTALL_DIR" "$DATA_DIR" \ "$(if [ "$WITH_SEADOC" -eq 1 ]; then echo yes; else echo no; fi)" confirm "Continue?" || die "Aborted by user." fi preflight ensure_docker check_name_conflicts if [ "$FRESH" -eq 1 ]; then make_dirs ENV_FILE=$INSTALL_DIR/.env.new download_files write_env mv -f "$ENV_FILE" "$ENV_FINAL" ENV_FILE=$ENV_FINAL else log "Existing configuration found in $INSTALL_DIR - keeping it (secrets are not regenerated)." if [ -n "$SF_HOST$PROTOCOL$ADMIN_EMAIL$ADMIN_PASSWORD$TZ_NAME" ]; then warn "Settings given on the command line are ignored for an existing installation - edit $ENV_FINAL and restart the service instead." fi fi compose_in_dir config -q \ || die "The compose configuration is invalid. Fix $ENV_FINAL (or delete it and re-run to regenerate it)." _have_service=1 install_service || _have_service=0 if [ "$START_NOW" -eq 0 ]; then if [ "$_have_service" -eq 0 ]; then _how="cd $INSTALL_DIR && docker compose up -d" elif [ "$INIT" = openrc ]; then _how="rc-service $SERVICE start" else _how="systemctl start $SERVICE" fi log "Prepared but not started (--no-start). Start it with: $_how" print_summary return 0 fi start_stack if wait_ready; then log "Seafile is up and running." print_summary else warn "Seafile did not report 'ready' within ${WAIT_TIMEOUT}s. The first start can be slow on small machines." warn "Check progress with: docker logs -f seafile" print_summary exit 2 fi } do_upgrade() { [ -f "$ENV_FINAL" ] || die "No Seafile installation found in $INSTALL_DIR (nothing to upgrade)." if ! docker_up || ! compose_ok; then die "Docker (with the Compose plugin) is not available or not running." fi log "Pulling the newest images for the installed release line..." compose_in_dir pull || die "Pulling the images failed." compose_in_dir up -d --remove-orphans || die "Recreating the containers failed." if wait_ready; then log "Upgrade finished and Seafile is healthy." else warn "Seafile did not report 'ready' in time - check: docker logs -f seafile" exit 2 fi log "Note: this follows the pinned tag (e.g. 13.0-latest). For a new major/minor line such as 14.0, read Seafile's upgrade notes first and back up your data." } do_uninstall() { log "Removing the $SERVICE service and containers (your data is NOT deleted)..." case $INIT in systemd) try systemctl disable --now "$SERVICE" || true rm -f "$UNIT_DIR/$SERVICE.service" try systemctl daemon-reload || true ;; openrc) try rc-service "$SERVICE" stop || true try rc-update del "$SERVICE" default || true rm -f "$INITD_DIR/$SERVICE" ;; esac if [ -f "$ENV_FINAL" ] && docker_up && compose_ok; then compose_in_dir down || warn "'docker compose down' reported a problem." fi _data=$DATA_DIR if [ -f "$ENV_FINAL" ]; then _data=$(get_env BASIC_STORAGE_PATH); fi log "Done. Kept: $INSTALL_DIR (compose files + .env) and the data under ${_data:-$DATA_DIR}/ (seafile-data, seafile-mysql, seafile-caddy, seadoc-data)." log "Delete those directories yourself only if you really want to erase everything." } # -------------------------------- main --------------------------------------- main() { for _a in "$@"; do case $_a in -h|--help) usage; exit 0 ;; esac done require_root "$@" parse_args "$@" validate_inputs if [ "$ASSUME_YES" -eq 0 ] && [ -t 0 ]; then INTERACTIVE=1; fi detect_platform case $CMD in install) do_install ;; upgrade) do_upgrade ;; uninstall) do_uninstall ;; esac } main "$@"