2017年6月29日木曜日

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

 前回,FreeBSD で fine-uploader を使ってみた(その1)として fine-uploader によるサーバーへのファイルのアップロードについて書いた。 そこでは,fine-uploader ライブラリを使ったアップロードのポータルサイト(入口サイト)について書いた。 今回は fine-uploader ライブラリを使う際のサーバー側(ファイルを受け取る)ルーチンについて書こう。

 サーバー側の受け取りルーチンは fine-uploader では Endpoint と呼ばれる。 fine-uploader のserver-example には,PHP,node.js,Java,Python のサンプルもあるが,ここではPerlによる Endpoint のサンプルをベースにして,多少アレンジしたものを用いた。

 以下に Perl による Endpoint を書こう。
#!/usr/bin/perl
#
use CGI::Carp qw(fatalsToBrowser);
use Digest::MD5;
use CGI;
my $IN = new CGI;
# ================================================================
$ext_check = 1; # Do extension check
$original_filename = 1; # use original filename (Not a random name)
$unique_file_max_number = 10; # maximum number for same file name
# ================================================================
print STDERR "\n"."<<<No chunk process>>>"."\n";
my $uploaddir = '../uploadFiles/';
my $uploadtmpdir = '../uploadFiles/'; # chunking 用。簡単のため upload directory と同じにした
my $maxFileSize = 20971520; # 20 MB

# 送られてきたファイル情報の取得
my $file;
if ($IN->param('POSTDATA')) { $file = $IN->param('POSTDATA'); }
                       else { $file = $IN->upload('qqfile'); }
my $qquuid = $IN->param('qquuid');
my $qqtotalfilesize = $IN->param('qqtotalfilesize');
my $qqtotalparts = $IN->param('qqtotalparts');
my $qqchunksize = $IN->param('qqchunksize');
my $qqpartbyteoffset = $IN->param('qqpartbyteoffset');
my $qqpartindex = $IN->param('qqpartindex');

my $filename4store = $IN->param('qqfilename');
if ($filename4store eq '') { $filename4store = $file; }

# make a random filename, and we guess the file type later on...
my $name = Digest::MD5::md5_base64(rand);
$name =~ s/\+/_/g;
$name =~ s/\//_/g;

# file head の取り出しと,拡張子のチェック
my $filehead, $ext, $extUC, $filename;
my $error = 0;
if ($filename4store =~ /(.*)\.(.*)/) {
    $filehead = $1;  $ext = $2;
    if ($ext_check == 1) {
        $extLC = lc($ext); # 小文字でチェックする
        if ($extLC !~ /jpg|jpeg|gif|png|zip|7z/) { $error =1; } # Invalid extension type error
    }
}

# オリジナルファイル名を使う際には,同じ名前があるかを調べてから書き込む。
# 上書きしない場合で同一名があれば,ファイル名に数字を付加する。
if ($original_filename == 1) {
    my $ii = 0;
    my $filename_work = $uploaddir.$filehead.'.'.$ext;
    if (-f $filename_work) {
# 同一名の時には後ろに番号を付加する
        while ((-f $filename_work) and ($ii<$unique_file_max_number)) {
            $ii++;
            $filename_work = $uploaddir.$filehead.'-'.$ii.'.'.$ext;
        }
        $filename = $filehead.'-'.$ii.'.'.$ext;
    } else { $filename = $filehead.'.'.$ext; }
} else {
    $filename = $name.'.'.$ext; # random file name
}

# ---------------------------------------------
# $qqpartindex がカラでない時は,chunking されたファイルの一部分と解釈する
if ($qqpartindex ne '') {
    $filename = $filename.'.tmp'.$qqpartindex.'.'.$filename4store; # like 'P114025-2.JPG.tmp0.P114025.JPG'
    $uploaddir = $uploadtmpdir; # directory の切替え
}
# ---------------------------------------------

