ajout d'un paneau admin

This commit is contained in:
cedrick2711
2026-08-25 13:29:22 -04:00
parent 9fc4ba0b98
commit 6232b77094
15 changed files with 1053 additions and 387 deletions
+121 -129
View File
@@ -1,27 +1,61 @@
"""Database backup script for the Team Tryouts application.
"""PostgreSQL database backup and restore helpers for the admin panel.
This module provides a simple backup mechanism for the SQLite database
and uploaded contract documents. Designed to be run as a scheduled task
(Windows Task Scheduler) or cron job.
Unlike the original SQLite-based script, this module works against the
PostgreSQL database configured via DATABASE_URL. It shells out to the
standard ``pg_dump`` / ``pg_restore`` / ``psql`` command-line utilities.
Usage:
python backup.py
Configuration via environment variables:
BACKUP_DIR: Directory to store backups (default: ./backups)
BACKUP_RETENTION_DAYS: Number of days to keep backups (default: 30)
DATABASE_URL: PostgreSQL connection string (required for connectivity)
"""
import os
import shutil
import sqlite3
import re
import subprocess
from datetime import datetime, timedelta
from urllib.parse import urlparse
# Configuration
BACKUP_DIR = os.getenv('BACKUP_DIR', os.path.join(os.getcwd(), 'backups'))
BACKUP_RETENTION_DAYS = int(os.getenv('BACKUP_RETENTION_DAYS', 30))
DATABASE_PATH = os.getenv('DATABASE_PATH', os.path.join(os.getcwd(), 'instance', 'team_tryouts.db'))
DOCUMENTS_DIR = os.path.join(os.getcwd(), 'documents')
def _database_url():
"""Return the DATABASE_URL, normalizing to the psycopg2 SQLAlchemy form."""
url = os.getenv('DATABASE_URL')
if not url:
raise RuntimeError('DATABASE_URL environment variable must be set')
# SQLAlchemy may prefix driver; strip it for command-line tools.
return re.sub(r'^postgresql\+[^:]+://', 'postgresql://', url)
def _pg_env():
"""Return the dict of PG* variables required by pg_dump / psql."""
parsed = urlparse(_database_url())
env = os.environ.copy()
# PGPASSWORD avoids interactive password prompts.
if parsed.password is not None:
env['PGPASSWORD'] = parsed.password
env.pop('PGHOST', None)
env.pop('PGPORT', None)
env.pop('PGUSER', None)
env.pop('PGDATABASE', None)
return env
def _pg_args():
"""Return [host, port, user, dbname] positional args for pg tools."""
parsed = urlparse(_database_url())
args = []
if parsed.hostname:
args += ['--host', parsed.hostname]
if parsed.port:
args += ['--port', str(parsed.port)]
if parsed.username:
args += ['--username', parsed.username]
if parsed.path and parsed.path.lstrip('/'):
args += ['--dbname', parsed.path.lstrip('/')]
return args
def create_backup_dir():
@@ -29,155 +63,113 @@ def create_backup_dir():
os.makedirs(BACKUP_DIR, exist_ok=True)
def backup_database():
"""Backup the SQLite database using sqlite3's built-in backup API.
def create_backup(backup_type='manual', notes=None):
"""Create a PostgreSQL dump backup.
Returns:
str: Path to the created backup file, or None if failed.
dict: {'filename', 'file_path', 'size_bytes'} or None on failure.
"""
if not os.path.exists(DATABASE_PATH):
print(f'[WARNING] Database not found at {DATABASE_PATH}. Skipping database backup.')
return None
create_backup_dir()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_filename = f'db_backup_{timestamp}.db'
backup_path = os.path.join(BACKUP_DIR, backup_filename)
filename = f'db_backup_{backup_type}_{timestamp}.sql'
file_path = os.path.join(BACKUP_DIR, filename)
cmd = ['pg_dump'] + _pg_args() + ['--format', 'plain', '--no-owner', '--file', file_path]
try:
source = sqlite3.connect(DATABASE_PATH)
destination = sqlite3.connect(backup_path)
source.backup(destination)
source.close()
destination.close()
print(f'[OK] Database backed up to: {backup_path}')
return backup_path
except Exception as e:
print(f'[ERROR] Database backup failed: {e}')
return None
result = subprocess.run(cmd, env=_pg_env(), capture_output=True, text=True)
if result.returncode != 0:
_cleanup_failed_file(file_path)
raise RuntimeError(result.stderr.strip() or 'pg_dump failed')
except FileNotFoundError:
_cleanup_failed_file(file_path)
raise RuntimeError('pg_dump executable not found on PATH')
except Exception:
_cleanup_failed_file(file_path)
raise
if not os.path.exists(file_path):
raise RuntimeError('Backup file was not created')
size = os.path.getsize(file_path)
return {'filename': filename, 'file_path': file_path, 'size_bytes': size}
def backup_documents():
"""Backup the uploaded contract documents directory.
Returns:
str: Path to the created archive, or None if no documents exist.
def _cleanup_failed_file(file_path):
"""Remove a partially-written dump file, ignoring errors."""
if file_path and os.path.exists(file_path):
try:
os.remove(file_path)
except OSError:
pass
def restore_backup(file_path):
"""Restore a PostgreSQL dump file using psql.
Args:
file_path: Path to a plain-text .sql dump produced by pg_dump.
Raises:
RuntimeError: If psql fails or is unavailable.
"""
if not os.path.exists(DOCUMENTS_DIR):
print('[INFO] No documents directory found. Skipping document backup.')
return None
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
archive_basename = f'documents_backup_{timestamp}'
archive_path = os.path.join(BACKUP_DIR, archive_basename)
if not file_path or not os.path.exists(file_path):
raise RuntimeError('Backup file does not exist')
# The dump was produced with --no-owner + plain format, so we can
# pipe it into psql. Use --set ON_ERROR_STOP=1 to fail fast.
cmd = ['psql'] + _pg_args() + ['--set', 'ON_ERROR_STOP=1', '--file', file_path]
try:
shutil.make_archive(archive_path, 'zip', DOCUMENTS_DIR)
zip_path = f'{archive_path}.zip'
print(f'[OK] Documents backed up to: {zip_path}')
return zip_path
except Exception as e:
print(f'[ERROR] Document backup failed: {e}')
return None
result = subprocess.run(cmd, env=_pg_env(), capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(result.stderr.strip() or 'psql restore failed')
except FileNotFoundError:
raise RuntimeError('psql executable not found on PATH')
def cleanup_old_backups():
"""Remove backup files older than BACKUP_RETENTION_DAYS."""
if not os.path.exists(BACKUP_DIR):
return
return 0
cutoff = datetime.now() - timedelta(days=BACKUP_RETENTION_DAYS)
removed_count = 0
for filename in os.listdir(BACKUP_DIR):
file_path = os.path.join(BACKUP_DIR, filename)
if os.path.isfile(file_path):
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_time < cutoff:
try:
os.remove(file_path)
removed_count += 1
print(f'[CLEANUP] Removed old backup: {filename}')
except OSError as e:
print(f'[WARNING] Could not remove {filename}: {e}')
if removed_count > 0:
print(f'[CLEANUP] Removed {removed_count} old backup(s).')
else:
print('[CLEANUP] No old backups to remove.')
def verify_backup(backup_path):
"""Verify a database backup by running a quick integrity check.
Args:
backup_path: Path to the backup file to verify.
Returns:
bool: True if backup is valid, False otherwise.
"""
if not backup_path or not os.path.exists(backup_path):
return False
try:
conn = sqlite3.connect(backup_path)
cursor = conn.cursor()
cursor.execute('PRAGMA integrity_check')
result = cursor.fetchone()
conn.close()
is_valid = result[0] == 'ok'
if is_valid:
print(f'[OK] Backup integrity verified: {backup_path}')
else:
print(f'[ERROR] Backup integrity check failed: {backup_path} - {result[0]}')
return is_valid
except Exception as e:
print(f'[ERROR] Backup verification failed: {e}')
return False
if not os.path.isfile(file_path):
continue
file_time = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_time < cutoff:
try:
os.remove(file_path)
removed_count += 1
except OSError:
pass
return removed_count
def main():
"""Run the full backup process.
Steps:
1. Create backup directory
2. Backup database
3. Backup documents (if any)
4. Verify database backup
5. Clean up old backups
Returns:
int: 0 on success, 1 on failure.
"""
print(f'=== Team Tryouts Backup ===')
"""Run a manual backup from the CLI (for cron/Task Scheduler)."""
print('=== Team Tryouts PostgreSQL Backup ===')
print(f'Started at: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
print(f'Backup directory: {BACKUP_DIR}')
print(f'Retention period: {BACKUP_RETENTION_DAYS} days')
print()
create_backup_dir()
try:
result = create_backup(backup_type='scheduled')
except RuntimeError as e:
print(f'[ERROR] {e}')
return 1
# 1. Backup database
db_backup_path = backup_database()
success = True
print(f'[OK] Database backed up to: {result["file_path"]}')
# 2. Verify database backup
if db_backup_path:
if not verify_backup(db_backup_path):
success = False
# 3. Backup documents
backup_documents()
# 4. Cleanup old backups
cleanup_old_backups()
removed = cleanup_old_backups()
if removed:
print(f'[CLEANUP] Removed {removed} old backup(s).')
print()
if success:
print('=== Backup completed successfully ===')
else:
print('=== Backup completed with warnings ===')
return 0 if success else 1
print('=== Backup completed successfully ===')
return 0
if __name__ == '__main__':