2017年6月28日水曜日

FreeBSD で fine-uploader を使ってみた(その1)

 続きとして,FreeBSD で fine-uploader を使ってみた(その2)に Endpoint ルーチンについて書いた。
さらに,FreeBSD で fine-uploader を使ってみた(その3)に,分割送信時のファイルの結合ルーチンについて書いた。

 なんかタイトルがごちゃごちゃしてしまったが,要は fine-uploader を使ってみた,というお話である。

 以前から,サーバーにファイルをアップロードしてもらうのに Uber Uploader というのを使っていた。Uber Uploader はいわゆる Ajax を使って,Progress Bar を表示しながらアップロードをしてくれる,というもので,2011 年にこのブログに Uber-Uploader 6.8.2 の使い方(その1)として Uber Uploder の使い方を書いてみた。
 しかし,Uber Uploader もそろそろ古くなり,更新もされなくなっており,php の新しいバージョンに対応していない,などの問題が出始めた。 そこで,progress bar を表示してくれるアップローダーを探し,fine-uploader というのを使ってみたので,ここに書いておこう。

 fine-uploader は,基本的は JavaScript を使ったファイルのアップローダー用ライブラリである。 一番いい点としては,他のライブラリ(例えば jQuery など)等を使わなくても済む,という点がある。 他のライブラリ等を使わないといけないとなると,その別ライブラリ等のバージョンが上がると uploader が使えなくなる可能性が出てくる。 Uber Uploader の場合も,Perl のパッケージが更新されると不具合が出たり,php のバージョンが上がると不具合が出たりした。 jQuery や php は,ときどき後方互換性がないバージョンアップがなされるため,ある日突然アップローダーが使えなくなることがある。 自分が書いたスクリプトなら,どこでどのライブラリやパッケージを使ったかを知っているので修正は比較的簡単だが,他の人が作ったライブラリだと,どこに問題があるのかを探るところから始めないといけないため修正が大変である。そのために,他のライブラリに依存してない,というのは大きな魅力である。

 その点,fine-uploader は jQuery を使うこともできるが,jQuery なしでも使えるので,何かあれば自分で対処しやすいと考えた。 fine-uploader の使い方については,fine-uploader の公式サイトのドキュメントのページに書いてある(英語だけど…)が,少しだけ説明しておこう(説明になってないかも…)。

 fine-uploader には,まず core と呼ばれるライブラリがある。 これがファイルのアップロードの主要パートを担っているみたいである。 この core だけでもアップロードは可能みたいだが,core の上で働くユーザーインターフェースを備えたライブラリが提供されている。 traditional/generic UI(ユーザーインターフェース)を持つものと,Amazon S3 用アップローダー,Azure Blob Storage 用アップローダーがあるらしい(後ろの2個はわかっていない…)。 今回は traditional/generic UI を使ってみた。

 しかし,実は上記の JavaScript ライブラリだけでは fine-uploader は動かない。 上記ライブラリが提供するのは,アップロードのポータルサイト(入口サイト)であり,そこから送られたファイルを受け取る Endpoint と呼ばれるルーチンが別途必要になる。 server-examples には,PHP,node.js,Java,Python のサンプルがある。他にも Perl (CGI) を使った例や, C# を使った azure のサーバー側サンプルもある。 ここでは Perlのserverサンプルを使うことにした。 注意点としては,この Endpoint のサンプルは CGI を使っているが,ユーザーには全く見えない

 ユーザーがアップロードのポータルサイトでファイルアップロードの手続きを実行する(アップロードボタンを押す)と,ポータルサイトにある fine-uploader ライブラリがファイルを Endpoint ルーチン(ここでは CGI で書かれる)に向かって投げる。 すると,Endpoint ルーチンがファイルを受け取りサーバーに保存する処理を行い,ファイルサイズやアップロード後のファイル名,場合によってはエラー情報,などをポータルサイトに投げ返す。 ポータルサイトでは,Endpoint ルーチンから戻ってきた情報に応じてユーザーに対して「アップロード完了」みたいな alert を出すか, あるいは次の処理を行うルーチンに飛ぶ,などの処理を行うことになる。

 最初,この Endpoint が必要なことをわかっていなかった。 他の人が書いた fine-uploader の使い方,みたいなサイトでは,ポータルサイトのことは書いているが,実際の受け取りの処理をする Endpoint のことが書かれていないことがほとんどで(私が見つけられなかっただけ??),どうやって fine-uploader を使えばいいかがよくわからなかった。 仕方なく fine-uploader の公式サイトでドキュメントを読んでなんとかわかった次第である。

 以下に実際に動作するサンプルのリンクを載せよう。 このサンプルは私が作ったもので,実際にファイルをアップロードすることができる。 しかし,一度にアップロードできるファイルの数は2個に制限している。 他にも,サイズは 20MB まで,拡張子は jpg,jpge,png,gif 等に制限されている。 念のため,アップロードされたファイルは,1日の終わりに自動で削除されるようになっている。 また,2MB を越えるサイズのものは 2MB ずつに分割送信される(chunking)ようになっている(ユーザーにはわからないが…)。 さらに,アップロード終了後に別の cgi に飛んで,アップロードの結果一覧を表示するようにしている。
 ちなみに,Firefox と Chrome, Opera では微妙に動作が異なる。 Firefox だと,2017/6/28 現在では Drag and Drop でエラーが発生してしまうので,Drag and Drop 機能を停止している。 しかし,Chrome や Opera なら問題ないので,Drag and Drop でファイルの選択が可能となっている。
