seafile/docker/entrypoint.sh

382 lines
12 KiB
Bash

#!/usr/bin/env bash
set -Eeuo pipefail
# Globals / defaults
SEAFILE_HOME=${SEAFILE_HOME:-/opt/seafile}
SEAF_RELEASE_DIR_DEFAULT="${SEAFILE_HOME}/seafile-server-latest"
SEAF_RELEASE_DIR="${SEAF_RELEASE_DIR:-$SEAF_RELEASE_DIR_DEFAULT}"
SEAFILE_CONF_DIR=${SEAFILE_CONF_DIR:-/data/conf}
SEAFILE_DATA_DIR=${SEAFILE_DATA_DIR:-/data/seafile-data}
SEAHUB_MEDIA_DIR=${SEAHUB_MEDIA_DIR:-/data/seahub-media}
LOG_DIR=${LOG_DIR:-/data/logs}
TIMEZONE=${TIMEZONE:-UTC}
SEAFILE_SERVER_HOSTNAME=${SEAFILE_SERVER_HOSTNAME:-localhost}
SEAFILE_SERVER_URL=${SEAFILE_SERVER_URL:-http://localhost}
REDIS_URL=${REDIS_URL:-redis://127.0.0.1:6379/0}
DB_NAME=${DB_NAME:-seafile}
DB_USER=${DB_USER:-seafile}
DB_PASSWORD=${DB_PASSWORD:-}
DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-}
NGINX_MAX_BODY=${NGINX_MAX_BODY:-200m}
SSL_ENABLE=${SSL_ENABLE:-false}
# Commands
MYSQLD_BIN="/usr/sbin/mariadbd"
MYSQL_BIN="/usr/bin/mysql"
MYSQLADMIN_BIN="/usr/bin/mysqladmin"
MARIADB_INSTALL_DB="/usr/bin/mariadb-install-db"
REDIS_SERVER_BIN="/usr/bin/redis-server"
NGINX_BIN="/usr/sbin/nginx"
SUPERVISORD_BIN="/usr/bin/supervisord"
SUPERVISORD_CONF="/etc/supervisor/supervisord.conf"
NGINX_CONF="/etc/nginx/nginx.conf"
REDIS_CONF="/etc/redis/redis.conf"
HEALTH_URL_SEAHUB="http://127.0.0.1:8000/accounts/login/"
HEALTH_URL_FILESERVER="http://127.0.0.1:8082/protocol-version"
log() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*"
}
die() {
echo "ERROR: $*" >&2
exit 1
}
ensure_dirs() {
log "Ensuring directory layout under /data and runtime dirs"
mkdir -p \
"${SEAFILE_HOME}" \
"${SEAFILE_HOME}/releases" \
"${SEAFILE_CONF_DIR}" \
"${SEAFILE_DATA_DIR}" \
/data/db \
/data/redis \
"${SEAHUB_MEDIA_DIR}" \
"${LOG_DIR}/nginx" \
"${LOG_DIR}/seahub" \
"${LOG_DIR}/seafile" \
"${LOG_DIR}/supervisor" \
"${LOG_DIR}/mariadb" \
"${LOG_DIR}/redis" \
/data/ssl \
/run/mysqld \
/run/redis
chown -R seafile:seafile "${SEAFILE_HOME}" "${SEAFILE_CONF_DIR}" "${SEAFILE_DATA_DIR}" "${SEAHUB_MEDIA_DIR}" "${LOG_DIR}"
chown -R mysql:mysql /data/db /run/mysqld || true
chown -R redis:redis /data/redis /run/redis || true
chown -R www-data:www-data /var/log/nginx || true
}
configure_timezone() {
log "Configuring timezone: ${TIMEZONE}"
echo "${TIMEZONE}" >/etc/timezone
ln -sf "/usr/share/zoneinfo/${TIMEZONE}" /etc/localtime || true
}
# Basic poor-man's template renderer: replace {{VAR}} tokens using sed
# Usage: render_file TEMPLATE DEST
render_file() {
local template="$1"
local dest="$2"
[[ -f "$template" ]] || die "Template not found: $template"
sed \
-e "s|{{SERVER_NAME}}|${SEAFILE_SERVER_HOSTNAME}|g" \
-e "s|{{SERVER_URL}}|${SEAFILE_SERVER_URL}|g" \
-e "s|{{NGINX_MAX_BODY}}|${NGINX_MAX_BODY}|g" \
-e "s|{{SEAF_RELEASE_DIR}}|${SEAF_RELEASE_DIR}|g" \
-e "s|{{SEAFILE_CONF_DIR}}|${SEAFILE_CONF_DIR}|g" \
-e "s|{{SEAFILE_DATA_DIR}}|${SEAFILE_DATA_DIR}|g" \
-e "s|{{SEAHUB_MEDIA_DIR}}|${SEAHUB_MEDIA_DIR}|g" \
-e "s|{{LOG_DIR}}|${LOG_DIR}|g" \
-e "s|{{DB_NAME}}|${DB_NAME}|g" \
-e "s|{{DB_USER}}|${DB_USER}|g" \
-e "s|{{DB_PASSWORD}}|${DB_PASSWORD}|g" \
-e "s|{{DB_NAME_SEAFILE}}|${DB_NAME_SEAFILE:-seafile_db}|g" \
-e "s|{{DB_NAME_CCNET}}|${DB_NAME_CCNET:-ccnet_db}|g" \
-e "s|{{REDIS_URL}}|${REDIS_URL}|g" \
<"$template" >"$dest"
}
init_redis() {
log "Writing Redis config"
cat >"${REDIS_CONF}" <<'EOF'
bind 127.0.0.1 ::1
protected-mode yes
port 6379
daemonize no
supervised no
pidfile /run/redis/redis-server.pid
dir /data/redis
save 900 1
save 300 10
save 60 10000
appendonly yes
EOF
chown -R redis:redis /data/redis
}
wait_for_mysql_socket() {
local timeout="${1:-60}"
local i=0
while ! "${MYSQLADMIN_BIN}" ping --silent --protocol=socket --socket=/run/mysqld/mysqld.sock >/dev/null 2>&1; do
sleep 1
i=$((i+1))
if [[ $i -ge $timeout ]]; then
die "MariaDB did not become ready in ${timeout}s"
fi
done
}
init_mariadb() {
if [[ -d /data/db/mysql ]]; then
log "MariaDB data directory already initialized"
return
fi
[[ -n "${DB_ROOT_PASSWORD}" ]] || die "DB_ROOT_PASSWORD must be set for MariaDB bootstrap"
log "Initializing MariaDB datadir at /data/db"
install -d -o mysql -g mysql /data/db /run/mysqld
"${MARIADB_INSTALL_DB}" --user=mysql --datadir=/data/db --skip-test-db
log "Starting temporary MariaDB to set root password and create DB/user"
"${MYSQLD_BIN}" \
--datadir=/data/db \
--socket=/run/mysqld/mysqld.sock \
--bind-address=127.0.0.1 \
--skip-networking=0 \
--skip-name-resolve \
--skip-symbolic-links \
--log-bin=OFF \
--pid-file=/run/mysqld/mysqld.pid \
--user=mysql >/dev/null 2>&1 &
local mysqld_pid=$!
wait_for_mysql_socket 60
log "Configuring root password and seafile database"
"${MYSQL_BIN}" --protocol=socket --socket=/run/mysqld/mysqld.sock -uroot <<-SQL
ALTER USER 'root'@'localhost' IDENTIFIED BY '${DB_ROOT_PASSWORD}';
FLUSH PRIVILEGES;
SQL
"${MYSQL_BIN}" --protocol=socket --socket=/run/mysqld/mysqld.sock -uroot -p"${DB_ROOT_PASSWORD}" <<-SQL
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE DATABASE IF NOT EXISTS \`${DB_NAME_SEAFILE:-seafile_db}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE DATABASE IF NOT EXISTS \`${DB_NAME_CCNET:-ccnet_db}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE DATABASE IF NOT EXISTS \`${DB_NAME_SEAHUB:-seahub_db}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASSWORD}';
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON \`${DB_NAME_SEAFILE:-seafile_db}\`.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON \`${DB_NAME_CCNET:-ccnet_db}\`.* TO '${DB_USER}'@'localhost';
GRANT ALL PRIVILEGES ON \`${DB_NAME_SEAHUB:-seahub_db}\`.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
log "Shutting down temporary MariaDB"
"${MYSQLADMIN_BIN}" --protocol=socket --socket=/run/mysqld/mysqld.sock -uroot -p"${DB_ROOT_PASSWORD}" shutdown || true
wait $mysqld_pid || true
}
# Optional runtime download of official Seafile server release if not present.
# If SEAFILE_TGZ_URL or SEAFILE_VERSION provided, try to fetch.
fetch_seafile_release_if_needed() {
if [[ -d "${SEAF_RELEASE_DIR}" ]]; then
log "Seafile release directory present: ${SEAF_RELEASE_DIR}"
return
fi
local url="${SEAFILE_TGZ_URL:-}"
if [[ -z "${url}" && -n "${SEAFILE_VERSION:-}" ]]; then
# Fallback to official static host if only version is provided
url="https://download.seadrive.org/seafile-server_${SEAFILE_VERSION}_x86-64.tar.gz"
fi
if [[ -z "${url}" ]]; then
log "No Seafile release present and no SEAFILE_TGZ_URL provided. Skipping download (expect release to be baked into image or mounted)."
return
fi
log "Downloading Seafile release from ${url}"
mkdir -p "${SEAFILE_HOME}/releases"
local tgz="${SEAFILE_HOME}/releases/seafile-server.tgz"
curl -fsSL "${url}" -o "${tgz}"
tar -xzf "${tgz}" -C "${SEAFILE_HOME}/releases"
# Try to locate extracted dir
local extracted
extracted="$(tar -tzf "${tgz}" | head -1 | cut -d/ -f1)"
if [[ -n "${extracted}" && -d "${SEAFILE_HOME}/releases/${extracted}" ]]; then
ln -s "${SEAFILE_HOME}/releases/${extracted}" "${SEAF_RELEASE_DIR_DEFAULT}"
log "Linked ${SEAF_RELEASE_DIR_DEFAULT} -> ${SEAFILE_HOME}/releases/${extracted}"
else
log "Could not detect extracted directory; ensure ${SEAF_RELEASE_DIR} exists"
fi
}
init_seahub_secret_key() {
local keyfile="${SEAFILE_CONF_DIR}/seahub_secret_key.txt"
if [[ ! -s "${keyfile}" ]]; then
log "Generating Django SECRET_KEY"
python3 - <<'PY'
import secrets, string, os, sys
alphabet = string.ascii_letters + string.digits + string.punctuation
key = ''.join(secrets.choice(alphabet) for _ in range(64))
print(key)
PY
python3 - <<PY >"${keyfile}"
import secrets, string
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*(-_=+)"
print(''.join(secrets.choice(alphabet) for _ in range(64)))
PY
chown seafile:seafile "${keyfile}"
chmod 600 "${keyfile}"
fi
}
render_configs() {
log "Rendering Nginx and Supervisord configs from templates"
render_file "${SEAFILE_HOME}/docker/nginx.conf.template" "${NGINX_CONF}"
render_file "${SEAFILE_HOME}/docker/supervisord.conf.template" "${SUPERVISORD_CONF}"
}
init_python_and_requirements() {
log "Setting up Python virtual environment"
local venv="${SEAFILE_HOME}/venv"
if [[ ! -x "${venv}/bin/python" ]]; then
python3 -m venv "${venv}"
"${venv}/bin/pip" install --no-cache-dir --upgrade pip wheel setuptools
fi
# Install Seahub requirements from release if available
local req="${SEAF_RELEASE_DIR}/seahub/requirements.txt"
if [[ -f "${req}" ]]; then
log "Installing Seahub Python requirements from ${req}"
"${venv}/bin/pip" install --no-cache-dir -r "${req}"
else
log "Seahub requirements.txt not found at ${req}. Ensure release is present or install separately."
fi
}
init_seahub_settings() {
init_seahub_secret_key
local settings_target="${SEAFILE_CONF_DIR}/seahub_settings.py"
if [[ ! -f "${settings_target}" ]]; then
log "Rendering seahub_settings.py"
render_file "${SEAFILE_HOME}/docker/seahub_settings.py.template" "${settings_target}"
chown seafile:seafile "${settings_target}"
fi
}
init_seafile_confs() {
# Render seafile core config files if missing
local seafile_conf="${SEAFILE_CONF_DIR}/seafile.conf"
local ccnet_conf="${SEAFILE_CONF_DIR}/ccnet.conf"
if [[ ! -f "${seafile_conf}" ]]; then
log "Rendering seafile.conf"
render_file "${SEAFILE_HOME}/docker/seafile.conf.template" "${seafile_conf}"
chown seafile:seafile "${seafile_conf}"
fi
if [[ ! -f "${ccnet_conf}" ]]; then
log "Rendering ccnet.conf"
render_file "${SEAFILE_HOME}/docker/ccnet.conf.template" "${ccnet_conf}"
chown seafile:seafile "${ccnet_conf}"
fi
}
django_manage() {
# Run Django manage.py commands within release's seahub directory using venv python
local venv="${SEAFILE_HOME}/venv"
local seahub_dir="${SEAF_RELEASE_DIR}/seahub"
[[ -x "${venv}/bin/python" ]] || die "Python venv missing"
[[ -d "${seahub_dir}" ]] || { log "Seahub dir not found at ${seahub_dir}, skipping manage.py"; return; }
pushd "${seahub_dir}" >/dev/null
# Ensure env
export PYTHONPATH="${SEAF_RELEASE_DIR}:${PYTHONPATH:-}"
export DJANGO_SETTINGS_MODULE="seahub.settings"
export CCNET_CONF_DIR="${SEAFILE_CONF_DIR}"
export SEAFILE_CENTRAL_CONF_DIR="${SEAFILE_CONF_DIR}"
export SEAFILE_CONF_DIR="${SEAFILE_CONF_DIR}"
export SEAFILE_DATA_DIR="${SEAFILE_DATA_DIR}"
log "Running Django collectstatic (noinput)"
"${venv}/bin/python" manage.py collectstatic --noinput || true
log "Running Django migrate"
"${venv}/bin/python" manage.py migrate --noinput || true
if [[ -n "${ADMIN_EMAIL}" && -n "${ADMIN_PASSWORD}" ]]; then
log "Ensuring admin user ${ADMIN_EMAIL}"
"${venv}/bin/python" manage.py shell <<PY || true
from django.contrib.auth import get_user_model
User = get_user_model()
email="${ADMIN_EMAIL}"
password="${ADMIN_PASSWORD}"
if not User.objects.filter(email=email).exists():
User.objects.create_superuser(email=email, password=password)
else:
u = User.objects.get(email=email)
if not u.is_superuser:
u.is_superuser = True
u.save()
PY
fi
popd >/dev/null
}
wait_for_services() {
log "Waiting for MariaDB socket..."
wait_for_mysql_socket 60
log "Waiting for Redis..."
local i=0
while ! (echo >/dev/tcp/127.0.0.1/6379) 2>/dev/null; do
sleep 1
i=$((i+1))
if [[ $i -ge 60 ]]; then
die "Redis did not open port 6379 in 60s"
fi
done
}
start_supervisord() {
log "Starting supervisord"
exec "${SUPERVISORD_BIN}" -n -c "${SUPERVISORD_CONF}"
}
bootstrap() {
ensure_dirs
configure_timezone
init_redis
init_mariadb
fetch_seafile_release_if_needed
render_configs
init_python_and_requirements
init_seahub_settings
init_seafile_confs
# Note: seafile core DB schema is created/managed by seafile services; Seahub migrations handled above
}
case "${1:-start}" in
start)
bootstrap
start_supervisord
;;
bash|sh)
exec "$@"
;;
*)
# Pass-through to allow running arbitrary commands
exec "$@"
;;
esac