Ajaxファイル転送

WordPressJavaScript

【PHP・JavaScript】大容量ファイルのアップロードとWordPressへの適用

更新日:2022/12/15

ブラウザからサーバーへファイルをPOSTでアップロードする際、PHPではファイルサイズに制限があります。
この制限で、大容量のファイルアップロードができません。
制限を変更するには、php.iniを変更しなければいけません。

またWebサーバーに「Nginx」を採用しているケースでは、Nginxの設定ファイルも併せて変更する必要があります。

これらの設定を変更せずに、ファイルを小さい単位(チャンク)に分割してアップロードをする方法をお伝えし、その方法を元にWordPressのAjax通信機能を利用したプラグインにアレンジする方法についてもお伝えします。

なお今回は、Flow.jsというライブラリを使用します。

 

Flow.jsのダウンロード

Flow.jsはComposerなどでインストールするのが本来の方法のようですが、サーバーでコマンドを叩けないレンタルサーバーなどでも利用できるような形にしてみます。

Flow.jsはブラウザで利用する「flow.js」と、サーバーで利用する「flow-php-server」にわかれているので、それぞれダウンロードします。

「flow.js」のダウンロード

次のリンクから、「flow.js」ダウンロードページを開きます。

https://github.com/flowjs/flow.js

「Clone or download」「Download ZIP」の順でクリックして、flow.js-master.zipを保存し解凍します。

github ダウンロード

今回は、圧縮版の「flow.min.js」を使用します。

flow.js-master.zipの内容

flow.js-master
    ├ .github
    ├ dist
    │   ├ flow.js
    │   └ flow.min.js

「flow-php-server」のダウンロード

次のリンクから、「flow-php-server」ダウンロードページを開きます。

https://github.com/flowjs/flow-php-server

flow-php-server-master.zipをダウンロードして解凍してください。

今回は、Flowディレクトリ内の全てのファイルを使用します。
その際、Flowディレクトリの名前を変更しないで、そのままサーバー内に保存してください。

flow-php-server-master.zipの内容

flow-php-server-master
├ src
│ └ Flow
│ ├ Monogo
│ ├ ・・・・.php
│ :
│ :
│ └ ・・・・.php

 

チャンクサイズについて

flow.jsは大容量のファイルを、複数のチャンクに分けてアップロードしています。
チャンクのファイルサイズは、初期化時にJavaScript上で指定します。
つまり自分でサイズ指定する必要があります。

flow.jsの初期化

flow.jsでアップロードするチャンクのサイズは、chunkSizeで指定します。
値はバイト数です。

また重要なのがforceChunkSizeです。
これはチャンクを分割したときに、端数のバイト数の取り扱いを指定します。

true : 端数を一つのチャンクとする

false : 端数を最後のチャンクに含める

つまり設定値がfalseの場合、最終チャンクはchunkSizeで指定した値の2倍に近いサイズになる可能性があります。
アップロード上限を超える可能性が多くなるので、forceChunkSizetrueをセットするべきです。

なおforceChunkSizeのデフォルトはfalseです。


flow = new Flow({
            target: 呼び出すサーバープログラムのURL,
            forceChunkSize:true,
            query:POSTまたはGETする独自クエリ,
            chunkSize: 1024 * 1024
        });

PHPのアップロード可能サイズを調べる

PHPは設定ファイル(pnp.ini)内に次の値でセットされています。

upload_max_filesize:アップロード可能なファイルサイズ
post_max_size:POST時の最大データサイズ
memory_limit:呼び出されたPHPが使用できる最大メモリサイズ

プログラム上では、ini_get('upload_max_filesize')とすることで値を取り出せます。

詳しくは次のページを参考にしてください。
【PHP】 アップロード可能な上限ファイルサイズの調べ方と注意点

チャンクサイズの設定

チャンクサイズを小さくすると、その分だけサーバーとの通信回数が増え、サーバー側の負担が増えます。
そのため、できる限り大きいサイズを指定するべきです。
※ただし大きすぎてもいけません。PHPの処理時間にも制限があり、これを超えるとエラーになります。

汎用的なプログラムを作成する場合は、upload_max_filesizeの値を取得してチャンクサイズを動的に設定するとよいかもしれません。

しかし問題もあります。

post_max_size と upload_max_filesize の設定方法の例として、この二つの値を次のように同じものにしているケースをよく見ます。

.htaccessでの設定例

php_value post_max_size 20M
php_value upload_max_filesize 20M

flow.jsの場合、ファイルのデータの他に、さまざまなデータがPOSTされます。
そのため20Mバイトのファイルをアップロードしようとすると、post_max_sizeの制限を受けてエラーとなります。

このようなケースの場合、 upload_max_filesize だけでなく post_max_size も考慮しないといけません。

