chore: update backup configurations and directory structure

- Modified `.gitignore` to exclude all files in the `/data/` directory.
- Enhanced `BACKUP.md` with customization options for backup storage paths and updated database path configurations.
- Updated `docker-compose.yml` to reflect new paths for database and backup storage.
- Adjusted `Dockerfile` to create a dedicated backups directory.
- Refactored `BackupService` to utilize environment variables for backup paths, improving flexibility and reliability.
- Deleted `dev.db` as it is no longer needed in the repository.
This commit is contained in:
Julien Froidefond
2025-09-20 15:45:56 +02:00
parent dfa8d34855
commit 329018161c
10 changed files with 529 additions and 39 deletions

2
.gitignore vendored
View File

@@ -43,4 +43,4 @@ next-env.d.ts
/src/generated/prisma /src/generated/prisma
/prisma/dev.db /prisma/dev.db
backups/ /data/*

View File

@@ -52,6 +52,19 @@ tsx scripts/backup-manager.ts config-set maxBackups=10
tsx scripts/backup-manager.ts config-set compression=true tsx scripts/backup-manager.ts config-set compression=true
``` ```
### Personnalisation du dossier de sauvegarde
```bash
# Via variable d'environnement permanente (.env)
BACKUP_STORAGE_PATH="./custom-backups"
# Via variable temporaire (une seule fois)
BACKUP_STORAGE_PATH="./my-backups" npm run backup:create
# Exemple avec un chemin absolu
BACKUP_STORAGE_PATH="/var/backups/towercontrol" npm run backup:create
```
## Utilisation ## Utilisation
### Interface graphique ### Interface graphique
@@ -272,8 +285,34 @@ export const prisma = globalThis.__prisma || new PrismaClient({
### Variables d'environnement ### Variables d'environnement
```bash ```bash
# Optionnel : personnaliser le chemin de la base # Configuration des chemins de base de données
DATABASE_URL="file:./custom/path/dev.db" DATABASE_URL="file:./prisma/dev.db" # Pour Prisma
BACKUP_DATABASE_PATH="./prisma/dev.db" # Base à sauvegarder (optionnel)
BACKUP_STORAGE_PATH="./backups" # Dossier des sauvegardes (optionnel)
```
### Docker
En environnement Docker, tout est centralisé dans le dossier `data/` :
```yaml
# docker-compose.yml
environment:
DATABASE_URL: "file:./data/prod.db" # Base de données Prisma
BACKUP_DATABASE_PATH: "./data/prod.db" # Base à sauvegarder
BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes
volumes:
- ./data:/app/data # Bind mount vers dossier local
```
**Structure des dossiers :**
```
./data/ # Dossier local mappé
├── prod.db # Base de données production
├── dev.db # Base de données développement
└── backups/ # Sauvegardes (créé automatiquement)
├── towercontrol_*.db.gz
└── ...
``` ```
## API ## API

201
DOCKER.md Normal file
View File

@@ -0,0 +1,201 @@
# 🐳 Docker - TowerControl
Guide d'utilisation de TowerControl avec Docker.
## 🚀 Démarrage rapide
### Production
```bash
# Démarrer le service de production
docker-compose up -d towercontrol
# Accéder à l'application
open http://localhost:3006
```
### Développement
```bash
# Démarrer le service de développement avec live reload
docker-compose --profile dev up towercontrol-dev
# Accéder à l'application
open http://localhost:3005
```
## 📋 Services disponibles
### 🚀 `towercontrol` (Production)
- **Port** : 3006
- **Base de données** : `./data/prod.db`
- **Sauvegardes** : `./data/backups/`
- **Mode** : Optimisé, standalone
- **Restart** : Automatique
### 🛠️ `towercontrol-dev` (Développement)
- **Port** : 3005
- **Base de données** : `./data/dev.db`
- **Sauvegardes** : `./data/backups/` (partagées)
- **Mode** : Live reload, debug
- **Profile** : `dev`
## 📁 Structure des données
```
./data/ # Mappé vers /app/data dans les conteneurs
├── README.md # Documentation du dossier data
├── prod.db # Base SQLite production
├── dev.db # Base SQLite développement
└── backups/ # Sauvegardes automatiques
├── towercontrol_2025-01-15T10-30-00-000Z.db.gz
└── ...
```
## 🔧 Configuration
### Variables d'environnement
| Variable | Production | Développement | Description |
|----------|------------|---------------|-------------|
| `NODE_ENV` | `production` | `development` | Mode d'exécution |
| `DATABASE_URL` | `file:./data/prod.db` | `file:./data/dev.db` | Base Prisma |
| `BACKUP_DATABASE_PATH` | `./data/prod.db` | `./data/dev.db` | Source backup |
| `BACKUP_STORAGE_PATH` | `./data/backups` | `./data/backups` | Dossier backup |
| `TZ` | `Europe/Paris` | `Europe/Paris` | Fuseau horaire |
### Ports
- **Production** : `3006:3000`
- **Développement** : `3005:3000`
## 📚 Commandes utiles
### Gestion des conteneurs
```bash
# Voir les logs
docker-compose logs -f towercontrol
docker-compose logs -f towercontrol-dev
# Arrêter les services
docker-compose down
# Reconstruire les images
docker-compose build
# Nettoyer tout
docker-compose down -v --rmi all
```
### Gestion des données
```bash
# Sauvegarder les données
docker-compose exec towercontrol npm run backup:create
# Lister les sauvegardes
docker-compose exec towercontrol npm run backup:list
# Accéder au shell du conteneur
docker-compose exec towercontrol sh
```
### Base de données
```bash
# Migrations Prisma
docker-compose exec towercontrol npx prisma migrate deploy
# Reset de la base (dev uniquement)
docker-compose exec towercontrol-dev npx prisma migrate reset
# Studio Prisma (dev)
docker-compose exec towercontrol-dev npx prisma studio
```
## 🔍 Debugging
### Vérifier la santé
```bash
# Health check
curl http://localhost:3006/api/health
curl http://localhost:3005/api/health
# Vérifier les variables d'env
docker-compose exec towercontrol env | grep -E "(DATABASE|BACKUP|NODE_ENV)"
```
### Logs détaillés
```bash
# Logs avec timestamps
docker-compose logs -f -t towercontrol
# Logs des 100 dernières lignes
docker-compose logs --tail=100 towercontrol
```
## 🚨 Dépannage
### Problèmes courants
**Port déjà utilisé**
```bash
# Trouver le processus qui utilise le port
lsof -i :3006
kill -9 <PID>
```
**Base de données corrompue**
```bash
# Restaurer depuis une sauvegarde
docker-compose exec towercontrol npm run backup:restore filename.db.gz
```
**Permissions**
```bash
# Corriger les permissions du dossier data
sudo chown -R $USER:$USER ./data
```
## 📊 Monitoring
### Espace disque
```bash
# Taille du dossier data
du -sh ./data
# Espace libre
df -h .
```
### Performance
```bash
# Stats des conteneurs
docker stats
# Utilisation mémoire
docker-compose exec towercontrol free -h
```
## 🔒 Production
### Recommandations
- Utiliser un reverse proxy (nginx, traefik)
- Configurer HTTPS
- Sauvegarder régulièrement `./data/`
- Monitorer l'espace disque
- Logs centralisés
### Exemple nginx
```nginx
server {
listen 80;
server_name towercontrol.example.com;
location / {
proxy_pass http://localhost:3006;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
📚 **Voir aussi** : [BACKUP.md](./BACKUP.md) | [data/README.md](./data/README.md)

View File

@@ -35,8 +35,8 @@ RUN npm run build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM base AS runner FROM base AS runner
# Set timezone to Europe/Paris # Set timezone to Europe/Paris and install sqlite3 for backups
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata sqlite
RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
WORKDIR /app WORKDIR /app
@@ -64,8 +64,8 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
# Create data directory for SQLite # Create data directory for SQLite and backups
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data RUN mkdir -p /app/data/backups && chown -R nextjs:nodejs /app/data
# Set all ENV vars before switching user # Set all ENV vars before switching user
ENV PORT=3000 ENV PORT=3000

0
dev.db
View File

View File

@@ -1,31 +1,27 @@
version: '3.8'
services: services:
towercontrol: towercontrol:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: runner
ports: ports:
- "3006:3000" - "3006:3000"
environment: environment:
- NODE_ENV=production NODE_ENV: production
- DATABASE_URL=file:/app/data/prod.db DATABASE_URL: "file:../data/prod.db" # Prisma
- TZ=Europe/Paris BACKUP_DATABASE_PATH: "./data/prod.db" # Base de données à sauvegarder
BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes
TZ: Europe/Paris
volumes: volumes:
# Volume persistant pour la base SQLite - ./data:/app/data # Dossier local data/ vers /app/data
- sqlite_data:/app/data
# Monter ta DB locale (décommente pour utiliser tes données locales)
- ./prisma/dev.db:/app/data/prod.db
- ./backups:/app/backups
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health || exit 1"] test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
# Service de développement (optionnel)
towercontrol-dev: towercontrol-dev:
build: build:
context: . context: .
@@ -34,20 +30,29 @@ services:
ports: ports:
- "3005:3000" - "3005:3000"
environment: environment:
- NODE_ENV=development NODE_ENV: development
- DATABASE_URL=file:/app/data/dev.db DATABASE_URL: "file:../data/dev.db" # Prisma
BACKUP_DATABASE_PATH: "./data/dev.db" # Base de données à sauvegarder
BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes
TZ: Europe/Paris
volumes: volumes:
- .:/app - .:/app # code en live
- /app/node_modules - /app/node_modules # vol anonyme pour ne pas écraser ceux du conteneur
- /app/.next - /app/.next
- sqlite_data_dev:/app/data - ./data:/app/data # Dossier local data/ vers /app/data
command: sh -c "npm install && npx prisma generate && npx prisma migrate deploy && npm run dev" command: >
sh -c "npm install &&
npx prisma generate &&
npx prisma migrate deploy &&
npm run dev"
profiles: profiles:
- dev - dev
volumes: # 📁 Structure des données :
sqlite_data: # ./data/ -> /app/data (bind mount)
driver: local # ├── prod.db -> Base de données production
sqlite_data_dev: # ├── dev.db -> Base de données développement
driver: local # └── backups/ -> Sauvegardes automatiques
#
# 🔧 Configuration via .env.docker
# 📚 Documentation : ./data/README.md

View File

@@ -1,5 +1,13 @@
# Base de données (requis) # Base de données (requis)
DATABASE_URL="file:./dev.db" DATABASE_URL="file:../data/dev.db"
# Chemin de la base de données pour les backups (optionnel)
# Si non défini, utilise DATABASE_URL ou le chemin par défaut
BACKUP_DATABASE_PATH="./data/dev.db"
# Dossier de stockage des sauvegardes (optionnel)
# Par défaut: ./backups en local, ./data/backups en production
BACKUP_STORAGE_PATH="./backups"
# Intégration Jira (optionnel) # Intégration Jira (optionnel)
JIRA_BASE_URL="" # https://votre-domaine.atlassian.net JIRA_BASE_URL="" # https://votre-domaine.atlassian.net

View File

@@ -31,11 +31,23 @@ export class BackupService {
enabled: true, enabled: true,
interval: 'hourly', interval: 'hourly',
maxBackups: 5, maxBackups: 5,
backupPath: path.join(process.cwd(), 'backups'), backupPath: this.getDefaultBackupPath(),
includeUploads: true, includeUploads: true,
compression: true, compression: true,
}; };
private getDefaultBackupPath(): string {
// 1. Variable d'environnement explicite
if (process.env.BACKUP_STORAGE_PATH) {
return path.resolve(process.cwd(), process.env.BACKUP_STORAGE_PATH);
}
// 2. Chemin par défaut selon l'environnement
return process.env.NODE_ENV === 'production'
? path.join(process.cwd(), 'data', 'backups') // Docker: /app/data/backups
: path.join(process.cwd(), 'backups'); // Local: ./backups
}
private config: BackupConfig; private config: BackupConfig;
constructor(config?: Partial<BackupConfig>) { constructor(config?: Partial<BackupConfig>) {
@@ -106,10 +118,7 @@ export class BackupService {
// Créer le dossier de backup si nécessaire // Créer le dossier de backup si nécessaire
await this.ensureBackupDirectory(); await this.ensureBackupDirectory();
// Vérifier l'état de la base de données // Créer la sauvegarde SQLite (sans vérification de santé pour éviter les conflits)
await this.verifyDatabaseHealth();
// Créer la sauvegarde SQLite
await this.createSQLiteBackup(backupPath); await this.createSQLiteBackup(backupPath);
// Compresser si activé // Compresser si activé
@@ -156,7 +165,26 @@ export class BackupService {
* Crée une sauvegarde SQLite en utilisant la commande .backup * Crée une sauvegarde SQLite en utilisant la commande .backup
*/ */
private async createSQLiteBackup(backupPath: string): Promise<void> { private async createSQLiteBackup(backupPath: string): Promise<void> {
const dbPath = path.resolve(process.env.DATABASE_URL?.replace('file:', '') || './prisma/dev.db'); // Résoudre le chemin de la base de données
let dbPath: string;
if (process.env.BACKUP_DATABASE_PATH) {
// Utiliser la variable spécifique aux backups
dbPath = path.resolve(process.cwd(), process.env.BACKUP_DATABASE_PATH);
} else if (process.env.DATABASE_URL) {
// Fallback sur DATABASE_URL si BACKUP_DATABASE_PATH n'est pas défini
dbPath = path.resolve(process.env.DATABASE_URL.replace('file:', ''));
} else {
// Chemin par défaut vers prisma/dev.db
dbPath = path.resolve(process.cwd(), 'prisma', 'dev.db');
}
// Vérifier que le fichier source existe
try {
await fs.stat(dbPath);
} catch (error) {
console.error(`❌ Source database not found: ${dbPath}`, error);
throw new Error(`Source database not found: ${dbPath}`);
}
// Méthode 1: Utiliser sqlite3 CLI (plus fiable) // Méthode 1: Utiliser sqlite3 CLI (plus fiable)
try { try {
@@ -199,7 +227,19 @@ export class BackupService {
*/ */
async restoreBackup(filename: string): Promise<void> { async restoreBackup(filename: string): Promise<void> {
const backupPath = path.join(this.config.backupPath, filename); const backupPath = path.join(this.config.backupPath, filename);
const dbPath = path.resolve(process.env.DATABASE_URL?.replace('file:', '') || './prisma/dev.db');
// Résoudre le chemin de la base de données
let dbPath: string;
if (process.env.BACKUP_DATABASE_PATH) {
// Utiliser la variable spécifique aux backups
dbPath = path.resolve(process.cwd(), process.env.BACKUP_DATABASE_PATH);
} else if (process.env.DATABASE_URL) {
// Fallback sur DATABASE_URL si BACKUP_DATABASE_PATH n'est pas défini
dbPath = path.resolve(process.env.DATABASE_URL.replace('file:', ''));
} else {
// Chemin par défaut vers prisma/dev.db
dbPath = path.resolve(process.cwd(), 'prisma', 'dev.db');
}
console.log(`🔄 Restore paths - backup: ${backupPath}, target: ${dbPath}`); console.log(`🔄 Restore paths - backup: ${backupPath}, target: ${dbPath}`);

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import { backupService } from '@/services/backup';
interface RouteParams {
params: Promise<{
filename: string;
}>;
}
export async function DELETE(
request: NextRequest,
{ params }: RouteParams
) {
try {
const { filename } = await params;
// Vérification de sécurité - s'assurer que c'est bien un fichier de backup
if (!filename.startsWith('towercontrol_') ||
(!filename.endsWith('.db') && !filename.endsWith('.db.gz'))) {
return NextResponse.json(
{ success: false, error: 'Invalid backup filename' },
{ status: 400 }
);
}
await backupService.deleteBackup(filename);
return NextResponse.json({
success: true,
message: `Backup ${filename} deleted successfully`
});
} catch (error) {
console.error('Error deleting backup:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to delete backup'
},
{ status: 500 }
);
}
}
export async function POST(
request: NextRequest,
{ params }: RouteParams
) {
try {
const { filename } = await params;
const body = await request.json();
const { action } = body;
if (action === 'restore') {
// Vérification de sécurité
if (!filename.startsWith('towercontrol_') ||
(!filename.endsWith('.db') && !filename.endsWith('.db.gz'))) {
return NextResponse.json(
{ success: false, error: 'Invalid backup filename' },
{ status: 400 }
);
}
// Protection environnement de production
if (process.env.NODE_ENV === 'production') {
return NextResponse.json(
{ success: false, error: 'Restore not allowed in production via API' },
{ status: 403 }
);
}
await backupService.restoreBackup(filename);
return NextResponse.json({
success: true,
message: `Database restored from ${filename}`
});
}
return NextResponse.json(
{ success: false, error: 'Invalid action' },
{ status: 400 }
);
} catch (error) {
console.error('Error in backup operation:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Operation failed'
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from 'next/server';
import { backupService } from '@/services/backup';
import { backupScheduler } from '@/services/backup-scheduler';
export async function GET() {
try {
console.log('🔄 API GET /api/backups called');
// Test de la configuration d'abord
const config = backupService.getConfig();
console.log('✅ Config loaded:', config);
// Test du scheduler
const schedulerStatus = backupScheduler.getStatus();
console.log('✅ Scheduler status:', schedulerStatus);
// Test de la liste des backups
const backups = await backupService.listBackups();
console.log('✅ Backups loaded:', backups.length);
const response = {
success: true,
data: {
backups,
scheduler: schedulerStatus,
config,
}
};
console.log('✅ API response ready');
return NextResponse.json(response);
} catch (error) {
console.error('❌ Error fetching backups:', error);
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown');
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch backups',
details: error instanceof Error ? error.stack : undefined
},
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { action, ...params } = body;
switch (action) {
case 'create':
const backup = await backupService.createBackup('manual');
return NextResponse.json({ success: true, data: backup });
case 'verify':
await backupService.verifyDatabaseHealth();
return NextResponse.json({
success: true,
message: 'Database health check passed'
});
case 'config':
await backupService.updateConfig(params.config);
// Redémarrer le scheduler si la config a changé
if (params.config.enabled !== undefined || params.config.interval !== undefined) {
backupScheduler.restart();
}
return NextResponse.json({
success: true,
message: 'Configuration updated',
data: backupService.getConfig()
});
case 'scheduler':
if (params.enabled) {
backupScheduler.start();
} else {
backupScheduler.stop();
}
return NextResponse.json({
success: true,
data: backupScheduler.getStatus()
});
default:
return NextResponse.json(
{ success: false, error: 'Invalid action' },
{ status: 400 }
);
}
} catch (error) {
console.error('Error in backup operation:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}