Initial commit: FuZip - Application de fusion interactive de fichiers ZIP

- Backend PHP: architecture MVC avec API REST (upload, merge, preview, extract)
  - Frontend JavaScript: composants modulaires (arborescence, upload, themes, i18n)
  - Fonctionnalités: drag&drop, sélection exclusive, détection conflits, persistance état
  - Sécurité: validation stricte, isolation sessions, sanitization chemins
  - UI/UX: responsive, thèmes clair/sombre, multi-langue (FR/EN)
  - Documentation: README complet avec installation et utilisation
This commit is contained in:
2026-01-12 03:29:01 +01:00
commit bd6d321ed7
24 changed files with 6463 additions and 0 deletions

82
api/cleanup.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
/**
* API Cleanup - Nettoyage des sessions expirées
*
* Endpoint : GET /api/cleanup.php
*
* Paramètres : Aucun
*
* Réponse JSON :
* {
* "success": true,
* "deleted_count": 3,
* "message": "3 session(s) expirée(s) supprimée(s)"
* }
*
* Note : Cette API est appelée automatiquement à chaque initialisation de session,
* mais peut aussi être appelée manuellement pour forcer un nettoyage.
*/
header('Content-Type: application/json; charset=utf-8');
// CORS
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0);
}
require_once __DIR__ . '/../core/Config.php';
require_once __DIR__ . '/../core/SessionManager.php';
function sendResponse(array $data, int $httpCode = 200): void {
http_response_code($httpCode);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
function sendError(string $message, int $httpCode = 400): void {
Config::log("Erreur API cleanup : {$message}", 'ERROR');
sendResponse([
'success' => false,
'error' => $message
], $httpCode);
}
// Vérifier la méthode HTTP
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
sendError('Méthode non autorisée. Utilisez GET.', 405);
}
Config::log("Cleanup API - Nettoyage manuel demandé");
try {
// Lancer le nettoyage
$deletedCount = SessionManager::cleanOldSessions();
$message = $deletedCount > 0
? "{$deletedCount} session(s) expirée(s) supprimée(s)"
: "Aucune session expirée à supprimer";
Config::log("Cleanup terminé : {$message}");
sendResponse([
'success' => true,
'deleted_count' => $deletedCount,
'message' => $message,
'session_lifetime_hours' => Config::SESSION_LIFETIME / 3600
]);
} catch (Exception $e) {
sendError('Erreur lors du nettoyage : ' . $e->getMessage(), 500);
}

153
api/extract.php Normal file
View File