またWebサーバー側で制限されている場合は、PHP側でアップロード可能はファイルサイズを確認できません。
エラーをサーバーから受け取り、チャンクサイズを減らすようにプログラムやユーザーに促す。
または最初から、小さなチャンクサイズを指定しておくしかありません。

ちなみにNginxのデフォルトサイズは1Mバイトです。
どんなサーバーでも問題なくアップロードできるようにするなら、通信回数の増加に目をつぶり、1024 * 1024バイトから適当なバイト数を引いた値をセットしておきましょう。

 

基本形

まずは他のサイトなどで紹介されている形式でプログラムを作成してみます。

基本形では、ファイル選択後、すぐにアップロードされます。

flow.jp 基本的な流れ

完成したプログラム群は次のような構成になり、赤文字のファイルを新たに作成していきます。

基本形のファイル構成

flow-test-kihonkei
    ├ Flow // flow-php-server
    ├ temp // アップロード中にチャンクデータが一時保管されるディレクトリ
    ├ upload // アップロード完了後にファイルを保存するディレクトリ
    ├ lib
    │   ├ flow.min.js
    │   ├ flow-lib.php
    │   ├ flow-test.css
    │   └ flowup.php flow-upload-kihonkei.phpから
    │                        // 呼び出されるアップロード制御関数
    ├ flow-test-kihonkei.php // Webページ
    ├ flow-test-kihonkei.js // Webページ上のJavaScript
    └ flow-upload-kihonkei.php // Webページ上から呼び出すサーバープログラム

Flowディレクトリは、ダウンロードしてきた「flow-php-server」のFlowディレクトリをそのままコピーします。
ダウンロードの説明でも書きましたが、Flowディレクトリの名前を変更してはいけません。

赤文字を作成していきます。

Webページの作成(基本形) | flow-test-kihonkei.php

ブラウザで表示される画面を作成します。

flow-test-kihonkei.php


<?php
require_once 'lib/flow-lib.php';

?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset= "UTF-8">
    <title>flow-test-kihon</title>
    <script src="./lib/flow.min.js"></script>
    <script src="./flow-test-kihonkei.js"></script>
    <link rel="stylesheet" href="./lib/flow-test.css">
</head>
<body>
<?php echo \myflow\checkDirAndMaxSize(); ?>
    <p>アップロードするファイルを指定してください</p>
    <button id="uploadButton">ファイル選択&アップロード</button>
    <div id="progress-info">
        アップロード状況:
        <div id="progress"><div id="progressbar"></div></div>
    </div>
</body>
</html>

今回僕は、テンポラリとアップロードディレクトリに書き込めないことに気が付かないで、かなり長い時間悩みました。
レンタルサーバーならほとんどないと思いますが、たまに同じようなことがあるので、flow-lib.php内でディレクトリの書き込みチェックをおこなっています。

共通ライブラリの作成 | flow-lib.php

ここでは一時ディレクトリと保存ディレクトリの定義、および、書き込むチェックとPHPのアップロード上限のチェック(表示のみ)をおこなっています。

flow-lib.php


<?php
namespace myflow;

const SSID = 'flow-test';

$flow_tempdir = dirname(__FILE__) .'/../temp';
$flow_updir = dirname(__FILE__) .'/../upload';

function checkDirAndMaxSize(){
    global $flow_tempdir,$flow_updir;

    $html = is_writable($flow_tempdir) 
        ? '' : '<span style="color:red">一時ディレクトリ書き込み不可</span><br>';
    $html .= is_writable($flow_updir) 
        ? '' : '<span style="color:red">アップロードディレクトリ書き込み不可</span><br>';

    $html .= 'upload_max_filesize=' . ini_get('upload_max_filesize') . '<br>';
    $html .= 'post_max_size=' . ini_get('post_max_size') . '<br>';
    return $html;
}

スタイルの作成 | flow-test.css

このcssファイルでは、アップロードの進捗状況を表示するプログレスバーを定義しています。

flow-test.css


#progress-info{
    display: none;
}
#progress{
    width: 100%;
    border: 1px solid #000;
    box-sizing: content-box;
    margin: 30px 0;
    max-width: 500px;
    border-radius: 10px;
    height: 20px;
    background: #fff;
    overflow: hidden;
}
#progressbar{
    height:20px;
    width:0;
    background:#ccc;
    box-sizing: border-box;
}

ブラウザ側Javascriptの作成(基本形) | flow-test-kihonkei.js

ブラウザ上でサーバーと通信するJavascriptを作成します。

flow-test-kihonkei.js