---------------------------------------------------
 fine-uploader のサンプル
---------------------------------------------------

 上記サンプルについて説明しよう。
サンプルのソースコードを見てほしいが,基本的には apache で書かれた単なる web ページである。 その中で JavaScript で書かれた fine-uploader ライブラリが取り込まれている。 それとは別にメールアドレスのチェック等のためにも JavaScript を使っている。 さらに,ライブラリを使うための css ファイルも読み込まれている。 アップロードするファイルの選択や,アップロード開始の処理は fine-uploader ライブラリが行っているので,web ページとしてはアップロード用のフォームがあるけど,submit ボタンが見当たらない,という構造になっている。

 fine-uploader ライブラリを使っている JavaScript 部分を書きだしておこう。 以下のソースを見ればわかるが,肝心な部分は「new qq.FineUploder({」の部分である。 その中で fine-uploader の設定を行っている。 スクリプト中に細かくコメントを書いたが,オプションの設定に関しては Core のオプションUI オプションを見て欲しい。ここで使ったオプションについてはスクリプトの後で説明しよう。
// アップロード後の次のルーチンのためのフォームに初期値(カラ)を設定
document.getElementById("formFILENAME").value = "";
document.getElementById("formSIZE").value = "";
document.getElementById("formENDTIME").value = "";

// fine-uploader の肝心の設定
var manualUploader = new qq.FineUploader({
    element: document.getElementById("fine-uploader-manual-trigger"), // fine-uploader のボタン等の領域の指定
    template: "qq-template-manual-trigger", // template の指定 (UI の指定)
    autoUpload: false, // これが true だと,drag and drop すると即アップロードが開始される
    validation: { // アップロード前のチェック(validation)の設定
        allowedExtensions: ["jpg","jpeg","gif","png","zip","7z"], // 許可する拡張子
        itemLimit: 2, // 一度にアップロードできるファイル数
        sizeLimit: 20971520, // ファイルサイズの上限
        minSizeLimit: 10 // ファイルサイズの下限
    },
    request: { // ここで Endpoint ルーチンを指定する
        endpoint: "./fine_upload_endpoint.cgi"
    },
    thumbnails: { // 選択したファイル用のアイコン等の指定
        placeholders: {
            waitingPath: "../../fine-uploader/placeholders/waiting-generic.png",
            notAvailablePath: "../../fine-uploader/placeholders/not_available-generic.png"
        }
    },
    chunking: { // 分割送信 (chunking) の設定
        enabled: true, // chunking を使う
        partSize: 2017152, // 分割ファイルのサイズ
        concurrent: { // 複数の分割済ファイルの同時送信を許可するか
            enabled: true
        },
        success: { // 分割済のファイルをすべて送信できた時の設定
            endpoint: "./fine_upload_endpoint_cnk.cgi" // 分割送信ができたら,結合ルーチンを呼び出す
        }
    },
    callbacks: { // Endpoint からの情報に対する応答を定義する
      onError: function(id, filename, message, xhr) { // エラー時の応答
        alert(message); // 返送されたエラーメッセージを alert で表示する
      },
      onComplete: function(id, filename, response, xhr) { // 個別のファイル送信が成功した際の処理
        if (response.success) { // ここでは次の処理のためにsizeやファイル名等をフォームにセットしている
          var tempstr; // 複数のファイルを送信している時は,情報をコンマで区切って並べている
            if (document.getElementById("formFILENAME").value == "") { tempstr = response.filename; }
            else { tempstr = document.getElementById("formFILENAME").value + "," + response.filename; }
            document.getElementById("formFILENAME").value = tempstr;
            if (document.getElementById("formSIZE").value == "") { tempstr = response.size; }
            else { tempstr = document.getElementById("formSIZE").value + "," + response.size; }
            document.getElementById("formSIZE").value = tempstr;
            if (document.getElementById("formENDTIME").value == "") { tempstr = response.endtime; }
            else { tempstr = document.getElementById("formENDTIME").value + "," + response.endtime; }
            document.getElementById("formENDTIME").value = tempstr;
        } // if (success)
      },
      onAllComplete: function(succeeded, failed) { // 選択した全ファイルの送信が終了(成功)した時の処理
        var num_success = succeeded.length; // ここでは送信できたファイル数をフォームにセットしている
        document.getElementById("formNUM").value = num_success;
// 下行でわざと alert を表示させているが,通常はなくていいと思う。
        alert("全アップロード完了!\nファイル数:"+num_success+"\n\nクリックすると次のルーチンへ飛びます。");
        if (num_success > 0) { // 送信済みファイルが1個でもあれば,次の処理へ飛ぶ
          document.redirectScript.submit();
        } // if
      } // onAllComplete
    }, // callbacks
    debug: true // デバッグの設定
});

// 2017/6現在,Firefox で Drag and Drop 時にチカチカするエラーがあったので,ドラッグゾーンを非表示にしている
// ここでは Browser の対応を示す変数で,フォルダーのドラッグを受け付けないブラウザではゾーンを非表示
if (!qq.supportedFeatures.folderDrop) {
  document.getElementById("DropArea").className="qq-uploader-selector";
  document.getElementById("dragAndDrop_explanation").style.display="none";
}

// アップロードボタンにアップロードの動作を設定している
// ユーザーの名前やメールアドレスのチェックをパスすれば,アップロードを実行
qq(document.getElementById("trigger-upload")).attach("click", function() {
    if ((checkOWNER()) && (checkEMAIL())) {
      manualUploader.uploadStoredFiles();
    }
});
 ここで使った qq.FileUploader のオプションについて書こう。
まず,element はアップローダーのボタン等の配置をするための DOM の指定である。 Core だけでなく UI を使う場合,ドラッグアンドドロップのための領域やアップロードボタンがここに表示される。
template は UI のテンプレートであり,fine-uploader をダウンロードした中にあったファイルを使っている。 その中で,どのような UI を使うかを指定している。具体的な内容をこの少し後で示そう。
autoUpload は true ならドラッグでファイルを指定した際に即アップロードが実行される。ここでは false にしている。
validation はアップロード前のチェックであり,拡張子の制限や一度にアップロードできるファイルの個数,ファイルサイズの最大値や最小値をチェックできる。
request はアップロード時に要求されるパラメーター(?)であり,ここでは Endpoint ルーチンを指定してる。
chunking は分割送信に関するオプションであり,使用するか,分割ファイルのサイズ,同時送信を許可するか,分割ファイルが全部送信できた時に呼び出すルーチン (Endpoint) の指定,などができる。

callbacks は Endpoint から戻ってきた時に,Endpoint から送られてきた情報に対する応答処理を記述している。 指定できるイベントについてはEvent オプションを見てほしい。
ここでは onErrors はエラーがあった時の処理で,ここでは alert を表示している。
onComplete は1個のファイル送信が完了した時の応答を記述する。ここでは送信された(受け取られた)ファイルのサイズや名前を次の処理のためのフォームにセットしている。 複数のファイルが選択されている場合も想定して,すでにフォームに値が入っている場合はコンマで区切って並べている。
onAllComplete は選択した全ファイルの送信が終わった時の処理であり,ここでは送信できたファイル数をフォームに入れ,送信完了の alert を表示した後,次の処理へジャンプしている。 alert 表示は一度止めるために(わかりやすくするため??)入れたもので,通常は alert 表示はなくていいと思う。

 fine-uploader の設定以外のものとして,下の方に2個ほど処理が書かれている。
1個目は,Firefox で Drag and Drop ゾーン(略してドラッグゾーン)を非表示にするルーチンである。 これは DropArea という ID を持つ DOM(web の要素)のclass名を "qq-uploader-selector" にし,dragAndDrop_explanation という DOM を非表示にしている。 ここで DropArea という DOM のクラス名だが,元は "qq-uploader-selector qq-uploader" となっている。 そこから "qq-uploader" を取り除いたものに設定し直している。 こうすることでドラッグゾーンが表示されなくなる。
 2個目はアップロードの実行処理である。 ここでは「upload」ボタンを押した際の処理を記述している。 送信者名とメールアドレスのチェックをして,パスしていればアップロードのプロセスを実行しなさい,としている。

 次に template ファイルを記載しよう。 これはサンプルのソースの初めの方に直接記載してある。 ここにあるのは fine-uploader のファイル群にあったもので,多少自分なりにアレンジしているが,基本的にはそのまま使用している。 これは UI feature と呼ばれるドラッグアンドドロップでファイルを選択できるルーチン用である。 ここでは UI にどの順序で何を配置するかを指定している(?)。 それぞれの見た目はこれとは別に fine-uploader 用 css ファイルに記載してある。
<script type="text/template" id="qq-template-manual-trigger">
    <div id="DropArea" class="qq-uploader-selector qq-uploader" qq-drop-area-text="ここに Drag & Drop でもファイル選択可能です">
        <div class="qq-total-progress-bar-container-selector qq-total-progress-bar-container">
            <div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-total-progress-bar-selector qq-progress-bar qq-total-progress-bar"></div>
        </div>
        <div class="qq-upload-drop-area-selector qq-upload-drop-area" qq-hide-dropzone>
            <span class="qq-upload-drop-area-text-selector"></span>
        </div>
        <div class="buttons">
            <div class="qq-upload-button-selector qq-upload-button">
                <div>ファイル選択</div>
            </div>
            <button type="button" id="trigger-upload" class="btn btn-primary">
                <i class="icon-upload icon-white"></i> Upload
            </button>
        </div>
        <span class="qq-drop-processing-selector qq-drop-processing">
            <span>処理中...</span>
            <span class="qq-drop-processing-spinner-selector qq-drop-processing-spinner"></span>
        </span>
        <ul class="qq-upload-list-selector qq-upload-list" aria-live="polite" aria-relevant="additions removals">
            <li>
                <div class="qq-progress-bar-container-selector">
                    <div role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" class="qq-progress-bar-selector qq-progress-bar"></div>
                </div>
                <span class="qq-upload-spinner-selector qq-upload-spinner"></span>
                <img class="qq-thumbnail-selector" qq-max-size="100" qq-server-scale>
                <span class="qq-upload-file-selector qq-upload-file"></span>
                <span class="qq-edit-filename-icon-selector qq-edit-filename-icon" aria-label="Edit filename"></span>
                <input class="qq-edit-filename-selector qq-edit-filename" tabindex="0" type="text">
                <span class="qq-upload-size-selector qq-upload-size"></span>
                <button type="button" class="qq-btn qq-upload-cancel-selector qq-upload-cancel">キャンセル</button>
                <button type="button" class="qq-btn qq-upload-retry-selector qq-upload-retry">リトライ</button>
                <button type="button" class="qq-btn qq-upload-delete-selector qq-upload-delete">削除</button>
                <span role="status" class="qq-upload-status-text-selector qq-upload-status-text"></span>
            </li>
        </ul>

        <dialog class="qq-alert-dialog-selector">
            <div class="qq-dialog-message-selector"></div>
            <div class="qq-dialog-buttons">
                <button type="button" class="qq-cancel-button-selector">Close</button>
            </div>
        </dialog>

        <dialog class="qq-confirm-dialog-selector">
            <div class="qq-dialog-message-selector"></div>
            <div class="qq-dialog-buttons">
                <button type="button" class="qq-cancel-button-selector">No</button>
                <button type="button" class="qq-ok-button-selector">Yes</button>
            </div>
        </dialog>

        <dialog class="qq-prompt-dialog-selector">
            <div class="qq-dialog-message-selector"></div>
            <input type="text">
            <div class="qq-dialog-buttons">
                <button type="button" class="qq-cancel-button-selector">Cancel</button>
                <button type="button" class="qq-ok-button-selector">Ok</button>
            </div>
        </dialog>
    </div>
</script>

<style>
    #trigger-upload {
        color: white;
        background-color: #00ABC7;
        font-size: 14px;
        padding: 7px 20px;
        background-image: none;
    }
    #fine-uploader-manual-trigger .qq-upload-button { margin-right: 15px; }
    #fine-uploader-manual-trigger .buttons { width: 36%; }
    #fine-uploader-manual-trigger .qq-uploader .qq-total-progress-bar-container { width: 60%; }
</style>

 最後に,ファイルの送信者とそのメールアドレスのチェックのための JavaScript を載せておこう。 ここでの処理は,送信者名がない場合,あるいは全角文字でない場合にはエラーとなる(false で return)。 また,メールアドレスは形式がおかしいとエラー(false で return)となる。 fine-uploader の callbacks の中の onAllComplete の処理の際に,以下のチェックスクリプトを呼び出している。
// Make sure the user input his name
function checkOWNER(){
    var sei_str = document.getElementById("OWNER").value;
    if (sei_str == "") {
        alert("名前の入力がありません。\n名前を入力してください。");
        return false;
    } else if (!(sei_str.match(/^[^\x01-\x7E\xA1-\xDF]+$/))) {
        alert("名前は全角文字のみです。\n名前を入力し直してください。");
        return false;
    } else { return true; }
}

// Make sure the user input his e-mail address
function checkEMAIL(){
    var found_email = true;
    var email_str = document.getElementById("EMAIL").value;
    if (email_str == "") {
        alert("メールの入力がありません。\nメールを入力してください。");
 found_email = false;
    } else if (!(email_str.match(/^[A-Za-z0-9]+[\w-]+@[\w\.-]+\.\w{2,}$/))) {
        alert("メールの文字列に不備があります。\nメールを入力し直してください。");
 found_email = false;
    }
    if(!found_email){ return false; }
               else { return true; }
}

 全体の構成は fine-uploader のサンプルのソースを見てほしい。 ここで書いた JavaScript と template の他にはフォームなどぐらいしか書かれていないのであまり難しくないと思う。

 少し長くなったので,今回はここでやめておこう。 続きとして Perl を使った Endpoint について書こう。
FreeBSD で fine-uploader を使ってみた(その2)

0 件のコメント: