feat: migrate to S3 storage with automatic download and timestamped filenames
- Replace local file storage with S3-compatible object storage - Add automatic PDF download after generation - Include timestamps in filenames to ensure uniqueness - Remove unused static volume from Kubernetes deployment - Update ConfigMap to remove unused variables and add S3 configuration - Configure S3 credentials via Kubernetes secrets for both dev and prod environments - Add boto3 dependency for S3 integration
This commit is contained in:
+118
-33
@@ -1,20 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
import os
|
||||
import io
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from typing import List
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import HTMLResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
from fpdf import FPDF
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Mount static files and templates
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||
# S3 Configuration
|
||||
S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME', 'math-exercises')
|
||||
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
def get_s3_client():
|
||||
"""Create and return an S3 client using environment variables"""
|
||||
s3_access_key = os.environ.get('S3_ACCESS_KEY')
|
||||
s3_secret_key = os.environ.get('S3_SECRET_KEY')
|
||||
s3_host_base = os.environ.get('S3_HOST_BASE')
|
||||
s3_host_bucket = os.environ.get('S3_HOST_BUCKET')
|
||||
|
||||
if not all([s3_access_key, s3_secret_key, s3_host_base]):
|
||||
raise ValueError("S3 environment variables not properly set")
|
||||
|
||||
s3 = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=s3_access_key,
|
||||
aws_secret_access_key=s3_secret_key,
|
||||
endpoint_url=s3_host_base,
|
||||
region_name='us-east-1' # Required but unused for Infomaniak
|
||||
)
|
||||
return s3
|
||||
|
||||
def create_bucket_if_not_exists(bucket_name):
|
||||
"""Create S3 bucket if it doesn't exist"""
|
||||
s3_client = get_s3_client()
|
||||
|
||||
try:
|
||||
s3_client.head_bucket(Bucket=bucket_name)
|
||||
except ClientError as e:
|
||||
error_code = int(e.response['Error']['Code'])
|
||||
if error_code == 404:
|
||||
# Bucket doesn't exist, create it
|
||||
try:
|
||||
s3_client.create_bucket(Bucket=bucket_name)
|
||||
print(f"Bucket {bucket_name} created successfully")
|
||||
except ClientError as create_error:
|
||||
print(f"Error creating bucket: {create_error}")
|
||||
raise
|
||||
else:
|
||||
# Some other error
|
||||
print(f"Error checking bucket: {e}")
|
||||
raise
|
||||
|
||||
# Create bucket on startup
|
||||
try:
|
||||
create_bucket_if_not_exists(S3_BUCKET_NAME)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not create/check S3 bucket: {e}")
|
||||
|
||||
def upload_to_s3(file_data, bucket_name, object_name, content_type='application/pdf'):
|
||||
"""Upload file data to S3 bucket"""
|
||||
s3_client = get_s3_client()
|
||||
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=bucket_name,
|
||||
Key=object_name,
|
||||
Body=file_data,
|
||||
ContentType=content_type
|
||||
)
|
||||
return True
|
||||
except ClientError as e:
|
||||
print(f"Error uploading to S3: {e}")
|
||||
return False
|
||||
|
||||
def download_from_s3(bucket_name, object_name):
|
||||
"""Download file data from S3 bucket"""
|
||||
s3_client = get_s3_client()
|
||||
|
||||
try:
|
||||
response = s3_client.get_object(Bucket=bucket_name, Key=object_name)
|
||||
return response['Body'].read()
|
||||
except ClientError as e:
|
||||
print(f"Error downloading from S3: {e}")
|
||||
return None
|
||||
|
||||
class MathExercisesPDF(FPDF):
|
||||
def header(self):
|
||||
self.set_font('Helvetica', 'B', 16)
|
||||
@@ -91,16 +167,23 @@ def generate_exercises(min_table: int, max_table: int, num_exercises: int = 15)
|
||||
return exercises
|
||||
|
||||
def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int = 15) -> str:
|
||||
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes"""
|
||||
pdf = MathExercisesPDF()
|
||||
pdf.add_page()
|
||||
pdf.set_font('Helvetica', '', 12)
|
||||
"""Crée un fichier PDF avec des exercices de mathématiques mélangés en 3 colonnes et l'upload sur S3"""
|
||||
import datetime
|
||||
|
||||
# Add timestamp to filename
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# Ajouter des informations sur la plage de tables
|
||||
if min_table == max_table:
|
||||
table_info = f'Tables de multiplication et division pour {min_table}'
|
||||
pdf_filename = f'exercices_mathematiques_table_{min_table}_{num_exercises}_exercices_{timestamp}.pdf'
|
||||
else:
|
||||
table_info = f'Tables de multiplication et division de {min_table} à {max_table}'
|
||||
pdf_filename = f'exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices_{timestamp}.pdf'
|
||||
|
||||
pdf = MathExercisesPDF()
|
||||
pdf.add_page()
|
||||
pdf.set_font('Helvetica', '', 12)
|
||||
|
||||
pdf.cell(0, 10, table_info, 0, 1, 'C', new_x="LMARGIN", new_y="NEXT")
|
||||
pdf.ln(5)
|
||||
@@ -146,18 +229,23 @@ def create_math_exercises_pdf(min_table: int, max_table: int, num_exercises: int
|
||||
pdf.cell(60, 10, col2_text, 0, 0, 'L', new_x="RIGHT", new_y="TOP")
|
||||
pdf.cell(60, 10, col3_text, 0, 1, 'L', new_x="LMARGIN", new_y="NEXT")
|
||||
|
||||
# Sauvegarder le PDF
|
||||
if min_table == max_table:
|
||||
filename = f'app/static/exercices_mathematiques_table_{min_table}_{num_exercises}_exercices.pdf'
|
||||
else:
|
||||
filename = f'app/static/exercices_mathematiques_tables_{min_table}_a_{max_table}_{num_exercises}_exercices.pdf'
|
||||
pdf.output(filename)
|
||||
return filename
|
||||
# Sauvegarder le PDF en mémoire
|
||||
pdf_data = pdf.output()
|
||||
|
||||
# Upload to S3
|
||||
upload_success = upload_to_s3(pdf_data, S3_BUCKET_NAME, pdf_filename)
|
||||
|
||||
if not upload_success:
|
||||
raise Exception("Failed to upload PDF to S3")
|
||||
|
||||
return pdf_filename
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def read_root(request: Request):
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
@app.post("/generate")
|
||||
async def generate_exercises_endpoint(request: ExerciseRequest):
|
||||
try:
|
||||
@@ -168,35 +256,32 @@ async def generate_exercises_endpoint(request: ExerciseRequest):
|
||||
if request.num_exercises < 1:
|
||||
return {"error": "Le nombre d'exercices doit être supérieur à 0"}
|
||||
|
||||
filename = create_math_exercises_pdf(
|
||||
pdf_filename = create_math_exercises_pdf(
|
||||
request.min_table,
|
||||
request.max_table,
|
||||
request.num_exercises
|
||||
)
|
||||
|
||||
# Extract just the filename for the download link
|
||||
pdf_filename = os.path.basename(filename)
|
||||
# Return redirect to automatically download the file
|
||||
return RedirectResponse(url=f"/download/{pdf_filename}", status_code=303)
|
||||
|
||||
return {
|
||||
"message": "PDF généré avec succès!",
|
||||
"pdf_filename": pdf_filename,
|
||||
"min_table": request.min_table,
|
||||
"max_table": request.max_table,
|
||||
"num_exercises": request.num_exercises
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Erreur lors de la création du PDF: {str(e)}"}
|
||||
|
||||
@app.get("/download/{filename}")
|
||||
async def download_pdf(filename: str):
|
||||
file_path = f"app/static/{filename}"
|
||||
if os.path.exists(file_path):
|
||||
return FileResponse(
|
||||
path=file_path,
|
||||
media_type='application/pdf',
|
||||
filename=filename
|
||||
)
|
||||
return {"error": "File not found"}
|
||||
# Download file from S3
|
||||
file_data = download_from_s3(S3_BUCKET_NAME, filename)
|
||||
|
||||
if file_data is None:
|
||||
return {"error": "File not found"}
|
||||
|
||||
# Return streaming response with PDF data
|
||||
return StreamingResponse(
|
||||
io.BytesIO(file_data),
|
||||
media_type='application/pdf',
|
||||
headers={'Content-Disposition': f'attachment; filename="{filename}"'}
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
Reference in New Issue
Block a user