From 4873117ec6aeb86ef169cbf8750123ca24041cdf Mon Sep 17 00:00:00 2001 From: polo Date: Mon, 28 Mar 2022 03:45:38 +0200 Subject: upload gros zip AJAX --- controller/backup.php | 145 +++++++++++++++++++++++++++++--------------- controller/installation.php | 12 +++- dependances.php | 6 +- index.php | 40 +++++++++++- model/Article.php | 2 +- model/Image.php | 8 ++- public/css/accueil.css | 58 ++++++------------ public/file_upload.js | 103 +++++++++++++++++++++++++++++++ public/main.js | 46 +------------- view/backup.php | 10 +-- view/template.php | 4 +- 11 files changed, 283 insertions(+), 151 deletions(-) create mode 100644 public/file_upload.js diff --git a/controller/backup.php b/controller/backup.php index 31974d0..b1a2491 100644 --- a/controller/backup.php +++ b/controller/backup.php @@ -2,7 +2,7 @@ // controller/backup.php // note: faire que cette fonction soit exécutée automatiquement de temps en temps -function sauvegarder($from) +function pageSauvegarde($from) { $title = 'extraction des données'; @@ -21,7 +21,7 @@ function sauvegarder($from) require('view/backup.php'); } -function creerMelainePHP() +function createZipMelainePHP() { $cheminDestination = 'data/'; $nomFichier = "melainePHP"; @@ -99,75 +99,124 @@ function createZip($destinationPath, $zipFileName, array $targetDirectories, arr } -function restaurer($from) +function restoreData($path) { - $title = 'Restauration des données'; - $message = ''; global $archiveFormat; + $message = ''; - // recharger la même page en écrivant les données - if(isset($_FILES['archive']) && $_FILES['archive']['error'] == 0) + // détecter le format (zip ou autre) + if($archiveFormat == 'zip') { - // détecter le format (zip ou autre) - if($archiveFormat == 'zip') + // extraction + try { - // une copie du zip est conservée dans data/ au cas où - move_uploaded_file($_FILES['archive']['tmp_name'], 'data/' . $_FILES['archive']['name']); - chmod('data/' . $_FILES['archive']['name'], 0666); - - $nomFichier = 'data/' . $_FILES['archive']['name']; - - // extraction - try - { - $Zip = new ZipArchive(); - } - catch (Throwable $e) // l'extension zip n'est pas activée - { - echo($e); - die(); - } - if($Zip->open($nomFichier, ZipArchive::RDONLY) === TRUE) + $Zip = new ZipArchive(); + } + catch (Throwable $e) // l'extension zip n'est pas activée + { + echo($e); + die(); + } + if($Zip->open($path, ZipArchive::RDONLY) === TRUE) + { + $j = 0; + for($i = 0; $i < $Zip->numFiles; $i++) { - $j = 0; - for($i = 0; $i < $Zip->numFiles; $i++) + $nomEntree = $Zip->getNameIndex($i); + if($Zip->extractTo('.', $nomEntree) === TRUE) { - $nomEntree = $Zip->getNameIndex($i); - if($Zip->extractTo('.', $nomEntree) === TRUE) - { - $j++; - } - else - { - $message = '

Erreur: extraction du zip impossible.

'; - } - chmod($nomEntree, 0666); + $j++; } - - if($j == $Zip->numFiles) + else { - $message = '

Restauration réussie !!

'; + $message = '

Erreur: extraction du zip impossible.

'; } - $Zip->close(); + chmod($nomEntree, 0666); } - else + + if($j == $Zip->numFiles) { - // mauvais fichier - $message = '

Erreur: Impossible d\'ouvrir l\'archive Zip."

'; + $message = '

Restauration réussie !!

'; } + $Zip->close(); } else { - // pas de module zip - $message = '

Erreur: Veuillez activer l\'extension zip dans le php.ini pour pouvoir gérer les sauvegardes.

'; + // mauvais fichier + $message = '

Erreur: Impossible d\'ouvrir l\'archive Zip."

'; } } + else + { + // pas de module zip + $message = '

