ファイルのアップロードを行う際に注意しないといけないのが「アップロード容量」と「タイムアウト」です。
どちらもサーバーの設定に関わるところでレンタルサーバーでは対処しきれないところもあり、大容量にするとそれだけアップロードに時間がかかりタイムアウトが発生しやすくなります。
そこでフロント側で分割してファイルをアップし、バックエンド側で受け取った後に結合するようにするとよいでしょう
注意すべき点として通常のPOSTでの送信ではないため、あらかじめそこら辺を考慮した処理が必要となります
今回はファイルのアップロードだけに絞ります。
フロント側(HTML+jQuery)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
<input type="file" id="fileInput"> <button id="uploadBtn">アップロード</button> <div id="progressContainer"> <div id="progressBar" style="width: 0%; background: green; height: 20px;"></div> </div> <div id="statusText"></div> <ul id="fileList"></ul> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $(document).ready(function () { $("#uploadBtn").click(function () { let file = $("#fileInput")[0].files[0]; if (!file) { alert("ファイルを選択してください"); return; } let originalFilename = file.name; // 日本語ファイル名保持 let tempFilename = generateTemporaryFilename(); // 半角英数字の一時ファイル名 uploadFile(file, tempFilename, originalFilename); }); function generateTemporaryFilename() { return "upload_" + Date.now() + "_" + Math.random().toString(36).substring(2, 10); } function uploadFile(file, tempFilename, originalFilename) { const chunkSize = 2 * 1024 * 1024; // 2MB const totalChunks = Math.ceil(file.size / chunkSize); let start = 0, chunkIndex = 0; function uploadNextChunk() { if (start >= file.size) { $("#statusText").html("<span class='text-success'>アップロード完了</span>"); loadFileList(); // アップロード完了後にリスト更新 return; } let chunk = file.slice(start, start + chunkSize); let formData = new FormData(); formData.append("file", chunk); formData.append("index", chunkIndex); formData.append("total", totalChunks); formData.append("tempFilename", tempFilename); formData.append("originalFilename", originalFilename); $.ajax({ url: "./upload.php", type: "POST", data: formData, processData: false, contentType: false, success: function (response) { let percent = Math.floor(((chunkIndex + 1) / totalChunks) * 100); $("#progressBar").css("width", percent + "%").text(percent + "%"); start += chunkSize; chunkIndex++; uploadNextChunk(); }, error: function () { $("#statusText").html("<span class='text-danger'>アップロード失敗</span>"); } }); } uploadNextChunk(); } function loadFileList() { $.get("file_list.php", function(data) { $("#fileList").html(data); }); } }); </script> |
ファイルを2MB単位で分割して送信を行い、最後に結合処理をサーバーに依頼します。(ファイルサイズを大きくするとその分1回あたりのアップロードサイズが大きくなります)
また、progressBarで進捗を表示するようにしています。
バックエンド側(PHP)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
<?php header("Content-Type: application/json"); // 一時保存ディレクトリ $uploadDir = __DIR__ . "/uploads/"; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0777, true); } // 必要なデータチェック if (!isset($_FILES["file"]) || !isset($_POST["index"]) || !isset($_POST["total"]) || !isset($_POST["tempFilename"]) || !isset($_POST["originalFilename"])) { echo json_encode(["error" => "パラメータが不足しています"]); exit; } $file = $_FILES["file"]; $chunkIndex = (int)$_POST["index"]; $totalChunks = (int)$_POST["total"]; $tempFilename = $_POST["tempFilename"]; $originalFilename = $_POST["originalFilename"]; $tempFile = $uploadDir . $tempFilename . ".part" . $chunkIndex; // チャンクを保存 if (!move_uploaded_file($file["tmp_name"], $tempFile)) { echo json_encode(["error" => "チャンクの保存に失敗しました"]); exit; } // 全チャンクが揃ったら統合処理 if ($chunkIndex + 1 === $totalChunks) { // 日本語のファイル名をそのまま使う $originalPath = $uploadDir . $originalFilename; // 同じ名前のファイルがある場合はリネーム $fileExt = pathinfo($originalFilename, PATHINFO_EXTENSION); $baseName = pathinfo($originalFilename, PATHINFO_FILENAME); $counter = 1; while (file_exists($originalPath)) { $originalPath = $uploadDir . $baseName . " ($counter)." . $fileExt; $counter++; } // 結合処理 if ($output = fopen($originalPath, "wb")) { for ($i = 0; $i < $totalChunks; $i++) { $chunkFile = $uploadDir . $tempFilename . ".part" . $i; if (!file_exists($chunkFile)) { echo json_encode(["error" => "チャンクファイルが足りません"]); exit; } fwrite($output, file_get_contents($chunkFile)); unlink($chunkFile); // 結合後にチャンク削除 } fclose($output); } else { echo json_encode(["error" => "ファイル統合に失敗しました"]); exit; } echo json_encode(["success" => true, "message" => "アップロード完了", "filename" => basename($originalPath)]); } else { echo json_encode(["success" => true, "message" => "チャンク受信成功", "chunkIndex" => $chunkIndex]); } |
こちらはREST APIで分割された分割ファイルを一時ファイルに保存し、全チャンクがそろったら結合して日本語の元ファイルで保存するプログラムです。
今回はプレーンなPHPで書いてますが、もちろんフレームワークで書いてもいいです!
ちなみに同じファイルがアップされた場合は(1)などをつけるようにしています。
例えばファイル情報をデータベースに保存したい場合は、結合完了したタイミングでファイル情報を保存すればよいかと思います。
最後に
フォームのような「入力」→「確認」→「完了」みたいな画面遷移だとめんどくさいですが、「入力」→アラート表示→「登録完了」のようなアプリテイストな挙動だとこっちの方がいいですね。ていうか本当はドロップアンドドラッグでファイル選択できた方がよかったかなと今更ながら思いました・・・