améliorations sur le téléversement multiple

This commit is contained in:
Esenjin 2025-01-15 17:25:27 +01:00
parent f8675a312b
commit 5d901cb8b2
2 changed files with 130 additions and 123 deletions

49
api.php
View File

@ -2,8 +2,12 @@
define('CYLA_CORE', true); define('CYLA_CORE', true);
require_once 'core.php'; 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é // Vérifier si l'utilisateur est connecté
if (!Cyla::isLoggedIn()) { if (!Cyla::isLoggedIn()) {
header('Content-Type: application/json');
http_response_code(401); http_response_code(401);
echo json_encode(['error' => 'Non autorisé']); echo json_encode(['error' => 'Non autorisé']);
exit; exit;
@ -11,6 +15,7 @@ if (!Cyla::isLoggedIn()) {
// Vérifier le token CSRF // Vérifier le token CSRF
if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) { if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])) {
header('Content-Type: application/json');
http_response_code(403); http_response_code(403);
echo json_encode(['error' => 'Token CSRF invalide']); echo json_encode(['error' => 'Token CSRF invalide']);
exit; exit;
@ -18,18 +23,35 @@ if (!isset($_POST['csrf_token']) || !Cyla::verifyCSRFToken($_POST['csrf_token'])
// Gérer l'upload de fichier // Gérer l'upload de fichier
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$validation = Cyla::validateUpload($_FILES['file']); header('Content-Type: application/json');
if (!$validation['valid']) { try {
http_response_code(400); // Vérification de l'espace disque disponible
echo json_encode(['error' => $validation['error']]); $uploadDir = UPLOAD_DIR;
exit; $freeSpace = disk_free_space($uploadDir);
} if ($freeSpace < $_FILES['file']['size']) {
throw new Exception('Espace disque insuffisant');
}
$filename = Cyla::generateUniqueFilename($_FILES['file']['name']); $validation = Cyla::validateUpload($_FILES['file']);
$destination = UPLOAD_DIR . $filename; 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');
}
if (move_uploaded_file($_FILES['file']['tmp_name'], $destination)) {
echo json_encode([ echo json_encode([
'success' => true, 'success' => true,
'file' => [ 'file' => [
@ -38,9 +60,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
'url' => 'share.php?file=' . urlencode($filename) 'url' => 'share.php?file=' . urlencode($filename)
] ]
]); ]);
} else { } catch (Exception $e) {
http_response_code(500); http_response_code(400);
echo json_encode(['error' => 'Erreur lors de l\'upload du fichier']); echo json_encode([
'error' => $e->getMessage(),
'details' => error_get_last()
]);
} }
exit; exit;
} }

View File

