今日は、Perlで作るファイルアップローダの基本ルーチンを書いてみようと思います。
最初に某業界のインターネットによるファイル送信の歴史を、僕の主観で書いてみます。関係ないようで、実は関係あるんです。
データ通信入稿の歴史
専用線やISDN Managerなど、インフラにコストをかけないと遠隔地への通信入稿ができない前インターネット時代が終わり、インターネット時代になると、首都圏にある全国ユーザを対象にした印刷サービス業や地方の通販印刷業がまず行ったのはFTPサーバまたはAnonymousFTPサーバを立てデータ通信入稿に利用することだったが、これらはユーザにFTPクライアントを設定してもらう説明のコストが非常に高いという欠点があり、次なる手段として、どこかの業者がWebブラウザによるファイル送信を設置したところ他の業者も同様の仕組みを設置して今に至る。
Webのフォームからのアップロードによって、データ通信入稿がものすごく容易になったのです。
本題行きますね。
Perlでファイル送信
さて、Perlでファイル送信するスクリプトですが、CGI.pmというモジュールは今時標準モジュールなので、これを使うと簡単にできます。
テンポラリファイルが書き込まれるディレクトリの設定
CGI.pmのテンポラリファイルが書き込まれる場所が変更できないと思われている人も多いですけれども、実は変更可能。
僕のコードはこんな感じ。
# -------------------------------------------------
# _set_tempdirectory ( $tempdir )
# -------------------------------------------------
# CGI.pm の内部設定の、TMPDIRECTORY を引数に変更する。
# CGI.pm 初期化する前に設定する必要がある
# ていうか、ここら辺アンドキュメンテッドなんじゃねー
# ていうきもしなくもない。後で調べる。
sub _set_tempdirectory{
my $tempdir = shift;
if ($TempFile::TMPDIRECTORY) {
$TempFile::TMPDIRECTORY = $tempdir;
}
elsif($CGITempFile::TMPDIRECTORY) {
$CGITempFile::TMPDIRECTORY = $tempdir;
}
return;
}
このルーチンを、use CGI; した後、かつ、my $q = CGI->new; する前に実行します。
これ何に役立つかっていうと、アップロードCGI呼び出しの引数によってアップロード先のURLを区別したいときに使います。つまり、アップロード中のステータス、たとえばファイル転送済みの容量表示をしたいときに。
ここで、CGI.pm をよく使っている人は、「my $q = CGI->new; する前なのに、どうやってアップロードCGI呼び出しの引数をとれるの?」って思うかもしれません。そう、おなじみの$q->param('foo')でとれないんです。
しかし、引数とるのに、CGI.pmを使う必要はありません。$ENV{'QUERY_STRING'}をパースしちゃいましょう! Perl4時代に逆戻りですね!
テンポラリディレクトリにアップロードされたファイルの保存
この手のルーチン作例は、いっぱいWebにあがっているのですが、テンポラリディレクトリにあるファイルを、実際のファイルに保存するルーチンがみんなまちまちで、どれを使っていいか悩みます。
http://perldoc.jp/docs/modules/CGI.pm-2.89/CGI.podだと
# バイナリ・ファイルをどこか安全なところへコピーします
open (OUTFILE,">>/usr/local/web/users/feedback");
while ($bytesread=read($filename,$buffer,1024)) {
print OUTFILE $buffer;
}
となっています。これスピード遅くね?
実際、最近の入稿データは余裕で1GBとかありますので、普通のやり方をしてたのでは、アップロード送信終了後の待ち時間がとても長くなりがちです。
とりあえず、僕は、File::Copyモジュールのmoveを使ってみました。これだったら、テンポラリディレクトリと、保存ディレクトリが同じファイルシステム上にあるのならば、一瞬で保存が終わってくれます。
ここで、CGI.pm をよく使っている人は、「moveって、たしか引数としてtarget pathとdestination pathの2つが必要で、ファイルハンドルだと駄目だったよね?」って思うかもしれません。そう、$q->upload('foo')をそのまま使えません。
でも、ちゃんと考えてあって、$q->tmpFileName( $q->upload('foo') )なんてすると、テンポラリファイルのパスが得られちゃうんです。これは便利。
実際には以下のようになります。
use CGI;
use File::Copy;
use File::Basename;
my $upload_dir = 'upload'; # 保存先のディレクトリ
my $q = CGI->new(); # CGIオブジェクト
my $fh = $q->upload('filename'); # ファイルハンドル兼ファイル名
my $temp_path = $q->tmpFileName($fh); # アップロードされた
#ファイルのフルパス
fileparse_set_fstype('MSDOS'); # WinIE用パス文字設定
my $filename = basename($fh); # アップロードされたファイルの
# ファイル名
my $upload_path
= "$upload_dir/$filename"; # 保存先フルパス
move ($temp_path, $upload_path) # File::Copy の moveメソッドで
or die $!; # 移動
close($fh); # おまじない
続きを読む以降では、僕が持っている実際のアップロードルーチンのソースが書いてあります。
サンプルソース
これ実際に使ってます。テンポラリディレクトリの位置を変える必要がないので、前出のテンポラリディレクトリ指定ルーチンは組み込んでません。
Fillename:upload.cgi
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use CGI::Carp qw(fatalsToBrowser);
use File::Copy;
use File::Basename;
my $upload_dir = 'upload'; # 保存先のディレクトリ
my $q = CGI->new(); # CGIオブジェクト
my $fh = $q->upload('filename'); # ファイルハンドル兼ファイル名
my $temp_path = $q->tmpFileName($fh); # アップロードされた
#ファイルのフルパス
fileparse_set_fstype('MSDOS'); # WinIE用パス文字設定
my $filename = basename($fh); # アップロードされたファイルの
# ファイル名
my $upload_path
= "$upload_dir/$filename"; # 保存先フルパス
move ($temp_path, $upload_path) # File::Copy の moveメソッドで
or die $!; # 移動
close($fh); # おまじない
print $q->header( -type=>'text/html', -charset=>'UTF-8', );
print <<"END_OF_HTML";
<body><p>done.</p></body>
END_OF_HTML
exit;
__END__
Filename: upload.html
<html>
<body>
<form action="upload.cgi" method="post"
enctype="multipart/form-data">
<p>
<label for="filename">file</label>
<br>
<input type="file" name="filename" id="filename">
</p>
<p>
<input type="submit" name="submit" value="submit">
</p>
</form>
</body>
</html>
さいごに
自分のblogに書いていてもコードの善し悪しを評価してもらえず、せっかくperl-mongers.orgていうのがあるので、ここに書いてみようと思いました。
おそらく、ファイルシステムに使うファイル名に、外来のデータ由来の文字をそのまま使うな、ていう指摘はあるとは思いますが、その点は全くアグリーです。
このエントリのオリジナルは、
M.C.P.C.: CGI.pmのアップロード後のファイル処理を高速に行う [blog.dtpwiki.jp] です。



Leave a comment