作成日:2024-03-29, 更新日:2024-05-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 = '';
$finalFilePath = '';
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, // すべて揃ったらファイル名が格納
'file_path' => $finalFilePath, // すべて揃ったらファイルPATHが格納
// 'chunk_name' => $chunkname, // javascriptで何かしたいなら、追加
);
echo json_encode($response); // レスポンスを返す
exit;