質問:
サーバーとクライアントの両方でブロックすることなく、サーバーでリアルタイムに書き込まれているアップロードされたファイルのファイルサイズを読み取ってエコーする方法は?
環境:
ファイルからサーバーに書き込まれ、アップロードの進捗状況POST
からの要求fetch()
、body
に設定されているBlob
、File
、TypedArray
、またはArrayBuffer
オブジェクト。
現在の実装では、の2番目のパラメータに渡されたFile
オブジェクトにbody
オブジェクトを設定しますfetch()
。
要件:
echo
サーバーのファイルシステムに書き込まれているファイルのファイルサイズをとして読み取り、クライアントに送信しますtext/event-stream
。GET
要求に応じてクエリ文字列パラメーターとしてスクリプトに変数として提供されたすべてのバイトが書き込まれたときに停止します。現在、ファイルの読み取りは別のスクリプト環境で行われ、ファイルをサーバーに書き込むスクリプトにGET
続いてPOST
、ファイルを読み取る必要のあるスクリプトが呼び出されます。
サーバーへのファイルの書き込みまたは現在のファイルサイズを取得するためのファイルの読み取りに関する潜在的な問題のエラー処理に到達していませんがecho
、ファイルサイズの部分が完了したら次のステップになります。
現在、を使用して要件を満たそうとしていますphp
。また、に興味があるけどc
、bash
、nodejs
、python
。または同じタスクを実行するために使用できる他の言語またはアプローチ。
クライアント側のjavascript
部分は問題ではありません。php
必要のない部分を含めずにパターンを実装するために、ワールドワイドウェブで使用される最も一般的なサーバーサイド言語の1つに精通しているわけではありません。
動機:
関連:
問題:
取得
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
、同じファイルが3回アップロードされたときに、それぞれ、、、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攻撃を気にしない場合、またはファイルに関する情報が失われる場合にのみ使用する必要があります。
CPU。usleep
スクリプトがないと、シングルコアの100%を消費します。長いスリープ状態では、1回の反復でファイル全体をアップロードするリスクがあり、終了条件が満たされることはありません。ローカルでテストしている場合は、usleep
MBをローカルにアップロードするのに数ミリ秒かかるため、を完全に削除する必要があります。
接続を開きます。apacheとnginx / fpmの両方に、リクエストを処理できるphpプロセスの数には限りがあります。1つのファイルのアップロードには、ファイルのアップロードに必要な時間2がかかります。帯域幅が遅い場合やリクエストが偽造されている場合、この時間は非常に長くなる可能性があり、Webサーバーがリクエストを拒否し始める可能性があります。
クライアントサイド部分。応答を分析し、ファイルが完全にアップロードされたら、最終的にイベントのリッスンを停止する必要があります。
編集:
多かれ少なかれ本番環境に対応するには、redisなどのメモリ内ストレージまたはファイルメタデータを保存するためのmemcacheが必要になります。
POSTリクエストを作成し、ファイルを識別する一意のトークンとファイルサイズを追加します。
あなたの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の応答時間はミリ秒の問題であり、1秒間に数百回の更新が必要でない限り、接続を常に開いたままにしておくのは非常に無駄です。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加