作成日:2024-03-29, 更新日:2024-04-01
以前に「js、php ファイルを分割して送信」でまとめたけど…改めてまとめ直す
方針
formからファイルを送る…っていうときの対応方針はざっくり2つ…かな?
- ファイルと他の入力情報を送る
- ファイルのみ送る
ファイルと他の入力情報を送る
気分的には、コッチがいい
コッチがいいけど、メンテのときが面倒になりそうな気がする
- form送信のときのjavascript制御
- javascriptでファイルを分割
- ファイル以外の入力情報の取り扱いを考える(分割したファイルと毎回 or 初回のみ or 最後のみ…送る)
- 分割したファイルたち、ファイル以外の入力情報をFetchやAjaxなどで送る
- リクエストをPHPで受け取る
- 分割されたファイルを受け取り、すべて揃ったら合体
- ファイル以外の入力情報の取り扱いを考える(初回のみ or 最後のみ or すべて揃ったときのみ…受け取る)
- 「すべて揃う前、揃った後」or「すべての処理完了前 or 完了後」でレスポンスに違いを持たせておく
- レスポンスをJavascriptで受け取る
- 「すべて揃う前、揃った後」or「すべての処理完了前 or 完了後」で処理を分ける(※すべて揃った際、「完了した」とメッセージを出力…等)
ファイルのみ送る
色々と面倒だけど、メンテのコトを考えるとコッチのほうがラクかも
- javascriptでファイルを送る
- javascriptでファイルを分割
- 分割したファイルをFetchやAjaxなどで送る
- リクエストをPHPで受け取る
- 分割されたファイルを受け取り、すべて揃ったら合体。フラグとして「仮完了」等、DBなどに保存しておく
- すべて揃う前、揃った後でレスポンスに違いを持たせておく(揃ったときのみファイル名を返す…など)
- レスポンスをJavascriptで受け取る
- すべて揃う前、揃った後で処理を分ける(※すべて揃った際、レスポンスにあるファイル名などをformにセット)
- form送信(ファイル以外の入力情報、レスポンスにあったファイル名など)
- PHPで受け取る
- ファイル名などから、受け取り済みのファイルか確認し、各処理(DBに保存、メール送信など)
- フラグの「仮完了」の取り下げ
- 任意のタイミングでフラグの「仮完了」になっているファイルを削除(※Form入力の途中で離脱した人たちの対応)
サンプル: 送信ボタンクリックでマルっと送りたいとき
送信ボタンクリックでマルっと送りたいとき
下記のサンプルのままだとチャンクが送られるたびに「type="text"」が送られる
→javascript側で初回のみ送る…等したほうが良いし、PHP側の制御もそれに合わせたほうが良い
HTML
<form method="post"> <input type="file" name="logo" id="file_logo"> <input type="file" name="face" id="file_face"> <input type="text" name="family_name"> <input type="text" name="personal_name"> <button type="submit"> </form>
Javascript
document.querySelector('form').addEventListener('submit', async function(event) { event.preventDefault(); // ファイル入力要素を取得 const fileInputs = document.querySelectorAll('input[type="file"]'); const textInputs = document.querySelectorAll('input[type="text"]'); // 各ファイルを送信 for (const fileInput of fileInputs) { const file = fileInput.files[0]; if (!file) continue; const chunkSize = 1024 * 1024; // 1MBのチャンクサイズ const chunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < chunks; i++) { const chunkStart = i * chunkSize; const chunkEnd = Math.min(file.size, chunkStart + chunkSize); const chunk = file.slice(chunkStart, chunkEnd); const formData = new FormData(); formData.append('chunkFile', chunk, file.name); // 分割されたファイルたちを「name="chunkFile"」にセット formData.append('chunkIndex', i); formData.append('totalChunks', chunks); formData.append('inputName', fileInput.name); // テキスト入力も同時に送信 textInputs.forEach(input => { formData.append(input.name, input.value); }); // 各チャンクをサーバーに送信 await fetch('/upload', { method: 'POST', body: formData }); } } // すべてのチャンクが送信されたことを通知(必要に応じて) fetch('/upload_complete', { method: 'POST' }); });
PHP
<?php $name_attr = 'chunkFile'; // ファイルのname属性 if (!empty($_FILES[$name_attr]['tmp_name'])) { $tmpName = $_FILES[$name_attr]['tmp_name']; $originalName = $_FILES[$name_attr]['name']; $chunkIndex = $_POST['chunkIndex']; $totalChunks = $_POST['totalChunks']; $inputName = $_POST['inputName']; $tempDir = 'temp_uploads'; // 一時ディレクトリの作成 if (!is_dir($tempDir)) { mkdir($tempDir, 0777, true); } // チャンクを一時ファイルとして保存 $tempFilePath = $tempDir . '/' . $inputName . '_' . $originalName . '.part' . $chunkIndex; move_uploaded_file($tmpName, $tempFilePath); if ($chunkIndex + 1 == $totalChunks) { // 最後だったら、全て結合 $finalFilePath = $tempDir . '/' . $inputName . '_' . $originalName; $fileHandle = fopen($finalFilePath, 'wb'); for ($i = 0; $i < $totalChunks; $i++) { $chunkFilePath = $tempDir . '/' . $inputName . '_' . $originalName . '.part' . $i; fwrite($fileHandle, file_get_contents($chunkFilePath)); unlink($chunkFilePath); // チャンクファイルを削除 } fclose($fileHandle); // DBなどへ保存 } } // ファイル以外のリクエストの対応 $familyName = $_POST['family_name'] ?? ''; $personalName = $_POST['personal_name'] ?? ''; echo json_encode(['success' => true]);
サンプル: ファイルが選択されたら都度、送信
流れとしては…
- ファイルを選択
- javascriptでチャンクに分けて送信
- PHP側ではファイルを保存して、ファイル名か何かをレスポンスにセット
- javascriptでレスポンスのファイル名か何かをどっかに保存( or formにセット)しておく
- form送信時、どっかに保存したファイル名か何かを送信
- PHP側ではリクエストからファイルを探してきて、処理続行
Javascript
PHPからのレスポンスを受け取る処理は省略。ひとまず送るトコだけ
document.getElementById('xxx').addEventListener('change', async function(event) { const file = event.target.files[0]; const chunkSize = 1024 * 1024; // 1MBのチャンクサイズ const chunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < chunks; i++) { const chunkStart = i * chunkSize; const chunkEnd = Math.min(file.size, chunkStart + chunkSize); const chunk = file.slice(chunkStart, chunkEnd); const formData = new FormData(); formData.append('chunkFile', chunk, file.name); // 分割されたファイルたちを「name="chunkFile"」にセット formData.append('chunkIndex', i); formData.append('totalChunks', chunks); await fetch('/upload', { // 各チャンクをサーバーに送信 method: 'POST', body: formData }); } });
PHP
$response = array( 'status' => 0, ); $name_attr = 'chunkFile'; // ファイルのname属性 $chunkname = ''; $finalFileName = ''; if ( !empty($_FILES[$name_attr]['tmp_name']) ) { // ファイルがupされたとき $tmpName = $_FILES[$name_attr]['tmp_name']; $originalName = $_FILES[$name_attr]['name']; $chunkIndex = $_POST['chunkIndex']; $totalChunks = $_POST['totalChunks']; if ( !is_numeric($totalChunks) ) { $response['err_message'] = '数字以外がきている'; echo json_encode($response); // レスポンスを返す exit; } $totalChunks = (int) $totalChunks; $path_img = '/images'; // ファイル置き場 if (!is_dir($path_img)) { mkdir($path_img, 0777, true); } // チャンクを一時ファイルとして保存 $chunkname = $originalName . '.part' . $chunkIndex; $tempFilePath = $path_img . '/' . $chunkname; move_uploaded_file($tmpName, $tempFilePath); // チャンクのインデックスを記録 $chunkIdxFilePath = $path_img . '/' . $originalName . '.index'; file_put_contents($chunkIdxFilePath, $chunkIndex . "\n", FILE_APPEND); // チャンクが揃ったら追加処理 $receivedChunks_ary = file($chunkIdxFilePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $receivedChunks = array_unique($receivedChunks_ary); if (count($receivedChunks) === $totalChunks) { $finalFileName = $originalName; $finalFilePath = $path_img . '/' . $finalFileName; $fileHandle = fopen($finalFilePath, 'wb'); // チャンクファイルを合体させたら削除 for ($i = 0; $i < $totalChunks; $i++) { $chunkFilePath = $path_img . '/' . $originalName . '.part' . $i; fwrite($fileHandle, file_get_contents($chunkFilePath)); unlink($chunkFilePath); } fclose($fileHandle); unlink($chunkIdxFilePath); // チャンクのインデックスを記録していたファイルも削除 } } $response['status'] = 1; $response['file'] = array( 'file_name' => $finalFileName, // すべて揃ったらファイル名が格納 // 'chunk_name' => $chunkname, // javascriptで何かしたいなら、追加 ); echo json_encode($response); // レスポンスを返す exit;