@ -1,25 +1,16 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef, useCallback } from 'react';
import { X, Upload, CheckCircle, AlertCircle } from 'lucide-react'; import { X, Upload, CheckCircle, AlertCircle, Loader } from 'lucide-react';
const UploadZone = () => { const UploadZone = () => {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 Mo const MAX_FILES = 10; // Limite stricte à 10 fichiers
const ALLOWED_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webm', 'mp4', 'wmv', 'mp3', 'flac', 'ogg', 'zip', 'css', 'pdf', 'rar', 'm3u', 'm3u8', 'txt']; 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) => { const validateFile = useCallback((file) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setIsDragging(false);
};
const validateFile = (file) => {
const extension = file.name.split('.').pop().toLowerCase(); const extension = file.name.split('.').pop().toLowerCase();
if (!ALLOWED_EXTENSIONS.includes(extension)) { if (!ALLOWED_EXTENSIONS.includes(extension)) {
return { valid: false, error: `Extension .${extension} non autorisée` }; 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: false, error: 'Fichier trop volumineux (max 100 Mo)' };
} }
return { valid: true, error: null }; 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 uploadFile = async (fileInfo) => {
const formData = new FormData();
formData.append('file', fileInfo.file);
formData.append('action', 'upload');
try { try {
// Simuler un upload progressif pour la démo const formData = new FormData();
await new Promise(resolve => { formData.append('file', fileInfo.file);
let progress = 0; formData.append('csrf_token', document.querySelector('input[name="csrf_token"]')?.value || '');
const interval = setInterval(() => {
progress += 10; // Mettre le statut en uploading
setFiles(files => setFiles(files =>
files.map(f => files.map(f =>
f.id === fileInfo.id f.id === fileInfo.id ? { ...f, status: 'uploading' } : f
? { ...f, progress, status: progress === 100 ? 'complete' : 'uploading' } )
: f );
)
); const response = await fetch('api.php', {
if (progress >= 100) { method: 'POST',
clearInterval(interval); body: formData
resolve();
}
}, 500);
}); });
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) { } catch (error) {
setFiles(files => setFiles(files =>
files.map(f => files.map(f =>
f.id === fileInfo.id f.id === fileInfo.id
? { ...f, status: 'error', error: 'Erreur lors du téléversement' } ? { ...f, status: 'error', error: error.message }
: f : f
) )
); );
} }
}; };
const uploadAllFiles = () => { const handleFiles = (newFiles) => {
const pendingFiles = files.filter(f => f.status === 'pending'); // Vérifier si on n'a pas déjà atteint la limite
pendingFiles.forEach(uploadFile); 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) => { const handleDrop = useCallback((e) => {
switch (status) { e.preventDefault();
case 'complete': return 'text-green-500'; setIsDragging(false);
case 'error': return 'text-red-500'; handleFiles(e.dataTransfer.files);
case 'uploading': return 'text-blue-500'; }, []);
default: return 'text-gray-500';
}
};
const getStatusIcon = (status) => { const removeFile = (id) => {
switch (status) { setFiles(files => files.filter(f => f.id !== id));
case 'complete': return <CheckCircle className="w-5 h-5" />;
case 'error': return <AlertCircle className="w-5 h-5" />;
default: return null;
}
}; };
return ( return (
@ -125,8 +107,14 @@ const UploadZone = () => {
className={`relative border-2 border-dashed rounded-lg p-8 text-center mb-4 transition-colors 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'} ${isDragging ? 'border-primary-500 bg-primary-50' : 'border-gray-600'}
hover:border-primary-500`} hover:border-primary-500`}
onDragOver={handleDragOver} onDragOver={e => {
onDragLeave={handleDragLeave} e.preventDefault();
setIsDragging(true);
}}
onDragLeave={e => {
e.preventDefault();
setIsDragging(false);
}}
onDrop={handleDrop} onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
> >
@ -134,7 +122,7 @@ const UploadZone = () => {
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
multiple multiple
onChange={handleFileSelect} onChange={e => handleFiles(e.target.files)}
className="hidden" className="hidden"
accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')} accept={ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',')}
/> />
@ -147,20 +135,16 @@ const UploadZone = () => {
ou cliquez pour sélectionner des fichiers ou cliquez pour sélectionner des fichiers
</p> </p>
<p className="text-xs text-gray-400 mt-2"> <p className="text-xs text-gray-400 mt-2">
Max 100 Mo par fichier · {ALLOWED_EXTENSIONS.join(', ')} Maximum {MAX_FILES} fichiers à la fois · 100 Mo par fichier
<br />
Extensions : {ALLOWED_EXTENSIONS.join(', ')}
</p> </p>
</div> </div>
{files.length > 0 && ( {files.length > 0 && (
<div className="border border-gray-700 rounded-lg overflow-hidden"> <div className="border border-gray-700 rounded-lg overflow-hidden">
<div className="p-4 bg-gray-800 border-b border-gray-700 flex justify-between items-center"> <div className="p-4 bg-gray-800 border-b border-gray-700">
<h3 className="font-medium">Files ({files.length})</h3> <h3 className="font-medium">Fichiers ({files.length}/{MAX_FILES})</h3>
<button
onClick={uploadAllFiles}
className="btn btn-sm bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded"
>
Tout téléverser
</button>
</div> </div>
<div className="divide-y divide-gray-700"> <div className="divide-y divide-gray-700">
@ -176,22 +160,20 @@ const UploadZone = () => {
)} )}
</div> </div>
{fileInfo.status === 'uploading' && ( <div className={`
<div className="w-32 h-2 bg-gray-700 rounded-full overflow-hidden"> ${fileInfo.status === 'complete' ? 'text-green-500' : ''}
<div ${fileInfo.status === 'error' ? 'text-red-500' : ''}
className="h-full bg-blue-500 transition-all duration-300" ${fileInfo.status === 'uploading' ? 'text-blue-500' : ''}
style={{ width: `${fileInfo.progress}%` }} `}>
/> {fileInfo.status === 'complete' && <CheckCircle className="w-5 h-5" />}
</div> {fileInfo.status === 'error' && <AlertCircle className="w-5 h-5" />}
)} {fileInfo.status === 'uploading' && <Loader className="w-5 h-5 animate-spin" />}
<div className={getStatusColor(fileInfo.status)}>
{getStatusIcon(fileInfo.status)}
</div> </div>
<button <button
onClick={() => removeFile(fileInfo.id)} onClick={() => removeFile(fileInfo.id)}
className="p-1 hover:bg-gray-700 rounded" className="p-1 hover:bg-gray-700 rounded"
title={fileInfo.status === 'uploading' ? 'Annuler' : 'Supprimer'}
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>