ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m15s
ci / api / api (push) Successful in 1m31s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 47s
ci / ts / ts (packages + web) (push) Successful in 39s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 26s
- users, refresh_tokens and audit_log schema (migration 00002) - argon2id passwords with rehash on login, password policy, lockout with exponential back-off and a per-IP rate limit in Redis - Ed25519 JWT access tokens with kid-based key rotation; rotating refresh tokens with reuse detection (cookie for web, body for Android) - bootstrap admin on first start; must_change_credentials and must_change_password gate every other endpoint - /auth/login, /auth/refresh, /auth/logout(-all), /me, /me/password, /me/bootstrap-credentials, /me/sessions; server-info reports bootstrap_required - role policy from docs/auth-and-roles.md with table tests - problem+json errors with field errors; ru/en translations of the codes - install.sh and update.sh generate JWT_PRIVATE_KEY Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
4.1 KiB
Bash
Executable File
86 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Первичная подготовка сервера: каталоги данных, общий корень библиотеки с mount propagation, .env с секретами.
|
||
# Запуск: sudo ./install.sh (повторный запуск безопасен)
|
||
set -euo pipefail
|
||
cd "$(dirname "$0")"
|
||
|
||
# .env и каталоги создаются от root, но сам репозиторий должен остаться у пользователя, который делает git pull.
|
||
OWNER="${SUDO_USER:-root}"
|
||
|
||
DATA="${PIPU_DATA:-/srv/pipu}"
|
||
|
||
echo "→ каталоги в $DATA"
|
||
mkdir -p "$DATA"/{library,library-host,cache/transcode,cache/covers,postgres,meili,redis,models,updates,uploads,backups,caddy/data,caddy/config}
|
||
|
||
# Каталоги, куда пишут сервисы без root: api (distroless nonroot, uid 65532) и analyzer (uid 10001).
|
||
# Без рекурсии: файлы внутри создают сами сервисы, а повторный запуск не обходит весь кэш.
|
||
chown 65532:65532 "$DATA"/{cache,cache/transcode,cache/covers,updates,uploads}
|
||
chown 10001:10001 "$DATA/models"
|
||
|
||
# Папка с локальной музыкой: sudo LIBRARY_HOST=/mnt/music ./install.sh (или LIBRARY_HOST в .env)
|
||
LIBRARY_HOST="${LIBRARY_HOST:-$( [ -f .env ] && sed -n 's/^LIBRARY_HOST=//p' .env || true )}"
|
||
if [ -n "$LIBRARY_HOST" ]; then
|
||
if [ ! -d "$LIBRARY_HOST" ]; then
|
||
echo "LIBRARY_HOST=$LIBRARY_HOST: каталога нет — смонтируйте диск с музыкой (docs/install-guide.md, шаг 4)" >&2
|
||
exit 1
|
||
fi
|
||
findmnt -n --target "$LIBRARY_HOST" -o TARGET,SOURCE | sed 's/^/→ музыка: /'
|
||
fi
|
||
|
||
# storage-agent монтирует NFS внутрь $DATA/library; чтобы точки монтирования видели другие контейнеры,
|
||
# каталог должен быть отдельной точкой монтирования с shared propagation.
|
||
if ! findmnt -n "$DATA/library" >/dev/null 2>&1; then
|
||
echo "→ bind-mount $DATA/library (rshared)"
|
||
mount --bind "$DATA/library" "$DATA/library"
|
||
fi
|
||
mount --make-rshared "$DATA/library"
|
||
|
||
UNIT=/etc/systemd/system/pipu-library-mount.service
|
||
if [ -d /etc/systemd/system ] && [ ! -f "$UNIT" ]; then
|
||
echo "→ systemd-юнит $UNIT (повторяет bind-mount после перезагрузки)"
|
||
cat > "$UNIT" <<EOF
|
||
[Unit]
|
||
Description=Pipu Music library mount propagation
|
||
Before=docker.service
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/bin/sh -c 'findmnt -n "$DATA/library" || mount --bind "$DATA/library" "$DATA/library"; mount --make-rshared "$DATA/library"'
|
||
RemainAfterExit=yes
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
systemctl daemon-reload
|
||
systemctl enable pipu-library-mount.service
|
||
fi
|
||
|
||
secret() { head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 40; }
|
||
|
||
if [ ! -f .env ]; then
|
||
echo "→ .env из .env.example"
|
||
cp .env.example .env
|
||
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(secret)|; s|^MEILI_MASTER_KEY=.*|MEILI_MASTER_KEY=$(secret)|; s|^INTERNAL_TOKEN=.*|INTERNAL_TOKEN=$(secret)|; s|^PIPU_DATA=.*|PIPU_DATA=$DATA|; s|^LIBRARY_HOST=.*|LIBRARY_HOST=$LIBRARY_HOST|" .env
|
||
chmod 600 .env
|
||
chown "$OWNER" .env
|
||
echo " Заполните в .env: DOMAIN и ACME_EMAIL (или настройте свой прокси — docs/install-guide.md)"
|
||
fi
|
||
|
||
# Ключ подписи токенов (M1): создаётся один раз, в том числе в .env, сделанном до его появления.
|
||
if ! grep -q '^JWT_PRIVATE_KEY=.' .env; then
|
||
sed -i '/^JWT_PRIVATE_KEY=$/d' .env
|
||
echo "JWT_PRIVATE_KEY=$(head -c 32 /dev/urandom | base64)" >> .env
|
||
echo "→ в .env добавлен JWT_PRIVATE_KEY"
|
||
fi
|
||
|
||
# LIBRARY_HOST, переданный при повторном запуске, попадает и в уже существующий .env
|
||
if [ -n "$LIBRARY_HOST" ]; then
|
||
if grep -q '^LIBRARY_HOST=' .env; then
|
||
sed -i "s|^LIBRARY_HOST=.*|LIBRARY_HOST=$LIBRARY_HOST|" .env
|
||
else
|
||
echo "LIBRARY_HOST=$LIBRARY_HOST" >> .env
|
||
fi
|
||
fi
|
||
|
||
echo "Готово. Сборка и запуск последнего релиза: ./update.sh"
|