フォームからアップロードされたファイルを受取り、サーバマシン上にテンポラリファイルを作成することなく、そのままデータベースにラージオブジェクトとして保存する方法を解説します。
- 使用する perl モジュール: CGI、DBI、DBD::Pg、File::Basename
- サンプルファイル: 送信元フォーム
pg_lo_load.html※ - サンプルファイル: CGIスクリプト
pg_lo_load.cgi
※ 普通にリンクを選択するとブラウザウィンドウ内にHTMLとして表示されてしまうので、ブラウザの「名前を付けて保存」等の機能を使用してファイルを保存してから、テキストエディタ等で参照して下さい。
処理のポイント
- むやみやたらとファイルを受け付けると、大量/大容量ファイルの処理でサーバ機能が停止してしまう可能性があるので、受付できるファイルの容量制限をします。
- サーバにウィルス対策を施していない場合、ウィルスの付いている可能性のあるようなファイルタイプは、可能であれば受付を拒否します。(特に、サーバマシンのOSがウィンドウズであるような場合は、注意する必要があります。)
- DBI(基本的には接続先のデータベースの種類に依存しない、perl のデータベースインターフェース)を使用しますが、ファイルをデータベースに保存する場合は、データベース特有の機能を処理する func 関数を使用(経由)して、PostgreSQL のラージオブジェクトをハンドリングします。
DBI と DBD::Pg についての詳細は、search.cpan.org などからモジュールのドキュメントを参照して下さい。
動作確認環境(2004-08-09追記)
2004-08-09 現在の最新バージョンである DBD::Pg 1.32 だとうまく動作しません(DBD::Pg側の仕様変更またはバグ(?)により)。
DBIモジュール | DBD::Pgモジュール | PostgreSQL | 結果 |
---|---|---|---|
1.15 | 1.22 | 7.3.2、7.4.3 | ○ |
1.43 | 1.31 | 7.3.2、7.4.3 | ○ |
1.43 | 1.32 <=これがネック! | 7.3.2、7.4.3 | × |
PostgreSQL のテーブル設定
ファイル(ラージオブジェクト)を格納する PostgreSQL のテーブルを作成するSQLコマンドの一例は以下の通りです。
ファイル(ラージオブジェクト)を格納するフィールドのタイプは、oid です。
|
サンプルと解説 – 送信元フォーム
以下が、送信元フォームのHTMLファイル全体です。
普通のフォームですが、フォームのエンコーディングタイプとして、「ENCTYPE=”multipart/form-data”」を指定しています。ファイルを転送する場合はこれにしなければなりません。
アップロードするファイルをクライアントのローカルマシンから選んでもらうための INPUT 要素は、TYPE=”file” です。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<body bgcolor=”#FFFFFF”> ファイル選択: <input type=”file” name=”upload_file” size=”60″> </form> </body> |
ちょっと雑学
|
サンプルと解説 – CGIスクリプト
ダウンロードできるファイル内にコメントとして大体のことは書いてあるので、面倒な人は読み飛ばして下さい。少しずつ分割しながら簡単に解説を加えます。
perl のパスの設定とモジュールのロード、変数宣言を行います。
#使用するモジュールをロード #変数宣言 |
環境設定をします。
@type_ok と @ext_ok は、受付可能な ファイルの MIMEタイプと拡張子を設定します。送信できるファイルの制限は、許可するものを指定する方法と拒否するものを指定する方法とが考えられますが、このサンプルでは、「許可するものを指定する」方法を採用しています。送信されたファイルのMIMEタイプか拡張子のいずれかが指定した中に含まれていれば、送信が許可されます(実際は、保存が許可される。MIMEタイプやファイル名を取得する時には既に、web サーバがファイルのデータを受取切って、そのデータを内部的に保持している。CGIスクリプトがそれを保存しない場合、そのデータは破棄される。)。
$connstr は、DBI で使用するデータベース接続用文字列です。詳細は、CPANのDBD::Pg のドキュメントを参照して下さい。
#受付可能な拡張子(正規表現) #データベース接続設定 |
web サーバとクライアントのセッションが切断された時に実行する関数を定義します。このサンプルの場合は、データベース接続が残っていたらそれを切断する処理を定義しています。
|
送信可能なサイズの上限を設定し、CGIオブジェクトを作成します。これらは モジュール CGI の機能です。詳細は、search.cpan.org などからモジュールのドキュメントを参照して下さい。
このように制限した場合でも、ファイルサイズの判断は、送信されてきたデータを全て受取り切った後です。送信そのものを打ち切りたい場合は、(WebサーバがApacheなら)Apacheの LimitRequestBody ディレクティブで制限できます。
[>> Apache のドキュメント(LimitRequestBody ディレクティブ)]
#CGIオブジェクトを作成 |
クライアントにヘッダを出力します。
|
まず、ファイルが送信されているかチェックします。
unless (defined($filename)){ $error = $form->cgi_error; |
ファイルが送信されている場合は、送信拒否すべきファイルでないかをチェックします。
「ベース名のチェック(アスキー文字列であること)」を行うと、ファイル名に日本語を含むファイルは送信できません。一般に、ファイル名に日本語等を使用すると何かと問題が出ることが多いのでこの例では制限を付けていますが、CGI(送信フォーム)とファイル名の使用文字コードが一致していれば日本語ファイルの送信自体は問題なく行えます。
例えば、フォームとCGIをShift_JISで作成し、WindowsやMacからファイルをアップロード、PostreSQLで使用する文字コードもShift_JISに設定してある場合等は全てShift_JISの環境なので、日本語ファイル名を受け付けても特に問題が出ません。
フォームとCGIをEUC-JPやその他の文字コードで作成している場合、WindowsやMacなど、Shift_JISコードを採用しているOSから日本語ファイル名のファイルを送信してしまうと、送信側のブラウザによっては、CGI側でコンテンツをうまく取得できないことがあります。うまくいかないことが明らかなブラウザは Netscape 4.x(Windows) です。IE 4.x 以上(Windows)、IE5.5(Mac)、Opera6.x以上(Windows)等では、特に問題が出ないようです。
#ファイルパス内の「\」を「/」に変換 #ファイル名を(ベース名, ディレクトリ名, 拡張子)に分解 #ファイル名のチェック(アスキー文字列であること) unless ($ok) { $ok = 0; #フラグのリセット #ファイルタイプのチェック unless ($ok){ #拡張子のチェック unless ($ok){ $error = “許可されていないファイルタイプ($type)・拡張子($filename[2])です。”; |
保存してよいファイルである場合、データベースに格納する処理を行います。
まず、$SIG{‘***’} に最初に定義した関数を設定して、web サーバとクライアントのセッション切断に備えます。%SIG は perl の特殊変数で、スクリプトがシグナルを受け取った時の処理関数を指定できます。詳細は perl のドキュメントを参照して下さい。
#転送を許可されたファイルの場合、データベースにインサートする
#セッションが切れたときのDBの切断処理を設定 |
データベースに接続します。
#データベースに接続 |
PostgreSQL の「ラージオブジェクト」を作成し、アップロードされたファイルの内容を書き込みます。
ラージオブジェクトの扱いの詳細に関しては、DBD::Pg や PostgreSQL のドキュメントを参照して下さい。
#ラージオブジェクトを作成
#ラージオブジェクトを書込み用にオープン # $filename から内容を読み出して |
補足注:
上記コードで単純に以下のようにしてしまうと、ファイルの末尾に余計なデータが追加されてしまいます。
while (read($filename,$buffer,1024)) {
$dbh->func($lobj_fd, $buffer, 1024, ‘lo_write’);
}
(DBD:Pgモジュール、PostgreSQLのバージョンによると思いますが、)固定で「1024」と書き出しサイズを指定してしまうと、1024バイトを上限とするのではなく、1024バイトまできっちり、$buffer に格納されているデータ長が足りなければ、直前に書き込んだデータで補完してしまうようです。
|
生成されたオブジェクトID($lobj_id)をデータベースに格納します。
#インサート用SQLを設定
#sql実行 #エラーが無ければコミット #データベースとの接続切断 |
ファイルが送信されていない場合は、メッセージだけ表示して終了です。
} else { |
インサートされたデータ
インサートされたデータを psql で参照すると、以下の様に表示されます。body のフィールドに格納されている番号は、perl スクリプト内で生成した $lobj_id(オブジェクトID)の値と同じです。
dbname=# select * from FILE_TABLE; data_id | file_name | file_type | body ---------+------------+-----------------+---------- 1 | myfile.pdf | application/pdf | 1234567 (1 row)