Added seafile
This commit is contained in:
1 parent
87a23b7f30
commit
1fbd67d047
2 files changed
+868
-1
No files matched your search
@@ -1,7 +1,10 @@
|
||||
# LinuxInstalls
|
||||
Personal Linux installation scripts.
|
||||
|
||||
## Git
|
||||
## Git-Host
|
||||
Gitea based git server.
|
||||
|
||||
## Gitea-Podman (Not Working)
|
||||
Gitea based git server.
|
||||
|
||||
## Nvim
|
||||
@@ -9,6 +12,9 @@ A scripted Neovim setup built around C/C++ development (CMake + clangd + codelld
|
||||
|
||||
Supports **Fedora, Ubuntu, Debian, and Arch Linux** (and their common derivatives — Pop!_OS, Linux Mint, Manjaro, EndeavourOS, etc. — via `/etc/os-release`'s `ID_LIKE`) — the script detects which one you're on and uses the right package manager and package names automatically.
|
||||
|
||||
## Seafile
|
||||
Google-Drive self-hosted alternative.
|
||||
|
||||
## Lan Mouse
|
||||
lan-mouse shares your mouse and keyboard across multiple computers over your local network. Moving your cursor off the edge of one screen seamlessly switches control to the other machine, and syncs your clipboard so you can copy and paste text between them.
|
||||
|
||||
|
||||
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