From 5d901cb8b238cbc9894a8f4d4a77c7b6fc33523f Mon Sep 17 00:00:00 2001 From: Esenjin Date: Wed, 15 Jan 2025 17:25:27 +0100 Subject: [PATCH] =?UTF-8?q?am=C3=A9liorations=20sur=20le=20t=C3=A9l=C3=A9v?= =?UTF-8?q?ersement=20multiple?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.php | 51 +++++++++--- js/UploadZone.jsx | 202 +++++++++++++++++++++------------------------- 2 files changed, 130 insertions(+), 123 deletions(-) diff --git a/api.php b/api.php index fd92937..ed54a64 100644 --- a/api.php +++ b/api.php @@ -2,8 +2,12 @@ define('CYLA_CORE', true); require_once 'core.php'; +// Augmenter la limite de temps d'exécution pour les uploads multiples +set_time_limit(300); // 5 minutes + // Vérifier si l'utilisateur est connecté if (!Cyla::isLoggedIn()) { + header('Content-Type: application/json'); http_response_code(401); echo json_encode(['error' => 'Non autorisé']); exit; @@ -11,6 +15,7 @@ if (!Cyla::isLoggedIn()) { // Vérifier le token CSRF if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) { + header('Content-Type: application/json'); http_response_code(403); echo json_encode(['error' => 'Token CSRF invalide']); exit; @@ -18,18 +23,35 @@ if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token']) // Gérer l'upload de fichier if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) { - $validation = Cyla::validateUpload($_FILES['file']); + header('Content-Type: application/json'); - if (!$validation['valid']) { - http_response_code(400); - echo json_encode(['error' => $validation['error']]); - exit; - } + try { + // Vérification de l'espace disque disponible + $uploadDir = UPLOAD_DIR; + $freeSpace = disk_free_space($uploadDir); + if ($freeSpace < $_FILES['file']['size']) { + throw new Exception('Espace disque insuffisant'); + } + + $validation = Cyla::validateUpload($_FILES['file']); + if (!$validation['valid']) { + throw new Exception($validation['error']); + } + + $filename = Cyla::generateUniqueFilename($_FILES['file']['name']); + $destination = $uploadDir . $filename; + + // Upload avec gestion de la mémoire + if (!move_uploaded_file($_FILES['file']['tmp_name'], $destination)) { + throw new Exception('Erreur lors du déplacement du fichier'); + } + + // Vérifier l'intégrité du fichier + if (!file_exists($destination) || filesize($destination) !== $_FILES['file']['size']) { + unlink($destination); // Nettoyer en cas d'erreur + throw new Exception('Erreur d\'intégrité du fichier'); + } - $filename = Cyla::generateUniqueFilename($_FILES['file']['name']); - $destination = UPLOAD_DIR . $filename; - - if (move_uploaded_file($_FILES['file']['tmp_name'], $destination)) { echo json_encode([ 'success' => true, 'file' => [ @@ -38,9 +60,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) { 'url' => 'share.php?file=' . urlencode($filename) ] ]); - } else { - http_response_code(500); - echo json_encode(['error' => 'Erreur lors de l\'upload du fichier']); + } catch (Exception $e) { + http_response_code(400); + echo json_encode([ + 'error' => $e->getMessage(), + 'details' => error_get_last() + ]); } exit; } diff --git a/js/UploadZone.jsx b/js/UploadZone.jsx index 4da6030..6e800d5 100644 --- a/js/UploadZone.jsx +++ b/js/UploadZone.jsx @@ -1,25 +1,16 @@ -import React, { useState, useRef } from 'react'; -import { X, Upload, CheckCircle, AlertCircle } from 'lucide-react'; +import React, { useState, useRef, useCallback } from 'react'; +import { X, Upload, CheckCircle, AlertCircle, Loader } from 'lucide-react'; const UploadZone = () => { const [isDragging, setIsDragging] = useState(false); const [files, setFiles] = useState([]); const fileInputRef = useRef(null); - const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 Mo - const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt']; + const MAX_FILES = 10; // Limite stricte à 10 fichiers + const MAX_FILE_SIZE = 100 * 1024 * 1024; + const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt']; - const handleDragOver = (e) => { - e.preventDefault(); - setIsDragging(true); - }; - - const handleDragLeave = (e) => { - e.preventDefault(); - setIsDragging(false); - }; - - const validateFile = (file) => { + const validateFile = useCallback((file) => { const extension = file.name.split('.').pop().toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(extension)) { return { valid: false, error: `Extension .${extension} non autorisée` }; @@ -28,95 +19,86 @@ const UploadZone = () => { return { valid: false, error: 'Fichier trop volumineux (max 100 Mo)' }; } return { valid: true, error: null }; - }; - - const handleDrop = (e) => { - e.preventDefault(); - setIsDragging(false); - - const droppedFiles = Array.from(e.dataTransfer.files); - addFiles(droppedFiles); - }; - - const handleFileSelect = (e) => { - const selectedFiles = Array.from(e.target.files); - addFiles(selectedFiles); - }; - - const addFiles = (newFiles) => { - const processedFiles = newFiles.map(file => { - const validation = validateFile(file); - return { - file, - id: Math.random().toString(36).substring(7), - status: validation.valid ? 'pending' : 'error', - error: validation.error, - progress: 0 - }; - }); - - setFiles(currentFiles => [...currentFiles, ...processedFiles]); - }; - - const removeFile = (fileId) => { - setFiles(files => files.filter(f => f.id !== fileId)); - }; + }, []); const uploadFile = async (fileInfo) => { - const formData = new FormData(); - formData.append('file', fileInfo.file); - formData.append('action', 'upload'); - try { - // Simuler un upload progressif pour la démo - await new Promise(resolve => { - let progress = 0; - const interval = setInterval(() => { - progress += 10; - setFiles(files => - files.map(f => - f.id === fileInfo.id - ? { ...f, progress, status: progress === 100 ? 'complete' : 'uploading' } - : f - ) - ); - if (progress >= 100) { - clearInterval(interval); - resolve(); - } - }, 500); + const formData = new FormData(); + formData.append('file', fileInfo.file); + formData.append('csrf_token', document.querySelector('input[name="csrf_token"]')?.value || ''); + + // Mettre le statut en uploading + setFiles(files => + files.map(f => + f.id === fileInfo.id ? { ...f, status: 'uploading' } : f + ) + ); + + const response = await fetch('api.php', { + method: 'POST', + body: formData }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + throw new Error(result.error || 'Erreur lors du téléversement'); + } + + setFiles(files => + files.map(f => + f.id === fileInfo.id ? { ...f, status: 'complete' } : f + ) + ); } catch (error) { setFiles(files => files.map(f => f.id === fileInfo.id - ? { ...f, status: 'error', error: 'Erreur lors du téléversement' } + ? { ...f, status: 'error', error: error.message } : f ) ); } }; - const uploadAllFiles = () => { - const pendingFiles = files.filter(f => f.status === 'pending'); - pendingFiles.forEach(uploadFile); + const handleFiles = (newFiles) => { + // Vérifier si on n'a pas déjà atteint la limite + if (files.length >= MAX_FILES) { + alert(`Vous pouvez téléverser uniquement ${MAX_FILES} fichiers à la fois`); + return; + } + + // Calculer combien de fichiers on peut encore ajouter + const remainingSlots = MAX_FILES - files.length; + const filesToAdd = Array.from(newFiles).slice(0, remainingSlots); + + if (filesToAdd.length < newFiles.length) { + alert(`Seuls les ${remainingSlots} premiers fichiers seront traités. Maximum ${MAX_FILES} fichiers à la fois.`); + } + + const processedFiles = filesToAdd.map(file => ({ + file, + id: Math.random().toString(36).substring(7), + status: validateFile(file).valid ? 'pending' : 'error', + error: validateFile(file).error + })); + + setFiles(current => [...current, ...processedFiles]); + + // Lancer l'upload pour chaque fichier valide + processedFiles + .filter(f => f.status === 'pending') + .forEach(uploadFile); }; - const getStatusColor = (status) => { - switch (status) { - case 'complete': return 'text-green-500'; - case 'error': return 'text-red-500'; - case 'uploading': return 'text-blue-500'; - default: return 'text-gray-500'; - } - }; + const handleDrop = useCallback((e) => { + e.preventDefault(); + setIsDragging(false); + handleFiles(e.dataTransfer.files); + }, []); - const getStatusIcon = (status) => { - switch (status) { - case 'complete': return ; - case 'error': return ; - default: return null; - } + const removeFile = (id) => { + setFiles(files => files.filter(f => f.id !== id)); }; return ( @@ -125,8 +107,14 @@ const UploadZone = () => { className={`relative border-2 border-dashed rounded-lg p-8 text-center mb-4 transition-colors ${isDragging ? 'border-primary-500 bg-primary-50' : 'border-gray-600'} hover:border-primary-500`} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} + onDragOver={e => { + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={e => { + e.preventDefault(); + setIsDragging(false); + }} onDrop={handleDrop} onClick={() => fileInputRef.current?.click()} > @@ -134,7 +122,7 @@ const UploadZone = () => { ref={fileInputRef} type="file" multiple - onChange={handleFileSelect} + onChange={e => handleFiles(e.target.files)} className="hidden" accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')} /> @@ -147,20 +135,16 @@ const UploadZone = () => { ou cliquez pour sélectionner des fichiers

- Max 100 Mo par fichier · {ALLOWED_EXTENSIONS.join(', ')} + Maximum {MAX_FILES} fichiers à la fois · 100 Mo par fichier +
+ Extensions : {ALLOWED_EXTENSIONS.join(', ')}

{files.length > 0 && (
-
-

Files ({files.length})

- +
+

Fichiers ({files.length}/{MAX_FILES})

@@ -176,22 +160,20 @@ const UploadZone = () => { )}
- {fileInfo.status === 'uploading' && ( -
-
-
- )} - -
- {getStatusIcon(fileInfo.status)} +
+ {fileInfo.status === 'complete' && } + {fileInfo.status === 'error' && } + {fileInfo.status === 'uploading' && }