# ファイルの書き出し(chunking の最終処理は別スクリプトで行う)
my $check_size;
if ($error == 0) {
# ---------------------------------------------
# 実際のファイルの書き込み処理
    binmode(WRITEIT);
    open(WRITEIT, ">$uploaddir$filename") or die "Cant write to $uploaddir$filename. Reason: $!";
    if ($IN->param('POSTDATA')) {
        print WRITEIT $file;
    } else {
        while (<$file>) { print WRITEIT; }
    }
    close(WRITEIT);
# ---------------------------------------------
    chmod 0664,"$uploaddir$filename"; # 後で扱いやすいように permission を変更

    $check_size = -s "$uploaddir$filename"; # upload されたファイルのサイズの取得
    if    ($check_size < 1)            { $error = 2; } # file empty error
    elsif ($check_size > $maxFileSize) { $error = 3; } # Too big error

    print STDERR qq|Main filesize: $check_size \n|; # /var/log/httpd-error.log に記載
} # if ($error == 0)

my $currentTimeStr = &GetDateTimeStr();
print $IN->header();
if ($error == 0) {
# 以下の行が Endpoint から送り返される情報
    print qq|{"success":true,"filename":"$filename","size":"$check_size","endtime":"$currentTimeStr" }|;
# 以下の行は /var/log/httpd-error.log へのログ
    print STDERR "File has been successfully uploaded.\n";
} elsif ($error == 2) {
    print qq|{"success":false,"error":"File is empty" }|;
    print STDERR "File is EMPTY !!\n";
    print STDERR "file has been NOT been uploaded... \n";
} elsif ($error == 3) {
    print qq|{"success":false,"error":"File is too large" }|;
    print STDERR "File size is TOO LARGE !!\n";
    print STDERR "file has been NOT been uploaded... \n";
} else {
    print qq|{"success":false,"error":"Invalid file type"}|;
    print STDERR "Invalid file extension ERROR !!\n";
    print STDERR "file has been NOT been uploaded... \n";
} # if $error == 0

print STDERR '       Date Time ="'.$currentTimeStr.'"'."\n";
print STDERR '      upload_dir ="'.$uploaddir.'"'."\n";
print STDERR '  filename(orig) ="'.$file.'"'."\n";
print STDERR 'filename(edited) ="'.$filename4store.'"'."\n";
print STDERR 'filename(stored) ="'.$filename.'"'."\n";
print STDERR '          qquuid ="'.$qquuid.'"'."\n";
print STDERR ' qqtotalfilesize ="'.$qqtotalfilesize.'",  qqchunksize ="'.$qqchunksize.'",  qqpartbyteoffset ="'.$qqpartbyteoffset.'",  qqtotalparts ="'.$qqtotalparts.'",  qqpartindex ="'.$qqpartindex.'"'."\n";
print STDERR "\n";

exit;
# ======================================================================
sub GetDateTimeStr {
    my(@GTS_date_array) = @_;
    if ($GTS_date_array[1] eq '') {
        @GTS_date_array = localtime(time);
    }
    my $dum=($GTS_date_array[5]+1900)."/".&ZeroPadding($GTS_date_array[4]+1)."/".&ZeroPadding($GTS_date_array[3])
        .' '.&ZeroPadding($GTS_date_array[2]).":".&ZeroPadding($GTS_date_array[1]).":".&ZeroPadding($GTS_date_array[0]);
    return $dum;
}

sub ZeroPadding {
    my($dum)=sprintf("%d",$_[0]);
    $dum="00".$dum;
    $dum=substr($dum,(length($dum)-2));
    return $dum;
}
 具体的にはスクリプトを読んで理解してほしいが,少し解説しておこう。
まず,CGI パッケージを使っている。先頭の辺りで2度「use CGI」が出て来るが,なんでいるんやろ?もともとのサンプルで2個出てるからそのまま使っておいた。 Digest::MD5 はランダムなファイル名をつけるために使っている。

