améliorations sur le téléversement multiple
This commit is contained in:
parent
f8675a312b
commit
5d901cb8b2
49
api.php
49
api.php
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user