システム奮闘記:その13

PostgreSQLのラージオブジェクトを活用した文書管理システム構築



Tweet

(2002年12月11日に掲載)
(2007年5月6日に改定)
 (注意事項)

この話で出てきますPHPのバージョンは、PHP3.0.18です。
はじめに

  社内で次のような声があがった。
 「画像やファイルをホームページに張り付けて、いつでもホームページから
ファイルをダウンロードして見れるシステムができへんかなぁ」だった。

  確かに、通達文などをホームページ上に自由にアップロードしたり、
ダウンロードできたら便利なはず。
  それに、Webサーバーにファイルを保存できるので、通達文をなくしたとか、
読んでいないということがなくなる。
  そこで、通達文や画像などのファイルをWeb上からダウンロードできないかと考えた。


ファイルのアップロード

さて、最初に考えたのは、ファイルのWebを使ってアップロードをし、 そのファイルを、ディレクトリーに保存をしておき、必要な時に ダウンロードできる仕組みを考えた。 しかし、毎度の如く、すぐに困難に直面した。 というわけで、いつものお決まりパターンで、七転八倒を書くことにしました。 はじまり、はじまり (^^) さて、ファイルのアップロード。 PHP言語を使って簡単な物をつくると、こんな感じにできる。
PHPで作った簡単なアップロードのプログラムソース
<HTML>
<HEAD><TITLE>ファイルアップロード<⁄TITLE><⁄HEAD>
<BODY>
<B>ファイルのアップロード<⁄B>
<BR><BR>
<?
if (empty($UserFile)) {
?>
<FORM ENCTYPE="multipart⁄form-data" ACTION="<?$PHPSELF?>" METHOD="post">
    <INPUT TYPE="file" NAME="UserFile">
    <INPUT TYPE="submit" VALUE="アップロード">
<⁄FORM>
<?
} else {
  printf("アップロードしたファイルは $UserFile_name <BR>\n");
  }
?>
<⁄BODY><⁄HTML>

  さて、実際に画像データのアップロードを行なうと

  アップロードしたファイルは C:\\WINDOWS\\デスクトップ\\picture.jpg 

  と表示された。
  Windows上からアップロードした結果だった。Linux上でアップロードしても
ファイル名がディレクトリーがくっついた形で表示される。
  どうすれば良いものかと思い、マンモス本(PHP4徹底攻略)を調べると
こんな関数を発見した。 basename(ファイル名);

  この関数を使うとパス名を取り除いてくれると書いてあったので、実際やってみると

  うまくいかへん (TT)

  結果は相変わらず、アップロードしたファイルは
  C:\\WINDOWS\\デスクトップ\\picture.jpg と表示された。
  Linux上からアップロードすると、うまくいくのに、Windowsでは
正常にいかないのはなぜ???

  マンモス本(PHP4徹底攻略)にも、Windowsでもいけると書いてあるのに (TT)