(function () {

    'use strict';

    // エラーメッセージ
    const error = {
        'UPLOAD_ERR_INI_SIZE' : 'upload_max_filesize ディレクティブの値を超えている',
        'UPLOAD_ERR_FORM_SIZE' : '指定されたMAX_FILE_SIZEを超えている',
        'UPLOAD_ERR_PARTIAL' : 'サーバーにチャンクの一部のみしかアップロードされていない',
        'UPLOAD_ERR_NO_FILE' : 'アップロードされなかった',
        'UPLOAD_ERR_NO_TMP_DIR' : 'サーバーにテンポラリフォルダがない' ,
        'UPLOAD_ERR_CANT_WRITE'  : 'サーバーに書き込みに失敗',
        'UPLOAD_ERR_UNKNOWN'  : '不明なエラー',
        'FAILED TO SAVE FILE'  : 'サーバーにてファイル結合失敗',
        'Request Entity Too Large' : 'Webサーバー(Nginx)のアップロード制限を超えた'
    };
    // サーバー受け取ったエラーメッセージを変換
    const errorString=m=>{
        let keys = Object.keys(error);
        let len = keys.length;
        for(let i = 0; i < len ; i++){
            if(m.indexOf(keys[i]) !== -1) return error[keys[i]];
        }
        return m;
    };

    document.addEventListener('DOMContentLoaded',()=>{

        let progress_bar = document.getElementById("progressbar");
        let progress_info = document.getElementById("progress-info");
        // flow初期化
        let flow = new Flow({
                target: 'flow-upload-kihonkei.php', // 呼び出すサーバープログラム
                forceChunkSize: true, // 最終チャンクのサイズをchunkSize以下にする
                /* allowDuplicateUploads: true */
                /* chunkSize: 1024 * 1024 */
            });
        // アップロードボタンとflowを関連付ける
        flow.assignBrowse(document.getElementById('uploadButton'));
        // 送信イベント処理
        flow.on('filesSubmitted', (file,message)=>{
            progress_bar.style.width='0';
            progress_info.style.display='block';
            if(file.length===0) alert("ファイルが指定されていないか、アップロード済みのファイルが指定されました");
            else flow.upload();
        });
        // プログレスイベント処理
        flow.on('progress',function(){
                    progress_bar.style.width
                        =Math.floor(flow.progress()*100).toString()+'%';
                });
        // 送信完了イベント処理
        flow.on('fileSuccess', (file,message)=>{
                    alert("アップロード完了:" + message);
                });
        // 送信失敗イベント処理
        flow.on('fileError', (file, message)=>{
                    alert('エラー:' +  errorString(message));
                });
    });
})();

flowの初期化では小分けにするファイル(チャンク)のサイズをchunkSizeで指定します。
デフォルト値が1024*1024バイト=1Mバイトのため、ここではコメントにしてあります。

またデフォルトでは、Webページを再ロードするまで同一ファイルを再アップロードできません。
同一ファイルを指定するとfilesSubmittedイベントでlength=0のfileオブジェクトが渡されます。
今回のコードでは「ファイルが指定されていないか、アップロード済みのファイルが指定されました」と出力していますが、サーバー上に存在しているかどうか確認していないため、このメッセージでは誤解を受けるかもしれません。

allowDuplicateUploadsにtrueを指定すると、同一ファイルを繰り返しアップロードすることが可能になります。
Webページを再ロードすると設定に関係なくアップロードできることを考慮すると、allowDuplicateUploadsはtrueにしておいた方がいいかもしれません。

assignBrowseでflow.jsとページ上のボタンを関連付けています。
これにより、ファイル選択とSubmitの通知をflow.jsに任せることができます。
ただしアップロードをするには、filesSubmittedイベントでuploadメソッドを呼び出す必要があります。

アップロード制御PHP | flowup.php

flow.jsからのアップロードを制御する、サーバー側のプログラムを作成します。

flowup.php


<?php
namespace myflow;

require_once dirname(__FILE__).'/../Flow/Autoloader.php';

function flowup($tempdir,$savedir){
    \Flow\Autoloader::register();
    $config = new \Flow\Config();
    $config->setTempDir( $tempdir);
    $file = new \Flow\File($config);

    $protocol = (isset($_SERVER['SERVER_PROTOCOL']) 
        ? $_SERVER['SERVER_PROTOCOL'] 
        : ;'HTTP/1.0');

    if ($_SERVER['REQUEST_METHOD'] === 'GET') { // チャンクの有無(アップロード済み)チェック
            if ($file->checkChunk()) {  // チャンクあり
                header($protocol . " 200 Ok");
            } else {    // チャンクなし
                header($protocol . " 204 No Content");
                die();
            }
    } else {        // チャンクアップロード
            if ($file->validateChunk()) {
                $file->saveChunk();
            } else {
            // エラー内容の確認
            $loadfile = (new \Flow\Request())->getFile();
            $messages='UPLOAD_ERR_UNKNOWN';
            switch($loadfile['error']){
                case UPLOAD_ERR_INI_SIZE:
                    // upload_max_filesize ディレクティブの値を超えている
                    $messages = 'UPLOAD_ERR_INI_SIZE';
                    break;

                case UPLOAD_ERR_FORM_SIZE:
                    // HTMLで指定されたMAX_FILE_SIZE を超えている
                    $messages = 'UPLOAD_ERR_FORM_SIZE';
                    break;

                case UPLOAD_ERR_PARTIAL:
                    // 一部のみしかアップロードされた
                    $messages = 'UPLOAD_ERR_PARTIAL';
                    break;

                case UPLOAD_ERR_NO_FILE:
                    // アップロードされなかった(ファイルが無い)
                    $messages = 'UPLOAD_ERR_NO_FILE';
                    break;

                case UPLOAD_ERR_NO_TMP_DIR:
                    // テンポラリフォルダがない
                    $messages= 'UPLOAD_ERR_NO_TMP_DIR';
                    break;
                case UPLOAD_ERR_CANT_WRITE:
                    // 書き込みに失敗
                    $messages= 'UPLOAD_ERR_CANT_WRITE';
                    break;

                case UPLOAD_ERR_EXTENSION:
                    // 拡張モジュールがファイルのアップロードを中止した
                    $messages= 'UPLOAD_ERR_EXTENSION';
                    break;
            }
            // error, invalid chunk upload request, retry
            header($protocol .' 400 Bad Request');
            echo $messages;
            die();
        }
    }

    if ($file->validateFile()){ // アップロード完了
            $request = new \Flow\Request();
            $filename = $savedir . $request->getFileName(); // アップロード後のファイル名(フルパス)
            try{
                if($file->save( $filename ) ) { // 結合&保存

                // アップロード完了後の処理

                }else{
                    header($protocol .' 400 Bad Request');
                    echo 'FAILED TO SAVE FILE';
                }
            }catch(\Exception   $e){
                    header($protocol .' 400 Bad Request');
                    echo 'FAILED TO SAVE FILE';
            }
    }
    die();
}

このソースはWordPress版でも使用するので、関数の多重定義を回避するためにnamespaceを定義しています。

なぜGETのチェックをしているのか、疑問に感じますね。

flow.jsでは、チャンクのアップロード前にGETメソッドでファイル確認をしています。
応答として、ファイルがあれば200を、なければ204をブラウザに返します。

この処理でアップロードの中断からのレジューム機能を実現しているようです。

flow.jsのPHPモジュールはエラーの種類を判断してくれないので、自分でチェックしています。
$loadfileには、$_FILEで渡されたファイル情報が入っているので、この情報からエラーを判断しています。

最後に$file->validateFile()で、アップロード完了の確認をして、チャンクを結合します。
結合後になんらかの処理を行う場合は、このプログラム中に作りこんでいきます。
今回は何もしていません。

問題点:

上のソースはアップロードしたファイルを、そのままのファイル名でアップロードしています。
しかしphpなどのスクリプトファイルをアップロードすると、外部から実行できてしまいます。

アップロードディレクトリのみスクリプトの実行を制限したり、そのままのファイル名で保存しないなどの措置が必要です。

サーバー側PHPの作成(基本形) | flow-upload-kihonkei.php

ブラウザ側のJavascriptから呼び出される、サーバープログラムを作成します。

flow-test-kihonkei.php


<?php

require_once './lib/flow-lib.php';
require_once './lib/flowup.php';

\myflow\flowup($flow_tempdir,$flow_updir);

このプログラムは、flowup()を呼び出しているだけです。
flowup.phpに手を加え、flow-test-kihonkei.jsから直接flowup.phpを呼び出しても問題ありません。

基本形の確認

サーバーにソースを設置して、ブラウザからflow-test-kihonkei.phpを呼び出します。

一時ディレクトリ書き込み不可」または「アップロードディレクトリ書き込み不可」が表示されたら、それぞれのディレクトリのパーミッションを777に変更してください。

問題なければ、サイズが大きめのファイルをアップロードして正常にサーバーに保存されているか確認してください。

 

改良版(CSRF対策&ファイル選択・送信分離)

ここまでのflow.jsを使用したプログラムで、アップロードをおこなうことができますが、セキュリティの面が不安なので、セッションを利用したXSRF対策を実装してみます。

また、今回のプログラムは大容量ファイルのアップロードを想定しているため、ファイル選択後に選択したファイルの名前を確認できたほうが安心です。
そこで、ファイル選択後にアップロードボタンを押すことで送信を開始するように変更してみます。

flow.jp 大容量ファイル アップロード 流れ

