Mojolicious でファイルがアップロードできる掲示板を作る

おはようございます。 若林(@nqounet)です。

Perl入学式のウェブアプリ編では、ファイルのアップロードについては説明していません。

理由は単純で、ファイルの扱い方を説明するには時間が足りないからです…。

とはいえ、画像を表示できるようにしたいというリクエストは多いので、その辺について書いてみたいと思います。

Mojolicious::Lite を使うとファイル1つで完結できるので簡単なのですが、そのために色々と無茶なことをしているので CGI で使いたいというような事情がない限りは Mojolicious を使うほうが良いと思います。

完成品は github に置いています。

複数のファイルがあり、かつ、ディレクトリの構成も重要なので、リポジトリとして用意しています。

適宜お使いください。

なお、画像(に限らずファイルをアップロードできるような)掲示板では、本来はファイルの形式を制限するなど、セキュリティについて考える必要がありますが、今回はその部分については考慮していません。あくまでサンプルコードとして扱ってください。

以下、ポイントを含めて解説してみます。

まず、画像(に限らずファイル)をアップロードするにはどのようにすればよいでしょうか。

ファイルをアップロードする場合には、ポイントが3つあります。これらはすべて HTML がポイントになります。

ひとつ目は、HTMLのinput要素のtype属性を file にすること、ふたつ目はform要素のmethod属性を POST にすること、最後は、form要素のenctype属性を multipart/form-data にすることです。

Mojolicious の場合は、 file_field を使うと、 <input type="file" ...> が生成できます。

method については、Perl入学式でも学んでいるとおり、 form_for で method => 'POST' を指定します。

同様に、 enctype についても、 form_for に enctype => 'multipart/form-data' を追加すれば可能です。

テンプレートのフォーム部分を抜き出すと以下の様な感じです。

1
2
3
4
5
6
7
8
<div>
    %= form_for '/', method => 'POST', enctype => 'multipart/form-data', begin
        タイトル:<%= text_field 'subject' %>
        画像ファイル:<%= file_field 'file' %>
        概要:<%= text_field 'description' %>
        <%= submit_button '投稿する' %>
    % end
</div>

これで、ファイルをアップロードすることができるようになりました。

ファイルがアップロードできるようになったので、次はアップロードされたファイルを保存します。

ファイルを保存するポイントは、2つです。

一つは、データをどうやって取得するか。もう一つは、取得したデータをどこに保存するかです。

Mojolicious を使う場合、データを取得するのは簡単です。

データを取得するには、これまで学んだ方法と同じで param を使用します。テンプレートで file_field には file という名前をつけているので、そのデータを取得する場合は $self->param('file') とすればよいです。

1
2
3
    my $subject     = $self->param('subject');
    my $description = $self->param('description');
    my $file        = $self->param('file');

これまでと違うのは、取得したデータは、データそのものではなくオブジェクト(Mojo::Uploadのインスタンス)になっている、ということです。

その点だけ注意すれば、他のパラメータと同じように扱うことができます。

データを取得できたので、あとは保存です。

保存するには、取得したオブジェクトの move_toメソッドを使います。

保存する場所は、(次の「表示する」にも関わってくるのですが)ウェブに公開されている場所になっている必要があります。

Mojolicious では、公開するディレクトリとして最初から /public というディレクトリが使用できます。

なので、本来はその中に upload のようなディレクトリを作成して、そこで管理すると良いと思います。

ただ、個人の好みかもしれませんが、サーバー上で保存するデータはなるべく固めておきたいので、サーバー上で書き込むためのディレクトリ(/var)を作成し、その中の upload というディレクトリを公開する場所にします。

Mojolicious では、公開するディレクトリは $app->static->paths で管理しているので、そこに新しくディレクトリを追加します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package MyApp;
use Mojo::Base 'Mojolicious';

has upload_dir => sub { shift->home . '/var/upload' };
has data_dir   => sub { shift->home . '/var/data' };

sub startup {
    my $app = shift;

    push @{$app->static->paths}, $app->upload_dir;

    my $r = $app->routes;

    $r->get('/')->to('index#get');
    $r->post('/')->to('index#post');
}

1;

保存する場所が決まったので、あとはファイル名です。

ファイル名をそのまま使用して保存すると、同じ名称の別のファイルがアップロードされると、新しいファイルで上書きされてしまいますので、サーバー側で名前を変更して管理するのが良いと思いますが、その場合でも拡張子には注意が必要です。

今回は、そのあたりを雑に解決する案として、アップロードごとにディレクトリを追加して、ファイル名はそのまま使用することにします。

1
2
3
4
    my $prefix = Mojo::Util::md5_sum(Mojo::Util::steady_time());
    my $path = path($self->app->upload_dir, 'images', $prefix, $file->filename);
    $path->parent->mkpath;
    $file->move_to($path);

Mojo::Utilの、 md5_sum と steady_time を組み合わせてランダムな文字列を作成してディレクトリ名にしています。

ファイルを保存する時に注意しなければいけないのが、親ディレクトリがないと move_to が失敗するところです。

ディレクトリの操作は Path::Tiny を使用しています(pathという関数が使えるようになっています)ので、 move_to をする前に、 $path->parent->mkpath のようにして親ディレクトリを作成しています。

