Files
WebDAV/start-prod.sh
user 1a4d750a4e feat: 增加开发/生产启动脚本,默认内网监听,并修复账号根 PROPFIND 404
默认监听改为 0.0.0.0:18900,内网设备可直接访问;sso 模式仍要求绑回环。
启动日志不再把 0.0.0.0 写成 http://0.0.0.0。

新增 start-dev.sh 与 start-prod.sh(后台 nohup,不占用终端),公共逻辑在
start-lib.sh。开发脚本可按源码指纹决定是否编译;生产脚本在普通启动和
注册 systemd 时同样比对二进制,不一致则 make build。无完整源码时只用现有
二进制。指纹与 pid/日志写入 .run/(已 gitignore)。

生产脚本交互菜单支持后台启动、注册/取消系统服务与开机自启。占用端口时:
本仓库开发或脚本实例先停再接管默认 18900;其它进程才改用相邻端口。临时
换端口不写回下次默认地址。启动信息为每个内网地址同时打印管理页和文件页。

WebDAV 账号根无尾斜杠(如 /ccswitch)会按 /ccswitch/ 处理,避免客户端
PROPFIND 得到 404。补充对应测试。README 同步启动、部署与 systemd 说明。
2026-10-03 11:39:14 +08:00

1355 lines
32 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 生产环境:后台启动或注册 systemd。二进制与源码不一致时先编译。
# 没有源码树时只使用现有二进制。启动后与当前终端分离。
set -euo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
cd "$ROOT"
# shellcheck source=start-lib.sh
. "$ROOT/start-lib.sh"
setup_build_path
umask 077
RUN_DIR="${DAVBOX_RUN_DIR:-$ROOT/.run/prod}"
PID_FILE="$RUN_DIR/davbox.pid"
LOG_FILE="$RUN_DIR/davbox.log"
CONF_FILE="$RUN_DIR/davbox.conf"
LOCK_FILE="$RUN_DIR/start.lock"
DEV_PID_FILE="$ROOT/.run/dev/davbox.pid"
DEV_CONF_FILE="$ROOT/.run/dev/start.conf"
FACTORY_ADDR="0.0.0.0:18900"
DEFAULT_ADDR="$FACTORY_ADDR"
DEFAULT_DATA="./data"
DEFAULT_AUTH="builtin"
DEFAULT_BIN="$ROOT/davbox"
LAST_LISTEN=""
SERVICE_NAME="${DAVBOX_SERVICE:-davbox}"
usage() {
cat <<'EOF'
用法(生产环境):
./start-prod.sh 后台启动已编译的 davbox
./start-prod.sh start 同上
./start-prod.sh stop 停止
./start-prod.sh restart 按已保存(或环境变量)配置重启
./start-prod.sh status 查看状态
./start-prod.sh log 打印最近日志
./start-prod.sh help 显示本说明
./start-prod.sh service install 注册 systemd 服务并开机自启
./start-prod.sh service uninstall 取消开机自启并移除单元
./start-prod.sh service status 查看 systemd 状态
./start-prod.sh service start|stop|restart
./start-prod.sh service enable|disable 只改开机自启,不立刻启停
有源码时会比对二进制与源码,不一致则先编译再启动或注册服务。没有源码时只使用现有二进制。
监听优先使用 0.0.0.0:18900。开发脚本或生产脚本拉起的实例会先停掉再接管该端口;其它进程占用时才改用相邻端口。临时换端口不会当成下次的默认地址。
已注册 systemd 后,start / stop / restart 走系统服务,不再用脚本托管进程。
可用环境变量(优先于上次保存的配置):
DAVBOX_BIN 二进制路径,默认 <脚本目录>/davbox
DAVBOX_ADDR 监听地址,默认 0.0.0.0:18900
DAVBOX_DATA 数据目录,默认 ./data
AUTH_MODE builtin 或 sso,默认 builtin
DAVBOX_RUN_DIR 运行时目录(pid / 日志 / 配置),默认 <脚本目录>/.run/prod
DAVBOX_SERVICE systemd 单元名(不含 .service),默认 davbox
EOF
}
ensure_run_dir() {
mkdir -p "$RUN_DIR"
chmod 700 "$RUN_DIR"
}
is_auto_bumped_addr() {
local base="$1" got="$2"
local bh="${base%:*}" bp="${base##*:}"
local gh="${got%:*}" gp="${got##*:}"
[[ "$bh" == "$gh" ]] || return 1
[[ "$bp" =~ ^[0-9]+$ && "$gp" =~ ^[0-9]+$ ]] || return 1
(( gp > bp && gp <= bp + 50 ))
}
load_conf() {
LAST_LISTEN=""
[[ -f "$CONF_FILE" ]] || return 0
local k v loaded_addr=""
while IFS='=' read -r k v; do
[[ -z "${k:-}" || "$k" == \#* ]] && continue
case "$k" in
addr) loaded_addr="$v" ;;
listen) LAST_LISTEN="$v" ;;
data) DEFAULT_DATA="$v" ;;
auth_mode) DEFAULT_AUTH="$v" ;;
bin) DEFAULT_BIN="$v" ;;
esac
done < "$CONF_FILE"
if [[ -n "$loaded_addr" ]]; then
if [[ -z "$LAST_LISTEN" ]] && is_auto_bumped_addr "$FACTORY_ADDR" "$loaded_addr"; then
LAST_LISTEN="$loaded_addr"
DEFAULT_ADDR="$FACTORY_ADDR"
else
DEFAULT_ADDR="$loaded_addr"
fi
fi
}
save_conf() {
ensure_run_dir
cat > "$CONF_FILE" <<EOF
addr=$1
listen=$5
data=$2
auth_mode=$3
bin=$4
EOF
chmod 600 "$CONF_FILE"
}
trim() {
local s="$1"
s="${s#"${s%%[![:space:]]*}"}"
s="${s%"${s##*[![:space:]]}"}"
printf '%s' "$s"
}
is_tty() {
[[ -t 0 && -t 1 ]]
}
prompt_value() {
local msg="$1" def="$2" val=""
if is_tty; then
read -r -p "$msg [$def]: " val || true
fi
val="$(trim "${val:-}")"
printf '%s' "${val:-$def}"
}
confirm() {
local msg="$1" def="${2:-Y}" ans=""
if ! is_tty; then
if [[ "$def" == [Yy] ]]; then
return 0
fi
return 1
fi
read -r -p "$msg " ans || true
ans="$(trim "${ans:-}")"
if [[ -z "$ans" ]]; then
ans="$def"
fi
if [[ "$ans" == [Yy] || "$ans" == [Yy][Ee][Ss] ]]; then
return 0
fi
return 1
}
pid_alive() {
local pid="${1:-}"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
return 1
}
pid_is_davbox() {
local pid="$1" cmd="" exe=""
if [[ -r "/proc/$pid/cmdline" ]]; then
cmd="$(tr '\0' ' ' < "/proc/$pid/cmdline")"
else
cmd="$(ps -p "$pid" -o args= 2>/dev/null || true)"
fi
if [[ "$cmd" == *davbox* ]]; then
return 0
fi
if [[ -r "/proc/$pid/exe" ]]; then
exe="$(readlink "/proc/$pid/exe" 2>/dev/null || true)"
if [[ "$exe" == *davbox* ]]; then
return 0
fi
fi
return 1
}
running_pid() {
local pid=""
[[ -f "$PID_FILE" ]] || return 1
pid="$(trim "$(cat "$PID_FILE" 2>/dev/null || true)")"
if pid_alive "$pid" && pid_is_davbox "$pid"; then
printf '%s' "$pid"
return 0
fi
rm -f "$PID_FILE"
return 1
}
lan_ipv4s() {
if command -v ip >/dev/null 2>&1; then
ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1
return 0
fi
if command -v hostname >/dev/null 2>&1; then
hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | grep -v '^127\.' || true
fi
}
print_access_lines() {
local addr="$1"
local host="${addr%:*}"
local port="${addr##*:}"
echo " 监听 $addr"
if [[ "$host" == "0.0.0.0" || "$host" == "::" || "$host" == "[::]" ]]; then
echo " 本机 http://127.0.0.1:${port}"
echo " 管理页 http://127.0.0.1:${port}/admin"
echo " 文件页 http://127.0.0.1:${port}/"
local ip
while read -r ip; do
[[ -z "$ip" ]] && continue
echo " 内网 http://${ip}:${port}"
echo " 管理页 http://${ip}:${port}/admin"
echo " 文件页 http://${ip}:${port}/"
done < <(lan_ipv4s || true)
else
echo " 管理页 http://${host}:${port}/admin"
echo " 文件页 http://${host}:${port}/"
fi
}
connect_host() {
local addr="$1" host
host="${addr%:*}"
if [[ "$host" == "0.0.0.0" || "$host" == "::" || "$host" == "[::]" ]]; then
host="127.0.0.1"
fi
printf '%s' "$host"
}
port_of() {
printf '%s' "${1##*:}"
}
port_busy() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
if ss -ltn 2>/dev/null | awk '{print $4}' | grep -Eq ":${port}\$"; then
return 0
fi
return 1
fi
if command -v lsof >/dev/null 2>&1; then
if lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
return 0
fi
return 1
fi
return 1
}
listen_pids_from_proc() {
local port="$1" hex ino pid fd tgt cmd
hex="$(printf '%04X' "$port")"
local inos=""
local table local_addr st
for table in /proc/net/tcp /proc/net/tcp6; do
[[ -r "$table" ]] || continue
while read -r _ local_addr _ st _ _ _ _ ino _; do
[[ "$st" == "0A" ]] || continue
[[ "${local_addr##*:}" == "$hex" ]] || continue
inos+="${ino} "
done < <(tail -n +2 "$table" 2>/dev/null || true)
done
[[ -n "$inos" ]] || return 0
for pid in /proc/[0-9]*; do
pid="${pid#/proc/}"
cmd="$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)"
[[ "$cmd" == *davbox* ]] || continue
for fd in /proc/${pid}/fd/*; do
tgt="$(readlink "$fd" 2>/dev/null || true)"
for ino in $inos; do
if [[ "$tgt" == "socket:[${ino}]" ]]; then
printf '%s\n' "$pid"
break 2
fi
done
done
done
}
listen_pids() {
local port="$1" out=""
if command -v lsof >/dev/null 2>&1; then
out="$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null || true)"
fi
if [[ -z "$(trim "${out:-}")" ]] && command -v fuser >/dev/null 2>&1; then
out="$(fuser "${port}/tcp" 2>/dev/null || true)"
fi
if [[ -z "$(trim "${out:-}")" ]] && command -v ss >/dev/null 2>&1; then
out="$(ss -ltnp 2>/dev/null | awk -v port="$port" '
{
n = split($4, a, ":")
lp = a[n]
gsub(/\]/, "", lp)
if (lp == port) {
s = $0
while (match(s, /pid=[0-9]+/)) {
print substr(s, RSTART + 4, RLENGTH - 4)
s = substr(s, RSTART + RLENGTH)
}
}
}')"
fi
if [[ -z "$(trim "${out:-}")" ]]; then
out="$(listen_pids_from_proc "$port" || true)"
fi
if [[ -n "$(trim "${out:-}")" ]]; then
# shellcheck disable=SC2086
printf '%s\n' $out | sort -u
fi
}
pidfile_pid() {
local file="$1" pid=""
[[ -f "$file" ]] || return 1
pid="$(trim "$(cat "$file" 2>/dev/null || true)")"
if pid_alive "$pid" && pid_is_davbox "$pid"; then
printf '%s' "$pid"
return 0
fi
return 1
}
dev_running_pid() {
pidfile_pid "$DEV_PID_FILE"
}
script_running_pid() {
pidfile_pid "$PID_FILE"
}
conf_port() {
local file="$1" daddr
[[ -f "$file" ]] || return 1
daddr="$(awk -F= '/^addr=/{print $2; exit}' "$file")"
[[ -n "$daddr" ]] || return 1
port_of "$daddr"
}
dev_saved_port() {
conf_port "$DEV_CONF_FILE"
}
prod_saved_port() {
conf_port "$CONF_FILE"
}
systemd_main_pid() {
command -v systemctl >/dev/null 2>&1 || return 1
local p
p="$(systemctl show -p MainPID --value "$(service_unit_name)" 2>/dev/null || true)"
if [[ -n "$p" && "$p" != "0" ]] && pid_alive "$p"; then
printf '%s' "$p"
return 0
fi
return 1
}
pid_is_our_binary() {
local pid="$1" exe="" cmd="" want
want="$(readlink -f "$ROOT/davbox" 2>/dev/null || printf '%s' "$ROOT/davbox")"
if [[ -r "/proc/${pid}/exe" ]]; then
exe="$(readlink "/proc/${pid}/exe" 2>/dev/null || true)"
exe="${exe% (deleted)}"
exe="$(readlink -f "$exe" 2>/dev/null || printf '%s' "$exe")"
if [[ "$exe" == "$want" ]]; then
return 0
fi
fi
cmd="$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)"
if [[ "$cmd" == *"$ROOT/davbox"* ]]; then
return 0
fi
return 1
}
stop_pid_wait() {
local pid="$1"
kill "$pid" 2>/dev/null || true
local i
for i in $(seq 1 80); do
if ! pid_alive "$pid"; then
return 0
fi
sleep 0.1
done
kill -9 "$pid" 2>/dev/null || true
sleep 0.2
if pid_alive "$pid"; then
return 1
fi
return 0
}
forget_pidfiles() {
local pid="$1" f cur
for f in "$PID_FILE" "$DEV_PID_FILE"; do
[[ -f "$f" ]] || continue
cur="$(trim "$(cat "$f" 2>/dev/null || true)")"
if [[ "$cur" == "$pid" ]]; then
rm -f "$f"
fi
done
}
stop_dev_instance() {
local pid=""
if ! pid="$(dev_running_pid)"; then
return 0
fi
echo "正在停止开发实例 PID $pid"
if ! stop_pid_wait "$pid"; then
echo "停止开发实例失败,进程仍在运行:$pid" >&2
exit 1
fi
rm -f "$DEV_PID_FILE"
echo "开发实例已停止"
}
stop_script_instance() {
local pid=""
if ! pid="$(script_running_pid)"; then
return 0
fi
echo "正在停止脚本托管的生产实例 PID $pid"
if ! stop_pid_wait "$pid"; then
echo "停止脚本托管实例失败,进程仍在运行:$pid" >&2
exit 1
fi
rm -f "$PID_FILE"
echo "脚本托管实例已停止"
}
cmdline_listen_addr() {
local pid="$1" tok next=0
local cmd
cmd="$(tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true)"
for tok in $cmd; do
if [[ "$next" -eq 1 ]]; then
printf '%s' "$tok"
return 0
fi
if [[ "$tok" == "-addr" ]]; then
next=1
fi
done
return 1
}
status_listen_addr() {
local pid="" a=""
if pid="$(systemd_main_pid)"; then
a="$(cmdline_listen_addr "$pid" || true)"
if [[ -n "$a" ]]; then
printf '%s' "$a"
return 0
fi
fi
if pid="$(running_pid)"; then
a="$(cmdline_listen_addr "$pid" || true)"
if [[ -n "$a" ]]; then
printf '%s' "$a"
return 0
fi
fi
if [[ -n "${LAST_LISTEN:-}" ]]; then
printf '%s' "$LAST_LISTEN"
return 0
fi
printf '%s' "$DEFAULT_ADDR"
}
stop_all_ours_except_systemd() {
local spid="" pid
spid="$(systemd_main_pid || true)"
if dev_running_pid >/dev/null; then
echo "停止开发脚本启动的实例。"
stop_dev_instance
fi
if script_running_pid >/dev/null; then
echo "停止生产脚本启动的实例。"
stop_script_instance
fi
for pid in /proc/[0-9]*; do
pid="${pid#/proc/}"
[[ "$pid" =~ ^[0-9]+$ ]] || continue
if [[ -n "$spid" && "$pid" == "$spid" ]]; then
continue
fi
if pid_is_our_binary "$pid"; then
echo "停止本仓库 davbox PID $pid"
if ! stop_pid_wait "$pid"; then
echo "停止失败,进程仍在运行:$pid" >&2
exit 1
fi
forget_pidfiles "$pid"
fi
done
}
wait_port_free() {
local port="$1" i
for i in $(seq 1 50); do
if ! port_busy "$port"; then
return 0
fi
sleep 0.1
done
return 1
}
next_free_port() {
local start="$1" p
for p in $(seq $((start + 1)) $((start + 50))); do
if (( p > 65535 )); then
break
fi
if ! port_busy "$p"; then
printf '%s' "$p"
return 0
fi
done
return 1
}
port_held_by_systemd() {
local port="$1" spid="" occ=""
if ! spid="$(systemd_main_pid)"; then
return 1
fi
local saw=0
while read -r occ; do
[[ -z "$occ" ]] && continue
saw=1
if [[ "$occ" == "$spid" ]]; then
return 0
fi
done < <(listen_pids "$port")
if [[ "$saw" -eq 0 ]]; then
local uaddr
uaddr="$(systemctl show -p ExecStart --value "$(service_unit_name)" 2>/dev/null || true)"
if [[ "$uaddr" == *":${port}"* || "$uaddr" == *":${port} "* ]]; then
return 0
fi
fi
return 1
}
# 停掉占用该端口的开发实例、脚本托管实例,以及 pid 文件丢失的本仓库 davbox。
# systemd 拉起的同名服务不在这里杀,交给 systemctl restart。
stop_ours_on_port() {
local port="$1"
local spid="" dpid="" ppid="" occ=""
spid="$(systemd_main_pid || true)"
dpid="$(dev_running_pid || true)"
ppid="$(script_running_pid || true)"
local saw=0
while read -r occ; do
[[ -z "$occ" ]] && continue
saw=1
if [[ -n "$spid" && "$occ" == "$spid" ]]; then
continue
fi
if [[ -n "$dpid" && "$occ" == "$dpid" ]]; then
echo "端口 $port 由开发脚本启动的实例占用(PID $dpid),正在停止后接管。"
stop_dev_instance
dpid=""
continue
fi
if [[ -n "$ppid" && "$occ" == "$ppid" ]]; then
echo "端口 $port 由生产脚本启动的实例占用(PID $ppid),正在停止后接管。"
stop_script_instance
ppid=""
continue
fi
if pid_is_our_binary "$occ"; then
echo "端口 $port 由本仓库 davbox(PID $occ)占用,正在停止后接管。"
if ! stop_pid_wait "$occ"; then
echo "停止失败,进程仍在运行:$occ" >&2
exit 1
fi
forget_pidfiles "$occ"
fi
done < <(listen_pids "$port")
if [[ "$saw" -eq 0 ]]; then
local dport="" pport=""
dport="$(dev_saved_port || true)"
pport="$(prod_saved_port || true)"
if [[ -n "$dpid" && "$dport" == "$port" ]]; then
echo "端口 $port 与开发脚本记录一致(PID $dpid),正在停止后接管。"
stop_dev_instance
fi
if [[ -n "$ppid" && "$pport" == "$port" ]]; then
echo "端口 $port 与生产脚本记录一致(PID $ppid),正在停止后接管。"
stop_script_instance
fi
fi
}
# 就地改写传入的地址变量(nameref),避免 $(...) 吃掉提示或吞掉 exit。
# 直接启动与注册 systemd 共用:先停本仓库全部开发/脚本实例,再抢回目标端口;
# 只有外来进程占用时才改相邻端口。
resolve_listen_addr() {
local -n _addr_ref=$1
local host="${_addr_ref%:*}"
local port
port="$(port_of "$_addr_ref")"
stop_all_ours_except_systemd
stop_ours_on_port "$port"
wait_port_free "$port" || true
if ! port_busy "$port"; then
return 0
fi
if port_held_by_systemd "$port"; then
return 0
fi
local np=""
if ! np="$(next_free_port "$port")"; then
echo "端口 $port 被其它进程占用,且没有相邻空闲端口。" >&2
exit 1
fi
echo "端口 $port 被其它进程占用,改用相邻端口 $np。"
_addr_ref="${host}:${np}"
}
tcp_ready() {
local addr="$1" host port
host="$(connect_host "$addr")"
port="$(port_of "$addr")"
if command -v timeout >/dev/null 2>&1; then
timeout 0.3 bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null && return 0
return 1
fi
bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null && return 0
return 1
}
normalize_auth() {
local m
m="$(trim "$1")"
m="${m,,}"
case "$m" in
""|builtin) printf 'builtin' ;;
sso) printf 'sso' ;;
*)
echo "AUTH_MODE 只支持 builtin 或 sso,收到:$1" >&2
exit 1
;;
esac
}
is_loopback_addr() {
local host="${1%:*}"
case "$host" in
127.0.0.1|localhost|::1|[::1]|127.*) return 0 ;;
*) return 1 ;;
esac
}
validate_addr() {
local addr="$1"
if [[ ! "$addr" =~ :[0-9]+$ ]]; then
echo "监听地址需要 host:port 形式,例如 0.0.0.0:18900" >&2
exit 1
fi
local port
port="$(port_of "$addr")"
if (( port < 1 || port > 65535 )); then
echo "端口不合法:$port" >&2
exit 1
fi
}
validate_data() {
if [[ -z "$1" ]]; then
echo "数据目录不能为空" >&2
exit 1
fi
}
require_binary() {
local bin="$1"
if [[ ! -e "$bin" ]]; then
echo "未找到二进制:$bin" >&2
echo "请先在源码目录执行 make build,或把 davbox 放到本脚本同目录。" >&2
exit 1
fi
if [[ ! -f "$bin" ]]; then
echo "不是普通文件:$bin" >&2
exit 1
fi
if [[ ! -x "$bin" ]]; then
echo "二进制不可执行:$bin" >&2
exit 1
fi
}
apply_env() {
if [[ -n "${DAVBOX_ADDR:-}" ]]; then
DEFAULT_ADDR="$DAVBOX_ADDR"
fi
if [[ -n "${DAVBOX_DATA:-}" ]]; then
DEFAULT_DATA="$DAVBOX_DATA"
fi
if [[ -n "${AUTH_MODE:-}" ]]; then
DEFAULT_AUTH="$AUTH_MODE"
fi
if [[ -n "${DAVBOX_BIN:-}" ]]; then
DEFAULT_BIN="$DAVBOX_BIN"
fi
}
abs_path() {
local p="$1"
case "$p" in
/*) printf '%s' "$p" ;;
*) printf '%s' "$ROOT/${p#./}" ;;
esac
}
service_unit_name() {
printf '%s.service' "$SERVICE_NAME"
}
service_unit_path() {
printf '/etc/systemd/system/%s' "$(service_unit_name)"
}
need_systemd() {
if ! command -v systemctl >/dev/null 2>&1; then
echo "未找到 systemctl,当前系统没有 systemd。" >&2
exit 1
fi
}
run_root() {
if [[ "$(id -u)" -eq 0 ]]; then
"$@"
elif command -v sudo >/dev/null 2>&1; then
sudo "$@"
else
echo "注册系统服务需要 root 权限(未找到 sudo)。" >&2
exit 1
fi
}
service_user() {
if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
printf '%s' "$SUDO_USER"
return 0
fi
id -un
}
service_group() {
local u
u="$(service_user)"
id -gn "$u" 2>/dev/null || id -gn
}
systemd_is_active() {
command -v systemctl >/dev/null 2>&1 || return 1
systemctl is-active --quiet "$(service_unit_name)" 2>/dev/null
}
systemd_is_enabled() {
command -v systemctl >/dev/null 2>&1 || return 1
systemctl is-enabled --quiet "$(service_unit_name)" 2>/dev/null
}
systemd_unit_installed() {
[[ -f "$(service_unit_path)" ]]
}
write_service_unit() {
local dest="$1" bin_abs data_abs addr auth user group
bin_abs="$(abs_path "$DEFAULT_BIN")"
data_abs="$(abs_path "$DEFAULT_DATA")"
addr="$DEFAULT_ADDR"
auth="$DEFAULT_AUTH"
user="$(service_user)"
group="$(service_group)"
cat >"$dest" <<EOF
[Unit]
Description=davbox WebDAV
After=network.target
[Service]
Type=simple
User=$user
Group=$group
WorkingDirectory=$ROOT
ExecStart=$bin_abs -addr $addr -data $data_abs
Environment=AUTH_MODE=$auth
Restart=on-failure
RestartSec=3
TimeoutStopSec=20
UMask=0077
NoNewPrivileges=true
StandardOutput=journal
StandardError=journal
SyslogIdentifier=$SERVICE_NAME
[Install]
WantedBy=multi-user.target
EOF
}
print_status() {
local pid="" boot="未开启"
if systemd_is_enabled; then
boot="已开启"
fi
if systemd_is_active; then
echo "davbox 由 systemd 运行"
echo " 单元 $(service_unit_name)"
echo " 开机自启 $boot"
echo " 二进制 $DEFAULT_BIN"
print_access_lines "$(status_listen_addr)"
echo " 数据 $DEFAULT_DATA"
echo " 认证 $DEFAULT_AUTH"
echo " 日志 journalctl -u $(service_unit_name)"
return 0
fi
if pid="$(running_pid)"; then
echo "davbox 生产实例正在运行(脚本托管)"
echo " PID $pid"
echo " 二进制 $DEFAULT_BIN"
print_access_lines "$(status_listen_addr)"
echo " 数据 $DEFAULT_DATA"
echo " 认证 $DEFAULT_AUTH"
echo " 日志 $LOG_FILE"
echo " 开机自启 $boot"
else
echo "davbox 生产实例未运行"
if systemd_unit_installed; then
echo " 单元 $(service_unit_name) 已安装,开机自启:$boot"
fi
fi
}
do_stop() {
if systemd_is_active; then
echo "正在通过 systemd 停止 $(service_unit_name)"
run_root systemctl stop "$(service_unit_name)"
rm -f "$PID_FILE"
echo "已停止"
return 0
fi
local pid=""
if ! pid="$(running_pid)"; then
echo "davbox 生产实例未运行"
return 0
fi
echo "正在停止 PID $pid"
kill "$pid" 2>/dev/null || true
local i
for i in $(seq 1 80); do
if ! pid_alive "$pid"; then
break
fi
sleep 0.1
done
if pid_alive "$pid"; then
kill -9 "$pid" 2>/dev/null || true
sleep 0.2
fi
rm -f "$PID_FILE"
if pid_alive "$pid"; then
echo "停止失败,进程仍在运行:$pid" >&2
exit 1
fi
echo "已停止"
}
show_log() {
if systemd_is_active || systemd_unit_installed; then
if command -v journalctl >/dev/null 2>&1; then
journalctl -u "$(service_unit_name)" -n 80 --no-pager || true
return 0
fi
fi
if [[ ! -f "$LOG_FILE" ]]; then
echo "还没有日志"
return 0
fi
tail -n 80 "$LOG_FILE"
}
extract_admin_pass() {
[[ -f "$LOG_FILE" ]] || return 0
sed -n 's/.*管理员初始口令(仅本次打印,请自行保存)://p' "$LOG_FILE" | tail -n 1
}
wait_ready() {
local pid="$1" addr="$2" i
for i in $(seq 1 150); do
if ! pid_alive "$pid"; then
return 1
fi
if grep -q 'davbox 已启动' "$LOG_FILE" 2>/dev/null; then
if tcp_ready "$addr"; then
return 0
fi
fi
if tcp_ready "$addr"; then
return 0
fi
sleep 0.1
done
if pid_alive "$pid"; then
return 0
fi
return 1
}
acquire_lock() {
ensure_run_dir
exec 9>"$LOCK_FILE"
if command -v flock >/dev/null 2>&1; then
if ! flock -n 9; then
echo "另一个启动脚本正在运行" >&2
exit 1
fi
fi
}
do_start() {
load_conf
apply_env
if systemd_is_active; then
print_status
if ! is_tty || [[ "${SKIP_PROMPTS:-0}" == "1" ]]; then
return 0
fi
echo
echo " [1] 保持运行并退出"
echo " [2] 停止"
echo " [3] 重新启动"
echo " [4] 关闭开机自启"
echo " [5] 取消系统服务"
local choice=""
read -r -p "选择 [1]: " choice || true
choice="$(trim "${choice:-1}")"
case "$choice" in
2)
do_stop
return 0
;;
3)
BIN="$ROOT/davbox"
DEFAULT_BIN="$BIN"
ensure_binary
echo "正在通过 systemd 重启 $(service_unit_name)"
run_root systemctl restart "$(service_unit_name)"
print_status
return 0
;;
4)
run_root systemctl disable "$(service_unit_name)"
echo "已关闭开机自启,服务仍在运行。"
return 0
;;
5)
service_uninstall
return 0
;;
*)
echo "当前终端已空闲,服务由 systemd 继续运行。"
return 0
;;
esac
fi
if systemd_is_enabled; then
BIN="$ROOT/davbox"
DEFAULT_BIN="$BIN"
ensure_binary
echo "已注册 systemd 开机自启,通过系统服务启动。"
if [[ "${BINARY_REBUILT:-0}" == "1" ]]; then
run_root systemctl restart "$(service_unit_name)"
else
run_root systemctl start "$(service_unit_name)"
fi
print_status
return 0
fi
local pid=""
if pid="$(running_pid)"; then
print_status
if ! is_tty || [[ "${SKIP_PROMPTS:-0}" == "1" ]]; then
return 0
fi
echo
echo " [1] 保持运行并退出"
echo " [2] 停止"
echo " [3] 重新启动"
if command -v systemctl >/dev/null 2>&1; then
echo " [4] 注册为系统服务并开机自启"
fi
local choice=""
read -r -p "选择 [1]: " choice || true
choice="$(trim "${choice:-1}")"
case "$choice" in
2)
do_stop
return 0
;;
3)
do_stop
;;
4)
if ! command -v systemctl >/dev/null 2>&1; then
echo "未找到 systemctl。" >&2
return 0
fi
SERVICE_FROM_MENU=1
service_install
return 0
;;
*)
echo "当前终端已空闲,服务继续在后台运行。"
return 0
;;
esac
fi
acquire_lock
if pid="$(running_pid)"; then
print_status
echo "当前终端已空闲,服务继续在后台运行。"
return 0
fi
local addr="$DEFAULT_ADDR"
local data="$DEFAULT_DATA"
local auth="$DEFAULT_AUTH"
local bin="$DEFAULT_BIN"
echo "生产环境:后台启动已编译的 davbox。"
echo
echo " 二进制 $bin"
echo " 监听地址 $addr"
echo " 数据目录 $data"
echo " 管理端认证 $auth"
echo
local edit="n"
if is_tty && [[ "${SKIP_PROMPTS:-0}" != "1" ]]; then
local ans=""
read -r -p "按回车使用以上配置,输入 n 修改配置: " ans || true
ans="$(trim "${ans:-}")"
if [[ "$ans" == [Nn] ]]; then
edit="y"
fi
fi
if [[ "$edit" == "y" ]]; then
bin="$(prompt_value "二进制路径" "$bin")"
addr="$(prompt_value "监听地址" "$addr")"
data="$(prompt_value "数据目录" "$data")"
auth="$(prompt_value "管理端认证(builtin / sso)" "$auth")"
fi
local start_mode="1"
if is_tty && [[ "${SKIP_PROMPTS:-0}" != "1" ]] && command -v systemctl >/dev/null 2>&1; then
echo " [1] 后台启动(本次有效)"
echo " [2] 注册为系统服务并开机自启"
read -r -p "选择 [1]: " start_mode || true
start_mode="$(trim "${start_mode:-1}")"
fi
bin="$(trim "$bin")"
addr="$(trim "$addr")"
data="$(trim "$data")"
auth="$(normalize_auth "$auth")"
validate_addr "$addr"
validate_data "$data"
BIN="$ROOT/davbox"
bin="$BIN"
DEFAULT_BIN="$BIN"
ensure_binary
if [[ "$auth" == "sso" ]] && ! is_loopback_addr "$addr"; then
echo "AUTH_MODE=sso 只应在回环 + 网关注入 X-Auth-User 并剥离该头时使用。" >&2
exit 1
fi
mkdir -p "$data"
if [[ "$start_mode" == "2" ]]; then
DEFAULT_ADDR="$addr"
DEFAULT_DATA="$data"
DEFAULT_AUTH="$auth"
DEFAULT_BIN="$bin"
SERVICE_FROM_MENU=1
service_install
return 0
fi
local requested="$addr"
resolve_listen_addr addr
validate_addr "$addr"
save_conf "$requested" "$data" "$auth" "$bin" "$addr"
local first_admin=0
if [[ "$auth" == "builtin" && ! -f "$data/admin.json" ]]; then
first_admin=1
fi
ensure_run_dir
touch "$LOG_FILE"
chmod 600 "$LOG_FILE"
{
echo
echo "-------- $(date '+%Y-%m-%d %H:%M:%S') 生产启动 --------"
echo "bin=$bin addr=$addr data=$data AUTH_MODE=$auth"
} >> "$LOG_FILE"
echo "正在后台启动…"
# 9>&-:不要把启动锁传给后台进程,否则再次运行本脚本会被 flock 挡住。
nohup env AUTH_MODE="$auth" "$bin" -addr "$addr" -data "$data" >>"$LOG_FILE" 2>&1 </dev/null 9>&- &
pid=$!
echo "$pid" > "$PID_FILE"
chmod 600 "$PID_FILE"
disown "$pid" 2>/dev/null || true
if ! wait_ready "$pid" "$addr"; then
echo "启动失败,最近日志:" >&2
tail -n 40 "$LOG_FILE" >&2 || true
rm -f "$PID_FILE"
exit 1
fi
echo
echo "生产实例已在后台运行,当前终端可以继续使用。"
echo
echo " PID $pid"
echo " 二进制 $bin"
print_access_lines "$addr"
echo " 数据 $data"
echo " 认证 $auth"
echo " 日志 $LOG_FILE"
echo " 停止 $ROOT/start-prod.sh stop"
echo " 开机自启 $ROOT/start-prod.sh service install"
echo
if [[ "$first_admin" -eq 1 ]]; then
local pass=""
pass="$(extract_admin_pass || true)"
if [[ -n "$pass" ]]; then
echo " 管理员初始口令(请立刻保存):$pass"
echo " 明文也写在 $data/admin-password.txt"
echo
fi
fi
}
service_install() {
need_systemd
if [[ "${SERVICE_FROM_MENU:-0}" != "1" ]]; then
load_conf
apply_env
fi
BIN="$ROOT/davbox"
DEFAULT_BIN="$BIN"
ensure_binary
validate_addr "$DEFAULT_ADDR"
validate_data "$DEFAULT_DATA"
if [[ "$DEFAULT_AUTH" == "sso" ]] && ! is_loopback_addr "$DEFAULT_ADDR"; then
echo "AUTH_MODE=sso 只应在回环 + 网关注入 X-Auth-User 并剥离该头时使用。" >&2
exit 1
fi
mkdir -p "$(abs_path "$DEFAULT_DATA")"
local requested="$DEFAULT_ADDR"
echo "注册系统服务:先停止本仓库已有实例,再监听 $requested"
resolve_listen_addr DEFAULT_ADDR
validate_addr "$DEFAULT_ADDR"
save_conf "$requested" "$DEFAULT_DATA" "$DEFAULT_AUTH" "$DEFAULT_BIN" "$DEFAULT_ADDR"
local tmp
tmp="$(mktemp)"
write_service_unit "$tmp"
echo "将写入 $(service_unit_path) 并设置开机自启。"
echo
echo " 用户 $(service_user)"
echo " 二进制 $(abs_path "$DEFAULT_BIN")"
echo " 监听 $DEFAULT_ADDR"
echo " 数据 $(abs_path "$DEFAULT_DATA")"
echo " 认证 $DEFAULT_AUTH"
echo
if is_tty && [[ "${SKIP_PROMPTS:-0}" != "1" && "${SERVICE_FROM_MENU:-0}" != "1" ]]; then
if ! confirm "写入 systemd 并开机自启? [Y/n]" "Y"; then
rm -f "$tmp"
echo "已取消"
return 0
fi
fi
run_root install -m 644 "$tmp" "$(service_unit_path)"
rm -f "$tmp"
run_root systemctl daemon-reload
run_root systemctl enable "$(service_unit_name)"
echo "已注册 $(service_unit_name),开机自启已打开。"
if is_tty && [[ "${SKIP_PROMPTS:-0}" != "1" ]]; then
if ! confirm "现在启动服务? [Y/n]" "Y"; then
echo "未启动。之后可执行:$ROOT/start-prod.sh service start"
return 0
fi
fi
if systemd_is_active; then
run_root systemctl restart "$(service_unit_name)"
else
run_root systemctl start "$(service_unit_name)"
fi
print_status
}
service_uninstall() {
need_systemd
if is_tty && [[ "${SKIP_PROMPTS:-0}" != "1" ]]; then
if ! confirm "取消开机自启并移除 $(service_unit_name)? [y/N]" "N"; then
echo "已取消"
return 0
fi
fi
if systemd_is_active; then
run_root systemctl stop "$(service_unit_name)"
fi
if systemd_is_enabled || systemd_unit_installed; then
run_root systemctl disable "$(service_unit_name)" 2>/dev/null || true
fi
if systemd_unit_installed; then
run_root rm -f "$(service_unit_path)"
fi
run_root systemctl daemon-reload
echo "已移除系统服务 $(service_unit_name)"
}
service_cmd() {
local sub="${1:-}"
case "$sub" in
install)
service_install
;;
uninstall)
service_uninstall
;;
status)
load_conf
apply_env
need_systemd
print_status
if command -v systemctl >/dev/null 2>&1; then
echo
systemctl --no-pager --full status "$(service_unit_name)" || true
fi
;;
start)
load_conf
apply_env
need_systemd
BIN="$ROOT/davbox"
DEFAULT_BIN="$BIN"
ensure_binary
if [[ "${BINARY_REBUILT:-0}" == "1" ]]; then
run_root systemctl restart "$(service_unit_name)"
else
run_root systemctl start "$(service_unit_name)"
fi
print_status
;;
stop)
load_conf
apply_env
need_systemd
run_root systemctl stop "$(service_unit_name)"
echo "已停止 $(service_unit_name)"
;;
restart)
load_conf
apply_env
need_systemd
BIN="$ROOT/davbox"
DEFAULT_BIN="$BIN"
ensure_binary
run_root systemctl restart "$(service_unit_name)"
print_status
;;
enable)
need_systemd
run_root systemctl enable "$(service_unit_name)"
echo "已打开开机自启"
;;
disable)
need_systemd
run_root systemctl disable "$(service_unit_name)"
echo "已关闭开机自启"
;;
""|help|-h|--help)
usage
;;
*)
echo "未知 service 命令:$sub" >&2
usage >&2
exit 1
;;
esac
}
cmd="${1:-start}"
case "$cmd" in
start)
do_start
;;
stop)
load_conf
apply_env
do_stop
;;
restart)
SKIP_PROMPTS=1
load_conf
apply_env
do_stop
do_start
;;
status)
load_conf
apply_env
print_status
;;
log)
show_log
;;
service)
shift || true
service_cmd "${1:-}"
;;
help|-h|--help)
usage
;;
*)
echo "未知命令:$cmd" >&2
usage >&2
exit 1
;;
esac