PerlでAtomPubサーバを作ろう!

第2回写真付きブログサーバを作ってWindows Live Writerで書いてみる

前回はエントリリソースを扱うAtomPubサーバを作りました。今回は、前回作成したサーバに画像を扱うコレクションを追加して、写真付きブログサーバに仕上げます。また、Windows Live WriterというAtomPubクライアントを使って、ブログを書いてみます。

サンプルコードはこちらからダウンロードできます。

メディアリソースとは

前回説明したように、AtomPubではエントリや画像ファイルなどを「メンバリソース」と呼び、それらの入れ物を「コレクションリソース」と呼びます。メンバリソースがAtomエントリのときは「エントリリソース」と呼ばれ、それ以外のときは「メディアリソース」のように呼び分けられます。今回はメディアリソースを扱うコレクションを追加します。

エントリリソースの場合と同様に、コレクションのURIにメディアリソースがPOSTされると、サーバはコレクションに追加します。

コレクションにメディアリソースを追加
コレクションにメディアリソースを追加

エントリリソースはXMLなので、著者名などのメタデータを記述できました。しかし、メディアリソースには記述することができません。そこでAtomPubサーバは、メディアリソースを追加するときにペアとなるエントリを作成します。このエントリはメディアリソースへのリンクを持つため、メディアリンクエントリと呼ばれます。図では、実線のボールがメディアリンクエントリで、点線のボールがメディアリソースになります。

コレクションのURIに対してGETが送られると、サーバはメディアリンクエントリを列挙したフィードを返します。

メディアリソースとメディアリンクエントリは、それぞれ別のURIを持ちます。このURI(Edit URI)に対する操作はAtomエントリの場合とほぼ同じです。GETで取得し、PUTで更新、DELETEで削除できます。

メディアリソースを取得

メディアリソースを取得

メディアリソースを更新

メディアリソースを更新
メディアリソースを削除
メディアリソースを削除

メディアリソースかメディアリンクエントリのいずれかが削除されたときは、対応するリソースも削除してください。必ずそうしなければいけないわけではないですが、そのほうが安全です。

テーブルの作成

メディアリソースとメディアリンクエントリを格納するテーブルを作成します。

MyBlog % sqlite3 test.db
sqlite> CREATE TABLE medias (
   ...>   id            INTEGER PRIMARY KEY,
   ...>   edited        INTEGER,
   ...>
   ...>   entry_uri     TEXT UNIQUE,
   ...>   entry_body    BLOB,             -- XML
   ...>
   ...>   media_uri     TEXT UNIQUE,
   ...>   media_body    BLOB,             -- Base64
   ...>   media_type    TEXT
   ...> );

最初の4カラムは、前回作成したエントリリソース用のテーブルと同じです。entry_uriとentry_bodyには、メディアリンクエントリのURIとXMLが入ります。

最後の3カラムにメディアリソースが格納されます。

media_uriにはメディアリソースのURIが入ります。

media_bodyはメディアリソース本体です。このコレクションで扱うメディアリソースは画像ファイルです。本来はパフォーマンスを高めるために画像サーバなどを用意すべきですが、今回は説明を簡単にするために、Base64エンコードでASCIIに変換してからSQLiteに格納することにします。

media_typeには、メディアリソースのメディアタイプ(image/jpegなど)が入ります。エントリリソースと異なり、メディアリソースの場合はメディアタイプが固定ではありません。HTTPレスポンスを返すときのために記憶しておく必要があります。

コレクションコントローラの作成

ヘルパスクリプトを使って、コントローラクラスのひな形を作成します。引数の意味は前回と同じです。

MyBlog % perl script/myblog_create.pl controller MediaCollection Atompub::Collection

前回のエントリコレクションと同じように、メンバリソースの列挙(List)と CRUD を行う5つのメソッドを実装します。

メンバの列挙(List)

列挙は前回のコレクションとほぼ同じです。フィードのひな型を取得し、メディアリンクエントリを追加します。

