javascript php multithreading file-upload language-agnostic

javascript - ¿Cómo leer y hacer eco del tamaño del archivo cargado que se escribe en el servidor en tiempo real sin bloquearlo tanto en el servidor como en el cliente?



php multithreading (2)

Necesita romper el archivo en fragmentos con javascript y enviar esos fragmentos. Cuando se carga un fragmento, sabe exactamente cuántos datos se enviaron.

Esta es la única manera y, por cierto, no es difícil.

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);

Pregunta:

¿Cómo leer y hacer eco del tamaño del archivo cargado que se escribe en el servidor en tiempo real sin bloquearlo tanto en el servidor como en el cliente?

Contexto:

El progreso de la carga de archivos se escribe en el servidor desde la solicitud POST realizada por fetch() , donde el body se establece en objeto Blob , File , TypedArray o ArrayBuffer .

La implementación actual establece el objeto File en body objeto body pasado al segundo parámetro de fetch() .

Requisito:

Lea y echo al cliente del tamaño del archivo que se está escribiendo en el sistema de archivos en el servidor como text/event-stream . Deténgase cuando se hayan escrito todos los bytes, proporcionados como una variable para el script como un parámetro de cadena de consulta en la solicitud GET . La lectura del archivo se lleva a cabo actualmente en un entorno de script separado, donde GET call to script que debería leer el archivo se realiza después de POST to script que escribe el archivo en el servidor.

No se ha alcanzado el manejo de errores de un problema potencial con la escritura del archivo en el servidor o la lectura del archivo para obtener el tamaño actual del archivo, aunque ese sería el siguiente paso una vez que se complete la porción del tamaño del archivo.

Actualmente intentando cumplir con los requisitos usando php . Aunque también está interesado en c , bash , nodejs , python ; u otros lenguajes o enfoques que pueden usarse para realizar la misma tarea.

La parte de javascript lado del cliente no es un problema. Simplemente no está tan versado en php , uno de los lenguajes más comunes del lado del servidor utilizado en la red mundial, para implementar el patrón sin incluir partes que no son necesarias.

Motivación:

Indicadores de progreso para buscar?

Relacionado:

Fetch con ReadableStream

Cuestiones:

Consiguiendo

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

en la terminal

Además, si sustituye

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

para

while(true)

produce un error en EventSource .

Sin sleep() llamada sleep() , se envió el tamaño de archivo correcto al evento de message para un archivo de 3.3MB , 3321824 , se imprimió en la console 61921 , 26214 y 38093 veces, respectivamente, cuando se cargó el mismo archivo tres veces. El resultado esperado es el tamaño del archivo, ya que el archivo se está escribiendo en

stream_copy_to_stream($input, $file);

en lugar del tamaño de archivo del objeto de archivo cargado. ¿Están bloqueando fopen() o stream_copy_to_stream() cuanto a otro proceso php diferente en stream.php ?

Probado hasta ahora:

php se atribuye a

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>


clearstatcache para obtener un tamaño de archivo real. Con algunos otros bits corregidos, su stream.php puede verse así:

<?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(); }

Pocas advertencias:

Seguridad. Me refiero a la suerte de eso. Según tengo entendido, es una prueba de concepto, y la seguridad es la menor de las preocupaciones, sin embargo, el descargo de responsabilidad debería estar allí. Este enfoque es fundamentalmente defectuoso, y debe usarse solo si no le importan los ataques de DOS o si sale información sobre sus archivos.

UPC. Sin usleep el script consumirá el 100% de un solo núcleo. Con un sueño prolongado, corre el riesgo de cargar todo el archivo en una sola iteración y la condición de salida nunca se cumplirá. Si lo está probando localmente, el usleep debe eliminarse por completo, ya que es cuestión de milisegundos cargar MB localmente.

Conexiones abiertas Tanto apache como nginx / fpm tienen un número finito de procesos php que pueden atender las solicitudes. Una carga de un solo archivo tomará 2 por el tiempo requerido para cargar el archivo. Con ancho de banda lento o solicitudes falsificadas, este tiempo puede ser bastante largo y el servidor web puede comenzar a rechazar solicitudes.

Parte del cliente. Debe analizar la respuesta y, finalmente, dejar de escuchar los eventos cuando el archivo esté completamente cargado.

EDITAR:

Para que sea más o menos amigable con la producción, necesitará un almacenamiento en memoria como redis o memcache para almacenar metadatos de archivos.

Al realizar una solicitud de publicación, agregue un token único que identifique el archivo y el tamaño del archivo.

En tu 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}`) ]; ....

En data.php, registre el token e informe el progreso por fragmentos:

.... $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, e.g. 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 }

Por lo tanto, su stream.php no necesita golpear el disco en absoluto, y puede dormir siempre que sea aceptable por 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 }

El problema con 2 conexiones abiertas no se puede resolver a menos que renuncies a EventSource por una buena extracción. El tiempo de respuesta de stream.php sin bucle es una cuestión de milisegundos, y es un desperdicio mantener la conexión abierta todo el tiempo, a menos que necesite cientos de actualizaciones por segundo.