Aggiunge lo script di backup completo per il disaster recovery
Scarica schema e dati del database (public e auth) e i file dei bucket storage, così si può ripartire se si perdono Supabase o Vercel. I dati generati restano fuori da git perché contengono dati personali reali. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cdd433cb15
commit
61d4dacb3a
6 files changed
+218
-11
No files matched your search
+8
-1
@@ -40,4 +40,11 @@ dist-ssr
|
||||
# Optional
|
||||
.vercel
|
||||
# Supabase CLI local state
|
||||
supabase/.temp/
|
||||
supabase/.temp/
|
||||
|
||||
# Backup locali del database (contengono dati reali degli utenti):
|
||||
# ignora tutto il contenuto generato, ma tieni in git lo script e il README.
|
||||
backups/*
|
||||
!backups/backup.sh
|
||||
!backups/download-storage.mjs
|
||||
!backups/README.md
|
||||
@@ -0,0 +1,99 @@
|
||||
# Backup di disaster recovery
|
||||
|
||||
Script per scaricare su questo host un backup completo del progetto Supabase:
|
||||
schema DB, dati, utenti auth e file storage. Serve per poter ripartire in fretta
|
||||
se il progetto Supabase (o Vercel) venisse perso.
|
||||
|
||||
## Preparazione, passo per passo
|
||||
|
||||
Da fare una volta sola per host (poi il backup si rilancia semplicemente con
|
||||
`./backups/backup.sh`).
|
||||
|
||||
1. **Installa/aggiorna la CLI Supabase** (via `npx`, non serve installarla a parte):
|
||||
```bash
|
||||
npx supabase --version
|
||||
```
|
||||
2. **Accedi con il tuo account Supabase**:
|
||||
```bash
|
||||
npx supabase login
|
||||
```
|
||||
Si apre il browser per l'OAuth; il token resta salvato in locale.
|
||||
3. **Collega il repo al progetto Supabase** (il `project_id` è già in
|
||||
`supabase/config.toml`, quindi in genere basta):
|
||||
```bash
|
||||
npx supabase link --project-ref <project_id>
|
||||
```
|
||||
Verifica che sia andato a buon fine con `npx supabase projects list`
|
||||
(il progetto deve comparire con `"linked":true`).
|
||||
4. **Crea/controlla il file `.env`** nella root del repo (accanto a
|
||||
`package.json`, non dentro `backups/`) con almeno queste due chiavi:
|
||||
```
|
||||
VITE_SUPABASE_URL="https://<project_id>.supabase.co"
|
||||
SUPABASE_SERVICE_ROLE_KEY="<service role key>"
|
||||
```
|
||||
Le trovi su [supabase.com/dashboard](https://supabase.com/dashboard) → progetto →
|
||||
Project Settings → API. La **service role key** è quella con permessi pieni
|
||||
(bypassa le RLS): serve solo per scaricare i file dai bucket, non va mai
|
||||
esposta lato client né committata.
|
||||
5. **(Solo se userai `--encrypt`) Verifica che gpg sia installato**:
|
||||
```bash
|
||||
gpg --version
|
||||
```
|
||||
Su Debian/Ubuntu: `sudo apt install gnupg` se manca.
|
||||
|
||||
A questo punto sei pronto per lanciare il backup vero e proprio (vedi sotto).
|
||||
|
||||
## Uso
|
||||
|
||||
```bash
|
||||
./backups/backup.sh # crea backups/YYYYMMDD_HHMM/ con tutto dentro
|
||||
./backups/backup.sh --encrypt # come sopra, poi comprime e cifra in un unico
|
||||
# backups/YYYYMMDD_HHMM.tar.gz.gpg (chiede una
|
||||
# passphrase) e cancella la cartella in chiaro
|
||||
```
|
||||
|
||||
Ogni esecuzione crea una cartella nuova con timestamp: non sovrascrive backup
|
||||
precedenti. Non c'è pulizia automatica dei vecchi backup — cancellali a mano
|
||||
quando non servono più.
|
||||
|
||||
Il backup contiene:
|
||||
|
||||
| File/cartella | Contenuto |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `schema_public.sql` | schema delle tabelle applicative (tabelle, funzioni, RLS) |
|
||||
| `data_public.sql` | dati delle tabelle applicative |
|
||||
| `schema_auth.sql` | schema utenti Supabase Auth |
|
||||
| `data_auth.sql` | utenti reali, incluso l'hash della password |
|
||||
| `storage/` | tutti i file dei bucket (`avatar-giocatori`, `profili-giocatore`: foto, documenti d'identità, certificati) |
|
||||
|
||||
## Ripristino
|
||||
|
||||
1. Nuovo progetto Supabase (o `supabase start` in locale via Docker).
|
||||
2. Importa nell'ordine: `schema_public.sql` → `schema_auth.sql` →
|
||||
`data_auth.sql` → `data_public.sql` (gli utenti auth prima dei dati
|
||||
pubblici, perché `giocatori.auth_user_id` referenzia `auth.users.id`).
|
||||
3. Ricrea i bucket `avatar-giocatori` e `profili-giocatore` e ricarica il
|
||||
contenuto di `storage/`.
|
||||
4. Reimposta le variabili d'ambiente su Vercel/locale (non incluse in questo
|
||||
backup — vedi sotto).
|
||||
5. Redeploy dal repo git (il codice non serve backupparlo qui, è già su
|
||||
GitHub e Gitea).
|
||||
|
||||
Per un archivio cifrato: `gpg -d backups/AAAAMMGG_hhmm.tar.gz.gpg | tar xz`.
|
||||
|
||||
## Cosa NON è incluso
|
||||
|
||||
- **Variabili d'ambiente Vercel**: richiede `vercel login` interattivo, non
|
||||
automatizzabile da questo script. Esportale a mano con `vercel env pull`.
|
||||
- **Codice applicativo**: vive nel repo git, non nel database.
|
||||
|
||||
## Sicurezza
|
||||
|
||||
Questi backup contengono password hash, documenti d'identità e altri dati
|
||||
personali reali. Per questo:
|
||||
|
||||
- la cartella `backups/` è in `.gitignore` (tranne questo README e gli script:
|
||||
quelli sì restano in git) — **non committare mai i dati generati**;
|
||||
- se sposti un backup fuori da questo host, usa sempre `--encrypt` o cifra tu
|
||||
l'archivio a mano;
|
||||
- cancella i backup in chiaro quando non ti servono più.
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Backup completo di disaster recovery: schema + dati DB, utenti auth, file storage.
|
||||
# Uso:
|
||||
# ./backups/backup.sh crea backups/YYYYMMDD_HHMM/ con dentro tutto
|
||||
# ./backups/backup.sh --encrypt come sopra, poi comprime e cifra con gpg (chiede una passphrase)
|
||||
#
|
||||
# Richiede: essere già collegati al progetto Supabase (supabase login + link, già fatto
|
||||
# in questo repo) e avere .env con VITE_SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STAMP="$(date +%Y%m%d_%H%M)"
|
||||
RUN_DIR="$SCRIPT_DIR/$STAMP"
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
echo "==> Backup in $RUN_DIR"
|
||||
|
||||
echo "==> Dump schema pubblico"
|
||||
npx supabase db dump --linked -f "$RUN_DIR/schema_public.sql"
|
||||
|
||||
echo "==> Dump dati pubblici"
|
||||
npx supabase db dump --linked --data-only -f "$RUN_DIR/data_public.sql"
|
||||
|
||||
echo "==> Dump schema auth"
|
||||
npx supabase db dump --linked --schema auth -f "$RUN_DIR/schema_auth.sql"
|
||||
|
||||
echo "==> Dump dati auth (utenti)"
|
||||
npx supabase db dump --linked --schema auth --data-only -f "$RUN_DIR/data_auth.sql"
|
||||
|
||||
echo "==> Download file storage (bucket)"
|
||||
node "$SCRIPT_DIR/download-storage.mjs" "$RUN_DIR/storage"
|
||||
|
||||
echo "==> Backup completato: $RUN_DIR"
|
||||
|
||||
if [[ "${1:-}" == "--encrypt" ]]; then
|
||||
ARCHIVE="$RUN_DIR.tar.gz.gpg"
|
||||
echo "==> Comprimo e cifro in $ARCHIVE (verrà chiesta una passphrase)"
|
||||
tar -C "$SCRIPT_DIR" -czf - "$STAMP" | gpg --symmetric --cipher-algo AES256 -o "$ARCHIVE"
|
||||
rm -rf "$RUN_DIR"
|
||||
echo "==> Archivio cifrato pronto: $ARCHIVE"
|
||||
echo " Per estrarlo: gpg -d $ARCHIVE | tar xz"
|
||||
fi
|
||||
@@ -0,0 +1,49 @@
|
||||
// Scarica tutti i file dei bucket Supabase Storage per il backup di disaster recovery.
|
||||
// Uso: node backups/download-storage.mjs [cartella-output]
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
for (const line of (await readFile(new URL("../.env", import.meta.url), "utf8")).split("\n")) {
|
||||
const match = /^([\w.]+)=(.*)$/.exec(line.trim());
|
||||
if (match) process.env[match[1]] ??= match[2].replace(/^["']|["']$/g, "");
|
||||
}
|
||||
|
||||
const BUCKETS = ["avatar-giocatori", "profili-giocatore"];
|
||||
const OUT_DIR = process.argv[2] ?? new URL("./storage/", import.meta.url).pathname;
|
||||
|
||||
const supabase = createClient(process.env.VITE_SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY);
|
||||
|
||||
async function listAll(bucket, prefix = "") {
|
||||
const { data, error } = await supabase.storage.from(bucket).list(prefix, { limit: 1000 });
|
||||
if (error) throw error;
|
||||
let files = [];
|
||||
for (const entry of data) {
|
||||
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
if (entry.id === null) {
|
||||
// è una "cartella" (nessun id): ricorri
|
||||
files = files.concat(await listAll(bucket, path));
|
||||
} else {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
for (const bucket of BUCKETS) {
|
||||
console.log(`Bucket: ${bucket}`);
|
||||
const files = await listAll(bucket);
|
||||
console.log(` ${files.length} file trovati`);
|
||||
for (const path of files) {
|
||||
const { data, error } = await supabase.storage.from(bucket).download(path);
|
||||
if (error) {
|
||||
console.error(` ERRORE ${path}:`, error.message);
|
||||
continue;
|
||||
}
|
||||
const destPath = join(OUT_DIR, bucket, path);
|
||||
await mkdir(dirname(destPath), { recursive: true });
|
||||
await writeFile(destPath, Buffer.from(await data.arrayBuffer()));
|
||||
console.log(` scaricato: ${bucket}/${path}`);
|
||||
}
|
||||
}
|
||||
console.log("Fatto.");
|
||||
@@ -27,6 +27,15 @@ senza dipendere da servizi esclusivi di Lovable Cloud.
|
||||
5. Job pianificati richiamabili via semplice HTTP POST (es. `/api/public/promemoria-palloni`),
|
||||
così funzionano con cron di sistema, pg_cron o qualsiasi scheduler.
|
||||
|
||||
## Backup e disaster recovery
|
||||
|
||||
Lo script [`backups/backup.sh`](../backups/backup.sh) scarica su un host locale schema e
|
||||
dati del database (`public` e `auth`), utenti Supabase Auth e file dei bucket storage, in
|
||||
modo da poter ripartire in fretta se il progetto Supabase (o Vercel) venisse perso.
|
||||
Uso, contenuto del backup e procedura di ripristino sono descritti in
|
||||
[`backups/README.md`](../backups/README.md). Non ancora automatizzato: l'export delle
|
||||
variabili d'ambiente Vercel (richiede login interattivo).
|
||||
|
||||
## Migrazione su server proprio (sintesi)
|
||||
|
||||
1. `bun run build` (o `npm run build`) → avvio del server Node generato.
|
||||
|
||||
+10
-10
@@ -6,16 +6,16 @@ ancora e va **prima documentata** (vedi [DD-002](DESIGN_DECISIONS.md#dd-002--svi
|
||||
|
||||
## Dove sta cosa
|
||||
|
||||
| Documento | Risponde a |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------- |
|
||||
| [ROADMAP.md](ROADMAP.md) | Cosa è fatto, cosa è previsto, cosa resta un'idea |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Com'è fatta l'app: stack, struttura del codice, flusso di sviluppo |
|
||||
| [DATABASE.md](DATABASE.md) | Quali tabelle esistono, a cosa servono, chi le usa |
|
||||
| [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) | Perché abbiamo scelto così, cosa abbiamo escluso e quando riaprire la scelta |
|
||||
| [PORTABILITA.md](PORTABILITA.md) | Cosa lega l'app a un fornitore e cosa no, come spostarla su server proprio |
|
||||
| [EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md) | Come tenere basso il consumo cloud: cache, query, push |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | Cosa è cambiato e quando |
|
||||
| [modules/](modules/) | Specifica funzionale di ogni modulo, una per file |
|
||||
| Documento | Risponde a |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
||||
| [ROADMAP.md](ROADMAP.md) | Cosa è fatto, cosa è previsto, cosa resta un'idea |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Com'è fatta l'app: stack, struttura del codice, flusso di sviluppo |
|
||||
| [DATABASE.md](DATABASE.md) | Quali tabelle esistono, a cosa servono, chi le usa |
|
||||
| [DESIGN_DECISIONS.md](DESIGN_DECISIONS.md) | Perché abbiamo scelto così, cosa abbiamo escluso e quando riaprire la scelta |
|
||||
| [PORTABILITA.md](PORTABILITA.md) | Cosa lega l'app a un fornitore e cosa no, come spostarla su server proprio (backup/disaster recovery incluso) |
|
||||
| [EFFICIENZA_CLOUD.md](EFFICIENZA_CLOUD.md) | Come tenere basso il consumo cloud: cache, query, push |
|
||||
| [CHANGELOG.md](CHANGELOG.md) | Cosa è cambiato e quando |
|
||||
| [modules/](modules/) | Specifica funzionale di ogni modulo, una per file |
|
||||
|
||||
Le regole vincolanti per gli assistenti AI stanno in [AGENTS.md](../AGENTS.md);
|
||||
lo stato corrente del lavoro in [PROJECT_STATE.md](../PROJECT_STATE.md).
|
||||
|
||||
Reference in new issue
Block a user