Sposta storage video su disco dedicato e completa ops/SMTP in produzione.
Recordings e Garage usano bind mount su /media/videos; aggiunge ntfy self-hosted, notifiche critical+warning, SMTP Aruba SSL/465 e fix cron replay. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,7 +13,7 @@ module Recordings
|
||||
return @recording unless @recording.expires_at <= WINDOW_DAYS.days.from_now
|
||||
|
||||
recipients.each do |user|
|
||||
Recordings::ReplayMailer.replay_expiring_soon(recording: @recording, recipient: user).deliver_now
|
||||
deliver_expiring_soon(user)
|
||||
end
|
||||
|
||||
@recording.update!(expiry_warning_sent_at: Time.current)
|
||||
@@ -22,6 +22,12 @@ module Recordings
|
||||
|
||||
private
|
||||
|
||||
def deliver_expiring_soon(user)
|
||||
Recordings::ReplayMailer.replay_expiring_soon(recording: @recording, recipient: user).deliver_now
|
||||
rescue EOFError => e
|
||||
Rails.logger.warn("[Recordings::NotifyExpiring] SMTP EOF on close for #{user.email}: #{e.message}")
|
||||
end
|
||||
|
||||
def recipients
|
||||
club = @recording.team.club
|
||||
owner = club.owner
|
||||
|
||||
@@ -9,7 +9,7 @@ module Recordings
|
||||
return @recording unless @recording.ready?
|
||||
|
||||
recipients.each do |user|
|
||||
Recordings::ReplayMailer.replay_ready(recording: @recording, recipient: user).deliver_now
|
||||
deliver_replay_ready(user)
|
||||
end
|
||||
|
||||
@recording.update!(ready_notified_at: Time.current)
|
||||
@@ -18,6 +18,13 @@ module Recordings
|
||||
|
||||
private
|
||||
|
||||
def deliver_replay_ready(user)
|
||||
Recordings::ReplayMailer.replay_ready(recording: @recording, recipient: user).deliver_now
|
||||
rescue EOFError => e
|
||||
# Aruba SMTP (465/SSL) chiude la socket prima del QUIT: la mail è già partita.
|
||||
Rails.logger.warn("[Recordings::NotifyReady] SMTP EOF on close for #{user.email}: #{e.message}")
|
||||
end
|
||||
|
||||
def recipients
|
||||
club = @recording.team.club
|
||||
owner = club.owner
|
||||
|
||||
@@ -54,13 +54,18 @@ Rails.application.configure do
|
||||
|
||||
if ENV["SMTP_ADDRESS"].present?
|
||||
config.action_mailer.delivery_method = :smtp
|
||||
smtp_port = ENV.fetch("SMTP_PORT", 587).to_i
|
||||
smtp_ssl = ENV.fetch("SMTP_SSL", smtp_port == 465 ? "true" : "false") == "true"
|
||||
config.action_mailer.smtp_settings = {
|
||||
address: ENV["SMTP_ADDRESS"],
|
||||
port: ENV.fetch("SMTP_PORT", 587).to_i,
|
||||
port: smtp_port,
|
||||
user_name: ENV["SMTP_USERNAME"],
|
||||
password: ENV["SMTP_PASSWORD"],
|
||||
authentication: ENV.fetch("SMTP_AUTH", "plain"),
|
||||
enable_starttls_auto: ENV.fetch("SMTP_STARTTLS", "true") == "true"
|
||||
ssl: smtp_ssl,
|
||||
enable_starttls_auto: !smtp_ssl && ENV.fetch("SMTP_STARTTLS", "true") == "true",
|
||||
open_timeout: ENV.fetch("SMTP_OPEN_TIMEOUT", "15").to_i,
|
||||
read_timeout: ENV.fetch("SMTP_READ_TIMEOUT", "60").to_i
|
||||
}
|
||||
end
|
||||
config.i18n.fallbacks = true
|
||||
|
||||
@@ -90,7 +90,7 @@ module MatchLiveTv
|
||||
end
|
||||
|
||||
def mail_from
|
||||
ENV.fetch("MAILER_FROM", "Match Live TV <noreply@matchlive.it>")
|
||||
ENV.fetch("MAILER_FROM", "Match Live TV <noreply@matchlivetv.it>")
|
||||
end
|
||||
|
||||
def password_reset_expiry_hours
|
||||
|
||||
@@ -14,25 +14,63 @@ Sistema integrato per health check continui, analisi log, dashboard incidenti e
|
||||
| `/admin/ops` | Dashboard incidenti |
|
||||
| `GET /up/deep` | Health check esteso (JSON) |
|
||||
|
||||
## Setup notifiche ntfy (telefono)
|
||||
## Setup notifiche ntfy self-hosted (telefono)
|
||||
|
||||
1. Installa l'app [ntfy](https://ntfy.sh) su Android/iOS
|
||||
2. Scegli un topic segreto (es. `matchlivetv-ops-eminux-a8f3b2`)
|
||||
3. Iscriviti al topic nell'app
|
||||
4. Sul server, in `infra/.env`:
|
||||
Le push **ops** (disco pieno, Sidekiq, servizi giù) non passano da Firebase: Rails invia un POST al server **ntfy** incluso in `docker-compose.prod.yml`.
|
||||
|
||||
### 1. Server (Docker)
|
||||
|
||||
```bash
|
||||
OPS_NTFY_URL=https://ntfy.sh/matchlivetv-ops-eminux-a8f3b2
|
||||
OPS_NOTIFY_SEVERITIES=critical
|
||||
cd /opt/matchlivetv/infra
|
||||
docker compose -f docker-compose.prod.yml --env-file .env up -d ntfy
|
||||
```
|
||||
|
||||
Servizio in ascolto su **LAN** `http://192.168.1.146:8090` (non esporre 8090 su Internet).
|
||||
|
||||
### 2. HTTPS (Nginx Proxy Manager)
|
||||
|
||||
Crea un **Proxy Host** (consigliato sottodominio dedicato):
|
||||
|
||||
| Campo | Valore |
|
||||
|-------|--------|
|
||||
| Domain | `ntfy.matchlivetv.it` |
|
||||
| Forward | `http://192.168.1.146:8090` |
|
||||
| WebSockets | **ON** |
|
||||
| SSL | Let's Encrypt |
|
||||
|
||||
In `infra/.env`:
|
||||
|
||||
```bash
|
||||
NTFY_PUBLIC_URL=https://ntfy.matchlivetv.it
|
||||
OPS_NTFY_TOPIC=matchlivetv-ops-eminux-a8f3b2 # topic segreto, a piacere
|
||||
OPS_NTFY_URL=http://ntfy:80/matchlivetv-ops-eminux-a8f3b2
|
||||
OPS_NOTIFY_SEVERITIES=critical,warning
|
||||
OPS_NOTIFY_COOLDOWN_MINUTES=30
|
||||
```
|
||||
|
||||
5. Test:
|
||||
`OPS_NTFY_URL` usa il nome Docker `ntfy` (rete interna). Il telefono usa invece `NTFY_PUBLIC_URL`.
|
||||
|
||||
### 3. App sul telefono
|
||||
|
||||
1. Installa [ntfy](https://ntfy.sh) (Android/iOS)
|
||||
2. **Impostazioni → Default server** (o «Usa altro server») → `https://ntfy.matchlivetv.it`
|
||||
In casa, in alternativa: `http://192.168.1.146:8090`
|
||||
3. **+** → iscriviti al topic `OPS_NTFY_TOPIC` (es. `matchlivetv-ops-eminux-a8f3b2`)
|
||||
4. Abilita notifiche per quel topic
|
||||
|
||||
### 4. Test
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml exec -T rails bundle exec rails ops:test_notify
|
||||
```
|
||||
|
||||
Dovresti ricevere una push «Test notifica ops» sul telefono.
|
||||
|
||||
### Sicurezza
|
||||
|
||||
- Il topic è la «password»: usa un nome lungo e casuale, non pubblicarlo
|
||||
- Opzionale: abilita auth su ntfy (`OPS_NTFY_TOKEN` + utente in `user.db`) per topic protetti
|
||||
|
||||
## Variabili ambiente
|
||||
|
||||
Vedi [`infra/.env.production.example`](../infra/.env.production.example).
|
||||
|
||||
@@ -12,7 +12,7 @@ YOUTUBE_MOCK_STREAM_KEY=mock-stream-key
|
||||
PRIVACY_CONTROLLER_NAME=Emiliano Frascaro
|
||||
PRIVACY_CONTROLLER_ADDRESS=Via Guido De Ruggiero, 89 - 20142 - Milano (MI)
|
||||
PRIVACY_CONTACT_EMAIL=privacy@matchlive.it
|
||||
MAILER_FROM=Match Live TV <noreply@matchlive.it>
|
||||
MAILER_FROM=Match Live TV <noreply@matchlivetv.it>
|
||||
PASSWORD_RESET_EXPIRY_HOURS=2
|
||||
|
||||
# Google Analytics 4 (opzionale — caricato solo dopo consenso cookie «Accetta tutti»)
|
||||
|
||||
@@ -42,13 +42,15 @@ PRIVACY_CONTACT_EMAIL=privacy@matchlive.it
|
||||
PRIVACY_CONTROLLER_VAT=
|
||||
|
||||
# Email transazionali (reset password, inviti)
|
||||
MAILER_FROM=Match Live TV <noreply@matchlive.it>
|
||||
SMTP_ADDRESS=
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=
|
||||
MAILER_FROM=Match Live TV <noreply@matchlivetv.it>
|
||||
# Aruba: smtps.aruba.it porta 465 con SSL (non STARTTLS su 587)
|
||||
SMTP_ADDRESS=smtps.aruba.it
|
||||
SMTP_PORT=465
|
||||
SMTP_USERNAME=noreply@matchlivetv.it
|
||||
SMTP_PASSWORD=
|
||||
SMTP_AUTH=plain
|
||||
SMTP_STARTTLS=true
|
||||
SMTP_SSL=true
|
||||
SMTP_STARTTLS=false
|
||||
PASSWORD_RESET_EXPIRY_HOURS=2
|
||||
|
||||
# Disco video dedicato (recordings MediaMTX + dati Garage). Montare es. 800GB su /media/videos
|
||||
@@ -63,12 +65,16 @@ REPLAY_STORAGE_ACCESS_KEY_ID=
|
||||
REPLAY_STORAGE_SECRET_ACCESS_KEY=
|
||||
REPLAY_STORAGE_FORCE_PATH_STYLE=true
|
||||
|
||||
# Ops monitoring — notifiche push (ntfy) e health check
|
||||
# App ntfy: https://ntfy.sh — iscriviti al topic e imposta OPS_NTFY_URL
|
||||
OPS_NTFY_URL=https://ntfy.sh/matchlivetv-ops-YOUR_SECRET_TOPIC
|
||||
# Ops monitoring — notifiche push (ntfy self-hosted) e health check
|
||||
# 1) Avvia servizio: docker compose up -d ntfy
|
||||
# 2) NPM: proxy HTTPS → http://192.168.1.146:8090 (es. ntfy.matchlivetv.it, WebSockets ON)
|
||||
# 3) App ntfy sul telefono: server custom = NTFY_PUBLIC_URL, iscriviti a OPS_NTFY_TOPIC
|
||||
NTFY_PUBLIC_URL=https://ntfy.matchlivetv.it
|
||||
# Topic segreto: stesso nome in app ntfy (subscribe) e in OPS_NTFY_URL
|
||||
OPS_NTFY_URL=http://ntfy:80/matchlivetv-ops-YOUR_SECRET_SUFFIX
|
||||
OPS_NTFY_TOKEN=
|
||||
OPS_ALERT_EMAIL=
|
||||
OPS_NOTIFY_SEVERITIES=critical
|
||||
OPS_NOTIFY_SEVERITIES=critical,warning
|
||||
OPS_NOTIFY_COOLDOWN_MINUTES=30
|
||||
OPS_HEALTH_INTERVAL_SECS=180
|
||||
OPS_HEALTH_TOKEN=
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
volumes:
|
||||
- ./mediamtx.yml:/mediamtx.yml:ro
|
||||
- ./slates:/slates:ro
|
||||
- recordings:/recordings
|
||||
- ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/recordings:/recordings
|
||||
command: /mediamtx.yml
|
||||
# Immagine distroless: niente shell/wget/curl; API :9997 non esposta sull'host
|
||||
healthcheck:
|
||||
@@ -77,13 +77,14 @@ services:
|
||||
PRIVACY_CONTROLLER_NAME: ${PRIVACY_CONTROLLER_NAME:-Emiliano Frascaro}
|
||||
PRIVACY_CONTROLLER_ADDRESS: ${PRIVACY_CONTROLLER_ADDRESS:-Via Guido De Ruggiero, 89 - 20142 - Milano (MI)}
|
||||
PRIVACY_CONTROLLER_VAT: ${PRIVACY_CONTROLLER_VAT:-}
|
||||
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlive.it>}
|
||||
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlivetv.it>}
|
||||
SMTP_ADDRESS: ${SMTP_ADDRESS:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_PORT: ${SMTP_PORT:-465}
|
||||
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_AUTH: ${SMTP_AUTH:-plain}
|
||||
SMTP_STARTTLS: ${SMTP_STARTTLS:-true}
|
||||
SMTP_SSL: ${SMTP_SSL:-true}
|
||||
SMTP_STARTTLS: ${SMTP_STARTTLS:-false}
|
||||
RAILS_LOG_TO_STDOUT: "true"
|
||||
RAILS_LOG_LEVEL: ${RAILS_LOG_LEVEL:-info}
|
||||
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-}
|
||||
@@ -102,6 +103,20 @@ services:
|
||||
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
|
||||
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
|
||||
GOOGLE_ANALYTICS_MEASUREMENT_ID: ${GOOGLE_ANALYTICS_MEASUREMENT_ID:-}
|
||||
OPS_NTFY_URL: ${OPS_NTFY_URL:-}
|
||||
OPS_NTFY_TOKEN: ${OPS_NTFY_TOKEN:-}
|
||||
OPS_ALERT_EMAIL: ${OPS_ALERT_EMAIL:-}
|
||||
OPS_NOTIFY_SEVERITIES: ${OPS_NOTIFY_SEVERITIES:-critical,warning}
|
||||
OPS_NOTIFY_COOLDOWN_MINUTES: ${OPS_NOTIFY_COOLDOWN_MINUTES:-30}
|
||||
OPS_HEALTH_INTERVAL_SECS: ${OPS_HEALTH_INTERVAL_SECS:-180}
|
||||
OPS_HEALTH_TOKEN: ${OPS_HEALTH_TOKEN:-}
|
||||
OPS_DISK_WARN_PERCENT: ${OPS_DISK_WARN_PERCENT:-80}
|
||||
OPS_DISK_CRIT_PERCENT: ${OPS_DISK_CRIT_PERCENT:-90}
|
||||
OPS_RECORDINGS_WARN_GB: ${OPS_RECORDINGS_WARN_GB:-20}
|
||||
OPS_RECORDINGS_CRIT_GB: ${OPS_RECORDINGS_CRIT_GB:-50}
|
||||
OPS_SIDEKIQ_STALE_SECS: ${OPS_SIDEKIQ_STALE_SECS:-300}
|
||||
OPS_LOG_SUBSCRIBER: ${OPS_LOG_SUBSCRIBER:-false}
|
||||
SENTRY_DSN: ${SENTRY_DSN:-}
|
||||
ports:
|
||||
- "3000:3000" # Solo LAN — NPM proxy HTTPS
|
||||
depends_on:
|
||||
@@ -114,7 +129,7 @@ services:
|
||||
garage:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- recordings:/recordings
|
||||
- ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/recordings:/recordings
|
||||
- rails_storage:/rails/storage
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:3000/up"]
|
||||
@@ -148,13 +163,14 @@ services:
|
||||
MEDIAMTX_INTERNAL_RTMP_URL: rtmp://mediamtx:1935
|
||||
HLS_PUBLIC_URL: ${HLS_PUBLIC_URL:-http://localhost:8888}
|
||||
APP_PUBLIC_URL: ${APP_PUBLIC_URL:-http://localhost:3000}
|
||||
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlive.it>}
|
||||
MAILER_FROM: ${MAILER_FROM:-Match Live TV <noreply@matchlivetv.it>}
|
||||
SMTP_ADDRESS: ${SMTP_ADDRESS:-}
|
||||
SMTP_PORT: ${SMTP_PORT:-587}
|
||||
SMTP_PORT: ${SMTP_PORT:-465}
|
||||
SMTP_USERNAME: ${SMTP_USERNAME:-}
|
||||
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
|
||||
SMTP_AUTH: ${SMTP_AUTH:-plain}
|
||||
SMTP_STARTTLS: ${SMTP_STARTTLS:-true}
|
||||
SMTP_SSL: ${SMTP_SSL:-true}
|
||||
SMTP_STARTTLS: ${SMTP_STARTTLS:-false}
|
||||
RAILS_LOG_LEVEL: ${RAILS_LOG_LEVEL:-info}
|
||||
RECORDINGS_PATH: /recordings
|
||||
REPLAY_STORAGE_ENDPOINT: ${REPLAY_STORAGE_ENDPOINT:-}
|
||||
@@ -163,13 +179,29 @@ services:
|
||||
REPLAY_STORAGE_ACCESS_KEY_ID: ${REPLAY_STORAGE_ACCESS_KEY_ID:-}
|
||||
REPLAY_STORAGE_SECRET_ACCESS_KEY: ${REPLAY_STORAGE_SECRET_ACCESS_KEY:-}
|
||||
REPLAY_STORAGE_FORCE_PATH_STYLE: ${REPLAY_STORAGE_FORCE_PATH_STYLE:-true}
|
||||
OPS_NTFY_URL: ${OPS_NTFY_URL:-}
|
||||
OPS_NTFY_TOKEN: ${OPS_NTFY_TOKEN:-}
|
||||
OPS_ALERT_EMAIL: ${OPS_ALERT_EMAIL:-}
|
||||
OPS_NOTIFY_SEVERITIES: ${OPS_NOTIFY_SEVERITIES:-critical,warning}
|
||||
OPS_NOTIFY_COOLDOWN_MINUTES: ${OPS_NOTIFY_COOLDOWN_MINUTES:-30}
|
||||
OPS_HEALTH_INTERVAL_SECS: ${OPS_HEALTH_INTERVAL_SECS:-180}
|
||||
OPS_HEALTH_TOKEN: ${OPS_HEALTH_TOKEN:-}
|
||||
OPS_DISK_WARN_PERCENT: ${OPS_DISK_WARN_PERCENT:-80}
|
||||
OPS_DISK_CRIT_PERCENT: ${OPS_DISK_CRIT_PERCENT:-90}
|
||||
OPS_RECORDINGS_WARN_GB: ${OPS_RECORDINGS_WARN_GB:-20}
|
||||
OPS_RECORDINGS_CRIT_GB: ${OPS_RECORDINGS_CRIT_GB:-50}
|
||||
OPS_SIDEKIQ_STALE_SECS: ${OPS_SIDEKIQ_STALE_SECS:-300}
|
||||
OPS_LOG_SUBSCRIBER: ${OPS_LOG_SUBSCRIBER:-false}
|
||||
SENTRY_DSN: ${SENTRY_DSN:-}
|
||||
depends_on:
|
||||
rails:
|
||||
condition: service_healthy
|
||||
garage:
|
||||
condition: service_started
|
||||
ntfy:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- recordings:/recordings
|
||||
- ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/recordings:/recordings
|
||||
- rails_storage:/rails/storage
|
||||
|
||||
garage:
|
||||
@@ -177,15 +209,31 @@ services:
|
||||
restart: unless-stopped
|
||||
# API S3 solo rete Docker (rails/sidekiq → http://garage:3900). Non pubblicare 3900 su WAN.
|
||||
volumes:
|
||||
- garage_meta:/var/lib/garage/meta
|
||||
- garage_data:/var/lib/garage/data
|
||||
- ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/garage/meta:/var/lib/garage/meta
|
||||
- ${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}/garage/data:/var/lib/garage/data
|
||||
- ./garage/garage.prod.toml:/etc/garage.toml:ro
|
||||
command: ["/garage", "-c", "/etc/garage.toml", "server"]
|
||||
|
||||
ntfy:
|
||||
image: binwiederhier/ntfy:latest
|
||||
restart: unless-stopped
|
||||
# Solo LAN :8090 — esporre su HTTPS via NPM (es. ntfy.matchlivetv.it). Non aprire 8090 su WAN.
|
||||
command: serve
|
||||
environment:
|
||||
NTFY_BASE_URL: ${NTFY_PUBLIC_URL:-http://127.0.0.1:8090}
|
||||
volumes:
|
||||
- ntfy_data:/var/lib/ntfy
|
||||
- ./ntfy/server.yml:/etc/ntfy/server.yml:ro
|
||||
ports:
|
||||
- "8090:80"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:80/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
recordings:
|
||||
rails_storage:
|
||||
garage_meta:
|
||||
garage_data:
|
||||
ntfy_data:
|
||||
|
||||
9
infra/ntfy/server.yml
Normal file
9
infra/ntfy/server.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
# ntfy self-hosted — Match Live TV (ops alerts)
|
||||
# base-url impostato via NTFY_BASE_URL in docker-compose (NTFY_PUBLIC_URL in .env)
|
||||
# Docs: https://docs.ntfy.sh/config/
|
||||
|
||||
behind-proxy: true
|
||||
cache-file: "/var/lib/ntfy/cache.db"
|
||||
attachment-cache-dir: "/var/lib/ntfy/attachments"
|
||||
message-size-limit: "4k"
|
||||
visitor-subscription-limit: 10
|
||||
@@ -6,6 +6,12 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DEST="${ROOT}/garage/garage.prod.toml"
|
||||
TEMPLATE="${ROOT}/garage/garage.prod.toml.example"
|
||||
|
||||
if [ -d "$DEST" ]; then
|
||||
echo "ERRORE: $DEST è una directory (bind mount Docker senza file sorgente)."
|
||||
echo "Rimuovi la directory e rilancia questo script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$DEST" ]; then
|
||||
echo "Config Garage produzione già presente: $DEST"
|
||||
exit 0
|
||||
|
||||
@@ -8,9 +8,14 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
|
||||
ENV_FILE="${ENV_FILE:-${ROOT}/.env}"
|
||||
GARAGE_CONTAINER="${GARAGE_CONTAINER:-}"
|
||||
VIDEOS_ROOT="${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}"
|
||||
|
||||
if [ -z "$CAPACITY" ]; then
|
||||
avail="$(df -BG "${ROOT}" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G' || echo 0)"
|
||||
capacity_df="${VIDEOS_ROOT}"
|
||||
if [ ! -d "$capacity_df" ]; then
|
||||
capacity_df="${ROOT}"
|
||||
fi
|
||||
avail="$(df -BG "${capacity_df}" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G' || echo 0)"
|
||||
if [ "$avail" -lt 1 ]; then
|
||||
avail="$(df -BG / 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G')"
|
||||
fi
|
||||
|
||||
@@ -14,14 +14,14 @@ mkdir -p "$LOG_DIR"
|
||||
|
||||
MARKER="# matchlivetv-replay-cron"
|
||||
CRON_BLOCK="${MARKER}
|
||||
30 * * * * ${RUNNER} recordings:cleanup_local >> ${LOG_FILE} 2>&1
|
||||
0 3 * * * ${RUNNER} recordings:purge_expired >> ${LOG_FILE} 2>&1
|
||||
0 8 * * * ${RUNNER} recordings:expiry_warnings >> ${LOG_FILE} 2>&1
|
||||
*/10 * * * * ${SCAN_LOGS} >> ${OPS_LOG_FILE} 2>&1
|
||||
30 * * * * mkdir -p ${LOG_DIR} && ${RUNNER} recordings:cleanup_local >> ${LOG_FILE} 2>&1
|
||||
0 3 * * * mkdir -p ${LOG_DIR} && ${RUNNER} recordings:purge_expired >> ${LOG_FILE} 2>&1
|
||||
0 8 * * * mkdir -p ${LOG_DIR} && ${RUNNER} recordings:expiry_warnings >> ${LOG_FILE} 2>&1
|
||||
*/10 * * * * mkdir -p ${LOG_DIR} && ${SCAN_LOGS} >> ${OPS_LOG_FILE} 2>&1
|
||||
"
|
||||
|
||||
existing="$(crontab -l 2>/dev/null || true)"
|
||||
filtered="$(echo "$existing" | grep -v "$MARKER" | grep -v 'run_rails_task\.sh' || true)"
|
||||
filtered="$(echo "$existing" | grep -v "$MARKER" | grep -v 'run_rails_task\.sh' | grep -v 'scan_ops_logs\.sh' || true)"
|
||||
if echo "$existing" | grep -q "$MARKER"; then
|
||||
echo "Crontab replay già presente, aggiorno..."
|
||||
fi
|
||||
|
||||
101
infra/scripts/migrate_videos_storage.sh
Executable file
101
infra/scripts/migrate_videos_storage.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migra recordings + Garage da volumi Docker al disco video (es. /media/videos/matchlivetv).
|
||||
# Uso: sudo bash scripts/migrate_videos_storage.sh
|
||||
# Richiede: docker compose prod fermo o in fase di stop; disco montato su MATCHLIVETV_VIDEOS_ROOT.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
VIDEOS_ROOT="${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}"
|
||||
ENV_FILE="${ENV_FILE:-${ROOT}/.env}"
|
||||
PROJECT="${COMPOSE_PROJECT_NAME:-infra}"
|
||||
|
||||
VIDEOS_MOUNT="$(dirname "$VIDEOS_ROOT")"
|
||||
if ! mountpoint -q "$VIDEOS_MOUNT" 2>/dev/null && [ ! -d "$VIDEOS_ROOT" ]; then
|
||||
echo "ERRORE: disco video non montato (atteso ${VIDEOS_MOUNT})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Destinazione: ${VIDEOS_ROOT}"
|
||||
df -h "$VIDEOS_ROOT" 2>/dev/null || df -h "$VIDEOS_MOUNT"
|
||||
|
||||
fs_mkdir() {
|
||||
if mkdir -p "$@" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
sudo mkdir -p "$@"
|
||||
}
|
||||
|
||||
fs_rsync() {
|
||||
if rsync -aH "$@" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
sudo rsync -aH "$@"
|
||||
}
|
||||
|
||||
fs_mkdir "${VIDEOS_ROOT}/recordings" "${VIDEOS_ROOT}/garage/meta" "${VIDEOS_ROOT}/garage/data"
|
||||
if [ ! -w "$VIDEOS_ROOT" ]; then
|
||||
sudo chown -R "$(id -un)":"$(id -gn)" "$VIDEOS_ROOT"
|
||||
fi
|
||||
|
||||
volume_path() {
|
||||
local mp
|
||||
mp="$(docker volume inspect "${PROJECT}_$1" --format '{{ .Mountpoint }}' 2>/dev/null || true)"
|
||||
[ -n "$mp" ] && [ -d "$mp" ] && echo "$mp" && return 0
|
||||
mp="$(sudo docker volume inspect "${PROJECT}_$1" --format '{{ .Mountpoint }}' 2>/dev/null || true)"
|
||||
[ -n "$mp" ] && echo "$mp"
|
||||
}
|
||||
|
||||
copy_volume() {
|
||||
local vol="$1" dest="$2"
|
||||
local src
|
||||
src="$(volume_path "$vol")"
|
||||
if [ -z "$src" ] || [ ! -d "$src" ]; then
|
||||
echo "Volume ${PROJECT}_${vol} assente, salto copia verso ${dest}"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$(sudo ls -A "$dest" 2>/dev/null || true)" ]; then
|
||||
echo "Già presente in ${dest}, salto copia da ${src}"
|
||||
return 0
|
||||
fi
|
||||
echo "Copia ${src} → ${dest}..."
|
||||
fs_rsync "${src}/" "${dest}/"
|
||||
}
|
||||
|
||||
echo "Arresto servizi che usano recordings/Garage..."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" stop mediamtx sidekiq rails garage
|
||||
|
||||
copy_volume recordings "${VIDEOS_ROOT}/recordings"
|
||||
copy_volume garage_meta "${VIDEOS_ROOT}/garage/meta"
|
||||
copy_volume garage_data "${VIDEOS_ROOT}/garage/data"
|
||||
|
||||
if ! grep -q '^MATCHLIVETV_VIDEOS_ROOT=' "$ENV_FILE" 2>/dev/null; then
|
||||
echo "MATCHLIVETV_VIDEOS_ROOT=${VIDEOS_ROOT}" >> "$ENV_FILE"
|
||||
echo "Aggiunto MATCHLIVETV_VIDEOS_ROOT a ${ENV_FILE}"
|
||||
fi
|
||||
|
||||
if [ -d "${ROOT}/garage/garage.prod.toml" ]; then
|
||||
echo "ERRORE: ${ROOT}/garage/garage.prod.toml è una directory (bind mount Docker corrotto)."
|
||||
echo "Rimuovi la directory e ripristina il file TOML prima di continuare."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Riavvio stack..."
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --force-recreate mediamtx garage rails sidekiq
|
||||
|
||||
echo "Ridimensiono capacità Garage sul disco video..."
|
||||
export MATCHLIVETV_VIDEOS_ROOT="$VIDEOS_ROOT"
|
||||
bash "${ROOT}/scripts/garage_set_capacity.sh"
|
||||
|
||||
echo "Verifica mount:"
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec -T rails \
|
||||
bundle exec rails runner \
|
||||
'puts({recordings: `du -sh /recordings 2>/dev/null`.strip, storage: Recordings::Storage.new.backend_name}.inspect)'
|
||||
|
||||
echo "Fatto. Volumi Docker obsoleti (rimuovi dopo verifica):"
|
||||
for v in recordings garage_data garage_meta; do
|
||||
docker volume inspect "${PROJECT}_${v}" --format '{{ .Name }} ({{ .Mountpoint }})' 2>/dev/null || true
|
||||
done
|
||||
echo " docker volume rm ${PROJECT}_recordings ${PROJECT}_garage_data ${PROJECT}_garage_meta"
|
||||
@@ -14,7 +14,10 @@ bash "${ROOT}/scripts/ensure_garage_prod_config.sh"
|
||||
echo "Avvio Garage..."
|
||||
docker compose -f docker-compose.prod.yml --env-file .env up -d garage
|
||||
|
||||
avail_gb="$(df -BG / 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G')"
|
||||
videos_root="${MATCHLIVETV_VIDEOS_ROOT:-/media/videos/matchlivetv}"
|
||||
capacity_df="${videos_root}"
|
||||
[ -d "$capacity_df" ] || capacity_df="/"
|
||||
avail_gb="$(df -BG "${capacity_df}" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G')"
|
||||
reserve="${GARAGE_RESERVE_GB:-20}"
|
||||
cap_gb=$((avail_gb - reserve))
|
||||
[ "$cap_gb" -lt 10 ] && cap_gb=10
|
||||
|
||||
Reference in New Issue
Block a user