Erreur: Veuillez activer l\'extension zip dans le php.ini pour pouvoir gérer les sauvegardes.

'; + } + + return($message); +} + +function pageRestauration($from) +{ + $title = 'Restauration des données'; + $message = ''; + global $maxWeight; + + // recharger la même page en écrivant les données + if(isset($_SESSION['fileName']) && isset($_SESSION['fileSize']) && $_SESSION['fileSize'] > $maxWeight) + { + // une copie du zip est conservée dans data/ au cas où + $path = 'data/' . $_SESSION['fileName']; + rename('data/tmp/' . $_SESSION['fileName'], $path); + chmod($path, 0666); + + // enregistrement + $message = restoreData($path); + } + + // variables obtenues en AJAX + if(isset($_SESSION['fileName'])) + { + unset($_SESSION['fileName']); + } + if(isset($_SESSION['fileSize'])) + { + unset($_SESSION['fileSize']); + } + + if(isset($_FILES['archive']) && $_FILES['archive']['error'] == 0) + { + // une copie du zip est conservée dans data/ au cas où + $path = 'data/' . $_FILES['archive']['name']; + move_uploaded_file($_FILES['archive']['tmp_name'], $path); + chmod($path, 0666); + + // enregistrement + $message = restoreData($path); + } elseif(isset($_FILES['archive']) && $_FILES['archive']['error'] != 0) { $message = '

Erreur: Le fichier n\'a pas pu être téléchargé correctement.
Au fait, "upload_max_filesize" dans le php.ini vaut ' . ini_get('upload_max_filesize') . '.