さらに、アップロード中に一時停止(pause)および再開(resume)・キャンセルできる機能を追加します。

クライアント

なおflow.jpの仕様上、キャンセルをしてもアップロード途中の一時ファイル(チャンクファイル)がサーバーから削除されません。
同じファイルを指定するとチェンクファイルを確認し、アップロードを再開するレジューム機能を実現するためです。

一時ファイルはアップロードが完了するまでサーバーに残り続けるので、別途削除する必要があります。
※今回は実装していません。

完成したプログラム群は次のような構成になり、赤文字のファイルを新たに作成していきます。

基本形のファイル構成

flow-test
    ├ Flow // flow-php-server
    ├ temp // アップロード中にチャンクデータが一時保管されるディレクトリ
    ├ upload // アップロード完了後にファイルを保存するディレクトリ
    ├ lib
    │   ├ flow.min.js
    │   ├ flow-lib.php // 基本形と同じもの
    │   ├ flow-test.css // 基本形と同じもの
    │   ├ flowup.php // 基本形と同じもの
    │   └ flow-test.js // Webページ上のJavaScript
    ├ flow-test.php // Webページ
    └ flow-upload.php // Webページ上から呼び出すサーバープログラム

次項のWordPress版でそのまま使用できるソースは、libディレクトリに移動しています。

Webページの作成(改良版) | flow-test.php

ブラウザで表示される画面を作成します。

flow-test.php


<?php
require_once 'lib/flow-lib.php';

function CreateToken(){
    return 'tkn' . bin2hex(openssl_random_pseudo_bytes(8));
}

const SSID = 'flow-test';

// セッション開始&トークンセット
session_start();
$token = (isset($_SESSION[SSID])) ? $_SESSION[SSID] :  CreateToken();
$_SESSION[SSID]=$token;

?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>flow-test</title>
    <script src="lib/flow.min.js"></script>
    <script>
        const flow_test_OPT = {"api":"flow-upload.php","chunksize":1024 * 1024,"data":{"token":"<?php echo $token; ?>"}};
    </script>
    <script src="lib/flow-test.js"></script>
    <link rel="stylesheet" href="lib/flow-test.css">
</head>
<body>
<?php echo \myflow\checkDirAndMaxSize(); ?>
    <p>アップロードするファイルを指定してください</p>
    <input type="file" id="filesel"><br>
    <button id="uploadButton">アップロード</button>
    <div id="progress-info">
        アップロード状況:
        <div id="progress"><div id="progressbar"></div></div>
        <div id="progressButton">
            <button id="pauseButton">中断</button>
            <button id="resumeButton">再開</button>
            <button id="canselButton">キャンセル</button>
        </div>
    </div>
</body>
</html>

基本形と同様に、まずは各ディレクトリの書き込みチェックをおこなっています。

その後セッションを開始して、トークンを取得します。
ここでは簡易的におこなっているので、セッションIDの変更やトークンの作成方法など適宜変更してください。
大容量ファイルのアップロードに時間がかかって、途中でセッションの有効期限がきれたらどうなるのか?という問題も考えておくべきかもしれません。

また、ブラウザ側Javascriptから呼び出されるサーバープログラムのURLと、トークンをhtml上に直接出力しています。


const flow_test_OPT = {
    "api"  : "flow-upload.php", // 呼び出されるサーバープログラム
    "chunksize"  : 1024 * 1024, // チャンクのサイズ
    "data" : { // サーバーに渡されるクエリ
           "token" : "<?php echo $token; ?>"
            }
};

これは、今回作成するJavascriptを、次項のWordPress版でそのまま使用するためです。
※WordPressのAjax利用時の仕様です。

ブラウザ側Javascriptの作成(改良形) | flow-test.js

ブラウザ上でサーバーと通信するJavascriptを作成します。

flow-test.js