@@ -0,0 +1,153 @@
<?php
/**
* API Extract - Extraction et téléchargement d'un fichier individuel
*
* Endpoint : GET /api/extract.php
*
* Paramètres :
* - side : 'left' ou 'right'
* - path : Chemin du fichier dans le ZIP
*
* Réponse :
* - Stream direct du fichier (Content-Disposition: attachment)
* - OU JSON avec erreur si échec
*
* Fonctionnalité : Option D du plan (Décompression fichiers individuels)
*/
require_once __DIR__ . '/../core/Config.php';
require_once __DIR__ . '/../core/SessionManager.php';
require_once __DIR__ . '/../core/ZipHandler.php';
function sendError(string $message, int $httpCode = 400): void {
http_response_code($httpCode);
header('Content-Type: application/json; charset=utf-8');
Config::log("Erreur API extract : {$message}", 'ERROR');
echo json_encode([
'success' => false,
'error' => $message
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
// CORS
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0);
}
// Vérifier la méthode HTTP
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
sendError('Méthode non autorisée. Utilisez GET.', 405);
}
// Initialiser la session
try {
$sessionId = SessionManager::init();
Config::log("Extract API - Session : {$sessionId}");
} catch (Exception $e) {
sendError('Erreur initialisation session : ' . $e->getMessage(), 500);
}
// Vérifier les paramètres
if (!isset($_GET['side']) || !in_array($_GET['side'], ['left', 'right'])) {
sendError('Paramètre "side" manquant ou invalide. Valeurs acceptées : "left", "right".');
}
if (!isset($_GET['path']) || empty($_GET['path'])) {
sendError('Paramètre "path" manquant. Spécifiez le chemin du fichier dans le ZIP.');
}
$side = $_GET['side'];
$filePath = $_GET['path'];
// Sanitize le chemin pour la sécurité
$filePath = Config::sanitizePath($filePath);
Config::log("Extraction demandée : {$filePath} depuis {$side}");
$uploadDir = SessionManager::getUploadDir($sessionId);
$zipPath = $uploadDir . $side . '.zip';
// Vérifier que le ZIP existe
if (!file_exists($zipPath)) {
sendError("ZIP '{$side}' non uploadé.", 404);
}
// Extraire le fichier
$zipHandler = new ZipHandler();
$tempOutputPath = $uploadDir . 'temp_extract_' . basename($filePath);
try {
$success = $zipHandler->extractFile($zipPath, $filePath, $tempOutputPath);
if (!$success) {
throw new Exception("Fichier introuvable dans le ZIP");
}
// Vérifier que le fichier a été extrait
if (!file_exists($tempOutputPath)) {
throw new Exception("Échec extraction du fichier");
}
$fileSize = filesize($tempOutputPath);
Config::log("Fichier extrait : " . Config::formatBytes($fileSize));
// Mettre à jour le timestamp
SessionManager::updateAccess($sessionId);
// Déterminer le type MIME
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $tempOutputPath);
finfo_close($finfo);
// Nom de fichier pour le téléchargement
$downloadName = basename($filePath);
// Headers pour téléchargement
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
header('Content-Length: ' . $fileSize);
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
// Stream le fichier
$handle = fopen($tempOutputPath, 'rb');
if ($handle === false) {
throw new Exception("Impossible d'ouvrir le fichier extrait");
}
while (!feof($handle)) {
echo fread($handle, Config::STREAM_BUFFER_SIZE);
flush();
}
fclose($handle);
// Nettoyer le fichier temporaire
unlink($tempOutputPath);
Config::log("Fichier téléchargé : {$downloadName}");
exit;
} catch (Exception $e) {
// Nettoyer le fichier temporaire en cas d'erreur
if (file_exists($tempOutputPath)) {
unlink($tempOutputPath);
}
sendError('Erreur lors de l\'extraction : ' . $e->getMessage(), 500);
}

166
api/merge.php Normal file
View File

