ラボ > PHP:HTML、Javascript絡み、ファイル関連、Javascript関連:form

js、php ファイルを分割して送信

大きいファイルを分割して送る

作成日:2024-03-29, 更新日:2024-04-01

以前に「js、php ファイルを分割して送信」でまとめたけど…改めてまとめ直す

方針

formからファイルを送る…っていうときの対応方針はざっくり2つ…かな?

  • ファイルと他の入力情報を送る
  • ファイルのみ送る

ファイルと他の入力情報を送る

気分的には、コッチがいい
コッチがいいけど、メンテのときが面倒になりそうな気がする

  1. form送信のときのjavascript制御
    • javascriptでファイルを分割
    • ファイル以外の入力情報の取り扱いを考える(分割したファイルと毎回 or 初回のみ or 最後のみ…送る)
    • 分割したファイルたち、ファイル以外の入力情報をFetchやAjaxなどで送る
  2. リクエストをPHPで受け取る
    • 分割されたファイルを受け取り、すべて揃ったら合体
    • ファイル以外の入力情報の取り扱いを考える(初回のみ or 最後のみ or すべて揃ったときのみ…受け取る)
    • 「すべて揃う前、揃った後」or「すべての処理完了前 or 完了後」でレスポンスに違いを持たせておく
  3. レスポンスをJavascriptで受け取る
    • 「すべて揃う前、揃った後」or「すべての処理完了前 or 完了後」で処理を分ける(※すべて揃った際、「完了した」とメッセージを出力…等)

ファイルのみ送る

色々と面倒だけど、メンテのコトを考えるとコッチのほうがラクかも

  1. javascriptでファイルを送る
    • javascriptでファイルを分割
    • 分割したファイルをFetchやAjaxなどで送る
  2. リクエストをPHPで受け取る
    • 分割されたファイルを受け取り、すべて揃ったら合体。フラグとして「仮完了」等、DBなどに保存しておく
    • すべて揃う前、揃った後でレスポンスに違いを持たせておく(揃ったときのみファイル名を返す…など)
  3. レスポンスをJavascriptで受け取る
    • すべて揃う前、揃った後で処理を分ける(※すべて揃った際、レスポンスにあるファイル名などをformにセット)
  4. form送信(ファイル以外の入力情報、レスポンスにあったファイル名など)
  5. PHPで受け取る
    • ファイル名などから、受け取り済みのファイルか確認し、各処理(DBに保存、メール送信など)
    • フラグの「仮完了」の取り下げ
  6. 任意のタイミングでフラグの「仮完了」になっているファイルを削除(※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]);

サンプル: ファイルが選択されたら都度、送信

流れとしては…

  1. ファイルを選択
  2. javascriptでチャンクに分けて送信
  3. PHP側ではファイルを保存して、ファイル名か何かをレスポンスにセット
  4. javascriptでレスポンスのファイル名か何かをどっかに保存( or formにセット)しておく
  5. form送信時、どっかに保存したファイル名か何かを送信
  6. 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;

関連項目