(function () {

    'use strict';

    // エラーメッセージ
    const error = {
        'UPLOAD_ERR_INI_SIZE' : 'upload_max_filesize ディレクティブの値を超えている',
        'UPLOAD_ERR_FORM_SIZE' : '指定されたMAX_FILE_SIZEを超えている',
        'UPLOAD_ERR_PARTIAL' : 'サーバーにチャンクの一部のみしかアップロードされていない',
        'UPLOAD_ERR_NO_FILE' : 'アップロードされなかった',
        'UPLOAD_ERR_NO_TMP_DIR' : 'サーバーにテンポラリフォルダがない' ,
        'UPLOAD_ERR_CANT_WRITE'  : 'サーバーに書き込みに失敗',
        'UPLOAD_ERR_UNKNOWN'  : '不明なエラー',
        'FAILED TO SAVE FILE'  : 'サーバーにてファイル結合失敗',
        'Request Entity Too Large' : 'Webサーバー(Nginx)のアップロード制限を超えた'
    };
    // サーバー受け取ったエラーメッセージを変換
    const errorString=m=>{
        let keys = Object.keys(error);
        let len = keys.length;
        for(let i = 0; i < len ; i++){
            if(m.indexOf(keys[i]) !== -1) return error[keys[i]];
        }
        return m;
    };

    let flow=null; // flowオブジェクト
    let progress_bar=null; // プログレスバー要素
    let progress_info=null; // プログレスバーエリア
    let progress_button=null; // 中断・再開・キャンセルボタンエリア
    let filesel = null; // ファイル選択ボタン
    let uploadButton = null; // アップロードボタン

    // idから要素取得
    const getEl=id=>document.getElementById(id);

    // flowのファイル情報をリセット
    const resetFlow=()=>{
        if(  flow.files.length === 0) return;
        for (let i = flow.files.length - 1; i >= 0;i--) {
            flow.removeFile(flow.files[i]);
        }
    };
    // アップロード中のボタン選択不可制御
    const busy=(f)=>{
        filesel.disabled=f;
        uploadButton.disabled=f;
    };
    // プログレスバー・レジュームボタン等エリアの表示・非表示
    const progressButtonShow = (f) =>{
        progress_button.style.display= (f) ? 'block' : 'none';
    };
    // プログレスバーの進捗表示
    const progress=v=>{progress_bar.style.width=v.toString()+'%'};

    // DOMツリーの構築が完了後の処理
    document.addEventListener('DOMContentLoaded',()=>{

        progress_bar = getEl("progressbar");
        progress_info = getEl("progress-info");
        progress_button = getEl("progressButton");
        filesel = getEl("filesel");
        uploadButton = getEl("uploadButton");

        // flow初期化
        flow = new Flow({
                target: flow_test_OPT.api,
                forceChunkSize: true, // 最終チャンクのサイズをchunkSize以下にする
                /* chunkSize: 1024 * 1024, */
                query:flow_test_OPT.data
            });

        // プログレスイベント処理
        flow.on('progress',()=>{
                    progress(Math.floor(flow.progress()*100));
            });

        // アップロード完了通知
        flow.on('fileSuccess', (file,message)=>{
                    alert("アップロード完了:" + message);
                    filesel.value="";
                    busy(false);progressButtonShow(false);
            });
        // エラー通知
        flow.on('fileError', (file, message)=>{
                    alert('エラー:' +  errorString(message));
                    busy(false);progressButtonShow(false);
            });

        // ファイル選択変更通知
        filesel.addEventListener('change',function(){
                    resetFlow(); // flowリセット
                    flow.addFile(this.files[0]); // flowにFile追加
                });

        // アップロードボタンクリック通知
        uploadButton.addEventListener('click', function() {
                    if(flow.files.length>0) {
                        progress(0); // プログレスバーリセット
                        progress_info.style.display='block'; // プログレスエリア表示
                        progressButtonShow(true); // 中断・再開・キャンセルボタン表示
                        busy(true); // ファイル選択・アップロードボタンオフ
                        flow.upload(); // アップロード開始
                    }
                });
        // 中断ボタンクリック通知
        getEl("pauseButton")
            .addEventListener('click',function(){
                    flow.pause();
                });
        // 再開ボタンクリック通知
        getEl("resumeButton")
            .addEventListener('click',function(){
                    flow.resume();
                });
        // キャンセルボタンクリック通知
        getEl("canselButton")
            .addEventListener('click',function(){
                    flow.cancel();
                    filesel.value="";
                    busy(false);progressButtonShow(false);
                });
            });
})();

今回はファイルの選択制御をflow.jsに頼らないで、独自に実装しています。
ファイルが選択されたらinputタグからFileオブジェクトを抜き出し、flow.addFile()でflowオブジェクトに追加します。

addFile()は呼び出されるたびにFileオブジェクトを追加していくので、内容をリセットしてからセットする必要があります。
そうしないと、ファイル選択を数回行うと、複数のファイルがアップロードされてしまうためです。
しかし、flow.jsにはリセットするメソッドが見当たらないので、独自に実装しています。

Javascriptから呼び出すサーバー側PHP | flow-upload.php

最後に、WebページからJavascript経由で呼び出される、サーバー側プログラムを作成します。

flow-upload.php


<?php
require_once './lib/flowup.php';

const SSID = 'flow-test';

session_start();

if(!isset($_SESSION[SSID]) || !isset($_REQUEST['token']) || strcmp($_SESSION[SSID],$_REQUEST['token']) !==0){

    $protocol = (isset($_SERVER['SERVER_PROTOCOL']) 
        ? $_SERVER['SERVER_PROTOCOL'] 
        : 'HTTP/1.0');
    header($protocol . " 400 Bad Request" );
    echo 'SESSION ERROR';
die();
}
require_once './lib/flow-lib.php';
\myflow\flowup($flow_tempdir,$flow_updir);