オブジェクト(Mojo::Uploadのインスタンス)の使い方は、ドキュメントを確認してください。

その他、Mojo::Util, Path::Tiny については最後にまとめてリンクしておきます。

さて、ようやく画像が保存ができたので、画像を記事として表示するために他の情報もまとめて保存します。

画像を表示するには、 HTML のimg要素を使用しますので、情報としてはsrc属性に入る文字列があれば良さそうです。

今回は、画像以外に、画像のタイトル(subject)と概要(description)も投稿できるようにしているので、それらをまとめてデータとして追記します。

src属性には「公開ディレクトリをルートとした絶対パス」を指定するのが良いでしょう。

まずは、先ほど保存したファイルの、公開ディレクトリからの相対パスを求めます。

Path::Tiny には、簡単に相対パスを求めるメソッド(relative)があるのでそれを使うと簡単です。

1
    my $src   = $path->relative($self->app->upload_dir)->absolute('/');

relative のあとの absolute は、絶対パスを求めるメソッドなのですが、引数を指定すると、指定した引数が現在の絶対パスであるとして設定されます。

この一連の動作で、 公開ディレクトリをルートとした絶対パスが求められます。

絶対パスにすることがとても重要で、これは後に出てくる url_for と組み合わせることで威力を発揮します。

Perlにかぎらず、プログラミングでは、データをどのように扱うのかが重要です。

今回は、3つのデータ(subject, src, description)を1つの記事(entry)として管理したいので、ハッシュか配列ですが、この場合はハッシュを使用するのが(テンプレートを書く時にも)便利です。

ハッシュだと、値を項目名で指定できるので、適切に項目名を考えておけば後でテンプレートを見ても何が書いてあるのかが見やすくなりますし、投稿日時や投稿者名などのデータを追加した場合でも、修正が比較的簡単です。

もし配列を使用した場合は、何番目に何が書いてあるのかを考えながらテンプレートを書く必要があります。データが増えると番号と項目名の対応表が欲しくなるでしょう。

たくさんの記事を扱うためには、 entry についてもハッシュか配列にしたほうが良いです。

今回は単純なループとして処理したいので、配列を選択します。

ということで、全体的な構造としては、記事の集まりは配列、一つの記事はハッシュで表現することにします。

1
2
3
4
5
6
7
    my $entry = +{
        subject     => $subject,
        src         => $src->stringify,
        description => $description,
    };
    my $model = $self->model;
    $model->add($entry);

ここで $model というのは、記事を扱いやすくするためのオブジェクトです。このオブジェクトに対して add メソッドで新しい記事を追加すると、最新の記事として追加するように作っています。

実装としては、 load して unshift して save するだけの構造です。

1
2
3
4
5
6
7
8
sub add {
    my $self = shift;

    my $entry   = shift;
    my $entries = $self->load;
    unshift @{$entries}, $entry;
    $self->save($entries);
}

load はデータをファイルから読み込むメソッド、 save は引数をファイルに書き込むメソッドです。

ここでは、 $entries がデータ全体(配列のリファレンス)になっているので、そのデータに unshift で $entry を追加しています。

今回は、データが単純な配列とハッシュの組み合わせなので、 Mojo::JSON を使用して、変数の中身をそのままファイルに書き込んでいます。

データも保存できましたので、実際に画像を含めた記事全体を表示します。

表示は HTML なので、テンプレートを使用します。

ここでポイントになるのは、画像の URL (src要素)です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<ul>
    % for my $entry (@{$entries}) {
    <li>
        <dl>
            <dt><%= $entry->{subject} %></dt>
            <dd>
                <img src="<%= url_for($entry->{src}) %>" width="100">
                
                    <%= $entry->{description} %>
                
            </dd>
        </dl>
    </li>
    % }
</ul>

$entries は、記事全体(配列のリファレンス)なので、 for を使用してすべての記事を表示します。

ここで $entry は一つの記事を示すハッシュのリファレンスです。

このハッシュは、 subject, src, description の3つの項目で値を持っています。

srcについては、 URL を扱うので url_for を使用しています。

morbo コマンドで起動した場合、通常は localhost:3000 のルートで起動しますが、 Mojolicious で作成するアプリは、幾つものアプリを組み合わせて一つの大きなシステムとして稼働させることができます。

main.pl は、画像掲示板を /uploader/ というディレクトリで使用できるようにしたものです。

morbo main.pl で起動しても、 morbo script/my_app で起動しても画像がちゃんと表示されるためには、このように url_for と絶対パスを組み合わせる必要があります。

なお、 form_for や、コントローラーで使用している redirect_to は url_for を介していませんが、こちらはフレームワークが自動で url_for に相当することを実行しているので使用する必要はありません。もちろん、使用しても正常に動作します。

テンプレートで URL を直接使用する場合には、必ず url_for を使用しましょう。

少々長くなりましたが、 Mojolicious で作った「ファイルをアップロードできる掲示板」を解説してみました。

途中でも書きましたが、本来ファイルのアップロードはセキュリティに気をつける必要がありますので、今回のコードについては、あくまでサンプルコードとして扱ってください。

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy