From c92e4391c68cd8e43ae596ee0235b0437b8e63f4 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:01:12 +0800 Subject: [PATCH] Adapt ServerKit data storage for 1Panel --- apps/serverkit/1.11.4/docker-compose.yml | 6 +- apps/serverkit/1.11.4/scripts/init.sh | 175 +++++++++++++++++++++++ apps/serverkit/1.11.4/scripts/upgrade.sh | 90 ++++++++++++ apps/serverkit/README.md | 12 +- apps/serverkit/README_en.md | 12 +- apps/serverkit/latest/docker-compose.yml | 6 +- apps/serverkit/latest/scripts/init.sh | 175 +++++++++++++++++++++++ apps/serverkit/latest/scripts/upgrade.sh | 90 ++++++++++++ 8 files changed, 554 insertions(+), 12 deletions(-) create mode 100755 apps/serverkit/1.11.4/scripts/init.sh create mode 100755 apps/serverkit/1.11.4/scripts/upgrade.sh create mode 100755 apps/serverkit/latest/scripts/init.sh create mode 100755 apps/serverkit/latest/scripts/upgrade.sh diff --git a/apps/serverkit/1.11.4/docker-compose.yml b/apps/serverkit/1.11.4/docker-compose.yml index 54b309a60..a4f5a5634 100644 --- a/apps/serverkit/1.11.4/docker-compose.yml +++ b/apps/serverkit/1.11.4/docker-compose.yml @@ -17,7 +17,7 @@ services: - "TRUSTED_PROXY_HOPS=1" - "SERVERKIT_GITHUB_REPO=jhd3197/ServerKit" volumes: - - "serverkit-data:/app/instance" + - "./data:/app/instance" networks: - "1panel-network" labels: @@ -35,10 +35,6 @@ services: max-size: "10m" max-file: "3" -volumes: - serverkit-data: - name: serverkit-data - networks: 1panel-network: external: true diff --git a/apps/serverkit/1.11.4/scripts/init.sh b/apps/serverkit/1.11.4/scripts/init.sh new file mode 100755 index 000000000..012c44a60 --- /dev/null +++ b/apps/serverkit/1.11.4/scripts/init.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +set -euo pipefail +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +export PATH + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}" + +read_env_value() { + local key="$1" + [[ -f "$ENV_FILE" ]] || return 0 + local value + value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +configured_value() { + local key="$1" + local default_value="$2" + local value + value="${!key:-}" + if [[ -z "$value" ]]; then + value="$(read_env_value "$key")" + fi + printf '%s\n' "${value:-$default_value}" +} + +resolve_app_path() { + local key="$1" + local raw="$2" + local clean candidate resolved current part + local -a parts=() + case "$raw" in + ""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;; + esac + if [[ "$raw" =~ [[:cntrl:]] ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + clean="${raw#./}" + [[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; } + command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; } + candidate="$ROOT_DIR/$clean" + resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; } + case "$resolved" in + "$ROOT_DIR"/*) ;; + *) echo "unsafe ${key} path" >&2; return 1 ;; + esac + current="$ROOT_DIR" + IFS='/' read -r -a parts <<< "$clean" + for part in "${parts[@]}"; do + [[ -z "$part" || "$part" == "." ]] && continue + current="$current/$part" + if [[ -L "$current" ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + done + printf '%s\n' "$resolved" +} + +ensure_dir() { + local key="$1" + local raw + local path + raw="$(configured_value "$key" "$2")" + path="$(resolve_app_path "$key" "$raw")" + mkdir -p -- "$path" + [[ "$(resolve_app_path "$key" "$raw")" == "$path" ]] || { echo "unsafe ${key} path" >&2; return 1; } +} + +resolve_direct_child() { + local key="$1" + local raw="$2" + local clean path + clean="${raw#./}" + if [[ -z "$clean" || "$clean" == */* ]]; then + echo "unsafe ${key} path: lifecycle directories must be direct children of the version root" >&2 + return 1 + fi + path="$(resolve_app_path "$key" "$raw")" + [[ "$path" == "$ROOT_DIR/$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; } + printf '%s\n' "$path" +} + +verify_trusted_root_chain() { + local current owner mode + [[ "$(id -u)" == "0" ]] || { echo "directory ownership initialization must run as root" >&2; return 1; } + command -v stat >/dev/null 2>&1 || { echo "stat is required" >&2; return 1; } + current="$ROOT_DIR" + while [[ "$current" != "/" ]]; do + [[ -d "$current" && ! -L "$current" ]] || { echo "unsafe version root chain: $current" >&2; return 1; } + IFS=':' read -r owner mode < <(stat -c '%u:%a' -- "$current") + if [[ "$current" == "$ROOT_DIR" ]]; then + # 1Panel local-app copies may be owned by the panel agent UID 1001. + [[ "$owner" == "0" || "$owner" == "1001" ]] || { echo "unsafe version root chain owner: $current" >&2; return 1; } + else + [[ "$owner" == "0" ]] || { echo "unsafe version root chain owner: $current" >&2; return 1; } + fi + [[ "$mode" =~ ^[0-7]{3,4}$ ]] || { echo "unsafe version root chain mode: $current" >&2; return 1; } + (( (8#$mode & 0022) == 0 )) || { echo "unsafe version root chain permissions: $current" >&2; return 1; } + current="$(dirname -- "$current")" + done +} + +declare -A OWNED_PATHS=() +declare -A OWNED_KEYS_BY_PATH=() + +register_owned_dir() { + local key="$1" + local raw="$2" + local path previous_key + path="$(resolve_direct_child "$key" "$raw")" + if [[ -n "${OWNED_KEYS_BY_PATH[$path]+x}" ]]; then + previous_key="${OWNED_KEYS_BY_PATH[$path]}" + echo "duplicate directory ownership target: $path ($previous_key and $key)" >&2 + return 1 + fi + OWNED_KEYS_BY_PATH["$path"]="$key" + OWNED_PATHS["$key"]="$path" +} + +register_configured_owned_dir() { + local key="$1" + local default_value="$2" + local raw + raw="$(configured_value "$key" "$default_value")" + register_owned_dir "$key" "$raw" +} + +register_fixed_owned_dir() { + local source="$1" + register_owned_dir "fixed directory $source" "$source" +} + +apply_owned_dir() { + local key="$1" + local uid="$2" + local gid="$3" + local mode="$4" + local path actual expected_mode + [[ -n "${OWNED_PATHS[$key]+x}" ]] || { echo "missing directory ownership preflight: $key" >&2; return 1; } + path="${OWNED_PATHS[$key]}" + verify_trusted_root_chain + if [[ -e "$path" || -L "$path" ]]; then + [[ -d "$path" && ! -L "$path" ]] || { echo "unsafe ${key} path" >&2; return 1; } + else + mkdir -- "$path" + fi + chmod "$mode" -- "$path" + [[ -d "$path" && ! -L "$path" ]] || { echo "unsafe ${key} path" >&2; return 1; } + chown --no-dereference "$uid:$gid" -- "$path" + expected_mode="${mode#0}" + actual="$(stat -c '%u:%g:%a' -- "$path")" + [[ "$actual" == "$uid:$gid:$expected_mode" ]] || { echo "${key} ownership/mode mismatch: expected ${uid}:${gid}:${expected_mode}, got ${actual}" >&2; return 1; } +} + +ensure_owned_dir() { + local key="$1" + local default_value="$2" + [[ -n "$default_value" ]] || { echo "missing directory ownership default: $key" >&2; return 1; } + apply_owned_dir "$key" "$3" "$4" "$5" +} + +ensure_fixed_owned_dir() { + local source="$1" + apply_owned_dir "fixed directory $source" "$2" "$3" "$4" +} + +register_fixed_owned_dir "./data" +ensure_fixed_owned_dir "./data" "999" "999" "0750" diff --git a/apps/serverkit/1.11.4/scripts/upgrade.sh b/apps/serverkit/1.11.4/scripts/upgrade.sh new file mode 100755 index 000000000..6abb887c4 --- /dev/null +++ b/apps/serverkit/1.11.4/scripts/upgrade.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +export PATH + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" +DATA_DIR="$ROOT_DIR/data" +COMPOSE_FILE="$ROOT_DIR/docker-compose.yml" +LEGACY_VOLUME="serverkit-data" +MIGRATION_IMAGE="jhd3197/serverkit:1.11.4" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +has_data_files() { + local entry + for entry in "$DATA_DIR"/* "$DATA_DIR"/.[!.]* "$DATA_DIR"/..?*; do + if [[ -e "$entry" || -L "$entry" ]]; then + return 0 + fi + done + return 1 +} + +bash "$SCRIPT_DIR/init.sh" + +if ! grep -Fq 'serverkit-data:/app/instance' "$COMPOSE_FILE"; then + exit 0 +fi + +if [[ -f "$DATA_DIR/serverkit.db" ]]; then + exit 0 +fi + +if has_data_files; then + fail "ServerKit data directory is non-empty but serverkit.db is missing; refusing legacy-volume migration" +fi + +command -v docker >/dev/null 2>&1 || fail "docker is required for legacy ServerKit volume migration" +if ! docker volume inspect "$LEGACY_VOLUME" >/dev/null 2>&1; then + exit 0 +fi +docker image inspect "$MIGRATION_IMAGE" >/dev/null 2>&1 || fail "migration image is unavailable: $MIGRATION_IMAGE" + +docker run --rm --network none --user 0 --entrypoint /bin/sh \ + --volume "$LEGACY_VOLUME:/legacy:ro" \ + --volume "$DATA_DIR:/target:rw" \ + "$MIGRATION_IMAGE" \ + -c ' +set -eu +stage=/target/.serverkit-data-migration +[ ! -e "$stage" ] && [ ! -L "$stage" ] || { echo "migration staging path already exists" >&2; exit 1; } +mkdir "$stage" +cleanup() { + rm -rf -- "$stage" +} +trap cleanup EXIT + +found=0 +found_db=0 +for item in /legacy/* /legacy/.[!.]* /legacy/..?*; do + [ -e "$item" ] || [ -L "$item" ] || continue + [ -f "$item" ] && [ ! -L "$item" ] || { echo "legacy volume contains an unsupported non-regular file" >&2; exit 1; } + name=${item##*/} + case "$name" in + serverkit.db|serverkit.db-wal|serverkit.db-shm) ;; + *) echo "legacy volume contains an unexpected file" >&2; exit 1 ;; + esac + cp -p -- "$item" "$stage/$name" + found=1 + [ "$name" = serverkit.db ] && found_db=1 +done + +[ "$found" = 0 ] && exit 0 +[ "$found_db" = 1 ] || { echo "legacy volume does not contain serverkit.db" >&2; exit 1; } + +for name in serverkit.db-wal serverkit.db-shm serverkit.db; do + if [ -e "$stage/$name" ]; then + [ ! -e "/target/$name" ] && [ ! -L "/target/$name" ] || { echo "target data file already exists" >&2; exit 1; } + chown 999:999 "$stage/$name" + chmod 0640 "$stage/$name" + mv -- "$stage/$name" "/target/$name" + fi +done +' + +bash "$SCRIPT_DIR/init.sh" diff --git a/apps/serverkit/README.md b/apps/serverkit/README.md index 73130dc97..0ce826245 100644 --- a/apps/serverkit/README.md +++ b/apps/serverkit/README.md @@ -29,7 +29,17 @@ ServerKit 是一个自托管的远程服务器管理与运维面板,提供服 3. 通过安装时配置的 HTTP 端口打开 ServerKit。首次打开时使用注册页面创建第一个用户;第一个用户会被授予管理员角色。 4. 如果使用反向代理,请填写 `SERVERKIT_PUBLIC_URL`,并仅在请求一定经过可信反向代理时将 `TRUST_PROXY_HEADERS` 设为 `true`。需要同时允许多个浏览器来源时,在 `CORS_ORIGINS` 中用逗号分隔填写。 -SQLite 数据库持久化在 `serverkit-data` 卷的 `/app/instance/serverkit.db`。不要删除该卷,否则会丢失 ServerKit 数据。 +SQLite 数据库持久化在应用安装目录的 `data/serverkit.db`,并以 bind mount 映射到容器内的 `/app/instance`。该目录位于 1Panel 应用目录中,便于随应用数据一起备份。不要删除该目录,否则会丢失 ServerKit 数据。由旧版 `serverkit-data` 卷升级时,目标版本会在新目录为空时自动迁移 SQLite 文件;旧卷会保留,不会自动删除。 + +## 安全提示 + +维护侧使用 Trivy 对 `serverkit` 的每个版本镜像进行了漏洞扫描;当前每个扫描报告包含 Critical=5、High=77、Total=82(两个版本标签解析到相同镜像内容)。这些风险来自上游镜像及其基础系统组件,本应用包未对其进行修复。请优先在可信内网中使用,并关注上游镜像更新。 + +高风险示例: + +- Critical `CVE-2025-7458`(`libsqlite3-0`):当前暂无修复版本。 +- Critical `CVE-2026-13221`、`CVE-2026-42496`、`CVE-2026-8376`(`perl-base`):当前暂无修复版本。 +- Critical `CVE-2023-45853`(`zlib1g`):当前暂无修复版本。 ## 版本 diff --git a/apps/serverkit/README_en.md b/apps/serverkit/README_en.md index 642e4c68c..fc4e0b383 100644 --- a/apps/serverkit/README_en.md +++ b/apps/serverkit/README_en.md @@ -29,7 +29,17 @@ This is ServerKit's containerized deployment. The package does not mount the Doc 3. Open ServerKit on the configured HTTP port. On the first visit, use the registration page to create the first user; the first user is given the administrator role. 4. When using a reverse proxy, set `SERVERKIT_PUBLIC_URL`. Set `TRUST_PROXY_HEADERS` to `true` only when every request is guaranteed to pass through a trusted proxy. Add multiple browser origins as a comma-separated `CORS_ORIGINS` value. -The SQLite database is persisted at `/app/instance/serverkit.db` in the `serverkit-data` volume. Do not remove that volume unless the ServerKit data can be discarded. +The SQLite database is persisted at `data/serverkit.db` in the application install directory and bind-mounted to `/app/instance`. This keeps the data under the 1Panel application directory for application backups. Do not remove this directory unless the ServerKit data can be discarded. When upgrading from the legacy `serverkit-data` volume, the target version migrates the SQLite files when the new directory is empty; the legacy volume is retained and is not deleted automatically. + +## Security notice + +The maintainer scanned each packaged `serverkit` image tag with Trivy; each report contains Critical=5, High=77, Total=82 (both tags resolve to the same image content). These findings originate from the upstream image and its base-system components and are not remediated by this package. Prefer trusted private networks and monitor upstream image updates. + +High-risk examples: + +- Critical `CVE-2025-7458` (`libsqlite3-0`): no fix is currently available. +- Critical `CVE-2026-13221`, `CVE-2026-42496`, and `CVE-2026-8376` (`perl-base`): no fix is currently available. +- Critical `CVE-2023-45853` (`zlib1g`): no fix is currently available. ## Versions diff --git a/apps/serverkit/latest/docker-compose.yml b/apps/serverkit/latest/docker-compose.yml index 45d66ffb0..8c52a285d 100644 --- a/apps/serverkit/latest/docker-compose.yml +++ b/apps/serverkit/latest/docker-compose.yml @@ -17,7 +17,7 @@ services: - "TRUSTED_PROXY_HOPS=1" - "SERVERKIT_GITHUB_REPO=jhd3197/ServerKit" volumes: - - "serverkit-data:/app/instance" + - "./data:/app/instance" networks: - "1panel-network" labels: @@ -35,10 +35,6 @@ services: max-size: "10m" max-file: "3" -volumes: - serverkit-data: - name: serverkit-data - networks: 1panel-network: external: true diff --git a/apps/serverkit/latest/scripts/init.sh b/apps/serverkit/latest/scripts/init.sh new file mode 100755 index 000000000..012c44a60 --- /dev/null +++ b/apps/serverkit/latest/scripts/init.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +set -euo pipefail +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +export PATH + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}" + +read_env_value() { + local key="$1" + [[ -f "$ENV_FILE" ]] || return 0 + local value + value="$(sed -n "s/^${key}=//p" "$ENV_FILE" | tail -n 1)" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +configured_value() { + local key="$1" + local default_value="$2" + local value + value="${!key:-}" + if [[ -z "$value" ]]; then + value="$(read_env_value "$key")" + fi + printf '%s\n' "${value:-$default_value}" +} + +resolve_app_path() { + local key="$1" + local raw="$2" + local clean candidate resolved current part + local -a parts=() + case "$raw" in + ""|/*|.|..|../*|*/../*|*/..) echo "unsafe ${key} path" >&2; return 1 ;; + esac + if [[ "$raw" =~ [[:cntrl:]] ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + clean="${raw#./}" + [[ -n "$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; } + command -v realpath >/dev/null 2>&1 || { echo "realpath is required" >&2; return 1; } + candidate="$ROOT_DIR/$clean" + resolved="$(realpath -m -- "$candidate")" || { echo "unsafe ${key} path" >&2; return 1; } + case "$resolved" in + "$ROOT_DIR"/*) ;; + *) echo "unsafe ${key} path" >&2; return 1 ;; + esac + current="$ROOT_DIR" + IFS='/' read -r -a parts <<< "$clean" + for part in "${parts[@]}"; do + [[ -z "$part" || "$part" == "." ]] && continue + current="$current/$part" + if [[ -L "$current" ]]; then + echo "unsafe ${key} path" >&2 + return 1 + fi + done + printf '%s\n' "$resolved" +} + +ensure_dir() { + local key="$1" + local raw + local path + raw="$(configured_value "$key" "$2")" + path="$(resolve_app_path "$key" "$raw")" + mkdir -p -- "$path" + [[ "$(resolve_app_path "$key" "$raw")" == "$path" ]] || { echo "unsafe ${key} path" >&2; return 1; } +} + +resolve_direct_child() { + local key="$1" + local raw="$2" + local clean path + clean="${raw#./}" + if [[ -z "$clean" || "$clean" == */* ]]; then + echo "unsafe ${key} path: lifecycle directories must be direct children of the version root" >&2 + return 1 + fi + path="$(resolve_app_path "$key" "$raw")" + [[ "$path" == "$ROOT_DIR/$clean" ]] || { echo "unsafe ${key} path" >&2; return 1; } + printf '%s\n' "$path" +} + +verify_trusted_root_chain() { + local current owner mode + [[ "$(id -u)" == "0" ]] || { echo "directory ownership initialization must run as root" >&2; return 1; } + command -v stat >/dev/null 2>&1 || { echo "stat is required" >&2; return 1; } + current="$ROOT_DIR" + while [[ "$current" != "/" ]]; do + [[ -d "$current" && ! -L "$current" ]] || { echo "unsafe version root chain: $current" >&2; return 1; } + IFS=':' read -r owner mode < <(stat -c '%u:%a' -- "$current") + if [[ "$current" == "$ROOT_DIR" ]]; then + # 1Panel local-app copies may be owned by the panel agent UID 1001. + [[ "$owner" == "0" || "$owner" == "1001" ]] || { echo "unsafe version root chain owner: $current" >&2; return 1; } + else + [[ "$owner" == "0" ]] || { echo "unsafe version root chain owner: $current" >&2; return 1; } + fi + [[ "$mode" =~ ^[0-7]{3,4}$ ]] || { echo "unsafe version root chain mode: $current" >&2; return 1; } + (( (8#$mode & 0022) == 0 )) || { echo "unsafe version root chain permissions: $current" >&2; return 1; } + current="$(dirname -- "$current")" + done +} + +declare -A OWNED_PATHS=() +declare -A OWNED_KEYS_BY_PATH=() + +register_owned_dir() { + local key="$1" + local raw="$2" + local path previous_key + path="$(resolve_direct_child "$key" "$raw")" + if [[ -n "${OWNED_KEYS_BY_PATH[$path]+x}" ]]; then + previous_key="${OWNED_KEYS_BY_PATH[$path]}" + echo "duplicate directory ownership target: $path ($previous_key and $key)" >&2 + return 1 + fi + OWNED_KEYS_BY_PATH["$path"]="$key" + OWNED_PATHS["$key"]="$path" +} + +register_configured_owned_dir() { + local key="$1" + local default_value="$2" + local raw + raw="$(configured_value "$key" "$default_value")" + register_owned_dir "$key" "$raw" +} + +register_fixed_owned_dir() { + local source="$1" + register_owned_dir "fixed directory $source" "$source" +} + +apply_owned_dir() { + local key="$1" + local uid="$2" + local gid="$3" + local mode="$4" + local path actual expected_mode + [[ -n "${OWNED_PATHS[$key]+x}" ]] || { echo "missing directory ownership preflight: $key" >&2; return 1; } + path="${OWNED_PATHS[$key]}" + verify_trusted_root_chain + if [[ -e "$path" || -L "$path" ]]; then + [[ -d "$path" && ! -L "$path" ]] || { echo "unsafe ${key} path" >&2; return 1; } + else + mkdir -- "$path" + fi + chmod "$mode" -- "$path" + [[ -d "$path" && ! -L "$path" ]] || { echo "unsafe ${key} path" >&2; return 1; } + chown --no-dereference "$uid:$gid" -- "$path" + expected_mode="${mode#0}" + actual="$(stat -c '%u:%g:%a' -- "$path")" + [[ "$actual" == "$uid:$gid:$expected_mode" ]] || { echo "${key} ownership/mode mismatch: expected ${uid}:${gid}:${expected_mode}, got ${actual}" >&2; return 1; } +} + +ensure_owned_dir() { + local key="$1" + local default_value="$2" + [[ -n "$default_value" ]] || { echo "missing directory ownership default: $key" >&2; return 1; } + apply_owned_dir "$key" "$3" "$4" "$5" +} + +ensure_fixed_owned_dir() { + local source="$1" + apply_owned_dir "fixed directory $source" "$2" "$3" "$4" +} + +register_fixed_owned_dir "./data" +ensure_fixed_owned_dir "./data" "999" "999" "0750" diff --git a/apps/serverkit/latest/scripts/upgrade.sh b/apps/serverkit/latest/scripts/upgrade.sh new file mode 100755 index 000000000..3d997cf94 --- /dev/null +++ b/apps/serverkit/latest/scripts/upgrade.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +export PATH + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)" +DATA_DIR="$ROOT_DIR/data" +COMPOSE_FILE="$ROOT_DIR/docker-compose.yml" +LEGACY_VOLUME="serverkit-data" +MIGRATION_IMAGE="jhd3197/serverkit:latest" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +has_data_files() { + local entry + for entry in "$DATA_DIR"/* "$DATA_DIR"/.[!.]* "$DATA_DIR"/..?*; do + if [[ -e "$entry" || -L "$entry" ]]; then + return 0 + fi + done + return 1 +} + +bash "$SCRIPT_DIR/init.sh" + +if ! grep -Fq 'serverkit-data:/app/instance' "$COMPOSE_FILE"; then + exit 0 +fi + +if [[ -f "$DATA_DIR/serverkit.db" ]]; then + exit 0 +fi + +if has_data_files; then + fail "ServerKit data directory is non-empty but serverkit.db is missing; refusing legacy-volume migration" +fi + +command -v docker >/dev/null 2>&1 || fail "docker is required for legacy ServerKit volume migration" +if ! docker volume inspect "$LEGACY_VOLUME" >/dev/null 2>&1; then + exit 0 +fi +docker image inspect "$MIGRATION_IMAGE" >/dev/null 2>&1 || fail "migration image is unavailable: $MIGRATION_IMAGE" + +docker run --rm --network none --user 0 --entrypoint /bin/sh \ + --volume "$LEGACY_VOLUME:/legacy:ro" \ + --volume "$DATA_DIR:/target:rw" \ + "$MIGRATION_IMAGE" \ + -c ' +set -eu +stage=/target/.serverkit-data-migration +[ ! -e "$stage" ] && [ ! -L "$stage" ] || { echo "migration staging path already exists" >&2; exit 1; } +mkdir "$stage" +cleanup() { + rm -rf -- "$stage" +} +trap cleanup EXIT + +found=0 +found_db=0 +for item in /legacy/* /legacy/.[!.]* /legacy/..?*; do + [ -e "$item" ] || [ -L "$item" ] || continue + [ -f "$item" ] && [ ! -L "$item" ] || { echo "legacy volume contains an unsupported non-regular file" >&2; exit 1; } + name=${item##*/} + case "$name" in + serverkit.db|serverkit.db-wal|serverkit.db-shm) ;; + *) echo "legacy volume contains an unexpected file" >&2; exit 1 ;; + esac + cp -p -- "$item" "$stage/$name" + found=1 + [ "$name" = serverkit.db ] && found_db=1 +done + +[ "$found" = 0 ] && exit 0 +[ "$found_db" = 1 ] || { echo "legacy volume does not contain serverkit.db" >&2; exit 1; } + +for name in serverkit.db-wal serverkit.db-shm serverkit.db; do + if [ -e "$stage/$name" ]; then + [ ! -e "/target/$name" ] && [ ! -L "/target/$name" ] || { echo "target data file already exists" >&2; exit 1; } + chown 999:999 "$stage/$name" + chmod 0640 "$stage/$name" + mv -- "$stage/$name" "/target/$name" + fi +done +' + +bash "$SCRIPT_DIR/init.sh"