メンバの列挙(lib/MyBlog/Controller/MediaCollection.pm)
sub get_feed :Atompub(list) {
    my($self, $c) = @_;

    # フィード(XML::Atom::Feed) のひな型
    my $feed = $self->collection_resource->body;

    # mediasテーブルからメンバリソースを取得する(更新日時の新しい順)
    my $rs = $c->model('DBIC::Medias')
               ->search({}, { order_by => 'edited DESC' });

    # フィードのひな形にメディアリンクエントリを追加する
    while (my $resource = $rs->next) {
        my $entry = XML::Atom::Entry->new(\$resource->entry_body);
        $feed->add_entry($entry);
    }

    # 成功したらtrueを返す
    return 1;
}

メンバの追加(Create)

前回作成したエントリコレクションはエントリのみを格納しましたが、今回はメディアリソースとメディアリンクエントリの両方を格納します。

メンバの追加(lib/MyBlog/Controller/MediaCollection.pm)
use MIME::Base64;
sub create_resource :Atompub(create) {
    my($self, $c) = @_;

    # スーパークラスが自動生成したURIと更新日時を取得する
    my $entry_uri = $self->media_link_entry->uri;
    my $media_uri = $self->media_resource->uri;
    my $edited    = $self->edited->epoch; # (UNIX time形式に変換)

    # POSTされたメディアリソースを取得する(バイナリデータ)
    my $media = $self->media_resource->body;

    # メディアリソースのメディアタイプ(image/jpegなど) を取得する
    my $media_type = $self->media_resource->type;

    # スーパークラスが自動生成したメディアリンクエントリ(XML::Atom::Entry) を取得する
    my $entry = $self->media_link_entry->body;

    # mediasテーブルにメディアリソースとメディアリンクエントリを格納する
    $c->model('DBIC::Medias')->update_or_create({
	edited     => $edited,
	entry_uri  => $entry_uri,
	entry_body => $entry->as_xml,
	media_uri  => $media_uri,
	media_body => MIME::Base64::encode($media), # ASCIIに変換する
	media_type => $media_type,
    });

    # 成功したらtrueを返す
    return 1;
}

このメソッドが呼ばれる前に、スーパークラスはメディアリンクエントリを生成します。

このメソッドでは、メディアリソースとメディアリンクエントリを受け取り、データベースに格納します。テーブルの説明で触れたように、メディアリソースはBase64エンコードして格納することにします。

メンバの取得(Read)

メンバの取得(lib/MyBlog/Controller/MediaCollection.pm)
sub get_resource :Atompub(read) {
    my($self, $c) = @_;

    # リクエストされたURI
    my $uri = $c->req->uri;

    # mediasテーブルからリソースを検索する
    my $rs = $c->model('DBIC::Medias')->search({
        '-or' => [
            { entry_uri => $uri },
            { media_uri => $uri },
        ]
    });

    # メディアリンクエントリのURIであった場合
    if ($rs->entry_uri eq $uri) {
        # メディアリンクエントリをセットする
	$self->media_link_entry->body( XML::Atom::Entry->new(\$rs->entry_body) );
    }
    # メディアリソースのURIであった場合
    else {
        # メディアリソースをセットする
	$self->media_resource->body( MIME::Base64::decode($rs->media_body) );
	$self->media_resource->type( $rs->media_type );
    }

    # 成功したらtrueを返す
    return 1;
}

メディアリソースを扱うコレクションでは、リクエストされたURIがメディアリソースかメディアリンクエントリかを判定する必要があります。ここでは、OR検索でレコードを取得した後に、URIを照合して判定します。その後、該当するリソースをセットします。

メンバの更新(Update)

メンバの更新(lib/MyBlog/Controller/MediaCollection.pm)
sub update_resource :Atompub(update) {
    my($self, $c) = @_;

    # リクエストされた URI
    my $uri = $c->req->uri;

    # medias テーブルからリソースを検索する
    my $rs = $c->model('DBIC::Medias')->search({
        '-or' => [
            { entry_uri => $uri },
            { media_uri => $uri },
        ]
    });

    # $valsには、更新するカラムの値をセットする
    my $vals = { edited => $self->edited->epoch };

    # メディアリンクエントリのURIであった場合
    if ($rs->entry_uri eq $uri) {
        # メディアリンクエントリをセットする
	$vals->{entry_body} = $self->media_link_entry->body->as_xml;
    }
    # メディアリソースのURIであった場合
    else {
        # メディアリンクエントリの更新日時(app:edited) を更新し、セットする
	my $entry = XML::Atom::Entry->new(\$rs->entry_body);
           $entry->edited( $self->edited->w3c );
	$vals->{entry_body} = $entry->as_xml;

        # メディアリソースをセットする
	$vals->{media_body} = MIME::Base64::encode( $self->media_resource->body );
	$vals->{media_type} = $self->media_resource->type;
    }

    # 更新を実行する
    $rs->update($vals);

    # 成功したら true を返す
    return 1;
}

