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:
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
- Más allá de $ _POST, $ _GET y $ _FILE: Trabajando con Blob en JavaScriptPHP
- Introducción a los eventos enviados por el servidor con ejemplo 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>
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.