From a51048652df72d002f150a5b840c0749f6e50402 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:26:50 +0800 Subject: [PATCH] chore(appstore): add Asciinema Postgres digest version --- .../.env.sample | 12 ++ .../data.yml | 195 ++++++++++++++++++ .../docker-compose.yml | 82 ++++++++ .../scripts/init.sh | 144 +++++++++++++ .../scripts/uninstall.sh | 4 + .../scripts/upgrade.sh | 4 + 6 files changed, 441 insertions(+) create mode 100644 apps/asciinema-server/20260626-postgres17-digest-b0f9560/.env.sample create mode 100644 apps/asciinema-server/20260626-postgres17-digest-b0f9560/data.yml create mode 100644 apps/asciinema-server/20260626-postgres17-digest-b0f9560/docker-compose.yml create mode 100755 apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/init.sh create mode 100755 apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/uninstall.sh create mode 100755 apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/upgrade.sh diff --git a/apps/asciinema-server/20260626-postgres17-digest-b0f9560/.env.sample b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/.env.sample new file mode 100644 index 000000000..8cdde8aa2 --- /dev/null +++ b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/.env.sample @@ -0,0 +1,12 @@ +CONTAINER_NAME=asciinema-server +PANEL_APP_BIND_ADDRESS=127.0.0.1 +PANEL_APP_PORT_HTTP=4000 +ASCIINEMA_URL_SCHEME=http +ASCIINEMA_URL_HOST=asciinema.local +ASCIINEMA_URL_PORT=4000 +ASCIINEMA_SECRET_KEY_BASE=generate +ASCIINEMA_POSTGRES_PASSWORD=generate +ASCIINEMA_SIGN_UP_DISABLED=false +ASCIINEMA_UPLOAD_AUTH_REQUIRED=true +ASCIINEMA_TIMEZONE=UTC +APP_DATA_DIR=./data diff --git a/apps/asciinema-server/20260626-postgres17-digest-b0f9560/data.yml b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/data.yml new file mode 100644 index 000000000..52526127b --- /dev/null +++ b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/data.yml @@ -0,0 +1,195 @@ +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: 4000 + 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: http + edit: true + envKey: ASCIINEMA_URL_SCHEME + labelEn: Public URL Scheme + labelZh: 公开 URL 协议 + label: + en: Public URL Scheme + zh: 公开 URL 协议 + zh-Hant: 公開 URL 協定 + ja: 公開 URL スキーム + ko: 공개 URL 스킴 + ru: Схема публичного URL + ms: Skema URL awam + pt-br: Esquema da URL publica + required: true + type: select + values: + - label: HTTP + value: http + - label: HTTPS + value: https + - default: asciinema.local + edit: true + envKey: ASCIINEMA_URL_HOST + labelEn: Public URL Host + labelZh: 公开 URL 主机名 + label: + en: Public URL Host + zh: 公开 URL 主机名 + zh-Hant: 公開 URL 主機名稱 + ja: 公開 URL ホスト + ko: 공개 URL 호스트 + ru: Хост публичного URL + ms: Hos URL awam + pt-br: Host da URL publica + required: true + type: text + - default: 4000 + edit: true + envKey: ASCIINEMA_URL_PORT + labelEn: Public URL Port + labelZh: 公开 URL 端口 + label: + en: Public URL Port + zh: 公开 URL 端口 + zh-Hant: 公開 URL 連接埠 + ja: 公開 URL ポート + ko: 공개 URL 포트 + ru: Порт публичного URL + ms: Port URL awam + pt-br: Porta da URL publica + required: true + rule: paramPort + type: number + - default: generate + edit: true + envKey: ASCIINEMA_SECRET_KEY_BASE + labelEn: Session Secret + labelZh: 会话密钥 + label: + en: Session Secret + zh: 会话密钥 + zh-Hant: 工作階段密鑰 + ja: セッションシークレット + ko: 세션 비밀 키 + ru: Секрет сеанса + ms: Rahsia sesi + pt-br: Segredo de sessao + required: true + type: password + - default: generate + edit: true + envKey: ASCIINEMA_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: "false" + edit: true + envKey: ASCIINEMA_SIGN_UP_DISABLED + labelEn: Disable Public Signup + labelZh: 禁用公开注册 + label: + en: Disable Public Signup + zh: 禁用公开注册 + zh-Hant: 停用公開註冊 + ja: 公開登録を無効化 + ko: 공개 가입 비활성화 + ru: Отключить публичную регистрацию + ms: Lumpuhkan pendaftaran awam + pt-br: Desativar cadastro publico + required: true + type: select + values: + - label: "False" + value: "false" + - label: "True" + value: "true" + - default: "true" + edit: true + envKey: ASCIINEMA_UPLOAD_AUTH_REQUIRED + labelEn: Require Upload Authentication + labelZh: 要求上传认证 + label: + en: Require Upload Authentication + zh: 要求上传认证 + zh-Hant: 要求上傳驗證 + ja: アップロード認証を必須化 + ko: 업로드 인증 필요 + ru: Требовать аутентификацию загрузки + ms: Wajibkan pengesahan muat naik + pt-br: Exigir autenticacao para envio + required: true + type: select + values: + - label: "True" + value: "true" + - label: "False" + value: "false" + - default: UTC + edit: true + envKey: ASCIINEMA_TIMEZONE + labelEn: Timezone + labelZh: 时区 + label: + en: Timezone + zh: 时区 + zh-Hant: 時區 + ja: タイムゾーン + ko: 시간대 + ru: Часовой пояс + ms: Zon waktu + pt-br: Fuso horario + required: true + type: text + - 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/asciinema-server/20260626-postgres17-digest-b0f9560/docker-compose.yml b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/docker-compose.yml new file mode 100644 index 000000000..2e9bcd1ee --- /dev/null +++ b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/docker-compose.yml @@ -0,0 +1,82 @@ +services: + asciinema-server: + image: "ghcr.io/asciinema/asciinema-server:20260626@sha256:0474b2186be46b23f5e16dbd4319727dbb68f27a303705fbe632821bf524b313" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + depends_on: + asciinema-postgres: + condition: service_healthy + user: "1000:0" + networks: + - 1panel-network + - asciinema-internal + ports: + - "${PANEL_APP_BIND_ADDRESS}:${PANEL_APP_PORT_HTTP}:4000" + environment: + - DATABASE_URL=postgresql://asciinema:${ASCIINEMA_POSTGRES_PASSWORD}@asciinema-postgres/asciinema + - SECRET_KEY_BASE=${ASCIINEMA_SECRET_KEY_BASE} + - URL_SCHEME=${ASCIINEMA_URL_SCHEME} + - URL_HOST=${ASCIINEMA_URL_HOST} + - URL_PORT=${ASCIINEMA_URL_PORT} + - TZ=${ASCIINEMA_TIMEZONE} + - BIND_ALL=1 + - ADMIN_BIND_ALL=0 + - ADMIN_PANEL_ON_MAIN_ENDPOINT=1 + - SIGN_UP_DISABLED=${ASCIINEMA_SIGN_UP_DISABLED} + - UPLOAD_AUTH_REQUIRED=${ASCIINEMA_UPLOAD_AUTH_REQUIRED} + - DATA_DIR=/var/lib/asciinema + - CACHE_PATH=/var/cache/asciinema + read_only: true + tmpfs: + - /tmp:uid=1000,gid=0,mode=1770 + - /var/cache/asciinema:uid=1000,gid=0,mode=0770 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + volumes: + - "${APP_DATA_DIR}/asciinema:/var/lib/asciinema" + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:4000/ || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 60s + labels: + createdBy: "Apps" + + asciinema-postgres: + image: "postgres:17-alpine@sha256:b0f9560a2de083e2cc7382e75f808c7381a32852a7ec49117deedb300e552b24" + container_name: ${CONTAINER_NAME}-postgres + restart: unless-stopped + user: "70:70" + networks: + - asciinema-internal + environment: + - POSTGRES_USER=asciinema + - POSTGRES_PASSWORD=${ASCIINEMA_POSTGRES_PASSWORD} + - POSTGRES_DB=asciinema + read_only: true + tmpfs: + - /run/postgresql:uid=70,gid=70,mode=0770 + - /tmp:uid=70,gid=70,mode=1770 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + volumes: + - "${APP_DATA_DIR}/postgres:/var/lib/postgresql/data" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U asciinema -d asciinema"] + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true + asciinema-internal: + internal: true diff --git a/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/init.sh b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/init.sh new file mode 100755 index 000000000..cc88da466 --- /dev/null +++ b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/init.sh @@ -0,0 +1,144 @@ +#!/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" +} + +set_env_value() { + local key="$1" + local value="$2" + local temp_file + temp_file="$(mktemp "${ROOT_DIR}/.asciinema-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_port() { + local key="$1" + local value="$2" + [[ "$value" =~ ^[0-9]+$ ]] || fail "$key must be an integer" + ((10#$value >= 1 && 10#$value <= 65535)) || fail "$key must be between 1 and 65535" +} + +validate_boolean() { + local key="$1" + local value="$2" + [[ "$value" == "true" || "$value" == "false" ]] || fail "$key must be true or false" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + +bind_address="$(read_env_value PANEL_APP_BIND_ADDRESS)" +[[ "$bind_address" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || fail "PANEL_APP_BIND_ADDRESS must be an IPv4 address" +IFS=. read -r octet1 octet2 octet3 octet4 <<< "$bind_address" +for octet in "$octet1" "$octet2" "$octet3" "$octet4"; do + ((10#$octet <= 255)) || fail "PANEL_APP_BIND_ADDRESS contains an invalid IPv4 octet" +done + +validate_port PANEL_APP_PORT_HTTP "$(read_env_value PANEL_APP_PORT_HTTP)" +validate_port ASCIINEMA_URL_PORT "$(read_env_value ASCIINEMA_URL_PORT)" + +url_scheme="$(read_env_value ASCIINEMA_URL_SCHEME)" +[[ "$url_scheme" == "http" || "$url_scheme" == "https" ]] || fail "ASCIINEMA_URL_SCHEME must be http or https" + +url_host="$(read_env_value ASCIINEMA_URL_HOST)" +[[ ${#url_host} -ge 1 && ${#url_host} -le 253 ]] || fail "ASCIINEMA_URL_HOST has an invalid length" +[[ "$url_host" != *..* ]] || fail "ASCIINEMA_URL_HOST contains an empty label" +IFS=. read -ra host_labels <<< "$url_host" +for host_label in "${host_labels[@]}"; do + [[ ${#host_label} -le 63 ]] || fail "ASCIINEMA_URL_HOST contains an oversized label" + [[ "$host_label" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$ ]] || fail "ASCIINEMA_URL_HOST must be a hostname or IPv4 address" +done + +validate_boolean ASCIINEMA_SIGN_UP_DISABLED "$(read_env_value ASCIINEMA_SIGN_UP_DISABLED)" +validate_boolean ASCIINEMA_UPLOAD_AUTH_REQUIRED "$(read_env_value ASCIINEMA_UPLOAD_AUTH_REQUIRED)" + +timezone="$(read_env_value ASCIINEMA_TIMEZONE)" +[[ "$timezone" =~ ^[A-Za-z0-9_+/-]{1,64}$ && "$timezone" != *..* ]] || fail "ASCIINEMA_TIMEZONE contains unsupported characters" + +secret="$(read_env_value ASCIINEMA_SECRET_KEY_BASE)" +if [[ -z "$secret" || "$secret" == "generate" ]]; then + command -v base64 >/dev/null 2>&1 || fail "base64 is required to generate ASCIINEMA_SECRET_KEY_BASE" + random_material="$(head -c 96 /dev/urandom | base64 | tr -dc A-Za-z0-9)" + secret="${random_material:0:64}" +fi +[[ "$secret" =~ ^[A-Za-z0-9]{64,128}$ ]] || fail "ASCIINEMA_SECRET_KEY_BASE must contain 64 to 128 alphanumeric characters" + +postgres_password="$(read_env_value ASCIINEMA_POSTGRES_PASSWORD)" +if [[ -z "$postgres_password" || "$postgres_password" == "generate" ]]; then + command -v base64 >/dev/null 2>&1 || fail "base64 is required to generate ASCIINEMA_POSTGRES_PASSWORD" + random_material="$(head -c 48 /dev/urandom | base64 | tr -dc A-Za-z0-9)" + postgres_password="${random_material:0:32}" +fi +[[ "$postgres_password" =~ ^[A-Za-z0-9]{16,128}$ ]] || fail "ASCIINEMA_POSTGRES_PASSWORD must contain 16 to 128 alphanumeric characters" + +data_dir_raw="$(read_env_value APP_DATA_DIR)" +[[ -n "$data_dir_raw" ]] || data_dir_raw="./data" +case "$data_dir_raw" in + /*) fail "APP_DATA_DIR must be relative to the application version directory" ;; + *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'") fail "APP_DATA_DIR contains unsupported characters" ;; +esac +data_dir="$(realpath -m -- "${ROOT_DIR}/${data_dir_raw#./}")" +case "$data_dir" in + "${ROOT_DIR}"/*) ;; + *) fail "APP_DATA_DIR must stay inside the application version directory" ;; +esac +[[ ! -L "$data_dir" ]] || fail "APP_DATA_DIR must not be a symbolic link" + +install -d -m 0750 -- "$data_dir" +resolved_data_dir="$(realpath -e -- "$data_dir")" +case "$resolved_data_dir" in + "${ROOT_DIR}"/*) ;; + *) fail "APP_DATA_DIR resolves outside the application version directory" ;; +esac +[[ "$(id -u)" -eq 0 ]] || fail "asciinema server init must run as root to prepare data ownership" + +for child_name in asciinema postgres; do + [[ ! -L "$data_dir/$child_name" ]] || fail "APP_DATA_DIR/$child_name must not be a symbolic link" +done +install -d -m 0770 -- "$data_dir/asciinema" +install -d -m 0700 -- "$data_dir/postgres" +for child_name in asciinema postgres; do + resolved_child="$(realpath -e -- "$data_dir/$child_name")" + case "$resolved_child" in + "${resolved_data_dir}"/*) ;; + *) fail "APP_DATA_DIR/$child_name resolves outside APP_DATA_DIR" ;; + esac +done +chown -R --no-dereference 1000:0 "$data_dir/asciinema" +chown -R --no-dereference 70:70 "$data_dir/postgres" +chmod 0770 "$data_dir/asciinema" +chmod 0700 "$data_dir/postgres" + +set_env_value ASCIINEMA_SECRET_KEY_BASE "$secret" +set_env_value ASCIINEMA_POSTGRES_PASSWORD "$postgres_password" +chmod 600 "$ENV_FILE" diff --git a/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/uninstall.sh b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/uninstall.sh new file mode 100755 index 000000000..2a53d75f3 --- /dev/null +++ b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/uninstall.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +exit 0 diff --git a/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/upgrade.sh b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/upgrade.sh new file mode 100755 index 000000000..cd089ba98 --- /dev/null +++ b/apps/asciinema-server/20260626-postgres17-digest-b0f9560/scripts/upgrade.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)/init.sh"