(注意!)
これを編集している時に気づいたのだが、basename()関数の説明書きの所に
「パス区切りの文字は/(Microsoft Windowsでは¥も使用可)です」と書いてある
もしかして、「/」と「¥」の区切りの違いは問題ないが、
「C:」といったドライブ名まで省くことは書かれていない。
そのため、Windowsからアップロードした場合、ドライブ名があるため、
フルパスで書かれたファイル名から、ファイル名を取り出せなくても、
おかしくはないと思う。その辺りは未確認です  (^^;;

  しかし、首を傾げても仕方がないので、次なる手を考えた。
  ファイル名のフォルダ─の区切りを見ると「¥¥」となっている。
  そこで、次なる関数str_replace("\\","/",ファイル名)を適用してみた。

       アップロードしたファイルは C:/WINDOWS/デスクトップ/picture.jpg に変化した。

  最初の2文字(この場合は、「C:」)が邪魔になるので、
お次にsubstr(ファイル名,2)を適用してみた。


  アップロードしたファイルは /WINDOWS/デスクトップ/picture.jpg に変化した。

  これで、Linux上からアップロードした時と同じフルパス付きファイル名の状態になった。
  そして、basename(ファイル名);を使った。

  見事、ファイル名だけを取り出すことに成功した  V(^o^)V

  普通、システム会社のSEの場合、利用者はWindowsだけとは限らないから
LinuxやUNIXからでも、アップロードできるように対応しないといけないと考える。
  しかし、私はユーザー企業の事務員。それに、どーせ社内の人間は、
Windowsしか使わへんから、LinuxやUNIXからのアップロードを考慮する必要はないと、
あっさり割り切り、Windowsだけに対応した。なんという発想 (^^;;


  ファイル名を取り出すことに成功して、ルンルン気分の私 ♪

  次に、アップロードしたファイルを、保存したディレクトリーに保存したい。
  単にアップロードしただけだと、/tmp ディレクトリーに保存されるが、
PHPのプログラム終了後、消えてしまう。
  そのため、保存したディレクトリにコピーする必要がある。

  そこで、関数copy(A,B)を使って、
ファイルを、自分の置きたいディレクトリーに保管させる所までいった。
  ちなみに、Aはコピー元で、アップロードした際に、
一時的に置かれるディレクトリーがくっついたファイル名。
  Bはコピー先で、自分が保管したいディレクトリーがくっついたファイル名。

  さて、アップロードしたファイルを、ダウンロードできるプログラムを
作成してみた。これで、ダウンロードができれば、大筋で完成だろうと思っていた。

PHPで作った簡単なアップロードのプログラムソース
<HTML>
<HEAD><TITLE>ファイルアップロード<⁄TITLE><⁄HEAD>
<BODY>
<B>ファイルのアップロード<⁄B>
<BR><BR>
<?
if (empty($UserFile)) {
?>
<FORM ENCTYPE="multipart⁄form-data" ACTION="<?$PHPSELF?>" METHOD="post">
    <INPUT TYPE="file" NAME="UserFile">
    <INPUT TYPE="submit" VALUE="アップロード">
<⁄FORM>
<?
} else {

  $file1 = str_replace("\\","⁄",$UserFile_name);
  $file2 = substr($file1,2);
  $file3 = basename($file3);
  $file = "ファイルを置くディレクトリー".$file3
  
  copy($UserFile,$file)
  printf("アップロードしたファイルは $file3 <BR>\n");
  printf("<A HREF='(ディレクトリー)⁄$file3'>$file3 を取り出す<⁄A><BR>\n");
  }
?>

  さて、これでファイルをアップロードしてみた。
  そして、出てくるリンク先をクリックすると、アップロードしたファイルが取り出せる。
  「よっしゃ!!  原型が完成した!」と喜んだ。
  しかし、次の問題が出てきた。ファイル名が日本語の場合だった。
  実際、やってみると、Not found とエラーが出てくる。

  そこで、ファイルを置いているディレクトリーで ls コマンドを使うと

lsコマンドを実行した結果
-rwxr-xr-x    1 www      www          5578 Sep 13 21:29 test.txt
-rwxr-xr-x    1 www      www         14848 Sep 13 21:03 ?f?[?^?x?[?X?r?\.xls
-rwxr-xr-x    1 www      www         14848 Sep 13 20:56 ?緋xls

  なんと日本語のファイル名が文字化けを起こしている!
  Windowsからアップロードされた日本語のファイル名の文字コードが、
SJISのまま、Linuxのディレクトリーに置かれてしまっていた。
  リンクの方に載るファイル名は、SJISのままで問題ないが、
リンク先をクリックすると、ファイル名をEUCのコードとして探そうとする。
  文字コードが違うため、違う文字という認識をしてしまい、リンク先がないという
エラーを吐き出してしまう。
  困ったものだ。でも、こんな事を気にせず、アップロードするファイルに関しては
ファイル名を英数にする取り決めでいこうと考えた。
  しかし、よく考えると、うちの会社で英数でファイル名を作る人はいない。

  日本語のファイル名でないと、便利さは大幅に下がるので、これはマズイと思った。
  そこで、ファイルの中身をデータベースに格納できないかと思った。
  マンモス本で関数を探してみるが見つからない。
  ファイルの中身をテーブルに、ぶち込む関数がない!
  「どないしたら、ええねん」と困った私 (--;;

PostgreSQLのラージオブジェクト活用

マンモス本を見ていると、ファイルからデータベースに格納する関数を発見した。 でも、PHP4でしか使えない。 その上、ファイルをラージオブジェクトに格納すると書いてあった。 ラージオブジェクトって何? 思わず自分の無知を露呈した私 (^^;;; 調べてみるかということで、シーラカンス本(PostgreSQL完全攻略ガイド:改訂3版)の インデックスで調べて、該当のページを開いてみた。 「ラージオブジェクトは、大きな画像データやテキストデータを格納する際に 使用します」と書いている。 内容を読むと、ファイルなどをデータベースに格納する時は、INSERTを使って テーブルに保存するわけでなく、ラージオブジェクトという収納する場所に保存する。 OIDって何? また、自分の無知を露呈した私 (^^;;; うーん、じゃぁ、調べてみるかで、シーラカンス本のインデックスで調べて、 該当のページを開いてみた。 「PostgreSQLでは、行を含めて、すべてのオブジェクトにオブジェクトID (OID)を割り当てています」と書いている。 しかし、それだけ読んだだけでは何を書いているのか意味が理解できず だから、OIDって何? だった (^^;;;;;;;;;;;;; しかし、適当にページをめくっていると、ラージオブジェクトの記述の部分に 「ラージオブジェクトを識別するのに使われます。」があった。 なるほど、データの格納場所の紐づけとして、OIDという数値が割り振られるのか! こんな調子だから、前に進むにも時間がかかる。 こんな突っ込みが来るかもしれない。 もっとじっくり本を読むように! しかし、使わない知識は、覚えても、すぐに忘れる私。 おそらく、シーラカンス本を購入して、色々、遊んでいた時、 OIDやラージオブジェクトが掲載されているページを読んでいると思う。 しかし、何の事か意味がわからず、飛ばしている可能性は非常に高い。 でも、後悔しない。だって、その時は、ラージオブジェクトなどを扱えるだけの 能力がなかったから、例え、知識を無理矢理押し込んでも、時間と労力の浪費だけで 終わってしまっているのが明白だもん (^^) さて、問題はラージオブジェクトの扱い方を知ることだった。 そこで、今度は、じっくりシーラカンス本を眺めることにした。 本に画像の保管システムのプログラムと解説が載っている。 画像をアップロードしている。しかも、プログラムはPHP3で書かれている。 「ソースを解読すれば、なんとかなるやろ!」と思い解読するが・・・ でも、ソースが読めない (TT) そのプログラム、アップロードだけでなく、セッション管理も使っていた。 セッション管理を全く知らない私がソースコードを読めるわけがない。
ここで暴露話
上記で「セッション管理を全く知らない私」と書いたので、
HTMLのフォームのタグや、リンク部分の変数の受け渡しぐらい
使っているやろと、突っ込むがきそうなので、ここで恥を捨てて暴露しまします
データの受け渡しの仕掛けの事をセッション管理ということを知らなかった上、
PHPLIBを使ったセッション管理の事だけを「セッション管理」と
ばかり思い込んでいたためです。

「PHPLIBを使ったセッション管理を知らない私」と書けば、
わざわざ暴露話を書く必要もなかったのですが、開発当時のことを、
忠実に再現するために、敢えて恥じをさらすことにしました (^^;;

ちなみに、セッション管理について詳しい事に関しては
「システム奮闘記:その62」のセッション管理とクッキー入門(cookie)
をご覧ください。

  閑話休題。
  さて、付属のCD-ROMからソフトを取り込み、インストールしてみることにした。
  使ってみれば、多少なりとも解決の糸口がわかると期待したからだった。
  さて、インストールが終わり、さっそく実行させてみると・・・

  エラーが出て、使えない (TT)

  ソースは読めない、ソフトは使えない。お手上げ状態だった。

  そこで、ソースがわからない場合は、シーラカンス本に書いてある
ソフトの解説を読むことにした。
  まずは、本に載っている挿絵に注目した。
  アップロードしたファイルを閲覧している絵があった。
  ならば、次のページに掲載されているソースが、ファイルなどを
PostgreSQLに格納する処理だろうと思った。
  次のページのソースの所に「オブジェクトの仮登録処理」と書いてあった。
  そこで「1からソースを読むと死ぬから、部分的に解読していこう!」と考えた。

  ソースのコメント文に「オブジェクト登録」と書いてあったので、
ここを理解するのが重要な鍵だと思った。

  そこで、ラージオブジェクトを扱う解説部分と、ソースを照らし合わせていくことにした。
  こういう時は、関数の解説が書いてあるマンモス本が凄く便利!

ラージオブジェクトで使う関数
$oid = pg_locreate(接続ID) ラージオブジェクトを作る。
その時できたラージオブジェクトとの紐付けID(OID)を
変数$oidに入れる
$lobj = pg_loopen(接続ID,$oid,"w") ラージオブジェクトを開く。"w"は書き込み。
"r"は読み込みの場合
そして、返り値としてラージオブジェクトIDの値が出てくる
$fd = fopen($userfile,"r") $userfileというファイル名を読み込みモードとして開く。
そして、ファイルポインタ$fdを返す。
$img = fread($fd,filesize($userfile)) $userfileというファイルを
最大filesize($userfile)バイト読み込む。
それを文字列として $img に納める。
pg_lowrite($lobj,&img) ラージオブジェクトIDに文字列$imgを格納する

  上の表を見て「えらい簡単に理解しているやん」と思う人がいるかもしれない。
  ここでカッコ良く「すぐに理解できました!」と書いてみたい所だが、
実際は、本のソースを丸写しでソースを書いた。
  上の表は、奮闘記の編集時に、知識の整理を行なった (^^;;
  そのため、システム構築時は、あまり理解していないかった (^^;;
  と書いても、実は、奮闘記編集時でも、理解していない事柄もある。
  「ラージオブジェクトID」というもので、それの意味付けや
オブジェクトIDとの違いを指摘されると、何も答えられない。
  うーん、私が「なるほど!」と思う記述が見つからないだけに、
ブラックボックス化にならざるえない・・・。

  ところで、上の表の関数を使っても、ファイルをラージオブジェクトに
格納することはできない。
  次のような呪文を唱える必要があった。
  テクニカテクニカ・シャランラン ← 魔女っ子メグ。懐かしいなぁ
  じゃなくて、まじめに呪文を書くと

ラージオブジェクトで使う際に必要な呪文
はじめの呪文 pg_exec(接続ID,"BEGIN");
終わりの呪文 pg_exec(接続ID,"COMMIT");

  簡単に書いている感じがするけど、実は、これを知るには、寄り道した。
  というのも、最初は上の表だけで動くと思っていたが、エラーが出た。
そこで何かが必要だと考えて、ラージオブジェクトの部分を読み返した。
  こんな記述があった。

ラージオブジェクトで使う際に必要な呪文
conn = PQconnectdb(constr);
PQexec(conn,"BEGIN");
ラージオブジェクトインターフェイスの呼び出し
:
:
PQexecconn,"COMMIT");

  おそらく「:」の部分は、oidなどを呼び出す部分で、その両端に、上の呪文を
唱える必要があると思い、さっそく呪文を唱えるとエラーがでた。
  プログラムを見直しても入力ミスなどもなかった。
  思わず・・・

  なんで動かへんねん (TT)

  もう一度、本を読み返すと、どうやらC言語での呪文みたいだった。
  PHPの場合の呪文を探さねばと思ったが、ここで推測。

  conn = PQconnectdb(constr); は、PostgreSQLへ接続する関数ではないか?
  接続ID = pg_connect("dbname=データベース名");
  PQexec(conn,"BEGIN"); は、pg_exec(接続ID、"BEGIN")の関数ではないのか?

  そう思って、呪文を唱えると、エラーが出なかった。
  偶然、うまくいった。偶然、うまくいっても、たまたま、動いただけで、
正しい答えでない事がある。
  今回は、この奮闘記を書いている時に、調べたら、正解だった (^^)

  さて、問題のOIDの値を保存させないと意味がないので、
次のようなソースを作ってみた。

ラージオブジェクトを使ったソースコード
接続ID = pg_connect("dbname=データベース名");
pg_exec(接続ID,"BEGIN");
$oid = pg_locreate(接続ID)
$lobj = pg_loopen(接続ID,$oid,"w")
$fd = fopen($userfile,"r")
$img = fread($fd,filesize($userfile))
pg_lowrite($lobj,&img)

$SQL = "INSERT INTO table VALUE($oid ,$userfile)" ;
pg_exec(接続ID,$SQL)

pg_exec(接続ID,"COMMIT");
pg_close(接続ID);


  そして、 SELECT * FROM table ; で、OIDの値($oid)が記録されているか確認する。
  しかし・・・

  記録されてへん(TT)

  そこで、困ったなぁと悩んだ末、OIDの値($oid)を追加する前に、
ラージオブジェクトを終了させる呪文を唱えてから、oidを記録させようと考えた。

ラージオブジェクトを使ったソースコード
接続ID = pg_connect("dbname=データベース名");
pg_exec(接続ID,"BEGIN");
$oid = pg_locreate(接続ID)
$lobj = pg_loopen(接続ID,$oid,"w")
$fd = fopen($userfile,"r")
$img = fread($fd,filesize($userfile))
pg_lowrite($lobj,&img)
pg_exec(接続ID,"COMMIT");
pg_close(接続ID);

接続ID = pg_connect("dbname=データベース名");
$SQL = "INSERT INTO table VALUE($oid ,$userfile)" ;
pg_exec(接続ID,$SQL)
pg_close(接続ID);

  そして、 SELECT * FROM table ; で、OIDの値($oid)が記録されているか確認する。

  見事、成功! V(^o^)V

  さて、記録されたOIDだが、実際にアップロードされたファイルが
きちんと保管されているか、きちんと取り出せるか確認する必要があった。

  そこで、シーラカンス本でオブジェクト表示処理というソースがあったので、
ソースを丸写し。

オブジェクト表示処理
header("Content-disposition: inline; filename=\"ファイル名\"");
header("Content-type: ファイルの形式");

$execid = pg_exec(接続ID,"BEGIN");
$obj = pg_loopen(接続ID,$oid,"r");
pg_loreadall($obj);

pg_loclose($obj);
$execid = pg_exec(接続ID,"COMMIT");

  稼働させると、無事、ファイルが表示させることができたが、文字化けしていた。
  エクセルのファイルだった。思わず「なぜ?」と考え込んだ。
  しかし、よくシーラカンス本の解説を読むと、初歩的なミスをしていた!!
  なんと、i18n_http_output("pass")を入れるのを忘れていた (^^;;;;
  気を取り直して、稼働させると見事、成功!!!

  しかし、シーラカンス本の解説を読むと、納得がいかない部分があった。
       header("Content-disposition: inline; filename=\"ファイル名\"");の部分だった。
  シーラカンス本では「できるだけ、オブジェクトが、ブラウザーのウィンドウの中に
表示されるようにするおまじない」と書かれていた。
  これだけだと意味がよくわからない。
  そこで、ネットで探してみることにした。そして、header関数の例で、似たような物があった。
       header("Content-disposition: attachement; filename=\"ファイル名\"");だった。

   inlineattachementの違いは何なのか、システムの動作で確かめることにした。

inline
ブラウザーに張り付けた感じでファイルを閲覧する方法
IEを使ってワードやエクセルなどのファイルを閲覧しようとすると
IEに張り付いた感じで閲覧する事ができる
張り付けられないファイル(圧縮ファイルなど)は
ダウンロードの表示がでてくる。
全てのファイルにおいて、ダウンロードの表示がでてくる

  ここまでくれば、あとは、応用するのみ。
  ファイルの登録、閲覧、削除の機能をつけたシステム構築をした。
  今まで、PostgreSQLを使って検索システムなどは構築してきたので、
困ったことにぶつかる事なく、スムーズにシステムが完成した。

       思わず万歳の大合唱!!!  V(^o^)V

  さて、なんとか構築ができ、社内にアナウンスした。
  安心したのは束の間、営業所から「画像データが張り付けできへん」
と苦情がやってきた。
  データの容量を聞くと1.7Mあった。
  うーん、もしかして、アップロードのデータの制限をがあるのかもと思い
/usr/local/lib/php3.ini を見てみた。
  デフォルト設定になっていた。というより、何も設定していなかった (^^;;
  何も設定してないと、1Mが限度らしい。経験的な推測では・・・。
  upload_max_filesize = (ファイルの大きさ)を書き込む。

  サーバーのメモリーが48Mなので、あまりムチャはできんやろうと思った上、
営業所〜本社間のフレームリレー回線が64Kを考えると、
巨大なデータのやりとりは、ムチャやろと考え、3Mにした。
 3Mに設定した根拠はないけど ← おいおい、科学的に議論せんかい!
  

  さて、解決したと思ったら、同僚から「アップロードできへんで」と言ってきた。
「なぜ??」と思って、そのファイル名を見ると空白があった。
  Windowsのファイルは、空白がある。空白が悪さをしている。
  例えば、こんなファイル名:空白の○ファイル名.xls ○は空白の意味
  しかし、解決の道がないと思ったので、諦めた。
  ホントは追求してみたかったのですが、多分、今の私の知識や技術では、
とても歯が立たないという事で、自信を持って断念しました ← 自慢にならへんやろ! (^^;;


  何はともあれ、完成した文書管理システム。
  使い道は色々あると思う。通達文の一括管理・保存。社内文書の雛形の掲載。
  ペーパーレスになるかもしれないし、各人がバラバラに保存しているものが
一括保存だから、なくして困ることもなくなる。

  運用面の話は、これからですが、どんな風に使われるか楽しみです。


その後の話(2007/5/6) このシステムは、その後、廃止になりました。 利用者が少なかった上、Pukiwikiの導入で、データベース不要で 簡単にファイルのアップロードができるようになったためです。
まとめ このシステムを構築して思ったのは、PostgreSQLはスルメだということです。 奥が深く、噛めば噛むほど味が出るという感じだからです。 PostgreSQLを導入して1年になりますが、私の知らない事が、まだまだ沢山あり、 PostgreSQLの真価を発揮させるレベルまで至っていないです。 今後は、PHPLIBを使ったセッション管理の導入の話や、 PostgreSQLのチューニングなどの話題があれば、取り上げようと考えています。 いつになるかは、わかりませんが、慌てず、着実に行こうと考えています (^^;; 最後に、PostgreSQLはフリーですが、性能は凄いです! 改めて実感しました! オラクル、SQLServerと違って、導入費用やライセンス費用で悩む必要はありません! オープンソースは中小企業の味方です。 みんなでオープンソースの恩恵を受けましょう!!

次章:「メールと文字コード」を読む
前章:「名ばかりの携帯ソリューション」を読む
目次:Linux,オープンソースで「システム奮闘記」に戻る

Tweet