メンバの取得と同様に、リクエストされたURIに対応するレコードを検索し、更新対象がメディアリソースかメディアリンクエントリかを判定します。その後、更新するカラムに値を設定し、更新を行います。

更新対象がメディアリンクエントリのときは、メディアリンクエントリのみを更新します。更新対象がメディアリソースのときは、メディアリンクエントリの更新日時タグ(app:edited)を更新するのを忘れないようにしてください。

メンバの削除(Delete)

メンバの削除(lib/MyBlog/Controller/MediaCollection.pm)
sub delete_resource :Atompub(delete) {
    my($self, $c) = @_;

    # リクエストされたURI
    my $uri = $c->req->uri;

    # mediasテーブルからエントリを削除する
    my $rs = $c->model('DBIC::Medias')->search({
        '-or' => [
            { entry_uri => $uri },
            { media_uri => $uri },
        ]
    })->delete;

    # 成功したらtrueを返す
    return 1;
}

メンバの取得と同様に、リクエストされたURIに対応するレコードを検索します。リソースが見つかったら、メディアリソースとメディアリンクエントリをまとめて(つまりレコードごと)削除します。

以上でメディアリソースを扱うコレクションの実装が終わりました。

コレクションの設定

デフォルトでは、コレクションコントローラはエントリリソースを扱うように設定されます(このため、前回は設定不要でした⁠⁠。メディアリソースを扱うときは、そのメディアタイプを指定する必要があります。エントリリソースのコレクションでは、オプションとしてカテゴリを指定することができます。設定はCatalystの設定ファイル(myblog.yaml)で行います。

エントリリソースを扱うコレクションの設定

エントリリソースのコレクションでは、カテゴリを指定することができます。カテゴリとは、ソーシャルブックマークなどで「タグ」と呼ばれる一種のジャンル情報です。

エントリリソースを扱うコレクションの設定(myblog.yaml)
Controller::EntryCollection:
    collection:
        title: Diary
        categories:
          - fixed: yes
            category:
              - term: animal
              - term: vegetable
              - term: mineral

このように設定すると、animal,vegetable,mineralという3つのカテゴリのみが許可されるようになります。fixedは、指定したカテゴリ以外を受け付けないという意味です。fixedを指定しなければ、任意のカテゴリを受け付けます。

Atomのカテゴリには、カテゴリの衝突(同音異義語のこと)を解決するためにschemeという仕組みがありますが、ここでは割愛します。

ここでは、コレクションのタイトルも指定しています。これはフィードのタイトルとして使われます。指定しなければ、パッケージ名のControllerより後ろの部分(この例ではEntryCollection)がタイトルとして使われます。

メディアリソースを扱うコレクションの設定

メディアリソースのコレクションでは、許可するメディアタイプを指定することができます。

メディアリソースを扱うコレクションの設定(myblog.yaml)
Controller::MediaCollection:
    collection:
        title: Photo
        accept:
          - image/png
          - image/jpeg
          - image/gif

ここでは、画像を表す3種類のメディアタイプを設定しました。メディアタイプはimage/*のようにワイルドカードで指定することもできます。

サービス文書コントローラの作成

コレクションの設定情報をクライアントに公開します。AtomPubでは、サービス文書というXML文書を使ってコレクションの情報を公開することができます。サービス文書についての説明は省略しますが、上のコレクション設定がほぼそのままXMLになっていると考えてもらって問題ありません。

ヘルパスクリプトを使って、サービス文書のコントローラを作成します。

MyBlog % perl script/myblog_create.pl controller Service Atompub::Service

コレクションと異なり、サービス文書のコントローラには実装しなければならないメソッドはありません。スーパークラス(Catalyst::Controller::Atompub::Service)が各コレクションの設定を読み取り、自動的にサービス文書を生成します。http://localhost:3000/serviceにアクセスすると、サービス文書を取得できます。

HTMLコントローラの作成

もう一息です。HTMLを表示するためのコントローラを作成して、ブログっぽくみせます。

ヘルパスクリプトを使ってコントローラを作成します。

MyBlog % perl script/myblog_create.pl controller Html

コマンドを実行すると、lib/MyBlog/Controller/Html.pmが作成されます。エントリを取得し、タイトルや本文をテンプレート用のデータ領域にセットします。

lib/MyBlog/Controller/Html.pm
use Digest::MD5 qw(md5_hex);
sub index : Private {
    my($self, $c) = @_;

    # entriesテーブルからエントリを取得する(更新日時の新しい順)
    my $rs = $c->model('DBIC::Entries')->search({}, { order_by => 'edited DESC' });

    # テンプレート用のデータ領域($c->stash) にエントリを追加する
    while (my $entry_resource = $rs->next) {
        my $entry   = XML::Atom::Entry->new(\$entry_resource->xml);
        my $content = $entry->content->body;
        utf8::encode $content;

        push @{ $c->stash->{entries} }, {
            link_hash => md5_hex($entry->link->href),
            title     => $entry->title,
            updated   => $entry->updated,
            content   => $content,
        };
    }
}

トップページへのアクセスは /html にリダイレクトしておきます。

lib/MyBlog/Controller/Root.pm
sub default : Private {
    my($self, $c) = @_;
    $c->res->redirect('/html');
}

ビューの作成

ヘルパスクリプトを使ってビューを作成します。

MyBlog % mkdir root/src/html

テンプレートを作成し、先ほどセットしたデータをHTMLとして表示します。

root/src/html/index
[% META title = 'My Blog' %]

<div class="hfeed">
[% FOREACH entry IN entries %]
  <a name="[% entry.link_hash -%]">
    <div class="hentry">
      <h2>
        <a href="#[% entry.link_hash -%]">_</a>
        <span class="entry-title">
          [% entry.title %]
        </span>
      </h2>
      <div><i class="updated">[% entry.updated %]</i></div>
      <div class="entry-content">[% entry.content %]</div>
    </div>
  </a>
[% END %]
</div>

これで写真付きブログサーバの完成です。Catalystを起動してhttp://localhost:3000/htmlにアクセスすると、My Blogというタイトルのページが表示されます。

Windows Live Writer で投稿してみる

Windows Live Writer というAtomPubクライアントを使って、ブログに記事を投稿してみます。

Windows Live Writerはhttp://writer.live.com/からダウンロードできます。インストールが完了するとウィザードが始まりますので、以下のように進めてください。

  1. ようこそ!というダイアログでは、⁠ブログを持っている」を選択してください。ブログ種類の選択では、⁠他のブログサービス」を選びます。
  2. ブログのホームページとログインでは、URLにhttp://<server>:3000/serviceを入力してください。ユーザー名とパスワードはまだ使わないので、適当に埋めておいてください。
  3. プロバイダの選択では、⁠Atom Publishing Protocol」を選択し、Service DocumentのURLに「http://<server>:3000/service」を入力します。
  4. 仮の記事の作成を有効にしますか?と尋ねられたら、⁠いいえ」を選んでください。
  5. 画像コレクションの選択では、⁠My Blog - Photo」を選択します。最後に「完了」をクリックします。

以上でウィザードが終了し、Windows Live Writerが起動します。

タイトルと本文に何か記入して(文字の拡大や色の変更もできます⁠⁠、⁠投稿」ボタンをクリックしてください。Internet Explorerが起動し、投稿した記事が表示されると思います(表示されないときは、再読込してください⁠⁠。

続いて、メニューから「挿入 - 画像」を選んで、画像ファイルを追加し、投稿してください。ブログに画像が貼り付けられます。

この連載では簡単なブログサーバを実装しただけですが、リッチテキストや画像を使ってブログを書くことができました。AtomPubが普及すると、クライアントを自由に選べるようになったり、サービス間の連携が容易になると考えられます。

おすすめ記事

記事・ニュース一覧