From 0090568b07548286166de0a2eb4a3eff338a4d93 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:18:14 +0800 Subject: [PATCH] fix(zipline): add staged 4.8 upgrade packages --- apps/zipline/4.6.5/scripts/credentials.sh | 290 ++++++++++++++++++++++ apps/zipline/4.6.5/scripts/init.sh | 47 +--- apps/zipline/4.6.5/scripts/uninstall.sh | 3 +- apps/zipline/4.7.0/.env.sample | 9 + apps/zipline/4.7.0/data.yml | 135 ++++++++++ apps/zipline/4.7.0/docker-compose.yml | 79 ++++++ apps/zipline/4.7.0/scripts/credentials.sh | 290 ++++++++++++++++++++++ apps/zipline/4.7.0/scripts/init.sh | 95 +++++++ apps/zipline/4.7.0/scripts/uninstall.sh | 6 + apps/zipline/4.7.0/scripts/upgrade.sh | 8 + apps/zipline/4.8.0/.env.sample | 9 + apps/zipline/4.8.0/data.yml | 135 ++++++++++ apps/zipline/4.8.0/docker-compose.yml | 79 ++++++ apps/zipline/4.8.0/scripts/credentials.sh | 290 ++++++++++++++++++++++ apps/zipline/4.8.0/scripts/init.sh | 95 +++++++ apps/zipline/4.8.0/scripts/uninstall.sh | 6 + apps/zipline/4.8.0/scripts/upgrade.sh | 36 +++ apps/zipline/README.md | 9 +- apps/zipline/latest/scripts/uninstall.sh | 3 +- 19 files changed, 1574 insertions(+), 50 deletions(-) create mode 100644 apps/zipline/4.6.5/scripts/credentials.sh create mode 100644 apps/zipline/4.7.0/.env.sample create mode 100644 apps/zipline/4.7.0/data.yml create mode 100644 apps/zipline/4.7.0/docker-compose.yml create mode 100644 apps/zipline/4.7.0/scripts/credentials.sh create mode 100755 apps/zipline/4.7.0/scripts/init.sh create mode 100755 apps/zipline/4.7.0/scripts/uninstall.sh create mode 100755 apps/zipline/4.7.0/scripts/upgrade.sh create mode 100644 apps/zipline/4.8.0/.env.sample create mode 100644 apps/zipline/4.8.0/data.yml create mode 100644 apps/zipline/4.8.0/docker-compose.yml create mode 100644 apps/zipline/4.8.0/scripts/credentials.sh create mode 100755 apps/zipline/4.8.0/scripts/init.sh create mode 100755 apps/zipline/4.8.0/scripts/uninstall.sh create mode 100755 apps/zipline/4.8.0/scripts/upgrade.sh diff --git a/apps/zipline/4.6.5/scripts/credentials.sh b/apps/zipline/4.6.5/scripts/credentials.sh new file mode 100644 index 000000000..f0d6b6cf2 --- /dev/null +++ b/apps/zipline/4.6.5/scripts/credentials.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +set -euo pipefail + +ZIPLINE_STORED_POSTGRES_PASSWORD="" +ZIPLINE_STORED_CORE_SECRET="" +ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD="" +ZIPLINE_SNAPSHOT_CORE_SECRET="" + +zipline_fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +zipline_read_env_value() { + local file="$1" + local key="$2" + local value + [[ -f "$file" && ! -L "$file" ]] || return 1 + value="$(awk -v key="$key" ' + index($0, key "=") == 1 { + count++ + value = substr($0, length(key) + 2) + } + END { + if (count != 1) exit 1 + print value + } + ' "$file")" || return 1 + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +zipline_resolve_data_dir() { + local root_dir="$1" + local raw="$2" + local clean current resolved part + local -a parts=() + [[ -n "$raw" && "$raw" != /* ]] || zipline_fail "APP_DATA_DIR must be a non-empty relative path" + [[ ! "$raw" =~ [[:cntrl:]] ]] || zipline_fail "APP_DATA_DIR contains control characters" + case "$raw" in + *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + zipline_fail "APP_DATA_DIR contains unsupported characters" + ;; + esac + clean="${raw#./}" + [[ -n "$clean" ]] || zipline_fail "APP_DATA_DIR must not resolve to the version root" + IFS=/ read -r -a parts <<< "$clean" + current="$root_dir" + for part in "${parts[@]}"; do + [[ -n "$part" && "$part" != . && "$part" != .. ]] || zipline_fail "APP_DATA_DIR contains traversal" + current="$current/$part" + [[ ! -L "$current" ]] || zipline_fail "APP_DATA_DIR must not contain symbolic-link components" + done + resolved="$(realpath -m -- "$root_dir/$clean")" || zipline_fail "APP_DATA_DIR could not be resolved" + case "$resolved" in + "$root_dir"/*) ;; + *) zipline_fail "APP_DATA_DIR must stay inside the application version directory" ;; + esac + printf '%s\n' "$resolved" +} + +zipline_validate_postgres_password() { + [[ "$1" =~ ^[A-Za-z0-9]{32,128}$ ]] +} + +zipline_validate_core_secret() { + [[ "$1" =~ ^[A-Fa-f0-9]{64,128}$ ]] +} + +zipline_generate_postgres_password() { + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge 48 ]] || zipline_fail "unable to generate sufficient database password material" + printf '%s\n' "${material:0:48}" +} + +zipline_write_env_credentials() { + local env_file="$1" + local postgres_password="$2" + local core_secret="$3" + local temp_file + temp_file="$(mktemp "${env_file}.zipline-tmp.XXXXXX")" || zipline_fail "unable to stage Zipline environment update" + if ! ZIPLINE_DB_PASSWORD="$postgres_password" ZIPLINE_CORE_SECRET="$core_secret" awk ' + BEGIN { + db_password = ENVIRON["ZIPLINE_DB_PASSWORD"] + core_secret = ENVIRON["ZIPLINE_CORE_SECRET"] + } + /^POSTGRES_PASSWORD=/ { + if (!db_written++) print "POSTGRES_PASSWORD=" db_password + next + } + /^CORE_SECRET=/ { + if (!core_written++) print "CORE_SECRET=" core_secret + next + } + { print } + END { + if (!db_written) print "POSTGRES_PASSWORD=" db_password + if (!core_written) print "CORE_SECRET=" core_secret + } + ' "$env_file" > "$temp_file"; then + rm -f -- "$temp_file" + zipline_fail "unable to render Zipline environment update" + fi + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$env_file" + chmod 600 "$env_file" +} + +zipline_write_secret_state() { + local data_dir="$1" + local postgres_password="$2" + local core_secret="$3" + local secret_file="$data_dir/.zipline-secrets" + local temp_file + [[ -d "$data_dir" && ! -L "$data_dir" ]] || zipline_fail "Zipline data directory is unavailable" + [[ ! -L "$secret_file" ]] || zipline_fail "Zipline secret state must not be a symbolic link" + temp_file="$(mktemp "${secret_file}.tmp.XXXXXX")" || zipline_fail "unable to stage Zipline secret state" + if ! printf 'POSTGRES_PASSWORD=%s\nCORE_SECRET=%s\n' "$postgres_password" "$core_secret" > "$temp_file"; then + rm -f -- "$temp_file" + zipline_fail "unable to write Zipline secret state" + fi + chmod 600 "$temp_file" + chown 0:0 -- "$temp_file" + mv -f -- "$temp_file" "$secret_file" + chmod 600 "$secret_file" + chown 0:0 -- "$secret_file" +} + +zipline_load_secret_state() { + local data_dir="$1" + local secret_file="$data_dir/.zipline-secrets" + [[ -e "$secret_file" || -L "$secret_file" ]] || return 1 + [[ -f "$secret_file" && ! -L "$secret_file" ]] || zipline_fail "Zipline secret state is not a regular file" + ZIPLINE_STORED_POSTGRES_PASSWORD="$(zipline_read_env_value "$secret_file" POSTGRES_PASSWORD)" || + zipline_fail "Zipline secret state is incomplete" + ZIPLINE_STORED_CORE_SECRET="$(zipline_read_env_value "$secret_file" CORE_SECRET)" || + zipline_fail "Zipline secret state is incomplete" + zipline_validate_postgres_password "$ZIPLINE_STORED_POSTGRES_PASSWORD" || + zipline_fail "Zipline secret state has an invalid database password" + zipline_validate_core_secret "$ZIPLINE_STORED_CORE_SECRET" || + zipline_fail "Zipline secret state has an invalid session secret" + chmod 600 "$secret_file" + chown 0:0 -- "$secret_file" +} + +zipline_init_credentials() { + local env_file="$1" + local data_dir="$2" + local postgres_password core_secret stored + local postgres_default=false core_default=false + [[ -f "$env_file" && ! -L "$env_file" ]] || zipline_fail "Zipline .env file is unavailable" + postgres_password="$(zipline_read_env_value "$env_file" POSTGRES_PASSWORD)" || + zipline_fail "POSTGRES_PASSWORD is missing from Zipline .env" + core_secret="$(zipline_read_env_value "$env_file" CORE_SECRET)" || + zipline_fail "CORE_SECRET is missing from Zipline .env" + [[ -n "$postgres_password" && "$postgres_password" != generate ]] || postgres_default=true + [[ -n "$core_secret" && "$core_secret" != generate ]] || core_default=true + + if zipline_load_secret_state "$data_dir"; then + if [[ "$postgres_default" == true ]]; then + postgres_password="$ZIPLINE_STORED_POSTGRES_PASSWORD" + elif [[ "$postgres_password" != "$ZIPLINE_STORED_POSTGRES_PASSWORD" ]]; then + zipline_fail "POSTGRES_PASSWORD cannot change while Zipline data is retained" + fi + if [[ "$core_default" == true ]]; then + core_secret="$ZIPLINE_STORED_CORE_SECRET" + elif [[ "$core_secret" != "$ZIPLINE_STORED_CORE_SECRET" ]]; then + zipline_fail "CORE_SECRET cannot change while Zipline data is retained" + fi + else + if [[ -f "$data_dir/postgres/PG_VERSION" && ( "$postgres_default" == true || "$core_default" == true ) ]]; then + zipline_fail "existing PostgreSQL data has no Zipline secret state; supply its original credentials to preserve it" + fi + if [[ "$postgres_default" == true ]]; then + postgres_password="$(zipline_generate_postgres_password)" + fi + if [[ "$core_default" == true ]]; then + core_secret="$(openssl rand -hex 32)" + fi + fi + + zipline_validate_postgres_password "$postgres_password" || + zipline_fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters" + zipline_validate_core_secret "$core_secret" || + zipline_fail "CORE_SECRET must contain 64 to 128 hexadecimal characters" + zipline_write_env_credentials "$env_file" "$postgres_password" "$core_secret" + zipline_write_secret_state "$data_dir" "$postgres_password" "$core_secret" +} + +zipline_find_upgrade_snapshot() { + local root_dir="$1" + local env_file="$2" + local current_name="$3" + local current_data_dir="$4" + local current_db="$5" + local current_user="$6" + local temp_root snapshot old_env old_compose old_name old_data old_db old_user + local snapshot_time env_time best_time=-1 tied=false snapshot_uid snapshot_mode old_env_uid old_env_mode + local best_snapshot="" + local -a candidates=() + + temp_root="$(realpath -e -- "${TMPDIR:-/tmp}")" || zipline_fail "1Panel upgrade snapshot directory is unavailable" + [[ -d "$temp_root" ]] || zipline_fail "1Panel upgrade snapshot directory is unavailable" + env_time="$(stat -c '%Y' "$env_file")" || zipline_fail "Zipline .env timestamp is unavailable" + shopt -s nullglob + candidates=("$temp_root"/1panel-app-upgrade-*) + shopt -u nullglob + for snapshot in "${candidates[@]}"; do + [[ -d "$snapshot" && ! -L "$snapshot" ]] || continue + snapshot_uid="$(stat -c '%u' "$snapshot")" || continue + snapshot_mode="$(stat -c '%a' "$snapshot")" || continue + [[ "$snapshot_uid" == 0 ]] || continue + (( (8#$snapshot_mode & 077) == 0 )) || continue + old_env="$snapshot/.env" + old_compose="$snapshot/docker-compose.yml" + [[ -f "$old_env" && ! -L "$old_env" && -f "$old_compose" && ! -L "$old_compose" ]] || continue + old_env_uid="$(stat -c '%u' "$old_env")" || continue + old_env_mode="$(stat -c '%a' "$old_env")" || continue + [[ "$old_env_uid" == 0 ]] || continue + (( (8#$old_env_mode & 077) == 0 )) || continue + grep -Eq 'image:[[:space:]]*"?ghcr\.io/diced/zipline:4\.(6\.5|7\.0|8\.0)@sha256:[[:xdigit:]]{64}"?[[:space:]]*$' "$old_compose" || continue + old_name="$(zipline_read_env_value "$old_env" CONTAINER_NAME)" || continue + [[ "$old_name" == "$current_name" ]] || continue + old_data="$(zipline_read_env_value "$old_env" APP_DATA_DIR)" || zipline_fail "matching 1Panel snapshot lacks APP_DATA_DIR" + old_db="$(zipline_read_env_value "$old_env" POSTGRES_DB)" || zipline_fail "matching 1Panel snapshot lacks POSTGRES_DB" + old_user="$(zipline_read_env_value "$old_env" POSTGRES_USER)" || zipline_fail "matching 1Panel snapshot lacks POSTGRES_USER" + [[ "$old_data" == "$current_data_dir" && "$old_db" == "$current_db" && "$old_user" == "$current_user" ]] || + zipline_fail "matching 1Panel snapshot does not preserve the configured data path and database identity" + ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD="$(zipline_read_env_value "$old_env" POSTGRES_PASSWORD)" || + zipline_fail "matching 1Panel snapshot lacks its database password" + ZIPLINE_SNAPSHOT_CORE_SECRET="$(zipline_read_env_value "$old_env" CORE_SECRET)" || + zipline_fail "matching 1Panel snapshot lacks its session secret" + zipline_validate_postgres_password "$ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD" || + zipline_fail "matching 1Panel snapshot has an invalid database password" + zipline_validate_core_secret "$ZIPLINE_SNAPSHOT_CORE_SECRET" || + zipline_fail "matching 1Panel snapshot has an invalid session secret" + snapshot_time="$(stat -c '%Y' "$snapshot")" || zipline_fail "1Panel upgrade snapshot timestamp is unavailable" + if (( snapshot_time > best_time )); then + best_time="$snapshot_time" + best_snapshot="$snapshot" + tied=false + elif (( snapshot_time == best_time )); then + tied=true + fi + done + [[ -n "$best_snapshot" ]] || zipline_fail "could not find this Zipline install's 1Panel upgrade snapshot" + [[ "$tied" == false ]] || zipline_fail "multiple matching 1Panel upgrade snapshots are ambiguous" + (( best_time <= env_time && env_time - best_time <= 600 )) || + zipline_fail "matching 1Panel upgrade snapshot is stale" +} + +zipline_restore_upgrade_credentials() { + local root_dir="$1" + local env_file="$2" + local raw_data_dir data_dir current_name current_db current_user + local postgres_password core_secret current_password current_core_secret + [[ -f "$env_file" && ! -L "$env_file" ]] || zipline_fail "Zipline .env file is unavailable" + current_name="$(zipline_read_env_value "$env_file" CONTAINER_NAME)" || zipline_fail "CONTAINER_NAME is missing from Zipline .env" + [[ "$current_name" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || zipline_fail "CONTAINER_NAME is invalid" + raw_data_dir="$(zipline_read_env_value "$env_file" APP_DATA_DIR)" || zipline_fail "APP_DATA_DIR is missing from Zipline .env" + data_dir="$(zipline_resolve_data_dir "$root_dir" "$raw_data_dir")" + [[ -d "$data_dir" && ! -L "$data_dir" ]] || zipline_fail "configured Zipline data directory is missing" + current_db="$(zipline_read_env_value "$env_file" POSTGRES_DB)" || zipline_fail "POSTGRES_DB is missing from Zipline .env" + current_user="$(zipline_read_env_value "$env_file" POSTGRES_USER)" || zipline_fail "POSTGRES_USER is missing from Zipline .env" + current_password="$(zipline_read_env_value "$env_file" POSTGRES_PASSWORD)" || zipline_fail "POSTGRES_PASSWORD is missing from Zipline .env" + current_core_secret="$(zipline_read_env_value "$env_file" CORE_SECRET)" || zipline_fail "CORE_SECRET is missing from Zipline .env" + + zipline_find_upgrade_snapshot "$root_dir" "$env_file" "$current_name" "$raw_data_dir" "$current_db" "$current_user" + postgres_password="$ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD" + core_secret="$ZIPLINE_SNAPSHOT_CORE_SECRET" + if zipline_load_secret_state "$data_dir"; then + [[ "$ZIPLINE_STORED_POSTGRES_PASSWORD" == "$postgres_password" && "$ZIPLINE_STORED_CORE_SECRET" == "$core_secret" ]] || + zipline_fail "Zipline secret state does not match the 1Panel upgrade snapshot" + fi + if [[ -n "$current_password" && "$current_password" != generate && "$current_password" != "$postgres_password" ]]; then + zipline_fail "POSTGRES_PASSWORD changed during upgrade; refusing to alter the existing database credential" + fi + if [[ -n "$current_core_secret" && "$current_core_secret" != generate && "$current_core_secret" != "$core_secret" ]]; then + zipline_fail "CORE_SECRET changed during upgrade; refusing to rotate the existing session secret" + fi + zipline_validate_postgres_password "$postgres_password" || zipline_fail "saved database password is invalid" + zipline_validate_core_secret "$core_secret" || zipline_fail "saved session secret is invalid" + zipline_write_env_credentials "$env_file" "$postgres_password" "$core_secret" + zipline_write_secret_state "$data_dir" "$postgres_password" "$core_secret" + printf '%s\n' "Zipline secrets were restored from the matching 1Panel upgrade snapshot." +} diff --git a/apps/zipline/4.6.5/scripts/init.sh b/apps/zipline/4.6.5/scripts/init.sh index 3da7c0b55..868a2fb3f 100755 --- a/apps/zipline/4.6.5/scripts/init.sh +++ b/apps/zipline/4.6.5/scripts/init.sh @@ -20,27 +20,6 @@ read_env_value() { printf '%s\n' "$value" } -set_env_value() { - local key="$1" - local value="$2" - local temp_file - temp_file="$(mktemp "${ROOT_DIR}/.zipline-env.tmp.XXXXXX")" - awk -v key="$key" -v value="$value" ' - BEGIN { written = 0 } - $0 ~ "^" key "=" { - if (!written) { - print key "=" value - written = 1 - } - next - } - { print } - END { if (!written) print key "=" value } - ' "$ENV_FILE" > "$temp_file" - chmod 600 "$temp_file" - mv -f -- "$temp_file" "$ENV_FILE" -} - validate_ipv4() { local key="$1" local value="$2" @@ -59,18 +38,11 @@ validate_port() { ((10#$value >= 1 && 10#$value <= 65535)) || fail "PANEL_APP_PORT_HTTP must be between 1 and 65535" } -generate_alphanumeric() { - local length="$1" - local material - material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" - [[ ${#material} -ge $length ]] || fail "unable to generate sufficient random material" - printf '%s\n' "${material:0:length}" -} - [[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" [[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" [[ "$(id -u)" -eq 0 ]] || fail "Zipline init must run as root" command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" +source "${ROOT_DIR}/scripts/credentials.sh" validate_ipv4 PANEL_APP_BIND_ADDRESS "$(read_env_value PANEL_APP_BIND_ADDRESS)" validate_port "$(read_env_value PANEL_APP_PORT_HTTP)" @@ -80,20 +52,6 @@ postgres_user="$(read_env_value POSTGRES_USER)" [[ "$postgres_db" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_DB is invalid" [[ "$postgres_user" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_USER is invalid" -postgres_password="$(read_env_value POSTGRES_PASSWORD)" -if [[ -z "$postgres_password" || "$postgres_password" == generate ]]; then - postgres_password="$(generate_alphanumeric 48)" -fi -[[ "$postgres_password" =~ ^[A-Za-z0-9]{32,128}$ ]] || - fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters" - -core_secret="$(read_env_value CORE_SECRET)" -if [[ -z "$core_secret" || "$core_secret" == generate ]]; then - core_secret="$(openssl rand -hex 32)" -fi -[[ "$core_secret" =~ ^[A-Fa-f0-9]{64,128}$ ]] || - fail "CORE_SECRET must contain 64 to 128 hexadecimal characters" - max_file_size="$(read_env_value FILES_MAX_FILE_SIZE)" [[ "$max_file_size" =~ ^[1-9][0-9]*(b|kb|mb|gb|tb)$ ]] || fail "FILES_MAX_FILE_SIZE must be a positive byte quantity such as 100mb" @@ -133,6 +91,5 @@ chown -R 70:70 -- "$resolved_data/postgres" chown -R 1000:1000 -- "$resolved_data/uploads" "$resolved_data/public" "$resolved_data/themes" chmod 0700 -- "$resolved_data/postgres" -set_env_value POSTGRES_PASSWORD "$postgres_password" -set_env_value CORE_SECRET "$core_secret" +zipline_init_credentials "$ENV_FILE" "$resolved_data" chmod 600 "$ENV_FILE" diff --git a/apps/zipline/4.6.5/scripts/uninstall.sh b/apps/zipline/4.6.5/scripts/uninstall.sh index 9365d9925..42b518ef4 100755 --- a/apps/zipline/4.6.5/scripts/uninstall.sh +++ b/apps/zipline/4.6.5/scripts/uninstall.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -# Persistent data is intentionally retained for recovery or reinstallation. +# 1Panel removes the installation directory after this hook, including relative APP_DATA_DIR contents. +# Back up required data outside the application directory before uninstalling. exit 0 diff --git a/apps/zipline/4.7.0/.env.sample b/apps/zipline/4.7.0/.env.sample new file mode 100644 index 000000000..845005ffa --- /dev/null +++ b/apps/zipline/4.7.0/.env.sample @@ -0,0 +1,9 @@ +CONTAINER_NAME=zipline +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=3000 +POSTGRES_DB=zipline +POSTGRES_USER=zipline +POSTGRES_PASSWORD=generate +CORE_SECRET=generate +FILES_MAX_FILE_SIZE=100mb +APP_DATA_DIR=./data diff --git a/apps/zipline/4.7.0/data.yml b/apps/zipline/4.7.0/data.yml new file mode 100644 index 000000000..8f8719194 --- /dev/null +++ b/apps/zipline/4.7.0/data.yml @@ -0,0 +1,135 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 3000 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: 100mb + edit: true + envKey: FILES_MAX_FILE_SIZE + labelEn: Maximum File Size + labelZh: 最大文件大小 + label: + en: Maximum File Size + zh: 最大文件大小 + zh-Hant: 最大檔案大小 + ja: 最大ファイルサイズ + ko: 최대 파일 크기 + ru: Максимальный размер файла + ms: Saiz fail maksimum + pt-br: Tamanho maximo do arquivo + required: true + type: text + - default: zipline + edit: true + envKey: POSTGRES_DB + labelEn: Database Name + labelZh: 数据库名称 + label: + en: Database Name + zh: 数据库名称 + zh-Hant: 資料庫名稱 + ja: データベース名 + ko: 데이터베이스 이름 + ru: Имя базы данных + ms: Nama pangkalan data + pt-br: Nome do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: zipline + edit: true + envKey: POSTGRES_USER + labelEn: Database User + labelZh: 数据库用户 + label: + en: Database User + zh: 数据库用户 + zh-Hant: 資料庫使用者 + ja: データベースユーザー + ko: 데이터베이스 사용자 + ru: Пользователь базы данных + ms: Pengguna pangkalan data + pt-br: Usuario do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: POSTGRES_PASSWORD + labelEn: Database Password + labelZh: 数据库密码 + label: + en: Database Password + zh: 数据库密码 + zh-Hant: 資料庫密碼 + ja: データベースパスワード + ko: 데이터베이스 비밀번호 + ru: Пароль базы данных + ms: Kata laluan pangkalan data + pt-br: Senha do banco de dados + required: true + type: password + - default: generate + edit: true + envKey: CORE_SECRET + labelEn: Session Secret + labelZh: 会话密钥 + label: + en: Session Secret + zh: 会话密钥 + zh-Hant: 工作階段密鑰 + ja: セッションシークレット + ko: 세션 비밀 키 + ru: Секрет сеанса + ms: Rahsia sesi + pt-br: Segredo da sessao + required: true + type: password + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/zipline/4.7.0/docker-compose.yml b/apps/zipline/4.7.0/docker-compose.yml new file mode 100644 index 000000000..10f8b871e --- /dev/null +++ b/apps/zipline/4.7.0/docker-compose.yml @@ -0,0 +1,79 @@ +services: + zipline-postgres: + image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 + container_name: ${CONTAINER_NAME}-postgres + restart: unless-stopped + user: "70:70" + networks: + - zipline-internal + environment: + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - "${APP_DATA_DIR}/postgres:/var/lib/postgresql/data" + read_only: true + tmpfs: + - /tmp:mode=1777,size=64m + - /var/run/postgresql:uid=70,gid=70,mode=0755,size=16m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + labels: + createdBy: "Apps" + + zipline: + image: ghcr.io/diced/zipline:4.7.0@sha256:526fa08d2d3cf46bd959b2008987ca8d2a38b1ba840991b68cd9ff4b8c6a3fab + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1000:1000" + depends_on: + zipline-postgres: + condition: service_healthy + networks: + - zipline-internal + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:3000" + environment: + - CORE_HOSTNAME=0.0.0.0 + - CORE_PORT=3000 + - CORE_SECRET=${CORE_SECRET} + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@zipline-postgres:5432/${POSTGRES_DB} + - DATASOURCE_TYPE=local + - DATASOURCE_LOCAL_DIRECTORY=/zipline/uploads + - CORE_TEMP_DIRECTORY=/tmp/zipline + - FILES_MAX_FILE_SIZE=${FILES_MAX_FILE_SIZE} + - FEATURES_HEALTHCHECK=true + volumes: + - "${APP_DATA_DIR}/uploads:/zipline/uploads" + - "${APP_DATA_DIR}/public:/zipline/public" + - "${APP_DATA_DIR}/themes:/zipline/themes" + read_only: true + tmpfs: + - /tmp:mode=1777,size=256m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/healthcheck"] + interval: 15s + timeout: 5s + retries: 8 + start_period: 60s + labels: + createdBy: "Apps" + +networks: + zipline-internal: + internal: true + 1panel-network: + external: true diff --git a/apps/zipline/4.7.0/scripts/credentials.sh b/apps/zipline/4.7.0/scripts/credentials.sh new file mode 100644 index 000000000..f0d6b6cf2 --- /dev/null +++ b/apps/zipline/4.7.0/scripts/credentials.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +set -euo pipefail + +ZIPLINE_STORED_POSTGRES_PASSWORD="" +ZIPLINE_STORED_CORE_SECRET="" +ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD="" +ZIPLINE_SNAPSHOT_CORE_SECRET="" + +zipline_fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +zipline_read_env_value() { + local file="$1" + local key="$2" + local value + [[ -f "$file" && ! -L "$file" ]] || return 1 + value="$(awk -v key="$key" ' + index($0, key "=") == 1 { + count++ + value = substr($0, length(key) + 2) + } + END { + if (count != 1) exit 1 + print value + } + ' "$file")" || return 1 + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +zipline_resolve_data_dir() { + local root_dir="$1" + local raw="$2" + local clean current resolved part + local -a parts=() + [[ -n "$raw" && "$raw" != /* ]] || zipline_fail "APP_DATA_DIR must be a non-empty relative path" + [[ ! "$raw" =~ [[:cntrl:]] ]] || zipline_fail "APP_DATA_DIR contains control characters" + case "$raw" in + *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + zipline_fail "APP_DATA_DIR contains unsupported characters" + ;; + esac + clean="${raw#./}" + [[ -n "$clean" ]] || zipline_fail "APP_DATA_DIR must not resolve to the version root" + IFS=/ read -r -a parts <<< "$clean" + current="$root_dir" + for part in "${parts[@]}"; do + [[ -n "$part" && "$part" != . && "$part" != .. ]] || zipline_fail "APP_DATA_DIR contains traversal" + current="$current/$part" + [[ ! -L "$current" ]] || zipline_fail "APP_DATA_DIR must not contain symbolic-link components" + done + resolved="$(realpath -m -- "$root_dir/$clean")" || zipline_fail "APP_DATA_DIR could not be resolved" + case "$resolved" in + "$root_dir"/*) ;; + *) zipline_fail "APP_DATA_DIR must stay inside the application version directory" ;; + esac + printf '%s\n' "$resolved" +} + +zipline_validate_postgres_password() { + [[ "$1" =~ ^[A-Za-z0-9]{32,128}$ ]] +} + +zipline_validate_core_secret() { + [[ "$1" =~ ^[A-Fa-f0-9]{64,128}$ ]] +} + +zipline_generate_postgres_password() { + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge 48 ]] || zipline_fail "unable to generate sufficient database password material" + printf '%s\n' "${material:0:48}" +} + +zipline_write_env_credentials() { + local env_file="$1" + local postgres_password="$2" + local core_secret="$3" + local temp_file + temp_file="$(mktemp "${env_file}.zipline-tmp.XXXXXX")" || zipline_fail "unable to stage Zipline environment update" + if ! ZIPLINE_DB_PASSWORD="$postgres_password" ZIPLINE_CORE_SECRET="$core_secret" awk ' + BEGIN { + db_password = ENVIRON["ZIPLINE_DB_PASSWORD"] + core_secret = ENVIRON["ZIPLINE_CORE_SECRET"] + } + /^POSTGRES_PASSWORD=/ { + if (!db_written++) print "POSTGRES_PASSWORD=" db_password + next + } + /^CORE_SECRET=/ { + if (!core_written++) print "CORE_SECRET=" core_secret + next + } + { print } + END { + if (!db_written) print "POSTGRES_PASSWORD=" db_password + if (!core_written) print "CORE_SECRET=" core_secret + } + ' "$env_file" > "$temp_file"; then + rm -f -- "$temp_file" + zipline_fail "unable to render Zipline environment update" + fi + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$env_file" + chmod 600 "$env_file" +} + +zipline_write_secret_state() { + local data_dir="$1" + local postgres_password="$2" + local core_secret="$3" + local secret_file="$data_dir/.zipline-secrets" + local temp_file + [[ -d "$data_dir" && ! -L "$data_dir" ]] || zipline_fail "Zipline data directory is unavailable" + [[ ! -L "$secret_file" ]] || zipline_fail "Zipline secret state must not be a symbolic link" + temp_file="$(mktemp "${secret_file}.tmp.XXXXXX")" || zipline_fail "unable to stage Zipline secret state" + if ! printf 'POSTGRES_PASSWORD=%s\nCORE_SECRET=%s\n' "$postgres_password" "$core_secret" > "$temp_file"; then + rm -f -- "$temp_file" + zipline_fail "unable to write Zipline secret state" + fi + chmod 600 "$temp_file" + chown 0:0 -- "$temp_file" + mv -f -- "$temp_file" "$secret_file" + chmod 600 "$secret_file" + chown 0:0 -- "$secret_file" +} + +zipline_load_secret_state() { + local data_dir="$1" + local secret_file="$data_dir/.zipline-secrets" + [[ -e "$secret_file" || -L "$secret_file" ]] || return 1 + [[ -f "$secret_file" && ! -L "$secret_file" ]] || zipline_fail "Zipline secret state is not a regular file" + ZIPLINE_STORED_POSTGRES_PASSWORD="$(zipline_read_env_value "$secret_file" POSTGRES_PASSWORD)" || + zipline_fail "Zipline secret state is incomplete" + ZIPLINE_STORED_CORE_SECRET="$(zipline_read_env_value "$secret_file" CORE_SECRET)" || + zipline_fail "Zipline secret state is incomplete" + zipline_validate_postgres_password "$ZIPLINE_STORED_POSTGRES_PASSWORD" || + zipline_fail "Zipline secret state has an invalid database password" + zipline_validate_core_secret "$ZIPLINE_STORED_CORE_SECRET" || + zipline_fail "Zipline secret state has an invalid session secret" + chmod 600 "$secret_file" + chown 0:0 -- "$secret_file" +} + +zipline_init_credentials() { + local env_file="$1" + local data_dir="$2" + local postgres_password core_secret stored + local postgres_default=false core_default=false + [[ -f "$env_file" && ! -L "$env_file" ]] || zipline_fail "Zipline .env file is unavailable" + postgres_password="$(zipline_read_env_value "$env_file" POSTGRES_PASSWORD)" || + zipline_fail "POSTGRES_PASSWORD is missing from Zipline .env" + core_secret="$(zipline_read_env_value "$env_file" CORE_SECRET)" || + zipline_fail "CORE_SECRET is missing from Zipline .env" + [[ -n "$postgres_password" && "$postgres_password" != generate ]] || postgres_default=true + [[ -n "$core_secret" && "$core_secret" != generate ]] || core_default=true + + if zipline_load_secret_state "$data_dir"; then + if [[ "$postgres_default" == true ]]; then + postgres_password="$ZIPLINE_STORED_POSTGRES_PASSWORD" + elif [[ "$postgres_password" != "$ZIPLINE_STORED_POSTGRES_PASSWORD" ]]; then + zipline_fail "POSTGRES_PASSWORD cannot change while Zipline data is retained" + fi + if [[ "$core_default" == true ]]; then + core_secret="$ZIPLINE_STORED_CORE_SECRET" + elif [[ "$core_secret" != "$ZIPLINE_STORED_CORE_SECRET" ]]; then + zipline_fail "CORE_SECRET cannot change while Zipline data is retained" + fi + else + if [[ -f "$data_dir/postgres/PG_VERSION" && ( "$postgres_default" == true || "$core_default" == true ) ]]; then + zipline_fail "existing PostgreSQL data has no Zipline secret state; supply its original credentials to preserve it" + fi + if [[ "$postgres_default" == true ]]; then + postgres_password="$(zipline_generate_postgres_password)" + fi + if [[ "$core_default" == true ]]; then + core_secret="$(openssl rand -hex 32)" + fi + fi + + zipline_validate_postgres_password "$postgres_password" || + zipline_fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters" + zipline_validate_core_secret "$core_secret" || + zipline_fail "CORE_SECRET must contain 64 to 128 hexadecimal characters" + zipline_write_env_credentials "$env_file" "$postgres_password" "$core_secret" + zipline_write_secret_state "$data_dir" "$postgres_password" "$core_secret" +} + +zipline_find_upgrade_snapshot() { + local root_dir="$1" + local env_file="$2" + local current_name="$3" + local current_data_dir="$4" + local current_db="$5" + local current_user="$6" + local temp_root snapshot old_env old_compose old_name old_data old_db old_user + local snapshot_time env_time best_time=-1 tied=false snapshot_uid snapshot_mode old_env_uid old_env_mode + local best_snapshot="" + local -a candidates=() + + temp_root="$(realpath -e -- "${TMPDIR:-/tmp}")" || zipline_fail "1Panel upgrade snapshot directory is unavailable" + [[ -d "$temp_root" ]] || zipline_fail "1Panel upgrade snapshot directory is unavailable" + env_time="$(stat -c '%Y' "$env_file")" || zipline_fail "Zipline .env timestamp is unavailable" + shopt -s nullglob + candidates=("$temp_root"/1panel-app-upgrade-*) + shopt -u nullglob + for snapshot in "${candidates[@]}"; do + [[ -d "$snapshot" && ! -L "$snapshot" ]] || continue + snapshot_uid="$(stat -c '%u' "$snapshot")" || continue + snapshot_mode="$(stat -c '%a' "$snapshot")" || continue + [[ "$snapshot_uid" == 0 ]] || continue + (( (8#$snapshot_mode & 077) == 0 )) || continue + old_env="$snapshot/.env" + old_compose="$snapshot/docker-compose.yml" + [[ -f "$old_env" && ! -L "$old_env" && -f "$old_compose" && ! -L "$old_compose" ]] || continue + old_env_uid="$(stat -c '%u' "$old_env")" || continue + old_env_mode="$(stat -c '%a' "$old_env")" || continue + [[ "$old_env_uid" == 0 ]] || continue + (( (8#$old_env_mode & 077) == 0 )) || continue + grep -Eq 'image:[[:space:]]*"?ghcr\.io/diced/zipline:4\.(6\.5|7\.0|8\.0)@sha256:[[:xdigit:]]{64}"?[[:space:]]*$' "$old_compose" || continue + old_name="$(zipline_read_env_value "$old_env" CONTAINER_NAME)" || continue + [[ "$old_name" == "$current_name" ]] || continue + old_data="$(zipline_read_env_value "$old_env" APP_DATA_DIR)" || zipline_fail "matching 1Panel snapshot lacks APP_DATA_DIR" + old_db="$(zipline_read_env_value "$old_env" POSTGRES_DB)" || zipline_fail "matching 1Panel snapshot lacks POSTGRES_DB" + old_user="$(zipline_read_env_value "$old_env" POSTGRES_USER)" || zipline_fail "matching 1Panel snapshot lacks POSTGRES_USER" + [[ "$old_data" == "$current_data_dir" && "$old_db" == "$current_db" && "$old_user" == "$current_user" ]] || + zipline_fail "matching 1Panel snapshot does not preserve the configured data path and database identity" + ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD="$(zipline_read_env_value "$old_env" POSTGRES_PASSWORD)" || + zipline_fail "matching 1Panel snapshot lacks its database password" + ZIPLINE_SNAPSHOT_CORE_SECRET="$(zipline_read_env_value "$old_env" CORE_SECRET)" || + zipline_fail "matching 1Panel snapshot lacks its session secret" + zipline_validate_postgres_password "$ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD" || + zipline_fail "matching 1Panel snapshot has an invalid database password" + zipline_validate_core_secret "$ZIPLINE_SNAPSHOT_CORE_SECRET" || + zipline_fail "matching 1Panel snapshot has an invalid session secret" + snapshot_time="$(stat -c '%Y' "$snapshot")" || zipline_fail "1Panel upgrade snapshot timestamp is unavailable" + if (( snapshot_time > best_time )); then + best_time="$snapshot_time" + best_snapshot="$snapshot" + tied=false + elif (( snapshot_time == best_time )); then + tied=true + fi + done + [[ -n "$best_snapshot" ]] || zipline_fail "could not find this Zipline install's 1Panel upgrade snapshot" + [[ "$tied" == false ]] || zipline_fail "multiple matching 1Panel upgrade snapshots are ambiguous" + (( best_time <= env_time && env_time - best_time <= 600 )) || + zipline_fail "matching 1Panel upgrade snapshot is stale" +} + +zipline_restore_upgrade_credentials() { + local root_dir="$1" + local env_file="$2" + local raw_data_dir data_dir current_name current_db current_user + local postgres_password core_secret current_password current_core_secret + [[ -f "$env_file" && ! -L "$env_file" ]] || zipline_fail "Zipline .env file is unavailable" + current_name="$(zipline_read_env_value "$env_file" CONTAINER_NAME)" || zipline_fail "CONTAINER_NAME is missing from Zipline .env" + [[ "$current_name" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || zipline_fail "CONTAINER_NAME is invalid" + raw_data_dir="$(zipline_read_env_value "$env_file" APP_DATA_DIR)" || zipline_fail "APP_DATA_DIR is missing from Zipline .env" + data_dir="$(zipline_resolve_data_dir "$root_dir" "$raw_data_dir")" + [[ -d "$data_dir" && ! -L "$data_dir" ]] || zipline_fail "configured Zipline data directory is missing" + current_db="$(zipline_read_env_value "$env_file" POSTGRES_DB)" || zipline_fail "POSTGRES_DB is missing from Zipline .env" + current_user="$(zipline_read_env_value "$env_file" POSTGRES_USER)" || zipline_fail "POSTGRES_USER is missing from Zipline .env" + current_password="$(zipline_read_env_value "$env_file" POSTGRES_PASSWORD)" || zipline_fail "POSTGRES_PASSWORD is missing from Zipline .env" + current_core_secret="$(zipline_read_env_value "$env_file" CORE_SECRET)" || zipline_fail "CORE_SECRET is missing from Zipline .env" + + zipline_find_upgrade_snapshot "$root_dir" "$env_file" "$current_name" "$raw_data_dir" "$current_db" "$current_user" + postgres_password="$ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD" + core_secret="$ZIPLINE_SNAPSHOT_CORE_SECRET" + if zipline_load_secret_state "$data_dir"; then + [[ "$ZIPLINE_STORED_POSTGRES_PASSWORD" == "$postgres_password" && "$ZIPLINE_STORED_CORE_SECRET" == "$core_secret" ]] || + zipline_fail "Zipline secret state does not match the 1Panel upgrade snapshot" + fi + if [[ -n "$current_password" && "$current_password" != generate && "$current_password" != "$postgres_password" ]]; then + zipline_fail "POSTGRES_PASSWORD changed during upgrade; refusing to alter the existing database credential" + fi + if [[ -n "$current_core_secret" && "$current_core_secret" != generate && "$current_core_secret" != "$core_secret" ]]; then + zipline_fail "CORE_SECRET changed during upgrade; refusing to rotate the existing session secret" + fi + zipline_validate_postgres_password "$postgres_password" || zipline_fail "saved database password is invalid" + zipline_validate_core_secret "$core_secret" || zipline_fail "saved session secret is invalid" + zipline_write_env_credentials "$env_file" "$postgres_password" "$core_secret" + zipline_write_secret_state "$data_dir" "$postgres_password" "$core_secret" + printf '%s\n' "Zipline secrets were restored from the matching 1Panel upgrade snapshot." +} diff --git a/apps/zipline/4.7.0/scripts/init.sh b/apps/zipline/4.7.0/scripts/init.sh new file mode 100755 index 000000000..868a2fb3f --- /dev/null +++ b/apps/zipline/4.7.0/scripts/init.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +read_env_value() { + local key="$1" + 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" +} + +validate_ipv4() { + local key="$1" + local value="$2" + local octet + local -a octets + [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "$key must be an IPv4 address" + IFS=. read -r -a octets <<< "$value" + for octet in "${octets[@]}"; do + ((10#$octet <= 255)) || fail "$key contains an invalid IPv4 octet" + done +} + +validate_port() { + local value="$1" + [[ "$value" =~ ^[0-9]+$ ]] || fail "PANEL_APP_PORT_HTTP must be an integer" + ((10#$value >= 1 && 10#$value <= 65535)) || fail "PANEL_APP_PORT_HTTP must be between 1 and 65535" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "Zipline init must run as root" +command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" +source "${ROOT_DIR}/scripts/credentials.sh" + +validate_ipv4 PANEL_APP_BIND_ADDRESS "$(read_env_value PANEL_APP_BIND_ADDRESS)" +validate_port "$(read_env_value PANEL_APP_PORT_HTTP)" + +postgres_db="$(read_env_value POSTGRES_DB)" +postgres_user="$(read_env_value POSTGRES_USER)" +[[ "$postgres_db" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_DB is invalid" +[[ "$postgres_user" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_USER is invalid" + +max_file_size="$(read_env_value FILES_MAX_FILE_SIZE)" +[[ "$max_file_size" =~ ^[1-9][0-9]*(b|kb|mb|gb|tb)$ ]] || + fail "FILES_MAX_FILE_SIZE must be a positive byte quantity such as 100mb" + +data_raw="$(read_env_value APP_DATA_DIR)" +[[ -n "$data_raw" && "$data_raw" != /* ]] || fail "APP_DATA_DIR must be a non-empty relative path" +case "$data_raw" in + *$'\n'* | *$'\r'* | *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + fail "APP_DATA_DIR contains unsupported characters" + ;; +esac + +relative_data="${data_raw#./}" +[[ -n "$relative_data" ]] || fail "APP_DATA_DIR must not resolve to the version root" +current="$ROOT_DIR" +IFS=/ read -r -a components <<< "$relative_data" +for component in "${components[@]}"; do + [[ -n "$component" && "$component" != . && "$component" != .. ]] || fail "APP_DATA_DIR contains traversal" + current="$current/$component" + [[ ! -L "$current" ]] || fail "APP_DATA_DIR must not contain symbolic-link components" +done + +data_dir="$(realpath -m -- "$ROOT_DIR/$relative_data")" +case "$data_dir" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR must stay inside the application version directory" ;; +esac + +install -d -m 0700 -- "$data_dir/postgres" +install -d -m 0750 -- "$data_dir/uploads" "$data_dir/public" "$data_dir/themes" +resolved_data="$(realpath -e -- "$data_dir")" +case "$resolved_data" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +chown -R 70:70 -- "$resolved_data/postgres" +chown -R 1000:1000 -- "$resolved_data/uploads" "$resolved_data/public" "$resolved_data/themes" +chmod 0700 -- "$resolved_data/postgres" + +zipline_init_credentials "$ENV_FILE" "$resolved_data" +chmod 600 "$ENV_FILE" diff --git a/apps/zipline/4.7.0/scripts/uninstall.sh b/apps/zipline/4.7.0/scripts/uninstall.sh new file mode 100755 index 000000000..42b518ef4 --- /dev/null +++ b/apps/zipline/4.7.0/scripts/uninstall.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 1Panel removes the installation directory after this hook, including relative APP_DATA_DIR contents. +# Back up required data outside the application directory before uninstalling. +exit 0 diff --git a/apps/zipline/4.7.0/scripts/upgrade.sh b/apps/zipline/4.7.0/scripts/upgrade.sh new file mode 100755 index 000000000..60ed3b247 --- /dev/null +++ b/apps/zipline/4.7.0/scripts/upgrade.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" +source "${ROOT_DIR}/scripts/credentials.sh" + +zipline_restore_upgrade_credentials "$ROOT_DIR" "$ENV_FILE" diff --git a/apps/zipline/4.8.0/.env.sample b/apps/zipline/4.8.0/.env.sample new file mode 100644 index 000000000..845005ffa --- /dev/null +++ b/apps/zipline/4.8.0/.env.sample @@ -0,0 +1,9 @@ +CONTAINER_NAME=zipline +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=3000 +POSTGRES_DB=zipline +POSTGRES_USER=zipline +POSTGRES_PASSWORD=generate +CORE_SECRET=generate +FILES_MAX_FILE_SIZE=100mb +APP_DATA_DIR=./data diff --git a/apps/zipline/4.8.0/data.yml b/apps/zipline/4.8.0/data.yml new file mode 100644 index 000000000..8f8719194 --- /dev/null +++ b/apps/zipline/4.8.0/data.yml @@ -0,0 +1,135 @@ +additionalProperties: + formFields: + - default: 127.0.0.1 + edit: true + envKey: PANEL_APP_BIND_ADDRESS + labelEn: Bind Address + labelZh: 绑定地址 + label: + en: Bind Address + zh: 绑定地址 + zh-Hant: 綁定位址 + ja: バインドアドレス + ko: 바인드 주소 + ru: Адрес привязки + ms: Alamat ikatan + pt-br: Endereco de vinculacao + required: true + type: text + - default: 3000 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: HTTP-порт + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: 100mb + edit: true + envKey: FILES_MAX_FILE_SIZE + labelEn: Maximum File Size + labelZh: 最大文件大小 + label: + en: Maximum File Size + zh: 最大文件大小 + zh-Hant: 最大檔案大小 + ja: 最大ファイルサイズ + ko: 최대 파일 크기 + ru: Максимальный размер файла + ms: Saiz fail maksimum + pt-br: Tamanho maximo do arquivo + required: true + type: text + - default: zipline + edit: true + envKey: POSTGRES_DB + labelEn: Database Name + labelZh: 数据库名称 + label: + en: Database Name + zh: 数据库名称 + zh-Hant: 資料庫名稱 + ja: データベース名 + ko: 데이터베이스 이름 + ru: Имя базы данных + ms: Nama pangkalan data + pt-br: Nome do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: zipline + edit: true + envKey: POSTGRES_USER + labelEn: Database User + labelZh: 数据库用户 + label: + en: Database User + zh: 数据库用户 + zh-Hant: 資料庫使用者 + ja: データベースユーザー + ko: 데이터베이스 사용자 + ru: Пользователь базы данных + ms: Pengguna pangkalan data + pt-br: Usuario do banco de dados + random: true + required: true + rule: paramCommon + type: text + - default: generate + edit: true + envKey: POSTGRES_PASSWORD + labelEn: Database Password + labelZh: 数据库密码 + label: + en: Database Password + zh: 数据库密码 + zh-Hant: 資料庫密碼 + ja: データベースパスワード + ko: 데이터베이스 비밀번호 + ru: Пароль базы данных + ms: Kata laluan pangkalan data + pt-br: Senha do banco de dados + required: true + type: password + - default: generate + edit: true + envKey: CORE_SECRET + labelEn: Session Secret + labelZh: 会话密钥 + label: + en: Session Secret + zh: 会话密钥 + zh-Hant: 工作階段密鑰 + ja: セッションシークレット + ko: 세션 비밀 키 + ru: Секрет сеанса + ms: Rahsia sesi + pt-br: Segredo da sessao + required: true + type: password + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori data + pt-br: Diretorio de dados + required: true + type: text diff --git a/apps/zipline/4.8.0/docker-compose.yml b/apps/zipline/4.8.0/docker-compose.yml new file mode 100644 index 000000000..6fbe63dbc --- /dev/null +++ b/apps/zipline/4.8.0/docker-compose.yml @@ -0,0 +1,79 @@ +services: + zipline-postgres: + image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 + container_name: ${CONTAINER_NAME}-postgres + restart: unless-stopped + user: "70:70" + networks: + - zipline-internal + environment: + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - "${APP_DATA_DIR}/postgres:/var/lib/postgresql/data" + read_only: true + tmpfs: + - /tmp:mode=1777,size=64m + - /var/run/postgresql:uid=70,gid=70,mode=0755,size=16m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + labels: + createdBy: "Apps" + + zipline: + image: ghcr.io/diced/zipline:4.8.0@sha256:8ae3cdf8cdbba2ac98754e9fc4ccec67bbaa86b6e6ab1ccf9683ddcff85eee72 + container_name: ${CONTAINER_NAME} + restart: unless-stopped + user: "1000:1000" + depends_on: + zipline-postgres: + condition: service_healthy + networks: + - zipline-internal + - 1panel-network + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:3000" + environment: + - CORE_HOSTNAME=0.0.0.0 + - CORE_PORT=3000 + - CORE_SECRET=${CORE_SECRET} + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@zipline-postgres:5432/${POSTGRES_DB} + - DATASOURCE_TYPE=local + - DATASOURCE_LOCAL_DIRECTORY=/zipline/uploads + - CORE_TEMP_DIRECTORY=/tmp/zipline + - FILES_MAX_FILE_SIZE=${FILES_MAX_FILE_SIZE} + - FEATURES_HEALTHCHECK=true + volumes: + - "${APP_DATA_DIR}/uploads:/zipline/uploads" + - "${APP_DATA_DIR}/public:/zipline/public" + - "${APP_DATA_DIR}/themes:/zipline/themes" + read_only: true + tmpfs: + - /tmp:mode=1777,size=256m + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/healthcheck"] + interval: 15s + timeout: 5s + retries: 8 + start_period: 60s + labels: + createdBy: "Apps" + +networks: + zipline-internal: + internal: true + 1panel-network: + external: true diff --git a/apps/zipline/4.8.0/scripts/credentials.sh b/apps/zipline/4.8.0/scripts/credentials.sh new file mode 100644 index 000000000..f0d6b6cf2 --- /dev/null +++ b/apps/zipline/4.8.0/scripts/credentials.sh @@ -0,0 +1,290 @@ +#!/usr/bin/env bash +set -euo pipefail + +ZIPLINE_STORED_POSTGRES_PASSWORD="" +ZIPLINE_STORED_CORE_SECRET="" +ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD="" +ZIPLINE_SNAPSHOT_CORE_SECRET="" + +zipline_fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +zipline_read_env_value() { + local file="$1" + local key="$2" + local value + [[ -f "$file" && ! -L "$file" ]] || return 1 + value="$(awk -v key="$key" ' + index($0, key "=") == 1 { + count++ + value = substr($0, length(key) + 2) + } + END { + if (count != 1) exit 1 + print value + } + ' "$file")" || return 1 + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + printf '%s\n' "$value" +} + +zipline_resolve_data_dir() { + local root_dir="$1" + local raw="$2" + local clean current resolved part + local -a parts=() + [[ -n "$raw" && "$raw" != /* ]] || zipline_fail "APP_DATA_DIR must be a non-empty relative path" + [[ ! "$raw" =~ [[:cntrl:]] ]] || zipline_fail "APP_DATA_DIR contains control characters" + case "$raw" in + *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + zipline_fail "APP_DATA_DIR contains unsupported characters" + ;; + esac + clean="${raw#./}" + [[ -n "$clean" ]] || zipline_fail "APP_DATA_DIR must not resolve to the version root" + IFS=/ read -r -a parts <<< "$clean" + current="$root_dir" + for part in "${parts[@]}"; do + [[ -n "$part" && "$part" != . && "$part" != .. ]] || zipline_fail "APP_DATA_DIR contains traversal" + current="$current/$part" + [[ ! -L "$current" ]] || zipline_fail "APP_DATA_DIR must not contain symbolic-link components" + done + resolved="$(realpath -m -- "$root_dir/$clean")" || zipline_fail "APP_DATA_DIR could not be resolved" + case "$resolved" in + "$root_dir"/*) ;; + *) zipline_fail "APP_DATA_DIR must stay inside the application version directory" ;; + esac + printf '%s\n' "$resolved" +} + +zipline_validate_postgres_password() { + [[ "$1" =~ ^[A-Za-z0-9]{32,128}$ ]] +} + +zipline_validate_core_secret() { + [[ "$1" =~ ^[A-Fa-f0-9]{64,128}$ ]] +} + +zipline_generate_postgres_password() { + local material + material="$(openssl rand -base64 96 | tr -dc A-Za-z0-9)" + [[ ${#material} -ge 48 ]] || zipline_fail "unable to generate sufficient database password material" + printf '%s\n' "${material:0:48}" +} + +zipline_write_env_credentials() { + local env_file="$1" + local postgres_password="$2" + local core_secret="$3" + local temp_file + temp_file="$(mktemp "${env_file}.zipline-tmp.XXXXXX")" || zipline_fail "unable to stage Zipline environment update" + if ! ZIPLINE_DB_PASSWORD="$postgres_password" ZIPLINE_CORE_SECRET="$core_secret" awk ' + BEGIN { + db_password = ENVIRON["ZIPLINE_DB_PASSWORD"] + core_secret = ENVIRON["ZIPLINE_CORE_SECRET"] + } + /^POSTGRES_PASSWORD=/ { + if (!db_written++) print "POSTGRES_PASSWORD=" db_password + next + } + /^CORE_SECRET=/ { + if (!core_written++) print "CORE_SECRET=" core_secret + next + } + { print } + END { + if (!db_written) print "POSTGRES_PASSWORD=" db_password + if (!core_written) print "CORE_SECRET=" core_secret + } + ' "$env_file" > "$temp_file"; then + rm -f -- "$temp_file" + zipline_fail "unable to render Zipline environment update" + fi + chmod 600 "$temp_file" + mv -f -- "$temp_file" "$env_file" + chmod 600 "$env_file" +} + +zipline_write_secret_state() { + local data_dir="$1" + local postgres_password="$2" + local core_secret="$3" + local secret_file="$data_dir/.zipline-secrets" + local temp_file + [[ -d "$data_dir" && ! -L "$data_dir" ]] || zipline_fail "Zipline data directory is unavailable" + [[ ! -L "$secret_file" ]] || zipline_fail "Zipline secret state must not be a symbolic link" + temp_file="$(mktemp "${secret_file}.tmp.XXXXXX")" || zipline_fail "unable to stage Zipline secret state" + if ! printf 'POSTGRES_PASSWORD=%s\nCORE_SECRET=%s\n' "$postgres_password" "$core_secret" > "$temp_file"; then + rm -f -- "$temp_file" + zipline_fail "unable to write Zipline secret state" + fi + chmod 600 "$temp_file" + chown 0:0 -- "$temp_file" + mv -f -- "$temp_file" "$secret_file" + chmod 600 "$secret_file" + chown 0:0 -- "$secret_file" +} + +zipline_load_secret_state() { + local data_dir="$1" + local secret_file="$data_dir/.zipline-secrets" + [[ -e "$secret_file" || -L "$secret_file" ]] || return 1 + [[ -f "$secret_file" && ! -L "$secret_file" ]] || zipline_fail "Zipline secret state is not a regular file" + ZIPLINE_STORED_POSTGRES_PASSWORD="$(zipline_read_env_value "$secret_file" POSTGRES_PASSWORD)" || + zipline_fail "Zipline secret state is incomplete" + ZIPLINE_STORED_CORE_SECRET="$(zipline_read_env_value "$secret_file" CORE_SECRET)" || + zipline_fail "Zipline secret state is incomplete" + zipline_validate_postgres_password "$ZIPLINE_STORED_POSTGRES_PASSWORD" || + zipline_fail "Zipline secret state has an invalid database password" + zipline_validate_core_secret "$ZIPLINE_STORED_CORE_SECRET" || + zipline_fail "Zipline secret state has an invalid session secret" + chmod 600 "$secret_file" + chown 0:0 -- "$secret_file" +} + +zipline_init_credentials() { + local env_file="$1" + local data_dir="$2" + local postgres_password core_secret stored + local postgres_default=false core_default=false + [[ -f "$env_file" && ! -L "$env_file" ]] || zipline_fail "Zipline .env file is unavailable" + postgres_password="$(zipline_read_env_value "$env_file" POSTGRES_PASSWORD)" || + zipline_fail "POSTGRES_PASSWORD is missing from Zipline .env" + core_secret="$(zipline_read_env_value "$env_file" CORE_SECRET)" || + zipline_fail "CORE_SECRET is missing from Zipline .env" + [[ -n "$postgres_password" && "$postgres_password" != generate ]] || postgres_default=true + [[ -n "$core_secret" && "$core_secret" != generate ]] || core_default=true + + if zipline_load_secret_state "$data_dir"; then + if [[ "$postgres_default" == true ]]; then + postgres_password="$ZIPLINE_STORED_POSTGRES_PASSWORD" + elif [[ "$postgres_password" != "$ZIPLINE_STORED_POSTGRES_PASSWORD" ]]; then + zipline_fail "POSTGRES_PASSWORD cannot change while Zipline data is retained" + fi + if [[ "$core_default" == true ]]; then + core_secret="$ZIPLINE_STORED_CORE_SECRET" + elif [[ "$core_secret" != "$ZIPLINE_STORED_CORE_SECRET" ]]; then + zipline_fail "CORE_SECRET cannot change while Zipline data is retained" + fi + else + if [[ -f "$data_dir/postgres/PG_VERSION" && ( "$postgres_default" == true || "$core_default" == true ) ]]; then + zipline_fail "existing PostgreSQL data has no Zipline secret state; supply its original credentials to preserve it" + fi + if [[ "$postgres_default" == true ]]; then + postgres_password="$(zipline_generate_postgres_password)" + fi + if [[ "$core_default" == true ]]; then + core_secret="$(openssl rand -hex 32)" + fi + fi + + zipline_validate_postgres_password "$postgres_password" || + zipline_fail "POSTGRES_PASSWORD must contain 32 to 128 alphanumeric characters" + zipline_validate_core_secret "$core_secret" || + zipline_fail "CORE_SECRET must contain 64 to 128 hexadecimal characters" + zipline_write_env_credentials "$env_file" "$postgres_password" "$core_secret" + zipline_write_secret_state "$data_dir" "$postgres_password" "$core_secret" +} + +zipline_find_upgrade_snapshot() { + local root_dir="$1" + local env_file="$2" + local current_name="$3" + local current_data_dir="$4" + local current_db="$5" + local current_user="$6" + local temp_root snapshot old_env old_compose old_name old_data old_db old_user + local snapshot_time env_time best_time=-1 tied=false snapshot_uid snapshot_mode old_env_uid old_env_mode + local best_snapshot="" + local -a candidates=() + + temp_root="$(realpath -e -- "${TMPDIR:-/tmp}")" || zipline_fail "1Panel upgrade snapshot directory is unavailable" + [[ -d "$temp_root" ]] || zipline_fail "1Panel upgrade snapshot directory is unavailable" + env_time="$(stat -c '%Y' "$env_file")" || zipline_fail "Zipline .env timestamp is unavailable" + shopt -s nullglob + candidates=("$temp_root"/1panel-app-upgrade-*) + shopt -u nullglob + for snapshot in "${candidates[@]}"; do + [[ -d "$snapshot" && ! -L "$snapshot" ]] || continue + snapshot_uid="$(stat -c '%u' "$snapshot")" || continue + snapshot_mode="$(stat -c '%a' "$snapshot")" || continue + [[ "$snapshot_uid" == 0 ]] || continue + (( (8#$snapshot_mode & 077) == 0 )) || continue + old_env="$snapshot/.env" + old_compose="$snapshot/docker-compose.yml" + [[ -f "$old_env" && ! -L "$old_env" && -f "$old_compose" && ! -L "$old_compose" ]] || continue + old_env_uid="$(stat -c '%u' "$old_env")" || continue + old_env_mode="$(stat -c '%a' "$old_env")" || continue + [[ "$old_env_uid" == 0 ]] || continue + (( (8#$old_env_mode & 077) == 0 )) || continue + grep -Eq 'image:[[:space:]]*"?ghcr\.io/diced/zipline:4\.(6\.5|7\.0|8\.0)@sha256:[[:xdigit:]]{64}"?[[:space:]]*$' "$old_compose" || continue + old_name="$(zipline_read_env_value "$old_env" CONTAINER_NAME)" || continue + [[ "$old_name" == "$current_name" ]] || continue + old_data="$(zipline_read_env_value "$old_env" APP_DATA_DIR)" || zipline_fail "matching 1Panel snapshot lacks APP_DATA_DIR" + old_db="$(zipline_read_env_value "$old_env" POSTGRES_DB)" || zipline_fail "matching 1Panel snapshot lacks POSTGRES_DB" + old_user="$(zipline_read_env_value "$old_env" POSTGRES_USER)" || zipline_fail "matching 1Panel snapshot lacks POSTGRES_USER" + [[ "$old_data" == "$current_data_dir" && "$old_db" == "$current_db" && "$old_user" == "$current_user" ]] || + zipline_fail "matching 1Panel snapshot does not preserve the configured data path and database identity" + ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD="$(zipline_read_env_value "$old_env" POSTGRES_PASSWORD)" || + zipline_fail "matching 1Panel snapshot lacks its database password" + ZIPLINE_SNAPSHOT_CORE_SECRET="$(zipline_read_env_value "$old_env" CORE_SECRET)" || + zipline_fail "matching 1Panel snapshot lacks its session secret" + zipline_validate_postgres_password "$ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD" || + zipline_fail "matching 1Panel snapshot has an invalid database password" + zipline_validate_core_secret "$ZIPLINE_SNAPSHOT_CORE_SECRET" || + zipline_fail "matching 1Panel snapshot has an invalid session secret" + snapshot_time="$(stat -c '%Y' "$snapshot")" || zipline_fail "1Panel upgrade snapshot timestamp is unavailable" + if (( snapshot_time > best_time )); then + best_time="$snapshot_time" + best_snapshot="$snapshot" + tied=false + elif (( snapshot_time == best_time )); then + tied=true + fi + done + [[ -n "$best_snapshot" ]] || zipline_fail "could not find this Zipline install's 1Panel upgrade snapshot" + [[ "$tied" == false ]] || zipline_fail "multiple matching 1Panel upgrade snapshots are ambiguous" + (( best_time <= env_time && env_time - best_time <= 600 )) || + zipline_fail "matching 1Panel upgrade snapshot is stale" +} + +zipline_restore_upgrade_credentials() { + local root_dir="$1" + local env_file="$2" + local raw_data_dir data_dir current_name current_db current_user + local postgres_password core_secret current_password current_core_secret + [[ -f "$env_file" && ! -L "$env_file" ]] || zipline_fail "Zipline .env file is unavailable" + current_name="$(zipline_read_env_value "$env_file" CONTAINER_NAME)" || zipline_fail "CONTAINER_NAME is missing from Zipline .env" + [[ "$current_name" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]] || zipline_fail "CONTAINER_NAME is invalid" + raw_data_dir="$(zipline_read_env_value "$env_file" APP_DATA_DIR)" || zipline_fail "APP_DATA_DIR is missing from Zipline .env" + data_dir="$(zipline_resolve_data_dir "$root_dir" "$raw_data_dir")" + [[ -d "$data_dir" && ! -L "$data_dir" ]] || zipline_fail "configured Zipline data directory is missing" + current_db="$(zipline_read_env_value "$env_file" POSTGRES_DB)" || zipline_fail "POSTGRES_DB is missing from Zipline .env" + current_user="$(zipline_read_env_value "$env_file" POSTGRES_USER)" || zipline_fail "POSTGRES_USER is missing from Zipline .env" + current_password="$(zipline_read_env_value "$env_file" POSTGRES_PASSWORD)" || zipline_fail "POSTGRES_PASSWORD is missing from Zipline .env" + current_core_secret="$(zipline_read_env_value "$env_file" CORE_SECRET)" || zipline_fail "CORE_SECRET is missing from Zipline .env" + + zipline_find_upgrade_snapshot "$root_dir" "$env_file" "$current_name" "$raw_data_dir" "$current_db" "$current_user" + postgres_password="$ZIPLINE_SNAPSHOT_POSTGRES_PASSWORD" + core_secret="$ZIPLINE_SNAPSHOT_CORE_SECRET" + if zipline_load_secret_state "$data_dir"; then + [[ "$ZIPLINE_STORED_POSTGRES_PASSWORD" == "$postgres_password" && "$ZIPLINE_STORED_CORE_SECRET" == "$core_secret" ]] || + zipline_fail "Zipline secret state does not match the 1Panel upgrade snapshot" + fi + if [[ -n "$current_password" && "$current_password" != generate && "$current_password" != "$postgres_password" ]]; then + zipline_fail "POSTGRES_PASSWORD changed during upgrade; refusing to alter the existing database credential" + fi + if [[ -n "$current_core_secret" && "$current_core_secret" != generate && "$current_core_secret" != "$core_secret" ]]; then + zipline_fail "CORE_SECRET changed during upgrade; refusing to rotate the existing session secret" + fi + zipline_validate_postgres_password "$postgres_password" || zipline_fail "saved database password is invalid" + zipline_validate_core_secret "$core_secret" || zipline_fail "saved session secret is invalid" + zipline_write_env_credentials "$env_file" "$postgres_password" "$core_secret" + zipline_write_secret_state "$data_dir" "$postgres_password" "$core_secret" + printf '%s\n' "Zipline secrets were restored from the matching 1Panel upgrade snapshot." +} diff --git a/apps/zipline/4.8.0/scripts/init.sh b/apps/zipline/4.8.0/scripts/init.sh new file mode 100755 index 000000000..868a2fb3f --- /dev/null +++ b/apps/zipline/4.8.0/scripts/init.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +read_env_value() { + local key="$1" + 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" +} + +validate_ipv4() { + local key="$1" + local value="$2" + local octet + local -a octets + [[ "$value" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "$key must be an IPv4 address" + IFS=. read -r -a octets <<< "$value" + for octet in "${octets[@]}"; do + ((10#$octet <= 255)) || fail "$key contains an invalid IPv4 octet" + done +} + +validate_port() { + local value="$1" + [[ "$value" =~ ^[0-9]+$ ]] || fail "PANEL_APP_PORT_HTTP must be an integer" + ((10#$value >= 1 && 10#$value <= 65535)) || fail "PANEL_APP_PORT_HTTP must be between 1 and 65535" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" +[[ "$(id -u)" -eq 0 ]] || fail "Zipline init must run as root" +command -v openssl >/dev/null 2>&1 || fail "openssl is required to generate credentials" +source "${ROOT_DIR}/scripts/credentials.sh" + +validate_ipv4 PANEL_APP_BIND_ADDRESS "$(read_env_value PANEL_APP_BIND_ADDRESS)" +validate_port "$(read_env_value PANEL_APP_PORT_HTTP)" + +postgres_db="$(read_env_value POSTGRES_DB)" +postgres_user="$(read_env_value POSTGRES_USER)" +[[ "$postgres_db" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_DB is invalid" +[[ "$postgres_user" =~ ^[A-Za-z_][A-Za-z0-9_]{0,62}$ ]] || fail "POSTGRES_USER is invalid" + +max_file_size="$(read_env_value FILES_MAX_FILE_SIZE)" +[[ "$max_file_size" =~ ^[1-9][0-9]*(b|kb|mb|gb|tb)$ ]] || + fail "FILES_MAX_FILE_SIZE must be a positive byte quantity such as 100mb" + +data_raw="$(read_env_value APP_DATA_DIR)" +[[ -n "$data_raw" && "$data_raw" != /* ]] || fail "APP_DATA_DIR must be a non-empty relative path" +case "$data_raw" in + *$'\n'* | *$'\r'* | *\\* | *:* | *'$'* | *'#'* | *'"'* | *"'"*) + fail "APP_DATA_DIR contains unsupported characters" + ;; +esac + +relative_data="${data_raw#./}" +[[ -n "$relative_data" ]] || fail "APP_DATA_DIR must not resolve to the version root" +current="$ROOT_DIR" +IFS=/ read -r -a components <<< "$relative_data" +for component in "${components[@]}"; do + [[ -n "$component" && "$component" != . && "$component" != .. ]] || fail "APP_DATA_DIR contains traversal" + current="$current/$component" + [[ ! -L "$current" ]] || fail "APP_DATA_DIR must not contain symbolic-link components" +done + +data_dir="$(realpath -m -- "$ROOT_DIR/$relative_data")" +case "$data_dir" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR must stay inside the application version directory" ;; +esac + +install -d -m 0700 -- "$data_dir/postgres" +install -d -m 0750 -- "$data_dir/uploads" "$data_dir/public" "$data_dir/themes" +resolved_data="$(realpath -e -- "$data_dir")" +case "$resolved_data" in + "$ROOT_DIR"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +chown -R 70:70 -- "$resolved_data/postgres" +chown -R 1000:1000 -- "$resolved_data/uploads" "$resolved_data/public" "$resolved_data/themes" +chmod 0700 -- "$resolved_data/postgres" + +zipline_init_credentials "$ENV_FILE" "$resolved_data" +chmod 600 "$ENV_FILE" diff --git a/apps/zipline/4.8.0/scripts/uninstall.sh b/apps/zipline/4.8.0/scripts/uninstall.sh new file mode 100755 index 000000000..42b518ef4 --- /dev/null +++ b/apps/zipline/4.8.0/scripts/uninstall.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 1Panel removes the installation directory after this hook, including relative APP_DATA_DIR contents. +# Back up required data outside the application directory before uninstalling. +exit 0 diff --git a/apps/zipline/4.8.0/scripts/upgrade.sh b/apps/zipline/4.8.0/scripts/upgrade.sh new file mode 100755 index 000000000..8d6e8ec31 --- /dev/null +++ b/apps/zipline/4.8.0/scripts/upgrade.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" +source "${ROOT_DIR}/scripts/credentials.sh" + +zipline_restore_upgrade_credentials "$ROOT_DIR" "$ENV_FILE" + +command -v docker >/dev/null 2>&1 || zipline_fail "Docker is required to verify the Zipline migration before upgrading" +command -v timeout >/dev/null 2>&1 || zipline_fail "timeout is required to verify the Zipline migration before upgrading" + +compose=(docker compose --project-directory "$ROOT_DIR" -f "$ROOT_DIR/docker-compose.yml") +"${compose[@]}" up -d zipline-postgres + +postgres_db="$(zipline_read_env_value "$ENV_FILE" POSTGRES_DB)" || + zipline_fail "POSTGRES_DB is missing from Zipline .env" +postgres_user="$(zipline_read_env_value "$ENV_FILE" POSTGRES_USER)" || + zipline_fail "POSTGRES_USER is missing from Zipline .env" + +ready=false +for attempt in {1..60}; do + if "${compose[@]}" exec -T zipline-postgres pg_isready -q -U "$postgres_user" -d "$postgres_db"; then + ready=true + break + fi + sleep 2 +done +[[ "$ready" == true ]] || zipline_fail "Zipline PostgreSQL did not become ready for the staged upgrade" + +migration_state="$(timeout 15 "${compose[@]}" exec -T zipline-postgres sh -ec 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atq -c "SELECT CASE WHEN count(*) = 1 AND bool_and(finished_at IS NOT NULL AND rolled_back_at IS NULL AND applied_steps_count >= 1) THEN '\''ok'\'' ELSE '\''bad'\'' END FROM public._prisma_migrations WHERE migration_name = '\''20260806081144_remove_version_api'\'' AND checksum = '\''08368d44344f934f07757dc014bd3a2022b14a8b377e1f8f5fb2d4836c3dc1ce'\'';"')" || + zipline_fail "Zipline 4.7 migration history could not be verified; keep the existing version and repair the database before upgrading" +[[ "$migration_state" == ok ]] || + zipline_fail "Zipline 4.7 migration is missing or incomplete; keep the existing version and allow its migration to finish before upgrading" + +printf '%s\n' "Zipline 4.7 migration verified; proceeding with the 4.8 upgrade." diff --git a/apps/zipline/README.md b/apps/zipline/README.md index cdff969ed..3825cb1b8 100644 --- a/apps/zipline/README.md +++ b/apps/zipline/README.md @@ -16,7 +16,9 @@ Zipline 是自托管的文件分享和 URL 缩短平台,提供用户管理、 ## 数据持久化 -`APP_DATA_DIR` 中的 `postgres`、`uploads`、`public` 和 `themes` 分别保存数据库、上传文件、公开资源和自定义主题。卸载不会删除这些目录,请定期独立备份。 +`APP_DATA_DIR` 中的 `postgres`、`uploads`、`public` 和 `themes` 分别保存数据库、上传文件、公开资源和自定义主题。升级时保持现有 `APP_DATA_DIR` 不变。1Panel 卸载应用时会删除应用安装目录,因此位于该目录内的相对数据目录也会被删除;卸载前请先将数据备份到应用目录之外。 + +从 `4.6.5` 升级到 `4.8.0` 时,先升级到 `4.7.0` 并等待应用完成数据库迁移、恢复健康后,再升级到 `4.8.0`。升级前请备份 PostgreSQL 数据;升级期间保留现有 `APP_DATA_DIR` 和数据库身份配置。 ## 安全与部署风险 @@ -38,12 +40,13 @@ Zipline is a self-hosted file sharing and URL shortening platform with user mana ## Usage Notes - Access the service at `http://:`. It binds to `127.0.0.1` by default; publish it through an HTTPS reverse proxy for remote access. The first account created becomes the administrator. -- `APP_DATA_DIR` contains PostgreSQL data, uploads, public assets, and custom themes. Uninstallation preserves these directories. +- `APP_DATA_DIR` contains PostgreSQL data, uploads, public assets, and custom themes. Keep its existing value during upgrades. 1Panel removes the app installation directory during uninstall, including relative data directories beneath it; back up the data outside the app directory before uninstalling. +- To upgrade an existing `4.6.5` installation to `4.8.0`, upgrade to `4.7.0` first and wait for its database migrations and health check to finish. Back up PostgreSQL before upgrading, and keep the existing `APP_DATA_DIR` and database identity settings. - Zipline runs as UID/GID `1000:1000`; PostgreSQL runs as `70:70` on an internal-only network. Both containers drop all capabilities, use read-only root filesystems, and enable `no-new-privileges`. ## Security Note -The audited official image snapshot contains upstream vulnerabilities. Two Critical `CVE-2026-59873` findings are confined to npm/corepack tooling that the default service entrypoint does not invoke. `CVE-2026-15074` in `@fastify/static 9.1.3` bypasses route-scoped guards but cannot escape a configured static root; Zipline exposes `build/client` as intentionally public, registers its temporary root with `serve:false`, and sends authenticated exports by a fixed server-selected filename. Other High findings are confined to build tooling, Prisma migration configuration, static schema/router dependencies, or features such as HTTP/2 and RSC that the default service does not enable. These are default-path reachability exceptions, not claims that the packages are absent from later images. A new image resolved from the moving `latest` tag, running package managers in the container, loading untrusted Prisma configuration, or replacing the official entrypoint invalidates them. +The prior audit applies to the official `4.6.5` image snapshot only. It found two Critical `CVE-2026-59873` findings confined to npm/corepack tooling that the default service entrypoint does not invoke. `CVE-2026-15074` in `@fastify/static 9.1.3` bypasses route-scoped guards but cannot escape a configured static root; Zipline exposes `build/client` as intentionally public, registers its temporary root with `serve:false`, and sends authenticated exports by a fixed server-selected filename. Other High findings are confined to build tooling, Prisma migration configuration, static schema/router dependencies, or features such as HTTP/2 and RSC that the default service does not enable. These are default-path reachability exceptions, not claims that the packages are absent from later images. The `4.7.0` and `4.8.0` image CVEs have not been assessed in this maintenance run. ## References diff --git a/apps/zipline/latest/scripts/uninstall.sh b/apps/zipline/latest/scripts/uninstall.sh index 9365d9925..42b518ef4 100755 --- a/apps/zipline/latest/scripts/uninstall.sh +++ b/apps/zipline/latest/scripts/uninstall.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -# Persistent data is intentionally retained for recovery or reinstallation. +# 1Panel removes the installation directory after this hook, including relative APP_DATA_DIR contents. +# Back up required data outside the application directory before uninstalling. exit 0