'; } - + require('view/backup.php'); } +// upload AJAX d'un zip dans file_upload.js +function uploadChunkAndMerge() +{ + // $_GET['chunk_name'] n'est pas utilisé pour l'instant + + if(isset($_SESSION['fileName'])) + { + $zipName = 'data/tmp/' . $_SESSION['fileName']; + $srcFile = fopen($_FILES['blob']['tmp_name'], 'r'); + $destFile = fopen($zipName, 'a'); // 'a' crée ou écrit à la fin + + // copie de données brutes + stream_copy_to_stream($srcFile, $destFile); + + fclose($srcFile); + fclose($destFile); + } +} \ No newline at end of file diff --git a/controller/installation.php b/controller/installation.php index 4966611..3b940c8 100644 --- a/controller/installation.php +++ b/controller/installation.php @@ -138,13 +138,23 @@ function installation() { createIndexPHP('data/discographie/images-mini/index.php', $droitsFichiers); } + // fichiers temporaires pour upload des grosses archives + if(!file_exists('data/tmp')) + { + mkdir('data/tmp'); + chmod('data/tmp', $droitsDossiers); + } + if(!file_exists('data/tmp/index.php')) + { + createIndexPHP('data/tmp/index.php', $droitsFichiers); + } // le modèle donnera les droits 0666 (octal) aux nouveaux fichiers à l'intérieur des dossiers // créer le melainePHP.zip if(!file_exists("data/melainePHP.zip")) { require('controller/backup.php'); - creerMelainePHP(); + createZipMelainePHP(); } // création d'un mot de passe si password.txt est vide diff --git a/dependances.php b/dependances.php index cd42e3f..216b639 100644 --- a/dependances.php +++ b/dependances.php @@ -54,7 +54,7 @@ $maxWeight = ini_get('upload_max_filesize'); // les fichiers un par un quand un zip est trop grand // conversion des mégas en octets -/*function return_bytes ($size_str) +function return_bytes ($size_str) { switch (substr ($size_str, -1)) { @@ -63,5 +63,5 @@ $maxWeight = ini_get('upload_max_filesize'); case 'G': case 'g': return (int)$size_str * 1073741824; default: return $size_str; } -}*/ -/*$maxWeight = return_bytes(ini_get('upload_max_filesize'));*/ +} +$maxWeight = return_bytes(ini_get('upload_max_filesize')); diff --git a/index.php b/index.php index 76d3b4f..907dd9b 100644 --- a/index.php +++ b/index.php @@ -25,6 +25,7 @@ installation(); // traitement des requêtes AJAX +// -> insertion d'une image dans l'éditeur if(isset($_GET['action']) && isset($_GET['page']) && $_GET['action'] == 'upload_image') { // sécurité !! @@ -40,10 +41,45 @@ if(isset($_GET['action']) && isset($_GET['page']) && $_GET['action'] == 'upload_ // paramètre "true" parce qu'on reçoit une requête AJAX $Image = new Image(true); $Image->upload(); + echo($Image->reponseAjax); // attendu par l'éditeur } exit; // stop !! } +// page restauration quand le fichier zip est lourd +// -> input file onchange +if(isset($_GET['action']) && $_GET['action'] == 'restauration' + && isset($_GET['file_name']) && isset($_GET['file_size'])) +{ + if(!isset($_SESSION['admin']) || $_SESSION['admin'] != 1) + { + header('Location: index.php?erreur=file_infos_ajax'); + } + else + { + $_SESSION['fileSize'] = $_GET['file_size']; + $_SESSION['fileName'] = $_GET['file_name']; + //echo("file infos send"); + var_dump($_SESSION['fileName']); + exit(); // stop !! + } +} +// -> input submit onclick +if(isset($_GET['action']) && $_GET['action'] == 'restauration' + && isset($_GET['chunk_name']) && isset($_FILES['blob'])) +{ + if(!isset($_SESSION['admin']) || $_SESSION['admin'] != 1) + { + header('Location: index.php?erreur=upload_ajax'); + } + else + { + require('controller/backup.php'); + uploadChunkAndMerge(); + echo('file send'); + exit(); // stop !! + } +} // traitement des POST du ckeditor // la fonction submitCKeditor est "autonome", elle n'affiche rien puis redirige sans GET @@ -230,12 +266,12 @@ elseif($_SESSION['admin'] == 1 && isset($_GET['action'])) // sauvegarde du dossier 'data' if($_GET['action'] == 'sauvegarde') { - sauvegarder($_GET['from']); + pageSauvegarde($_GET['from']); } // restauration avec une sauvegarde elseif($_GET['action'] == 'restauration') { - restaurer($_GET['from']); + pageRestauration($_GET['from']); } else { diff --git a/model/Article.php b/model/Article.php index 4ef3a5b..4063feb 100644 --- a/model/Article.php +++ b/model/Article.php @@ -87,7 +87,7 @@ class Article // nommer les fichiers avec le timestamp pour: // - les trier par ordre chronologique - // - rendre quasi impossible d'avoir deux fois le même nom (à la condition de gérer la "concurrence") + // - rendre quasi impossible d'avoir deux fois le même nom $fileName = 'data/' . $this->page . '/' . $this->format . '/' . $this->time . '.' . $this->format; $file = fopen($fileName, 'w'); // w pour créer ou écraser diff --git a/model/Image.php b/model/Image.php index e8bf3f9..05773e9 100644 --- a/model/Image.php +++ b/model/Image.php @@ -4,7 +4,10 @@ class Image { private $page; + private $ajax; // vaut true avec le ckeditor + public $reponseAjax; + public $path; public $pathMini; public $pathInfos; @@ -51,9 +54,8 @@ class Image // retour des rêquetes AJAX if($this->ajax && empty($Image->erreur)) { - // nouveau chemin à renvoyer en format json - $chemin = '{"url": "data/' . $this->page . '/images/' . $_FILES['upload']['name'] . '"}'; - echo $chemin; + // chemin en JSON attendu par l'éditeur + $this->reponseAjax = '{"url": "data/' . $this->page . '/images/' . $_FILES['upload']['name'] . '"}'; } } diff --git a/public/css/accueil.css b/public/css/accueil.css index 0bf838d..8425cfa 100644 --- a/public/css/accueil.css +++ b/public/css/accueil.css @@ -82,7 +82,6 @@ img .boutonAnnuler:hover { - /*text-decoration: none;*/ border: none; } @@ -126,10 +125,6 @@ form text-decoration: none; } -#courriel a:hover -{ - /*padding: 5px;*/ -} #courriel>button /* bouton pour remonter */ { float: right; @@ -163,11 +158,6 @@ form margin: 0px; padding: 2px; } -#modeAdmin>div p -{ - /*margin: 5px;*/ - /*padding: 0px;*/ -} #modeAdmin>p a { text-decoration: none; @@ -176,26 +166,18 @@ form #lienModeAdmin { - margin: 0px 20px; -} - -#lienModeAdmin p -{ + font-size: 90%; + margin-top: 10px; + margin-right: 30px; text-align: right; /* enlever la bande bleue en dessous du bloc_page */ - margin-bottom: 0; - padding-bottom: 5px; -} - -#lienModeAdmin p a -{ - color: #666; - font-weight: bold; - padding: 2px; + /*margin-bottom: 0;*/ + padding-bottom: 8px; } -#lienModeAdmin p a:hover +#lienModeAdmin a { + text-decoration: none; color: black; } @@ -239,16 +221,17 @@ form color: initial; } +button +{ + padding: 1px; +} + /* options au survol */ #options { /*display: none;*/ display: flex; } -/*#modeAdmin:hover #options -{ - display: flex; -}*/ /* PC, y compris vieux écrans 800x600 */ @@ -494,6 +477,11 @@ form max-width: 180px; } + #lienModeAdmin + { + margin-right: 20px; + } + .zoneVideAdmin, #modeAdmin { height: 61px; @@ -503,18 +491,6 @@ form { max-width: 380px; } - - .boutonBackup - { - /*margin: 5px;*/ - /*padding: 1px;*/ - - } - #modeAdmin button - { - font-size: 95%; - padding: 1px; - } } /* spécialement pour les petits smartphones*/ diff --git a/public/file_upload.js b/public/file_upload.js new file mode 100644 index 0000000..8106806 --- /dev/null +++ b/public/file_upload.js @@ -0,0 +1,103 @@ +// public/file_upload.js + +// envoie gros fichier ZIP +// ce fichier est "caché", le serveur ne l'envoit +// qu'un utilisateur connecté et sur la page "restauration" + +// -> input file onchange +function sendFileSize() +{ + var tagId = 'archiveUpload'; + var fileInfos = getFileInfo(tagId); + //var fileSize = document.getElementById(tagId).files[0].size; + + const xhr = new XMLHttpRequest(); + url = 'index.php?action=restauration&file_name='+fileInfos.name+'&file_size='+fileInfos.size; + xhr.open("GET", url); + xhr.send(); +} + +// -> input submit onclick +function uploadDespiteServerMaxWeightLimit(maxPHPiniWeight, archiveFormat) +{ + // dans + var tagId = 'archiveUpload'; + //var fileInfos = getFileInfo(tagId); + var file = document.getElementById(tagId).files[0]; + + // si le le fichier est assez léger, javascript s'arrête ici + if(file.size > maxPHPiniWeight) + { + // découpage + // envoyer et recevoir des pointeurs pour les perfs + // chunksArray est un tableau de "blob" + var nbChunks = Math.ceil(file.size / maxPHPiniWeight); + var chunksArray = sliceFile(file, nbChunks); + + // requêtes AJAX + chunkIndex = 0; // une variable globale + uploadChunksAJAX(chunksArray, tagId); + + // annule l'envoi normal par POST + event.preventDefault(); + + // vider le formulaire et recharger + // reload() est un F5 et non un Ctrl + F5 + //document.getElementById(tagId).value = ''; + //location.reload(); + + location.href = "index.php?from=menu&action=restauration"; + } +} + +function getFileInfo(tagId) +{ + var infos = + { + name: document.getElementById(tagId).files[0].name, + size: document.getElementById(tagId).files[0].size, + type: document.getElementById(tagId).files[0].type, + } + + return(infos); +} + +function sliceFile(file, nbChunks) +{ + var byteIndex = 0; // octet du début + var chunks = []; // données + + for (var i = 0; i < nbChunks; i += 1) + { + // octet de fin + var byteEnd = Math.ceil((file.size / nbChunks) * (i + 1)); + + // un morceau du fichier va dans une case du tableau + chunks.push(file.slice(byteIndex, byteEnd)); + + // nouvel octet du début + byteIndex += (byteEnd - byteIndex); + } + + return chunks; +} + +function uploadChunksAJAX(chunksArray, tagId) +{ + var fileName = document.getElementById(tagId).files[0].name; + var formData = new FormData(); + const xhr = new XMLHttpRequest(); + + for(var i = 0; i < chunksArray.length; i++) + { + formData.append('blob', chunksArray[i]); + + url = 'index.php?from=menu&action=restauration&chunk_name='+fileName+'_'+i; + // false => synchrone, déprécié parce que fige le navigateur + // mais ici on s'en fout + xhr.open("POST", url, false); + xhr.send(formData); + + formData.delete('blob'); + } +} diff --git a/public/main.js b/public/main.js index 5ca23c7..ede1a30 100644 --- a/public/main.js +++ b/public/main.js @@ -29,16 +29,7 @@ function confirmerSuppression() // appel 'onCLick' // code exécuté à la validation du formulaire function envoiDonnees() -{ - // supprimer le positionnement absolu de l'iframe - /*let balisesIframe = document.getElementsByTagName("iframe"); - for(var i = 0; i < balisesIframe.length; i++) - { - alert(balisesIframe[i].getAttribute("style")); // affiche le CSS - balisesIframe[i].removeAttribute("style"); - alert(balisesIframe[i].getAttribute("style")); // affiche null - }*/ -} +{} // bouton "mailto", le visiteur ne quitte pas la page function clientCourriel() @@ -89,38 +80,3 @@ function nouveauMotdepasse(page) alert('Le mot de passe a été modifié.'); window.setTimeout(location=('index.php?page=' + page + '&message=nouveau_mdp'), 0); } - -// envoie gros fichier ZIP -// si le fichier ne passe pas la limite de l'hébergeur (php.ini) -// l'ouvrir en javascript chaque envoyer progressivement -function getFileInfo() -{ - // l'idi est dans le formulaire - var name = document.getElementById('myFile').files[0].name; - var size = document.getElementById('myFile').files[0].size; - var type = document.getElementById('myFile').files[0].type; - var date = document.getElementById('myFile').files[0].lastModifiedDate; - - var infos = name+" "+size+" "+type+" "+date; - alert(infos) - return(infos); -} - -function extraireZIPetEnvoyerUnParUn(maxPHPiniWeight, archiveFormat) -{ - // taille du fichier? - var fileInfos = getFileInfo(); - alert(fileInfos); - - // taille limite autorisée? - // obtenue par php avec: ini_get('upload_max_filesize'); - - // si le fichier est plus gros que la limite: - // extraire l'archive - // envoyer les fichiers un par un par des requêtes AJAX - // le serveur peut aussi limiter le nombre de fichiers - // lors d'un envoie multiple - // en les envoyant un par un ça devrait être bon - - // sinon ne rien faire et laisser l'envoi normal se faire -} diff --git a/view/backup.php b/view/backup.php index 30b644b..bbea60d 100644 --- a/view/backup.php +++ b/view/backup.php @@ -54,15 +54,15 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'restauration') ob_start(); ?>

Restauration des données à partir d'une sauvegarde.

-

Vous devez avoir créé précédemment un fichier melaineDATA
+

Vous devez avoir créé précédemment un fichier dont le nom commence par melaineDATA
à la page Sauvegarder les données.


-
+
- +
@@ -82,7 +82,7 @@ elseif(isset($_GET['action']) && $_GET['action'] == 'restauration') '); + echo(''); } ?> @@ -91,5 +91,5 @@ if(isset($_GET['action']) && $_GET['action'] == 'restauration')
- + \ No newline at end of file diff --git a/view/template.php b/view/template.php index 93881f4..7fafbbb 100644 --- a/view/template.php +++ b/view/template.php @@ -8,7 +8,7 @@ - + @@ -57,7 +57,7 @@ if($_SESSION['admin'] == 0 && isset($_GET['page']) && $_GET['page'] != 'accueil' { ?>
-

Mode Administrateur

+