Comment lire et faire écho à la taille du fichier uploadé qui est écrit au serveur en temps réel sans bloquer à la fois sur le serveur et sur le client?

Question:

Comment lire et faire écho à la taille du fichier uploadé qui est écrit au serveur en temps réel sans bloquer à la fois sur le serveur et sur le client?

Le contexte:

Progression du transfert de fichier en cours d'écriture sur le serveur à partir de la demande POST effectuée par fetch() , où le body est défini sur Blob , File , TypedArray ou ArrayBuffer .

L'implémentation actuelle définit l'objet File objet du body passé au deuxième paramètre de fetch() .

Exigence:

Lisez et echo au client la taille du fichier écrit sur le système de fichiers au serveur en tant que text/event-stream . Arrêtez lorsque tous les octets, fournis en tant que variable au script en tant que paramètre de chaîne de requête à la demande GET ont été écrits. La lecture du fichier se déroule actuellement dans un environnement de script distinct, où GET appel au script qui doit lire le fichier est effectué après POST au script qui écrit le fichier sur le serveur.

N'a pas eu l'erreur de gérer les problèmes potentiels avec l'écriture du fichier au serveur ou la lecture du fichier pour obtenir la taille du fichier actuel, mais ce serait la prochaine étape une fois que l' echo de la partie de la taille du fichier est terminé.

En essayant actuellement de satisfaire aux exigences en utilisant php . Bien que également intéressé par c , bash , nodejs , python ; Ou d'autres langues ou approches qui peuvent être utilisées pour effectuer la même tâche.

La partie javascript côté client n'est pas un problème. Tout simplement pas versé dans php , l'une des langues du côté du serveur les plus courantes utilisées dans le monde entier, pour implémenter le motif sans inclure les pièces qui ne sont pas nécessaires.

Motivation:

Indicateurs de progression pour l'extraction?

En relation:

Travailler avec ReadableStream

Problèmes:

Obtenir

 PHP Notice: Undefined index: HTTP_LAST_EVENT_ID in stream.php on line 7 

Au terminal .

Aussi, si substitut

 while(file_exists($_GET["filename"]) && filesize($_GET["filename"]) < intval($_GET["filesize"])) 

pour

 while(true) 

Produit une erreur sur EventSource .

Sans sleep() appel de sleep() , la taille de fichier correcte a été envoyée à l'événement de message pour un fichier de 3.3MB , 3321824 , a été imprimé à la console 61921 , 26214 et 38093 fois, respectivement, lors du 38093 même fichier trois fois. Le résultat attendu est la taille du fichier à mesure que le fichier est écrit au

 stream_copy_to_stream($input, $file); 

Au lieu de la taille du fichier de l'objet du fichier téléchargé. Est-ce que fopen() ou stream_copy_to_stream() bloque sur un autre processus php dans stream.php ?

Essayé jusqu'à présent:

php est attribué à

  • Au-delà de $ _POST, $ _GET et $ _FILE: Utilisation de Blob dans JavaScriptPHP
  • Introduction aux événements envoyés par serveur avec l'exemple de PHP

php

 // can we merge `data.php`, `stream.php` to same file? // can we use `STREAM_NOTIFY_PROGRESS` // "Indicates current progress of the stream transfer // in bytes_transferred and possibly bytes_max as well" to read bytes? // do we need to call `stream_set_blocking` to `false` // data.php <?php $filename = $_SERVER["HTTP_X_FILENAME"]; $input = fopen("php://input", "rb"); $file = fopen($filename, "wb"); stream_copy_to_stream($input, $file); fclose($input); fclose($file); echo "upload of " . $filename . " successful"; ?> 

 // stream.php <?php header("Content-Type: text/event-stream"); header("Cache-Control: no-cache"); header("Connection: keep-alive"); // `PHP Notice: Undefined index: HTTP_LAST_EVENT_ID in stream.php on line 7` ? $lastId = $_SERVER["HTTP_LAST_EVENT_ID"] || 0; if (isset($lastId) && !empty($lastId) && is_numeric($lastId)) { $lastId = intval($lastId); $lastId++; } // else { // $lastId = 0; // } // while current file size read is less than or equal to // `$_GET["filesize"]` of `$_GET["filename"]` // how to loop only when above is `true` while (true) { $upload = $_GET["filename"]; // is this the correct function and variable to use // to get written bytes of `stream_copy_to_stream($input, $file);`? $data = filesize($upload); // $data = $_GET["filename"] . " " . $_GET["filesize"]; if ($data) { sendMessage($lastId, $data); $lastId++; } // else { // close stream // } // not necessary here, though without thousands of `message` events // will be dispatched // sleep(1); } function sendMessage($id, $data) { echo "id: $id\n"; echo "data: $data\n\n"; ob_flush(); flush(); } ?> 