やっていることは、セッションを開始してトークンを確認。
問題なければ、flowup.phpのflowup()を呼び出しているだけです。

改良版の確認

サーバーにソースを設置して、ブラウザからflow-test.phpを呼び出します。

一時ディレクトリ書き込み不可」または「アップロードディレクトリ書き込み不可」が表示されたら、それぞれのディレクトリのパーミッションを777に変更してください。

問題なければ、サイズが大きめのファイルをアップロードして正常にサーバーに保存されているか確認してください。

 

WordPress/Ajax対応版

あまり需要がなさそうですが、改良版のコードをWordPressのプラグイン化してみます。
管理画面からAjaxを利用して、大容量ファイルのアップロードができるようになります。

完成したプログラム群は次のような構成になり、赤文字のファイルを新たに作成していきます。

基本形のファイル構成

wp-flow-test
    ├ Flow // flow-php-server
    ├ temp // アップロード中にチャンクデータが一時保管されるディレクトリ
    ├ upload // アップロード完了後にファイルを保存するディレクトリ
    ├ lib
    │   ├ flow.min.js
    │   ├ flow-lib.php // 基本形と同じもの
    │   ├ flow-test.css // 基本形と同じもの
    │   ├ flowup.php // 基本形と同じもの
    │   └ flow-test.js //改良版と同じもの
    ├ wp-flow-test.php // WordPressプラグイン本体
    └ wp-flow-test-menu.php // 管理画面の登録・表示
    ├ wp-flow-test-ajax.php // Webページ上からの応答処理

WordPressプラグイン本体 | wp-flow-test.php

まずはプラグインの入り口となるコードを作成していきます。

wp-flow-test.php


<?php
/*
Plugin Name: wp-flow-test
Plugin URI: https://affi-sapo-sv.com/
Description: flow.jsを使用して大容量ファイルをアップロードするプラグインサンプル
Version: 1.0
Author: k-chan(s.Kanazawa)
Author URI: https://affi-sapo.com/
*/

namespace myflow;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}
require_once 'lib/flow-lib.php';

global $flow_tempdir,$flow_updir;

$opt=[
        'tempdir'=>$flow_tempdir,
        'savedir'=>$flow_updir,
        'action'=>'flow_test'
];

if ( is_admin() ) {
    require_once 'wp-flow-test-menu.php';
    new WpFlowTest_menu($opt);

    require_once 'wp-flow-test-ajax.php';
    new WpFlowTest_ajax($opt);
}

最初にお約束のコメントを書いていきます。
これがあると、WordPressはプラグインとして認識してくれます。

次のコードもお約束のようです。

WordPressプラグインお約束コード


if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

PHPファイルを外部から直接呼び出されるのを、これで防いでいるようです。

後は管理画面表示時かどうか確認して、メニューとAjaxの登録クラスを呼び出しています。

管理画面の登録・表示 | wp-flow-test-menu.php

WordPressの管理画面の左端にメニューを追加し、ファイルをアップロードする画面を登録するコードを作成します。

wp-flow-test-menu.php


<?php

namespace myflow;
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class WpFlowTest_menu {

    private $opt;

    public function __construct($opt) {
        $this->opt = $opt;
        add_action( 'admin_menu', array( $this, 'admin_menu' ) );
    }

    // 管理画面登録
    public function admin_menu() {
        $hook = add_menu_page( 'wp-flow-test', 'wp-flow-test',
            'manage_options', __FILE__, array( $this, 'page_html' ), '', '100.5' );
        add_action( "admin_print_scripts-{$hook}"
                    , array( $this, 'admin_print_scripts' ) );
    }
    // 管理画面に表示するhtml
    public function page_html() {

        echo \myflow\checkDirAndMaxSize();

        $html = <<<EOF
        <div id="affs-contents"><h2>大容量ファイルアップロード</h2>
        <input type="file" id="filesel"><br>
        <button id="uploadButton">アップロード</button>
        <div id="progress-info">
            アップロード状況:
            <div id="progress"><div id="progressbar"></div></div>
            <div id="progressButton">
                <button id="pauseButton">中断</button>
                <button id="resumeButton">再開</button>
                <button id="canselButton">キャンセル</button>
            </div>
        </div>
EOF;
        echo $html;
    }
    // ロードするスクリプトの定義
    function admin_print_scripts() {
        $flow = 'flow_js';
        wp_register_script( $flow, plugins_url( 'lib/flow.min.js', __FILE__ ) );

        $handle = 'flow_test';
        wp_register_script( $handle,
             plugins_url( 'lib/flow-test.js', __FILE__ ), array( $flow ) );
        wp_localize_script( $handle, 'flow_test_OPT', [
            'api'    => admin_url( 'admin-ajax.php' ),
            'chunksize'    => 1024 * 1024,
            'data'  =>[
                'action' => $this->opt['action'],
                'nonce'  => wp_create_nonce( $this->opt['action'] )
                ]
        ] );
        wp_enqueue_style( $handle . '_css', 
             plugins_url( 'lib/flow-test.css', __FILE__ ) );
        wp_enqueue_script( $flow );
        wp_enqueue_script( $handle );
    }
}

