#!/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 - <"${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 </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