Как читать и эхо-размер файла загружаемого файла, написанного на сервере в реальном времени, без блокировки на сервере и на клиенте?

Вопрос:

Как читать и эхо-размер файла загружаемого файла, написанного на сервере в реальном времени, без блокировки на сервере и на клиенте?

Контекст:

Выполнение загрузки файла записывается на сервер из запроса POST выполненного с помощью fetch() , где body установлено в объект Blob , File , TypedArray или ArrayBuffer .

Текущая реализация устанавливает объект File объекте body переданном второму параметру fetch() .

Требование:

Чтение и echo клиенту размер файла файла, записываемого в файловую систему на сервере в виде text/event-stream . Остановите, когда все байты, предоставленные как переменная скрипту в качестве параметра строки запроса в запросе GET , были написаны. Чтение файла в настоящее время происходит в отдельной среде сценариев, где GET вызов сценария, который должен читать файл, выполняется после POST для скрипта, который записывает файл на сервер.

Не удалось обработать проблему с потенциальной проблемой при записи файла на сервер или прочитать файл, чтобы получить текущий размер файла, хотя это будет следующим шагом после завершения echo -файла.

В настоящее время пытается удовлетворить требования, используя php . Хотя он также интересуется c , bash , nodejs , python ; или другие языки или подходы, которые могут использоваться для выполнения одной и той же задачи.

Часть javascript стороне клиента не является проблемой. Просто не тот, кто разбирается в php , одном из самых распространенных серверных языков, используемых во всемирной паутине, для реализации шаблона без включения частей, которые не нужны.

Мотивация:

Индикаторы прогресса для извлечения?

Связанный:

Получить с помощью ReadableStream

Вопросы:

Получение

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

на terminal .

Кроме того, если заменить

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

для

 while(true) 

вызывает ошибку в EventSource .

Без sleep() корректный размер файла был отправлен на событие message для 3.3MB файла, 3321824 , был напечатан на console 61921 , 26214 и 38093 раза, соответственно, при загрузке одного файла три раза. Ожидаемый результат – размер файла файла, так как файл записывается в

 stream_copy_to_stream($input, $file); 

вместо размера файла загруженного файлового объекта. stream_copy_to_stream() ли fopen() или stream_copy_to_stream() другие процессы php в stream.php ?

Пробовал до сих пор:

php объясняется

  • Помимо $ _POST, $ _GET и $ _FILE: Работа с Blob в JavaScriptPHP
  • Введение в события Server-Sent с примером 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> 

Related of "Как читать и эхо-размер файла загружаемого файла, написанного на сервере в реальном времени, без блокировки на сервере и на клиенте?"

Чтобы получить реальный размер файла, вам необходимо очистить статический файл. При исправлении нескольких других битов ваш stream.php может выглядеть следующим образом:

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

Немного оговорок:

Безопасность. Я имею в виду удачу. Насколько я понимаю, это доказательство концепции, а безопасность – это наименьшее из проблем, но отказ от ответственности должен быть там. Этот подход в корне ошибочен и должен использоваться только в том случае, если вам не нужны атаки DOS или информация о ваших файлах отсутствует.

ЦПУ. Без usleep сценарий будет потреблять 100% одного ядра. При длительном сном вы рискуете загрузить весь файл за одну итерацию, и условие выхода никогда не будет выполнено. Если вы тестируете его локально, usleep должно быть полностью удалено, так как в миллисекундах необходимо загружать MBs локально.

Открытые соединения. Оба apache и nginx / fpm имеют конечное число php-процессов, которые могут обслуживать запросы. При загрузке одного файла потребуется 2 раза, чтобы загрузить файл. При медленной пропускной способности или поддельных запросах это время может быть довольно большим, и веб-сервер может начать отклонять запросы.

Клиентс. Часть. Вам нужно проанализировать ответ и, наконец, прекратить прослушивание событий, когда файл полностью загружен.

РЕДАКТИРОВАТЬ:

Чтобы сделать его более или менее удобным для производства, вам понадобится память в памяти, например redis, или memcache для хранения метаданных файлов.

Создавая почтовый запрос, добавьте уникальный токен, который идентифицирует файл, и размер файла.

В вашем 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}`) ]; .... 

В data.php зарегистрируйте токен и сообщите о прогрессе кусками:

 .... $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 } 

Таким образом, ваш stream.php не нужно вообще ударять по диску и может спать до тех пор, пока 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 } 

Проблема с двумя открытыми соединениями не может быть решена, если вы не откажетесь от EventSource для старого хорошего вытягивания. Время отклика stream.php без цикла – это миллисекунды, и довольно бесполезно поддерживать соединение открытым все время, если вам не нужны сотни обновлений в секунду.

Вам нужно разбить файл на куски с помощью javascript и отправить эти куски. Когда кусок загружается, вы точно знаете, сколько данных было отправлено.

Это единственный способ и, кстати, это не сложно.

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