mirror of
https://github.com/okxlin/appstore.git
synced 2026-09-22 16:00:58 +00:00
Merge pull request #6954 from okxlin/codex/serverkit-bind-final-20260917
fix: keep ServerKit data inside the 1Panel app directory
This commit is contained in:
@@ -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
|
||||
|
||||
Executable
+175
@@ -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"
|
||||
Executable
+90
@@ -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"
|
||||
@@ -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`):当前暂无修复版本。
|
||||
|
||||
## 版本
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+175
@@ -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"
|
||||
Executable
+90
@@ -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"
|
||||
Reference in New Issue
Block a user