Вопрос:
Как читать и отображать размер загружаемого файла, записываемого на сервер, в режиме реального времени, не блокируя его как на сервере, так и на клиенте?
Контекст:
Ход загрузки файла записывается на сервер с 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
Это один из наиболее распространенных серверных языков, используемых во всемирной паутине, для реализации шаблона без включения ненужных частей.
Мотивация:
Индикаторы прогресса для получения?
Связанные с:
Вопросы:
Получение
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(true)
выдает ошибку при EventSource
,
Без sleep()
вызов, правильный размер файла был отправлен message
событие для 3.3MB
файл, 3321824
был напечатан на console
61921
, 26214
, а также 38093
раз, когда загружены один и тот же файл три раза. Ожидаемый результат — размер файла, поскольку файл записывается в
stream_copy_to_stream($input, $file);
вместо размера файла загружаемого файлового объекта. Являются fopen()
или же stream_copy_to_stream()
блокировка как к другому другому php
обрабатывать в stream.php
?
До сих пор пробовал:
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 чтобы получить реальный размер файла. С исправлением нескольких других битов ваш 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
должен быть полностью удален, поскольку загрузка локальных МБ занимает считанные миллисекунды.
Открытые соединения. И 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, 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
}
Таким образом, ваш 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
}
Проблема с 2 открытыми соединениями не может быть решена, если вы не откажетесь от 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);