$ext_check や $original_filename 等は,サーバー側で拡張子のチェックを行うかや,オリジナルのファイル名で保存するか,などのフラグとして使っている。

「print STDERR xxxxx」みたいな行がいくつも出てくるが,これは /var/log/httpd-error.log への出力であり,エラーがあった時には有効である。

次に「$IN->param('XXXX')」という行が複数並んでいる。 これはポータルサイトから送られた情報を POST で受け取っているものである。 Endpoint ではこれらを使って実際にアップロードされたファイルをサーバーに書き込む作業を行う。 特に分割送信(chunking)の場合には,分割された何個目のファイルかを表したりするので,重要な情報である。
POSTDATA または qqfile がファイル名を示す。分割送信時には「blob」などが入るため,後の qqfilename を参照しないといけない。
qquuid は,fine-uploader によって与えられた,ファイルのアップロード固有の id である。
qqtotalfilesize は,送信中のファイルの全サイズ(分割していない場合は単にファイルサイズ)を表す。
qqtotalparts は,分割送信の場合に,いくつに分割されたかを示す。
qqchunksize は,分割送信の際の分割された当該ファイルのサイズである。
qqpartbyteoffset は,分割送信の際,当該ファイルが先頭から何バイト目からのものか,を表している。
qqpartindex は,当該ファイルが分割ファイルとして何番目のファイルか(0番から始まる)を表している。
qqfilename は,分割送信時にファイル名が入る。一括の場合はカラである。

 途中に「if ($qqpartindex ne '')」で始まる行があるが,これは分割送信のための処理である。 分割送信では後処理でファイルを結合させないといけないが,その時の便宜のために,ファイル名に何番目の分割ファイルか,と,最終的なファイル名をつける処理である。 また,一時保存ディレクトリを別個に用意することもできる(ここでは面倒くさいのでアップロードディレクトリと同じにしている)。

 「実際のファイルの書き込み処理」と書かれた部分が実際にサーバーにファイルを保存している部分である。 実は Endpoint の重要な部分はこの部分に集約されている。後はおまけみたいなものである。

 後は,ファイルサイズが大きすぎればエラーと判断し,最後にポータルサイトに情報を送り返している。 送り返す情報は,
 {"success":true}
 {"success":false, "error":"File is empty."}
のように「{}」の中に「"xxx":yyy」の形式で情報を並べる。 情報としては,最低限「"success":」が必要である。 "success": が true なら送信成功であり,そうでなければエラーが発生した判断される。 それ以外は自由に情報を返すことができる。 ここでは送信がうまくできた場合には,サーバーに保存された際のファイル名,サイズ,保存時刻を返している。

 最後に,デバッグのために /var/log/httpd-error.log にも情報を書き込んで終了となっている。

以下に,サーバーの /var/log/httpd-error.log に残った情報を書いておこう。
<<<No chunk process>>>
Main filesize: 1041983
File has been successfully uploaded.
       Date Time = "2017/06/xx 22:00:40"
      upload_dir = "../uploadFiles/"
  filename(orig) = "DSC_0482.jpg"
filename(edited) = "DSC_0482.jpg"
filename(stored) = "DSC_0482-2.jpg"
          qquuid = "19a43a62-6061-41e9-a238-42d4142baec8"
 qqtotalfilesize = "1041983",  qqchunksize ="",  qqpartbyteoffset ="",  qqtotalparts ="",  qqpartindex =""
 これは単独のファイルアップロードの際のログである。 そのため qqchunksize や qqpartbyteoffset 等はカラとなっている。 また,テストで何度も同じファイルをアップロードした際のものなので,保存されたファイル名に「-2」が加えられているのがわかる。

 今回は fine-uploader ライブラリ使用時の Endpoint ルーチンについて書いてみた。 最後に chunking の際のファイル結合ルーチンのことを次の投稿で書こう。
FreeBSD で fine-uploader を使ってみた(その3)

0 件のコメント: