PHP 4G 大容量のファイルアップロード: enable_post_data_reading

PHPで大容量のダウンロードはサーバ側でfreadで数Kバイト単位で送出すればいいだけですが、アップロードは、そうはいかず、enable_post_data_readingをoff設定にして、自力でpostデータをパースし、データ部を数Kバイト単位で読むしかないのです。実装しました。
(1Gのファイルのダウンロード、アップロードは、オンメモリーで処理すると、
例えば同時接続10でアップロードすると10Gにもなって破綻します。)

[設定]

プログラムはアップロード専用フォルダーにして、
.htaccess

[js] php_flag enable_post_data_reading Off
[/js]

・enable_post_data_reading Offにすると、$_POST,$_FILEは使えません。$_GET,$SESSIONは有効。
・ini_set()ではenable_post_data_readingは設定できません。

test.html

[js] <html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<link rel="stylesheet" href="/css/jquery-ui.css" type="text/css" />
<script type="text/javascript" src="/js/jquery.min.js"></script>
<title></title>
</head>

<SCRIPT Language="Javascript1.2">
<!–

$(function() {
$(‘input[type=file]’).change(function() {
file1 = document.getElementById("userfile1").files[0];
file2 = document.getElementById("userfile2").files[0];
file3 = document.getElementById("userfile3").files[0];

if(typeof file1 === "undefined"){
document.sedesupload.up_size1.value = "0\t";
}else{
document.sedesupload.up_size1.value = file1.size + "\t" + file1.name;
}

if(typeof file2 === "undefined"){
document.sedesupload.up_size2.value = "0\t";
}else{
document.sedesupload.up_size2.value = file2.size + "\t" + file2.name;
}

if(typeof file3 === "undefined"){
document.sedesupload.up_size3.value = "0\t";
}else{
document.sedesupload.up_size3.value = file3.size + "\t" + file3.name;
}

});
});

–>
</SCRIPT>

<body>
<br>
<h1>TEST</h1>

<form enctype="multipart/form-data" name="sedesupload" action="binupload-multi.php" method="POST">
<input type="hidden" name="trkno" value="DD00008">
<input type="hidden" name="user" value="T">

<input type="hidden" name="up_size1" id="up_size1" value="0">
<input type="hidden" name="up_size2" id="up_size2" value="0">
<input type="hidden" name="up_size3" id="up_size3" value="0">

No1: <input name="userfile1" id="userfile1" type="file" /><br>
No2: <input name="userfile2" id="userfile2" type="file" /><br>
No3: <input name="userfile3" id="userfile3" type="file" /><br>

<br>
<input type="submit" value="ファイルを送信"/>
</form>
</html>

[/js]

・最大3ファイルを選択
・POSTデータに3ファイルのファイルサイズ、ファイル名を同時にセット。ファイルサイズをサーバ側に知らせないと、受け側のプログラムが面倒な処理になるので、入れてます。

binupload-multi.php

[js] <?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",180 * 3);
$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 "sucess\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>";

if($pname === "up_size1"){
$upbuf = explode("\t",$pdata);
$upfile_size[0] = $upbuf[0];
$upfile_name[0] = $upbuf[1];
}elseif($pname === "up_size2"){
$upbuf = explode("\t",$pdata);
$upfile_size[1] = $upbuf[0];
$upfile_name[1] = $upbuf[1];
}elseif($pname === "up_size3"){
$upbuf = explode("\t",$pdata);
$upfile_size[2] = $upbuf[0];
$upfile_name[2] = $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 "FLENMAE=[".$filename . "]<br>";

if($filename == ""){
fgets($handle_in); //skip null line;
$header_sec = 2;
continue;
}else{
for($i = 0 ; $i < 3 ; $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){
$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);

?>
[/js]

・max_execution_timeは、540秒(9分)
・マルチパートを区切りに、POSTデータをパース
・ファイルの場合は、ファイルサイズを基に8192バイト単位で受信しファイルに書き込み。

※テストコードなのでコードレビューはしておりません。
プログラム中に固定化(3ファイル前提)されているところもありますので利用される方はご注意を。日本語ファイル名等はそれなりの処理をして下さい。

112

113

合計9.5G アップロードできました。
この時、httpdのCPU使用率は50%でした。
※usleep(1000) 1msecのsleepを入れるとCPU使用率は13%になります。当然アップロード時間も増加しますので、max_execution_timeは要調整。

httpd CPU使用率
———————-
50% なし
34% usleep(100)
28% usleep(300)
13% usleep(1000) 1msec
———————–

まあ、昔のperlのcgi時代は全て自分でPOSTデータはパースしてたので、
全然違和感はないですが、今は全部やってくれるので知らない内に
オンメモリーで処理されていること忘れがちあるいはそもそも、そんな考えも
しないという新人の技術者も多いかと思いますのでご注意を。

コメントを残す