@@ -0,0 +1,166 @@
<?php
/**
* API Merge - Fusion de ZIP et téléchargement
*
* Endpoint : POST /api/merge.php
*
* Paramètres (JSON body) :
* {
* "selection": {
* "path/to/file": "left"|"right"|null,
* ...
* }
* }
*
* Réponse :
* - Stream direct du fichier ZIP fusionné (Content-Disposition: attachment)
* - OU JSON avec erreur si échec
*/
// Augmenter les limites PHP pour les gros fichiers
set_time_limit(300); // 5 minutes max
ini_set('memory_limit', '512M'); // 512 MB de mémoire
require_once __DIR__ . '/../core/Config.php';
require_once __DIR__ . '/../core/SessionManager.php';
require_once __DIR__ . '/../core/ZipHandler.php';
require_once __DIR__ . '/../core/FileTree.php';
function sendError(string $message, int $httpCode = 400): void {
http_response_code($httpCode);
header('Content-Type: application/json; charset=utf-8');
Config::log("Erreur API merge : {$message}", 'ERROR');
echo json_encode([
'success' => false,
'error' => $message
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
// CORS
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0);
}
// Vérifier la méthode HTTP
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendError('Méthode non autorisée. Utilisez POST.', 405);
}
// Initialiser la session
try {
$sessionId = SessionManager::init();
Config::log("Merge API - Session : {$sessionId}");
} catch (Exception $e) {
sendError('Erreur initialisation session : ' . $e->getMessage(), 500);
}
// Récupérer le body JSON
$input = file_get_contents('php://input');
if (empty($input)) {
sendError('Body JSON manquant. Envoyez la sélection au format JSON.');
}
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
sendError('JSON invalide : ' . json_last_error_msg());
}
// Vérifier la sélection
if (!isset($data['selection']) || !is_array($data['selection'])) {
sendError('Paramètre "selection" manquant ou invalide.');
}
$selection = $data['selection'];
Config::log("Sélection reçue : " . count($selection) . " entrées");
// Valider la sélection
$validation = FileTree::validateSelection($selection);
if (!$validation['valid']) {
$conflicts = implode(', ', $validation['conflicts']);
sendError("Sélection invalide. Conflits détectés : {$conflicts}");
}
$uploadDir = SessionManager::getUploadDir($sessionId);
$leftZipPath = $uploadDir . 'left.zip';
$rightZipPath = $uploadDir . 'right.zip';
// Vérifier que les 2 ZIP existent
if (!file_exists($leftZipPath)) {
sendError('ZIP gauche non uploadé.', 404);
}
if (!file_exists($rightZipPath)) {
sendError('ZIP droite non uploadé.', 404);
}
// Préparer le chemin du ZIP fusionné
$mergedZipPath = $uploadDir . 'merged.zip';
// Fusionner
$zipHandler = new ZipHandler();
try {
Config::log("Début fusion : " . count(array_filter($selection)) . " fichiers sélectionnés");
$result = $zipHandler->merge($leftZipPath, $rightZipPath, $selection, $mergedZipPath);
Config::log("Fusion réussie : {$result}");
// Vérifier que le fichier a été créé
if (!file_exists($mergedZipPath)) {
throw new Exception("Le fichier fusionné n'a pas été créé");
}
$fileSize = filesize($mergedZipPath);
Config::log("ZIP fusionné créé : " . Config::formatBytes($fileSize));
// Mettre à jour le timestamp
SessionManager::updateAccess($sessionId);
// Envoyer le fichier en téléchargement
// Nom de fichier avec timestamp
$timestamp = date('Y-m-d_His');
$filename = "fuzip_merged_{$timestamp}.zip";
// Headers pour téléchargement
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . $fileSize);
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
// Stream le fichier
$handle = fopen($mergedZipPath, 'rb');
if ($handle === false) {
throw new Exception("Impossible d'ouvrir le fichier fusionné");
}
// Envoyer par chunks pour économiser la mémoire
while (!feof($handle)) {
echo fread($handle, Config::STREAM_BUFFER_SIZE);
flush();
}
fclose($handle);
Config::log("ZIP fusionné envoyé : {$filename} (" . Config::formatBytes($fileSize) . ")");
exit;
} catch (Exception $e) {
sendError('Erreur lors de la fusion : ' . $e->getMessage(), 500);
}

135
api/preview.php Normal file
View File