__construct()で管理メニューが表示された時のアクションを登録。

admin_menu()でメニューへの登録と、メニューが選択された時の画面表示関数を登録しています。

page_html()は、表示されるhtmlです。

admin_print_scripts()は、JavaScriptやスタイルシートを読み込むためのタグを出力しています。
同時に、flow-test.jsで使用するflow_test_OPTを出力しています。

WordPressをAjax経由で利用するときは、admin-ajax.phpをJavaScriptから呼び出します。
そのためadmin-ajax.phpのURLを、flow_test_OPT.apiにセットしています。
'action'と'nonce'は、セキュリティのチェックに利用します。

Webページ上からの応答処理 | wp-flow-test-ajax.php

次にAjax経由でWordPressにアクセスされたときの処理を作成していきます。

wp-flow-test-ajax.php

<?php

namespace myflow;

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

require_once 'lib/flowup.php';

class WpFlowTest_ajax {
    private $opt;

    public function __construct($opt) {
        $this->opt = $opt;
        add_action( "wp_ajax_{$this->opt['action']}",array($this,'wp_ajax') );
    }
    public function wp_ajax () {
        if( !check_ajax_referer( $this->opt['action'], 'nonce', false )){
            header("HTTP/1.1 400 Bad Request");
            die();
        }
        flowup($this->opt['tempdir'],$this->opt['savedir']);
    }
}

__construct()で、admin-ajax.phpが呼び出されたときに実行する関数(wp_ajax())を登録しています。
その際"wp_ajax_アクション名"を指定します。
JavaScriptからアクセスがあると、クエリ中の'action'の内容とアクション名が一致する関数を実行します。

wp_ajax()内のcheck_ajax_referer()では、正規の呼び出しかどうかを確認しています。
具体的にはアクション名とクエリとして渡された'nonce'の値から、WordPressがチェックをおこなっています。

問題がなければ、flowup()に制御を渡します。

WordPress/Ajax対応版の確認

作成したプログラム一式をZIPファイルにまとめ、WordPressのプラグイン追加からインストールしてください。

「一時ディレクトリ書き込み不可」または「アップロードディレクトリ書き込み不可」が表示されたら、それぞれのディレクトリのパーミッションを777に変更してください。

問題なければ、サイズが大きめのファイルをアップロードして正常にサーバーに保存されているか確認してください。

 

問題点

通信回数が多すぎる

Flow.jsは大きなファイルを小分けにしてアップロードするため、短い時間に繰り返しサーバーにアクセスします。

今回はチャンクのサイズを1Mバイトにしているので、200Mバイトなら約200のファイルに分割されます。
さらにレジューム機能を実現するために、GETメソッドでサーバにファイル確認をおこなっています。
つまり2倍の400回。
ごく短い時間にこれだけの回数を処理するとなると、サーバーに大きな負担がかかります。
レンタルサーバーを使用している場合は、意図的な攻撃ととられるかもしれません。

アップロード済みのチャンク確認を毎回おこなうのではなく、1回で済ますような方法にして欲しかったと思います。

たまにチャンク送信に失敗している

サーバーにチャンクデータをPOSTする際、次の図のようなヘッダーというものが送信されます。

クライアント

しかし時々、ヘッダーのないデータが送信されます。

クライアント

flow.js上では送信したことになっていますが、サーバーからの応答がなくいつまでたっても処理が終わりません。

実際のところ、ブラウザの問題なのかタイミングの問題なのか、はたまた僕の環境のせいなのかわかりませんが、アップロードに失敗するケースとして覚えておくといいかもしれません。

アップロード先のセキュリティ

アップロード制御PHP | flowup.phpでも書いていますが、phpなどのスクリプトファイルをそのままのファイル名で保存すると、不特定多数のユーザーがサーバー上で任意のコマンドを実行可能となります。

・アップロードフォルダでのスクリプト実行を禁止
・ドメインディレクトリの外にアップロードフォルダを確保する
・そのままのファイル名で保存しない

などの対策が必要です。

更新日:2022/12/15

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。