javascript

 <!DOCTYPE html> <html> <head> </head> <body> <input type="file"> <progress value="0" max="0" step="1"></progress> <script> const [url, stream, header] = ["data.php", "stream.php", "x-filename"]; const [input, progress, handleFile] = [ document.querySelector("input[type=file]") , document.querySelector("progress") , (event) => { const [file] = input.files; const [{size:filesize, name:filename}, headers, params] = [ file, new Headers(), new URLSearchParams() ]; // set `filename`, `filesize` as search parameters for `stream` URL Object.entries({filename, filesize}) .forEach(([...props]) => params.append.apply(params, props)); // set header for `POST` headers.append(header, filename); // reset `progress.value` set `progress.max` to `filesize` [progress.value, progress.max] = [0, filesize]; const [request, source] = [ new Request(url, { method:"POST", headers:headers, body:file }) // https://stackoverflow.com/a/42330433/ , new EventSource(`${stream}?${params.toString()}`) ]; source.addEventListener("message", (e) => { // update `progress` here, // call `.close()` when `e.data === filesize` // `progress.value = e.data`, should be this simple console.log(e.data, e.lastEventId); }, true); source.addEventListener("open", (e) => { console.log("fetch upload progress open"); }, true); source.addEventListener("error", (e) => { console.error("fetch upload progress error"); }, true); // sanity check for tests, // we don't need `source` when `e.data === filesize`; // we could call `.close()` within `message` event handler setTimeout(() => source.close(), 30000); // we don't need `source' to be in `Promise` chain, // though we could resolve if `e.data === filesize` // before `response`, then wait for `.text()`; etc. // TODO: if and where to merge or branch `EventSource`, // `fetch` to single or two `Promise` chains const upload = fetch(request); upload .then(response => response.text()) .then(res => console.log(res)) .catch(err => console.error(err)); } ]; input.addEventListener("change", handleFile, true); </script> </body> </html> 

Vous devez effacer la mémoire pour obtenir une taille de fichier réelle. Avec quelques autres bits corrigés, votre stream.php peut ressembler à:

 <?php header("Content-Type: text/event-stream"); header("Cache-Control: no-cache"); header("Connection: keep-alive"); // Check if the header's been sent to avoid `PHP Notice: Undefined index: HTTP_LAST_EVENT_ID in stream.php on line ` // php 7+ //$lastId = $_SERVER["HTTP_LAST_EVENT_ID"] ?? 0; // php < 7 $lastId = isset($_SERVER["HTTP_LAST_EVENT_ID"]) ? intval($_SERVER["HTTP_LAST_EVENT_ID"]) : 0; $upload = $_GET["filename"]; $data = 0; // if file already exists, its initial size can be bigger than the new one, so we need to ignore it $wasLess = $lastId != 0; while ($data < $_GET["filesize"] || !$wasLess) { // system calls are expensive and are being cached with assumption that in most cases file stats do not change often // so we clear cache to get most up to date data clearstatcache(true, $upload); $data = filesize($upload); $wasLess |= $data < $_GET["filesize"]; // don't send stale filesize if ($wasLess) { sendMessage($lastId, $data); $lastId++; } // not necessary here, though without thousands of `message` events will be dispatched //sleep(1); // millions on poor connection and large files. 1 second might be too much, but 50 messages a second must be okay usleep(20000); } function sendMessage($id, $data) { echo "id: $id\n"; echo "data: $data\n\n"; ob_flush(); // no need to flush(). It adds content length of the chunk to the stream // flush(); } 

Quelques réserves:

Sécurité. Je veux dire de la chance. Si je comprends bien, c'est une preuve de concept, et la sécurité est la moindre préoccupation, mais la clause de non-responsabilité devrait être là. Cette approche est fondamentalement erronée, et devrait être utilisée uniquement si vous ne vous occupez pas des attaques DOS ou des informations sur vos fichiers disparaissent.

CPU. Sans usleep le script consommera 100% d'un seul noyau. Avec un sommeil prolongé, vous risquez de télécharger tout le fichier dans une seule itération et la condition de sortie ne sera jamais remplie. Si vous le testez localement, l' usleep doit être supprimé complètement, car il est important de faire des millisecondes pour télécharger des MB localement.

Ouvrez les connexions. Apache et nginx / fpm ont un nombre fini de processus php qui peuvent répondre aux demandes. Un chargement de fichier unique prend 2 pour le temps requis pour télécharger le fichier. Avec la bande passante lente ou les demandes forgées, cette fois peut être assez longue, et le serveur Web peut commencer à rejeter les demandes.

Partie client. Vous devez analyser la réponse et finalement cesser d'écouter les événements lorsque le fichier est complètement téléchargé.

MODIFIER:

Pour rendre plus ou moins productif, vous aurez besoin d'un stockage en mémoire comme redis, ou memcache pour stocker des métadonnées de fichiers.

Faire une demande de publication, ajouter un jeton unique qui identifie le fichier et la taille du fichier.

Dans votre javascript:

 const fileId = Math.random().toString(36).substr(2); // or anything more unique ... const [request, source] = [ new Request(`${url}?fileId=${fileId}&size=${filesize}`, { method:"POST", headers:headers, body:file }) , new EventSource(`${stream}?fileId=${fileId}`) ]; .... 

Dans data.php, enregistrez le jeton et signalez l'avancement par les morceaux:

 .... $fileId = $_GET['fileId']; $fileSize = $_GET['size']; setUnique($fileId, 0, $fileSize); while ($uploaded = stream_copy_to_stream($input, $file, 1024)) { updateProgress($id, $uploaded); } .... /** * Check if Id is unique, and store processed as 0, and full_size as $size * Set reasonable TTL for the key, eg 1hr * * @param string $id * @param int $size * @throws Exception if id is not unique */ function setUnique($id, $size) { // implement with your storage of choice } /** * Updates uploaded size for the given file * * @param string $id * @param int $processed */ function updateProgress($id, $processed) { // implement with your storage of choice } 

Donc, votre stream.php n'a pas besoin de toucher le disque et peut dormir aussi longtemps qu'il est acceptable par UX:

 .... list($progress, $size) = getProgress('non_existing_key_to_init_default_values'); $lastId = 0; while ($progress < $size) { list($progress, $size) = getProgress($_GET["fileId"]); sendMessage($lastId, $progress); $lastId++; sleep(1); } ..... /** * Get progress of the file upload. * If id is not there yet, returns [0, PHP_INT_MAX] * * @param $id * @return array $bytesUploaded, $fileSize */ function getProgress($id) { // implement with your storage of choice } 

Le problème avec 2 connexions ouvertes ne peut être résolu que si vous renoncez à EventSource pour l'ancien bon tir. Le temps de réponse de stream.php sans boucle est une question de millisecondes, et il est assez inutile de garder la connexion ouverte tout le temps, sauf si vous avez besoin de centaines de mises à jour par seconde.

Vous avez besoin de briser le fichier sur les blocs avec javascript et envoyer ces morceaux. Lorsque le fragment est téléchargé, vous savez exactement combien de données ont été envoyées.

C'est le seul moyen et par le fait qu'il n'est pas difficile.

 file.startByte += 100000; file.stopByte += 100000; var reader = new FileReader(); reader.onloadend = function(evt) { data.blob = btoa(evt.target.result); /// Do upload here, I do with jQuery ajax } var blob = file.slice(file.startByte, file.stopByte); reader.readAsBinaryString(blob);