@@ -0,0 +1,135 @@
<?php
/**
* API Preview - Prévisualisation du contenu d'un fichier
*
* Endpoint : GET /api/preview.php
*
* Paramètres :
* - side : 'left' ou 'right'
* - path : Chemin du fichier dans le ZIP
* - max_length : (optionnel) Longueur max en octets (défaut: 10000)
*
* Réponse JSON :
* {
* "success": true,
* "content": "...",
* "is_binary": false,
* "truncated": false
* }
*
* Fonctionnalité : Option A du plan (Prévisualisation contenu fichiers)
*/
header('Content-Type: application/json; charset=utf-8');
// CORS
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0);
}
require_once __DIR__ . '/../core/Config.php';
require_once __DIR__ . '/../core/SessionManager.php';
require_once __DIR__ . '/../core/ZipHandler.php';
function sendResponse(array $data, int $httpCode = 200): void {
http_response_code($httpCode);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
function sendError(string $message, int $httpCode = 400): void {
Config::log("Erreur API preview : {$message}", 'ERROR');
sendResponse([
'success' => false,
'error' => $message
], $httpCode);
}
// Vérifier la méthode HTTP
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
sendError('Méthode non autorisée. Utilisez GET.', 405);
}
// Initialiser la session
try {
$sessionId = SessionManager::init();
Config::log("Preview API - Session : {$sessionId}");
} catch (Exception $e) {
sendError('Erreur initialisation session : ' . $e->getMessage(), 500);
}
// Vérifier les paramètres
if (!isset($_GET['side']) || !in_array($_GET['side'], ['left', 'right'])) {
sendError('Paramètre "side" manquant ou invalide. Valeurs acceptées : "left", "right".');
}
if (!isset($_GET['path']) || empty($_GET['path'])) {
sendError('Paramètre "path" manquant. Spécifiez le chemin du fichier dans le ZIP.');
}
$side = $_GET['side'];
$filePath = $_GET['path'];
$maxLength = isset($_GET['max_length']) ? (int)$_GET['max_length'] : 10000;
// Limiter la longueur max pour éviter surcharge
$maxLength = min($maxLength, 50000); // Max 50 KB
// Sanitize le chemin
$filePath = Config::sanitizePath($filePath);
Config::log("Prévisualisation demandée : {$filePath} depuis {$side} (max: {$maxLength} octets)");
$uploadDir = SessionManager::getUploadDir($sessionId);
$zipPath = $uploadDir . $side . '.zip';
// Vérifier que le ZIP existe
if (!file_exists($zipPath)) {
sendError("ZIP '{$side}' non uploadé.", 404);
}
// Prévisualiser le fichier
$zipHandler = new ZipHandler();
try {
$content = $zipHandler->previewFile($zipPath, $filePath, $maxLength);
if ($content === null) {
throw new Exception("Fichier introuvable dans le ZIP");
}
// Déterminer si c'est un fichier binaire
$isBinary = strpos($content, '[Fichier binaire - prévisualisation impossible]') === 0;
// Déterminer si le contenu a été tronqué
$truncated = strpos($content, '... (tronqué)') !== false;
// Mettre à jour le timestamp
SessionManager::updateAccess($sessionId);
Config::log("Prévisualisation envoyée : " . strlen($content) . " caractères");
sendResponse([
'success' => true,
'file_path' => $filePath,
'side' => $side,
'content' => $content,
'is_binary' => $isBinary,
'truncated' => $truncated,
'content_length' => strlen($content)
]);
} catch (Exception $e) {
sendError('Erreur lors de la prévisualisation : ' . $e->getMessage(), 500);
}

