Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
fac44d2246
|
|||
|
3c07c89f24
|
+144
-1
@@ -3,9 +3,11 @@ import random
|
||||
import os
|
||||
import io
|
||||
import boto3
|
||||
import zipfile
|
||||
import tempfile
|
||||
from botocore.exceptions import ClientError
|
||||
from typing import List
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi import FastAPI, Request, Response, Form
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
@@ -91,6 +93,34 @@ def download_from_s3(bucket_name, object_name):
|
||||
print(f"Error downloading from S3: {e}")
|
||||
return None
|
||||
|
||||
def list_objects_in_s3(bucket_name):
|
||||
"""List all objects in S3 bucket (sorted from newest to oldest)"""
|
||||
s3_client = get_s3_client()
|
||||
|
||||
try:
|
||||
response = s3_client.list_objects_v2(Bucket=bucket_name)
|
||||
if 'Contents' in response:
|
||||
# Filter for PDF files only and sort by last modified (newest first)
|
||||
pdf_files = [obj for obj in response['Contents'] if obj['Key'].endswith('.pdf')]
|
||||
pdf_files.sort(key=lambda x: x['LastModified'], reverse=True)
|
||||
return pdf_files
|
||||
else:
|
||||
return []
|
||||
except ClientError as e:
|
||||
print(f"Error listing objects in S3: {e}")
|
||||
return []
|
||||
|
||||
def delete_from_s3(bucket_name, object_name):
|
||||
"""Delete file from S3 bucket"""
|
||||
s3_client = get_s3_client()
|
||||
|
||||
try:
|
||||
s3_client.delete_object(Bucket=bucket_name, Key=object_name)
|
||||
return True
|
||||
except ClientError as e:
|
||||
print(f"Error deleting from S3: {e}")
|
||||
return False
|
||||
|
||||
class MathExercisesPDF(FPDF):
|
||||
def header(self):
|
||||
self.set_font('Helvetica', 'B', 16)
|
||||
@@ -283,6 +313,119 @@ async def download_pdf(filename: str):
|
||||
headers={'Content-Disposition': f'attachment; filename="{filename}"'}
|
||||
)
|
||||
|
||||
@app.get("/list")
|
||||
async def list_pdfs(page: int = 1, page_size: int = 10):
|
||||
"""List PDF files in the S3 bucket with pagination (sorted from newest to oldest)"""
|
||||
try:
|
||||
# Validate page and page_size parameters
|
||||
if page < 1:
|
||||
page = 1
|
||||
if page_size < 1 or page_size > 100:
|
||||
page_size = 10
|
||||
|
||||
pdf_files = list_objects_in_s3(S3_BUCKET_NAME)
|
||||
|
||||
# Calculate pagination
|
||||
total_files = len(pdf_files)
|
||||
total_pages = (total_files + page_size - 1) // page_size # Ceiling division
|
||||
|
||||
# Slice the files for the current page
|
||||
start_index = (page - 1) * page_size
|
||||
end_index = start_index + page_size
|
||||
paginated_files = pdf_files[start_index:end_index]
|
||||
|
||||
return {
|
||||
"files": paginated_files,
|
||||
"pagination": {
|
||||
"current_page": page,
|
||||
"page_size": page_size,
|
||||
"total_files": total_files,
|
||||
"total_pages": total_pages,
|
||||
"has_next": page < total_pages,
|
||||
"has_prev": page > 1
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Error listing files: {str(e)}"}
|
||||
|
||||
@app.delete("/delete/{filename}")
|
||||
async def delete_pdf(filename: str):
|
||||
"""Delete a PDF file from S3 bucket"""
|
||||
try:
|
||||
# Decode URL encoded filename
|
||||
import urllib.parse
|
||||
decoded_filename = urllib.parse.unquote(filename)
|
||||
|
||||
success = delete_from_s3(S3_BUCKET_NAME, decoded_filename)
|
||||
if success:
|
||||
return {"message": f"Fichier {decoded_filename} supprimé avec succès"}
|
||||
else:
|
||||
return {"error": f"Erreur lors de la suppression du fichier {decoded_filename}"}
|
||||
except Exception as e:
|
||||
return {"error": f"Error deleting file: {str(e)}"}
|
||||
|
||||
@app.post("/bulk-delete")
|
||||
async def bulk_delete(filenames: str = Form(...)):
|
||||
"""Delete multiple PDF files from S3 bucket"""
|
||||
try:
|
||||
import json
|
||||
filename_list = json.loads(filenames)
|
||||
deleted_files = []
|
||||
errors = []
|
||||
|
||||
for filename in filename_list:
|
||||
success = delete_from_s3(S3_BUCKET_NAME, filename)
|
||||
if success:
|
||||
deleted_files.append(filename)
|
||||
else:
|
||||
errors.append(filename)
|
||||
|
||||
if errors:
|
||||
return {"message": f"Fichiers supprimés: {len(deleted_files)}", "errors": errors}
|
||||
else:
|
||||
return {"message": f"{len(deleted_files)} fichiers supprimés avec succès"}
|
||||
except Exception as e:
|
||||
return {"error": f"Error deleting files: {str(e)}"}
|
||||
|
||||
from fastapi import Form
|
||||
|
||||
@app.post("/bulk-download")
|
||||
async def bulk_download(filenames: str = Form(...)):
|
||||
"""Download multiple PDF files as a zip archive"""
|
||||
try:
|
||||
import zipfile
|
||||
import tempfile
|
||||
import json
|
||||
|
||||
# Parse the JSON string to get the list of filenames
|
||||
filename_list = json.loads(filenames)
|
||||
|
||||
# Create a temporary zip file
|
||||
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_zip:
|
||||
with zipfile.ZipFile(tmp_zip.name, 'w') as zipf:
|
||||
for filename in filename_list:
|
||||
file_data = download_from_s3(S3_BUCKET_NAME, filename)
|
||||
if file_data:
|
||||
zipf.writestr(filename, file_data)
|
||||
|
||||
# Read the zip file
|
||||
with open(tmp_zip.name, 'rb') as f:
|
||||
zip_data = f.read()
|
||||
|
||||
# Clean up temporary file
|
||||
import os
|
||||
os.unlink(tmp_zip.name)
|
||||
|
||||
# Return streaming response with zip data
|
||||
zip_filename = f"math_exercises_{len(filename_list)}_files.zip"
|
||||
return StreamingResponse(
|
||||
io.BytesIO(zip_data),
|
||||
media_type='application/zip',
|
||||
headers={'Content-Disposition': f'attachment; filename="{zip_filename}"'}
|
||||
)
|
||||
except Exception as e:
|
||||
return {"error": f"Error downloading files: {str(e)}"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
+308
-1
@@ -36,6 +36,8 @@
|
||||
<p class="lead">Créez des exercices de multiplication et division personnalisés en PDF</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Paramètres des Exercices</h5>
|
||||
@@ -68,6 +70,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Existing PDFs Section -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0">Fichiers PDF Existants</h5>
|
||||
<button id="refreshListBtn" class="btn btn-outline-primary btn-sm">
|
||||
<span id="refreshText">Actualiser</span>
|
||||
<span id="refreshSpinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button id="downloadSelectedBtn" class="btn btn-success btn-sm" disabled>
|
||||
Télécharger Sélectionnés
|
||||
</button>
|
||||
<button id="deleteSelectedBtn" class="btn btn-danger btn-sm" disabled>
|
||||
Supprimer Sélectionnés
|
||||
</button>
|
||||
<button id="selectAllBtn" class="btn btn-outline-secondary btn-sm">
|
||||
Tout Sélectionner
|
||||
</button>
|
||||
<button id="clearSelectionBtn" class="btn btn-outline-secondary btn-sm" disabled>
|
||||
Désélectionner
|
||||
</button>
|
||||
</div>
|
||||
<div id="pdfListContainer" class="mt-3">
|
||||
<p class="text-muted">Cliquez sur "Actualiser" pour charger la liste des fichiers existants.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resultContainer" class="exercise-result d-none">
|
||||
<h5>Résultat</h5>
|
||||
<div id="resultMessage"></div>
|
||||
@@ -145,8 +177,280 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Current page for pagination
|
||||
let currentPage = 1;
|
||||
const pageSize = 10;
|
||||
|
||||
// Function to load and display existing PDFs with pagination (sorted from newest to oldest)
|
||||
async function loadPdfList(page = 1) {
|
||||
const refreshBtn = document.getElementById('refreshListBtn');
|
||||
const refreshText = document.getElementById('refreshText');
|
||||
const refreshSpinner = document.getElementById('refreshSpinner');
|
||||
const pdfListContainer = document.getElementById('pdfListContainer');
|
||||
|
||||
// Show loading state
|
||||
refreshText.textContent = 'Chargement...';
|
||||
refreshSpinner.classList.remove('d-none');
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/list?page=${page}&page_size=${pageSize}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
pdfListContainer.innerHTML = `<div class="alert alert-danger">${result.error}</div>`;
|
||||
} else {
|
||||
if (result.files.length === 0) {
|
||||
pdfListContainer.innerHTML = '<p class="text-muted">Aucun fichier PDF trouvé.</p>';
|
||||
} else {
|
||||
let html = '<div class="table-responsive"><table class="table table-striped table-hover">';
|
||||
html += '<thead><tr><th><input type="checkbox" id="selectAllHeader"></th><th>Nom du fichier</th><th>Date de création</th><th>Taille</th><th>Actions</th></tr></thead><tbody>';
|
||||
|
||||
result.files.forEach(file => {
|
||||
// Format date
|
||||
const date = new Date(file.LastModified);
|
||||
const formattedDate = date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
// Format size
|
||||
const sizeInKB = Math.round(file.Size / 1024);
|
||||
|
||||
html += '<tr>';
|
||||
html += `<td><input type="checkbox" class="file-checkbox" data-filename="${file.Key}"></td>`;
|
||||
html += `<td>${file.Key}</td>`;
|
||||
html += `<td>${formattedDate}</td>`;
|
||||
html += `<td>${sizeInKB} KB</td>`;
|
||||
html += `<td>
|
||||
<a href="/download/${encodeURIComponent(file.Key)}" class="btn btn-success btn-sm me-2">Télécharger</a>
|
||||
<button class="btn btn-danger btn-sm delete-btn" data-filename="${file.Key}">Supprimer</button>
|
||||
</td>`;
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
|
||||
// Add pagination controls if needed
|
||||
if (result.pagination.total_pages > 1) {
|
||||
html += '<nav aria-label="Pagination des fichiers"><ul class="pagination justify-content-center">';
|
||||
|
||||
// Previous button
|
||||
if (result.pagination.has_prev) {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${result.pagination.current_page - 1}">Précédent</a></li>`;
|
||||
} else {
|
||||
html += '<li class="page-item disabled"><span class="page-link">Précédent</span></li>';
|
||||
}
|
||||
|
||||
// Page numbers
|
||||
for (let i = 1; i <= result.pagination.total_pages; i++) {
|
||||
if (i === result.pagination.current_page) {
|
||||
html += `<li class="page-item active"><span class="page-link">${i}</span></li>`;
|
||||
} else {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${i}">${i}</a></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Next button
|
||||
if (result.pagination.has_next) {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${result.pagination.current_page + 1}">Suivant</a></li>`;
|
||||
} else {
|
||||
html += '<li class="page-item disabled"><span class="page-link">Suivant</span></li>';
|
||||
}
|
||||
|
||||
html += '</ul></nav>';
|
||||
|
||||
// Display total count
|
||||
html += `<div class="text-center text-muted small">
|
||||
Affichage ${(result.pagination.current_page - 1) * pageSize + 1}-${Math.min(result.pagination.current_page * pageSize, result.pagination.total_files)}
|
||||
sur ${result.pagination.total_files} fichiers
|
||||
</div>`;
|
||||
}
|
||||
|
||||
pdfListContainer.innerHTML = html;
|
||||
|
||||
// Add event listeners to pagination links
|
||||
document.querySelectorAll('.page-link[data-page]').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const page = parseInt(this.getAttribute('data-page'));
|
||||
loadPdfList(page);
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listeners to delete buttons
|
||||
document.querySelectorAll('.delete-btn').forEach(button => {
|
||||
button.addEventListener('click', async function() {
|
||||
const filename = this.getAttribute('data-filename');
|
||||
if (confirm(`Êtes-vous sûr de vouloir supprimer le fichier "${filename}" ?`)) {
|
||||
try {
|
||||
const response = await fetch(`/delete/${encodeURIComponent(filename)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
alert(`Erreur: ${result.error}`);
|
||||
} else {
|
||||
// Refresh the list (stay on current page)
|
||||
loadPdfList(currentPage);
|
||||
alert(result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Erreur lors de la suppression: ${error.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add event listener for select all checkbox in header
|
||||
document.getElementById('selectAllHeader').addEventListener('change', function() {
|
||||
const checkboxes = document.querySelectorAll('.file-checkbox');
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.checked = this.checked;
|
||||
});
|
||||
updateBulkButtons();
|
||||
});
|
||||
|
||||
// Add event listeners to individual checkboxes
|
||||
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateBulkButtons);
|
||||
});
|
||||
|
||||
// Update current page
|
||||
currentPage = page;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
pdfListContainer.innerHTML = `<div class="alert alert-danger">Erreur lors du chargement de la liste: ${error.message}</div>`;
|
||||
} finally {
|
||||
// Reset button state
|
||||
refreshText.textContent = 'Actualiser';
|
||||
refreshSpinner.classList.add('d-none');
|
||||
refreshBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listener for refresh button
|
||||
document.getElementById('refreshListBtn').addEventListener('click', function() {
|
||||
loadPdfList(1); // Reset to first page on refresh
|
||||
});
|
||||
|
||||
// Function to update bulk action buttons state
|
||||
function updateBulkButtons() {
|
||||
const selectedCount = document.querySelectorAll('.file-checkbox:checked').length;
|
||||
document.getElementById('downloadSelectedBtn').disabled = selectedCount === 0;
|
||||
document.getElementById('deleteSelectedBtn').disabled = selectedCount === 0;
|
||||
document.getElementById('clearSelectionBtn').disabled = selectedCount === 0;
|
||||
|
||||
// Update select all header checkbox
|
||||
const allChecked = selectedCount === document.querySelectorAll('.file-checkbox').length && selectedCount > 0;
|
||||
document.getElementById('selectAllHeader').checked = allChecked;
|
||||
}
|
||||
|
||||
// Load PDF list on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadPdfList(currentPage);
|
||||
});
|
||||
|
||||
// Event listener for select all button
|
||||
document.getElementById('selectAllBtn').addEventListener('click', function() {
|
||||
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = true;
|
||||
});
|
||||
updateBulkButtons();
|
||||
});
|
||||
|
||||
// Event listener for clear selection button
|
||||
document.getElementById('clearSelectionBtn').addEventListener('click', function() {
|
||||
document.querySelectorAll('.file-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
updateBulkButtons();
|
||||
});
|
||||
|
||||
// Event listener for download selected button
|
||||
document.getElementById('downloadSelectedBtn').addEventListener('click', async function() {
|
||||
const selectedFiles = Array.from(document.querySelectorAll('.file-checkbox:checked'))
|
||||
.map(checkbox => checkbox.getAttribute('data-filename'));
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
alert('Veuillez sélectionner au moins un fichier à télécharger.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a form to submit the request (needed for file download)
|
||||
const formData = new FormData();
|
||||
formData.append('filenames', JSON.stringify(selectedFiles));
|
||||
|
||||
// Create a hidden form and submit it
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/bulk-download';
|
||||
form.style.display = 'none';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'filenames';
|
||||
input.value = JSON.stringify(selectedFiles);
|
||||
form.appendChild(input);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
} catch (error) {
|
||||
alert(`Erreur lors du téléchargement: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Event listener for delete selected button
|
||||
document.getElementById('deleteSelectedBtn').addEventListener('click', async function() {
|
||||
const selectedFiles = Array.from(document.querySelectorAll('.file-checkbox:checked'))
|
||||
.map(checkbox => checkbox.getAttribute('data-filename'));
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
alert('Veuillez sélectionner au moins un fichier à supprimer.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Êtes-vous sûr de vouloir supprimer ${selectedFiles.length} fichier(s) ?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create form data
|
||||
const formData = new FormData();
|
||||
formData.append('filenames', JSON.stringify(selectedFiles));
|
||||
|
||||
const response = await fetch('/bulk-delete', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.error) {
|
||||
alert(`Erreur: ${result.error}`);
|
||||
} else {
|
||||
// Refresh the list (stay on current page)
|
||||
loadPdfList(currentPage);
|
||||
let message = result.message;
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
message += `\nErreurs pour les fichiers: ${result.errors.join(', ')}`;
|
||||
}
|
||||
alert(message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Erreur lors de la suppression: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('exerciseForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -181,6 +485,9 @@
|
||||
// Automatically trigger download
|
||||
window.location.href = response.url;
|
||||
|
||||
// Refresh the PDF list (reset to first page)
|
||||
setTimeout(() => loadPdfList(1), 1000);
|
||||
|
||||
// Show success message
|
||||
const resultContainer = document.getElementById('resultContainer');
|
||||
const resultMessage = document.getElementById('resultMessage');
|
||||
|
||||
@@ -33,4 +33,4 @@ spec:
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
cpu: "1"
|
||||
@@ -17,7 +17,7 @@ patchesStrategicMerge:
|
||||
images:
|
||||
- name: math-exercises
|
||||
newName: harbor.cl1.parano.ch/library/math-exercice
|
||||
newTag: 1.0.0
|
||||
newTag: 1.0.2
|
||||
|
||||
# Production-specific labels
|
||||
commonLabels:
|
||||
|
||||
Reference in New Issue
Block a user