KazuProg's notes

技術メモや備忘録などを自由気ままに書き連ねています

PHPでファイルのアップロード処理を自作する

作成日: ・ 更新日:

大容量ファイルをアップロードしたかったのですが、 PHP標準の$_FILES変数を使用する場合、一旦全ての内容を メモリに格納してから処理するため、 メモリサイズが足りなくてエラーになってしまうため、 その対策として自分で実装してみました。

まあ、自分で実装といっても、以下のページの内容を参考にして、 複数ファイルのアップロードに対応させただけですけどね。

PHP 4G 大容量のファイルアップロード: enable_post_data_reading | SEIWA TECHNOLOGY SI-Blog

動作には関係ないのですが、私なりに (jQueryを使ったことがないので、素のJavaScriptで書いてるなどの) 変更をしたところもあります。

変更後のソースはこちらになります。 実際に使用する際には、パスなどをそれぞれのサーバに合わせて、 適切な値に書き換えるなどしてください。

.htaccess

php_flag enable_post_data_reading Off

test.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Upload Test</title>
    </head>
<body>
    <br>
    <h1>TEST</h1>
    <form enctype="multipart/form-data" action="binupload-multi.php" method="POST">
        <div id="up_info"></div>
        <input name="files" id="files" type="file" multiple>
        <br>
        <input type="submit" value="ファイルを送信">
    </form>
    <script>
    <!--
        var selectFile = document.querySelector("input#files");
        var fileInfo = document.querySelector("div#up_info");
        selectFile.addEventListener('change',function() {
            fileInfo.innerHTML = '';
            var fileList = selectFile.files;
            for(var i = 0; i < fileList.length; i++) {
                var info = document.createElement('input');
                info.type = 'hidden';
                info.name = 'fileinfo_'+i;
                info.value = fileList[i].size + '\t' + fileList[i].name;
                fileInfo.appendChild(info);
            }
        });
    -->
    </script>
</html>

binupload-multi.php

<?php
# php 5.4 or higher
#.htaccess php_flag enable_post_data_reading Off
#
#print_r($_SESSION);    // is
#print_r($_REQUEST);    // is
#print_r($_GET);        // is
#print_r($_POST);       // null

//-- config base -----------------
ini_set("max_execution_time", 0);//タイムアウトを無効にする。
$base_dir = "/home/temp";
$READ_BUF_SIZE = 8192;
//-------------------------------

$handle_in = fopen("php://input", "rb");
if (FALSE === $handle_in) {
    exit("Failed to open stream to URL");
}

$header_sec = 0;

$contents = "";
$multi_part ="";
$multi_part_now = "";

while (!feof($handle_in)) {
    // ------------------------------------
    // STEP 0: first GET multi part strings
    // ------------------------------------
    if($header_sec == 0){
        //get multipart string
        $multi_part = str_replace("\r\n", '', fgets($handle_in));
        $header_sec = 1; //Next Content
        if(strlen($multi_part) == 0){
            print "multi_part_len 0 error \r\n";
            exit;
        }
        print "[". $multi_part . "]<br><br>";

        continue;
    }

    // ------------------------------------
    // STEP 2: next GET multi part strings & check
    // ------------------------------------
    if($header_sec == 2){
        $multi_part_now = str_replace("\r\n", '', fgets($handle_in));

        if($multi_part_now === $multi_part){
            $header_sec = 1;
            continue;
        }else{
            // Last part check
            if($multi_part_now == $multi_part . "--"){
                print "success\r\n";
            }else{
                print "sequence error step 2\r\n";
            }
            break;
        }
    }

    // --------------------------------------------------
    // STEP 1: GET Content-Disposition (post name & data)
    // --------------------------------------------------
    if($header_sec == 1){
        $line = str_replace("\r\n", '', fgets($handle_in));
        $sbuf = explode('filename="',$line);
        if(count($sbuf) == 1){
            //parameter
            $pbuf = explode('name="',$sbuf[0]);
            $pname = mb_substr($pbuf[1], 0, -1);

            fgets($handle_in); //null line;
            $pdata = str_replace("\r\n", '', fgets($handle_in));
            print "NAME=[".$pname . "] DATA=[" . $pdata . "]<br>";

            //複数のアップロードに対応させるために変更
            $num = intval(explode('_',$pname)[1]);
            $upbuf = explode("\t",$pdata);
            $upfile_size[$num] = $upbuf[0];
            $upfile_name[$num] = $upbuf[1];

            $header_sec = 2;
            continue;

        }else{
            //file parameter
            $filename = mb_substr($sbuf[1], 0, -1);

            fgets($handle_in); //skip Content-Type:;
            fgets($handle_in); //skip null line;
            print "FILENAME=[".$filename . "]<br>";

            if($filename == ""){
                fgets($handle_in); //skip null line;
                $header_sec = 2;
                continue;
            }else{
                //複数のアップロードに対応させるために変更
                for($i = 0 ; $i < count($upfile_name) ; $i++){
                    if($filename === $upfile_name[$i]){
                        $filesize = $upfile_size[$i];
                        $handle_out = fopen($base_dir . "/" . $filename, "w+b");
                        $header_sec = 3;
                        break;
                    }
                }
            }
        }
    }

    // --------------------------------------------------
    // STEP 3: GET file binary contests
    // --------------------------------------------------
    if($header_sec == 3){
        if($filesize < $READ_BUF_SIZE){
            //ファイルサイズが0の時の処理を追加
            if($filesize == 0){
                touch($base_dir . "/" . $filename);
            }else{
                $contents = fread($handle_in, $filesize);
                fwrite($handle_out,$contents);
            }
        }else{
            $readsize = 0;
            while (!feof($handle_in)) {
                $contents = fread($handle_in, $READ_BUF_SIZE);

                $readsize += $READ_BUF_SIZE;
                fwrite($handle_out,$contents);

                if(($readsize) + $READ_BUF_SIZE >= $filesize){
                    $contents = fread($handle_in, $filesize - $readsize);
                    fwrite($handle_out,$contents);
                    break;
                }
                //usleep(1000);
            }

        }
        fgets($handle_in); //null line;
        $header_sec = 2;
        fclose($handle_out);
    }
}
fclose($handle_in);

?>

Chromeで実際にアップロードしてみると、 以下のようなレスポンスが返ってきます。 (ファイル名などは各環境によって変わります。)

[------WebKitFormBoundaryu015gANUp9jglfd2]

NAME=[fileinfo_0] DATA=[2345 testdata1.tmp]
NAME=[fileinfo_1] DATA=[6752 testdata2.tmp]
NAME=[fileinfo_2] DATA=[856323 testdata3.tmp]
NAME=[fileinfo_3] DATA=[2457445 testdata4.tmp]
FILENAME=[testdata1.tmp]
FILENAME=[testdata2.tmp]
FILENAME=[testdata3.tmp]
FILENAME=[testdata4.tmp]
success

これで、大きなファイルでもメモリをあまり使わずに アップロードができるようになりました。

しかし、大きなファイルをアップロードするとなると、 通信回線にもよりますが、結構時間がかかることも考えられます。

アップロードがなかなか終わらなくてイライラしてしまうこともあるかと思います。

ということで、次回はアップロードの進捗状況を 表示できるよう、プログラムを修正していこうと思います。

巨大なファイルのアップロードで、進捗状況を表示させる