181
api/structure.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
/**
* API Structure - Récupération de structure et détection de conflits
*
* Endpoint : GET /api/structure.php
*
* Paramètres :
* - action : 'get' (structure d'un ZIP) ou 'conflicts' (conflits entre 2 ZIP)
* - side : 'left' ou 'right' (pour action=get uniquement)
*
* Réponse JSON :
* Pour action=get :
* {
* "success": true,
* "structure": {...},
* "stats": {...}
* }
*
* Pour action=conflicts :
* {
* "success": true,
* "conflicts": [...]
* }
*/
header('Content-Type: application/json; charset=utf-8');
// CORS
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0);
}
require_once __DIR__ . '/../core/Config.php';
require_once __DIR__ . '/../core/SessionManager.php';
require_once __DIR__ . '/../core/ZipHandler.php';
require_once __DIR__ . '/../core/FileTree.php';
function sendResponse(array $data, int $httpCode = 200): void {
http_response_code($httpCode);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
function sendError(string $message, int $httpCode = 400): void {
Config::log("Erreur API structure : {$message}", 'ERROR');
sendResponse([
'success' => false,
'error' => $message
], $httpCode);
}
// Vérifier la méthode HTTP
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
sendError('Méthode non autorisée. Utilisez GET.', 405);
}
// Initialiser la session
try {
$sessionId = SessionManager::init();
Config::log("Structure API - Session : {$sessionId}");
} catch (Exception $e) {
sendError('Erreur initialisation session : ' . $e->getMessage(), 500);
}
// Récupérer le paramètre action
$action = $_GET['action'] ?? 'get';
if (!in_array($action, ['get', 'conflicts'])) {
sendError('Paramètre "action" invalide. Valeurs acceptées : "get", "conflicts".');
}
Config::log("Action demandée : {$action}");
$uploadDir = SessionManager::getUploadDir($sessionId);
$zipHandler = new ZipHandler();
// ACTION : get - Récupérer la structure d'un ZIP
if ($action === 'get') {
// Vérifier le paramètre side
if (!isset($_GET['side']) || !in_array($_GET['side'], ['left', 'right'])) {
sendError('Paramètre "side" manquant ou invalide pour action=get.');
}
$side = $_GET['side'];
$zipPath = $uploadDir . $side . '.zip';
// Vérifier que le fichier existe
if (!file_exists($zipPath)) {
sendError("Aucun fichier ZIP uploadé pour le côté '{$side}'. Uploadez d'abord un fichier.", 404);
}
Config::log("Récupération structure : {$side}");
try {
// Extraire la structure
$structure = $zipHandler->getStructure($zipPath);
// Construire l'arborescence
$tree = FileTree::buildTree($structure['files']);
$stats = FileTree::getTreeStats($tree);
// Mettre à jour le timestamp
SessionManager::updateAccess($sessionId);
sendResponse([
'success' => true,
'side' => $side,
'structure' => [
'tree' => $tree,
'files_list' => $structure['files']
],
'stats' => [
'total_files' => $stats['total_files'],
'total_folders' => $stats['total_folders'],
'total_size' => $stats['total_size'],
'total_size_formatted' => Config::formatBytes($stats['total_size']),
'max_depth' => $stats['max_depth']
]
]);
} catch (Exception $e) {
sendError('Erreur lors de l\'extraction : ' . $e->getMessage(), 500);
}
}
// ACTION : conflicts - Détecter les conflits entre les 2 ZIP
if ($action === 'conflicts') {
$leftZipPath = $uploadDir . 'left.zip';
$rightZipPath = $uploadDir . 'right.zip';
// Vérifier que les 2 fichiers existent
if (!file_exists($leftZipPath)) {
sendError('ZIP gauche non uploadé. Uploadez les 2 fichiers avant de détecter les conflits.', 404);
}
if (!file_exists($rightZipPath)) {
sendError('ZIP droite non uploadé. Uploadez les 2 fichiers avant de détecter les conflits.', 404);
}
Config::log("Détection conflits entre left.zip et right.zip");
try {
// Extraire les structures
$leftStructure = $zipHandler->getStructure($leftZipPath);
$rightStructure = $zipHandler->getStructure($rightZipPath);
// Détecter les conflits
$conflicts = FileTree::detectConflicts($leftStructure['files'], $rightStructure['files']);
Config::log("Conflits détectés : " . count($conflicts));
// Mettre à jour le timestamp
SessionManager::updateAccess($sessionId);
sendResponse([
'success' => true,
'conflicts' => $conflicts,
'total_conflicts' => count($conflicts),
'summary' => [
'left_total_files' => $leftStructure['total_files'],
'right_total_files' => $rightStructure['total_files'],
'conflicts_count' => count($conflicts)
]
]);
} catch (Exception $e) {
sendError('Erreur lors de la détection des conflits : ' . $e->getMessage(), 500);
}
}

165
api/upload.php Normal file
View File

