Added seafile
This commit is contained in:
1 parent
87a23b7f30
commit
1fbd67d047
2 files changed
+868
-1
No files matched your search
Executable
+861
@@ -0,0 +1,861 @@
|
||||
#!/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 <dir>/.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 <<EOF
|
||||
Usage: sudo sh $PROG [command] [options]
|
||||
|
||||
Commands:
|
||||
install Install Docker if needed, deploy Seafile, enable the service (default)
|
||||
upgrade Pull the newest images of the installed release line and recreate containers
|
||||
uninstall Stop Seafile and remove the service (your data is kept)
|
||||
|
||||
Options for install:
|
||||
--hostname HOST Domain name or IP address users will connect to
|
||||
(default: ask, else this machine's primary IP)
|
||||
--protocol http|https Default: https for public-looking domain names, http otherwise.
|
||||
With https, Caddy obtains a Let's Encrypt certificate
|
||||
(DNS must point here; ports 80 and 443 must be reachable).
|
||||
--admin-email EMAIL First admin account (default: admin@<domain> 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/urandom | head -c "$1"
|
||||
}
|
||||
|
||||
is_ip() { printf '%s' "$1" | grep -Eq '^[0-9]{1,3}(\.[0-9]{1,3}){3}$|:'; }
|
||||
|
||||
wants_https() { # true if $1 looks like a public DNS name
|
||||
case $1 in
|
||||
localhost|*.local|*.lan|*.localdomain|*.internal|*.home.arpa|*.test|*.invalid) return 1 ;;
|
||||
*.*) if is_ip "$1"; then return 1; fi; return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_ip() {
|
||||
_ip=
|
||||
if have ip; then
|
||||
_ip=$(ip -4 route get 1.1.1.1 2>/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" <<EOF
|
||||
[Unit]
|
||||
Description=Seafile server (Docker Compose stack in $INSTALL_DIR)
|
||||
Documentation=https://manual.seafile.com/
|
||||
$_dep
|
||||
After=docker.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
Environment=HOME=/root
|
||||
ExecStart=$DOCKER_BIN compose up -d --remove-orphans
|
||||
ExecStop=$DOCKER_BIN compose stop --timeout 120
|
||||
TimeoutStartSec=0
|
||||
TimeoutStopSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
try systemctl enable "$SERVICE" || warn "Could not enable the $SERVICE service at boot."
|
||||
log "Installed systemd unit $UNIT_DIR/$SERVICE.service (enabled at boot)."
|
||||
;;
|
||||
openrc)
|
||||
mkdir -p "$INITD_DIR"
|
||||
cat > "$INITD_DIR/$SERVICE" <<EOF
|
||||
#!/sbin/openrc-run
|
||||
# Generated by $PROG
|
||||
description="Seafile server (Docker Compose stack in $INSTALL_DIR)"
|
||||
|
||||
depend() {
|
||||
need docker
|
||||
after net
|
||||
}
|
||||
|
||||
start() {
|
||||
ebegin "Starting Seafile"
|
||||
cd "$INSTALL_DIR" && "$DOCKER_BIN" compose up -d --remove-orphans
|
||||
eend \$?
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping Seafile"
|
||||
cd "$INSTALL_DIR" && "$DOCKER_BIN" compose stop --timeout 120
|
||||
eend \$?
|
||||
}
|
||||
|
||||
status() {
|
||||
cd "$INSTALL_DIR" && "$DOCKER_BIN" compose ps
|
||||
}
|
||||
EOF
|
||||
chmod 755 "$INITD_DIR/$SERVICE"
|
||||
try rc-update add "$SERVICE" default || warn "Could not add $SERVICE to the default runlevel."
|
||||
log "Installed OpenRC service $INITD_DIR/$SERVICE (added to the default runlevel)."
|
||||
;;
|
||||
*)
|
||||
warn "No systemd or OpenRC detected, so no service file was created."
|
||||
warn "The containers use 'restart: unless-stopped', so they come back by themselves whenever the Docker daemon starts at boot - make sure Docker is enabled at boot on this system."
|
||||
return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
compose_in_dir() { (cd "$INSTALL_DIR" && docker compose "$@"); }
|
||||
|
||||
start_stack() {
|
||||
log "Pulling container images (this can take a few minutes)..."
|
||||
compose_in_dir pull || die "Pulling the images failed. Check the network connection (and Docker Hub rate limits)."
|
||||
log "Starting Seafile..."
|
||||
case $INIT in
|
||||
systemd)
|
||||
if ! systemctl start "$SERVICE"; then
|
||||
systemctl status "$SERVICE" --no-pager -l || true
|
||||
die "The $SERVICE service failed to start (see: journalctl -u $SERVICE)."
|
||||
fi ;;
|
||||
openrc)
|
||||
rc-service "$SERVICE" start || die "The $SERVICE service failed to start." ;;
|
||||
*)
|
||||
compose_in_dir up -d --remove-orphans || die "'docker compose up' failed." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
wait_ready() {
|
||||
log "Waiting for Seafile to finish its first-time setup (can take several minutes)..."
|
||||
_t=0
|
||||
while [ "$_t" -lt "$WAIT_TIMEOUT" ]; do
|
||||
_h=$(docker inspect -f '{{.State.Health.Status}}' seafile 2>/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 "$@"
|
||||
|
||||
Reference in new issue
Block a user