@@ -0,0 +1,165 @@
<?php
/**
* API Upload - Gestion de l'upload de fichiers ZIP
*
* Endpoint : POST /api/upload.php
*
* Paramètres :
* - file : Fichier ZIP (multipart/form-data)
* - side : 'left' ou 'right'
*
* Réponse JSON :
* {
* "success": true,
* "structure": {...},
* "stats": {...}
* }
*/
header('Content-Type: application/json; charset=utf-8');
// CORS (si nécessaire pour développement)
if (isset($_SERVER['HTTP_ORIGIN'])) {
header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}");
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
}
if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
}
exit(0);
}
require_once __DIR__ . '/../core/Config.php';
require_once __DIR__ . '/../core/SessionManager.php';
require_once __DIR__ . '/../core/ZipHandler.php';
require_once __DIR__ . '/../core/FileTree.php';
/**
* Envoie une réponse JSON et termine le script
*/
function sendResponse(array $data, int $httpCode = 200): void {
http_response_code($httpCode);
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
/**
* Envoie une erreur JSON
*/
function sendError(string $message, int $httpCode = 400): void {
Config::log("Erreur API upload : {$message}", 'ERROR');
sendResponse([
'success' => false,
'error' => $message
], $httpCode);
}
// Vérifier la méthode HTTP
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendError('Méthode non autorisée. Utilisez POST.', 405);
}
// Initialiser la session
try {
$sessionId = SessionManager::init();
Config::log("Upload API - Session : {$sessionId}");
} catch (Exception $e) {
sendError('Erreur initialisation session : ' . $e->getMessage(), 500);
}
// Vérifier le paramètre 'side'
if (!isset($_POST['side']) || !in_array($_POST['side'], ['left', 'right'])) {
sendError('Paramètre "side" manquant ou invalide. Valeurs acceptées : "left", "right".');
}
$side = $_POST['side'];
Config::log("Upload côté : {$side}");
// Vérifier qu'un fichier a été uploadé
if (!isset($_FILES['file'])) {
sendError('Aucun fichier uploadé. Utilisez le champ "file".');
}
$uploadedFile = $_FILES['file'];
Config::log("Fichier reçu : {$uploadedFile['name']} ({$uploadedFile['size']} octets)");
// Valider le fichier avec ZipHandler
$zipHandler = new ZipHandler();
$validation = $zipHandler->validateUpload($uploadedFile);
if (!$validation['valid']) {
sendError($validation['error'], 400);
}
// Déplacer le fichier vers le dossier de session
try {
$uploadDir = SessionManager::getUploadDir($sessionId);
$targetPath = $uploadDir . $side . '.zip';
// Supprimer l'ancien fichier s'il existe
if (file_exists($targetPath)) {
unlink($targetPath);
Config::log("Ancien fichier {$side}.zip supprimé");
}
if (!move_uploaded_file($uploadedFile['tmp_name'], $targetPath)) {
throw new Exception("Impossible de déplacer le fichier uploadé");
}
Config::log("Fichier sauvegardé : {$targetPath}");
} catch (Exception $e) {
sendError('Erreur lors de la sauvegarde : ' . $e->getMessage(), 500);
}
// Extraire la structure du ZIP
try {
$structure = $zipHandler->getStructure($targetPath);
Config::log("Structure extraite : {$structure['total_files']} fichiers");
// Construire l'arborescence
$tree = FileTree::buildTree($structure['files']);
$stats = FileTree::getTreeStats($tree);
Config::log("Arborescence construite : {$stats['total_files']} fichiers, {$stats['total_folders']} dossiers");
} catch (Exception $e) {
// Supprimer le fichier en cas d'erreur
if (file_exists($targetPath)) {
unlink($targetPath);
}
sendError('Erreur lors de l\'extraction : ' . $e->getMessage(), 500);
}
// Mettre à jour le timestamp de session
SessionManager::updateAccess($sessionId);
// Réponse de succès
sendResponse([
'success' => true,
'message' => 'Fichier ZIP uploadé et analysé avec succès',
'side' => $side,
'session_id' => $sessionId,
'file' => [
'name' => $uploadedFile['name'],
'size' => $uploadedFile['size'],
'size_formatted' => Config::formatBytes($uploadedFile['size'])
],
'structure' => [
'tree' => $tree,
'files_list' => $structure['files']
],
'stats' => [
'total_files' => $stats['total_files'],
'total_folders' => $stats['total_folders'],
'total_size' => $stats['total_size'],
'total_size_formatted' => Config::formatBytes($stats['total_size']),
'max_depth' => $stats['max_depth']
]
], 200);