前回までに写真付きブログサーバを作成しました。今回は、
サンプルコードはこちらからダウンロードできます。
認証
AtomPubでは認証方式は決められておらず、
テーブルの作成
ユーザ情報を格納するテーブルを作成し、
ユーザ名とパスワードをguest,abcとします。パスワードはMD5で変換してから格納しますので、
MyBlog % perl -MDigest::MD5 -e "print Digest::MD5::md5_hex('abc')" 900150983cd24fb0d6963f7d28e17f72
テーブルを作成します。テーブル名をusersとし、
MyBlog % sqlite3 test.db sqlite> CREATE TABLE users ( ...> id INTEGER PRIMARY KEY, ...> username TEXT UNIQUE, ...> password TEXT ...> );
ユーザをテーブルに追加します。
sqlite> INSERT INTO users (username, password) VALUES ('guest', '900150983cd24fb0d6963f7d28e17f72');
認証処理の実装
MyBlogに認証プラグインを追加します。認証情報の格納先は、
use Catalyst qw(
-Debug
ConfigLoader
Static::Simple
Authentication
Authentication::Store::DBIC
Authentication::Credential::HTTP
);
Catalyst認証モジュールを設定します。詳細は省略します。
authentication:
dbic:
user_class: DBIC::User
user_field: username
password_field: password
password_type: hashed
password_hash_type: MD5
http:
type: basic
認証処理を実装します。認証処理は、
どのようなルールで認証を行うかはサービスによりますが、
sub auto :Private {
my($self, $c) = @_;
# GETでもHEADでもなければ認証を実行する
$c->authorization_required(realm => 'My Blog')
if $c->req->method ne 'GET' && $c->req->method ne 'HEAD';
return 1;
}
ここでは、
キャッシュ・バージョンチェック
AtomPubにはキャッシュやバージョンチェックが定義されています。クライアントは、
キャッシュとバージョンチェックの仕組み
サーバがこれらの機能を実装するかどうかは自由です。実装すると、
- メンバリソースがGETされるとき、
リソースが変更されていなければ304 Not Modifiedを返すことができる。リソース本体を返さないので、 帯域幅が節約される。 - メンバリソースがPUTされるとき
(更新されようとしているとき)、 変更が加えられたバージョンをチェックすることができる。古いバージョンに対して変更が加えられたときには、 更新を拒否できる (412 Precondition Failedエラーが返る)。
これらを実現するために、
サーバは、
クライアントは、
クライアントがメンバリソースをPUTするときには、
Last-Modifiedはリソースの最終更新日時を表します。それ以外はETagとほぼ同様に使われます。If-None-Match/
ただし、
キャッシュとバージョンチェックを実装
Catalyst::Controller::Atompubでキャッシュやバージョン管理を実現するには、
第1回で作成したエントリコレクションにfind_
まず、
MyBlog % sqlite3 test.db sqlite> ALTER TABLE entries ADD COLUMN etag TEXT;
リソースを追加あるいは更新するときにETagを計算し、
use Digest::MD5 qw(md5_hex);
sub create_entry :Atompub(create) {
# 省略...
# entriesテーブルにエントリとメタデータを格納する
$c->model('DBIC::Entries')->update_or_create({
edited => $edited,
uri => $uri,
xml => $entry->as_xml,
etag => md5_hex($entry->as_xml),
});
# 成功したらtrueを返す
return 1;
}
メンバ更新時にETag を計算
sub update_entry :Atompub(update) {
# 省略...
# entriesテーブルからエントリを検索し、更新する
$c->model('DBIC::Entries')->search({ uri => $uri })->update({
edited => $edited,
xml => $entry->as_xml,
etag => md5_hex($entry->as_xml),
});
# 成功したらtrueを返す
return 1;
}
find_
sub find_version {
# $uriはリクエストされた URI
my($self, $c, $uri) = @_;
# $uriに対応するリソースを取得する(リソースが存在しなければ空配列を返す)
my $rs = $c->model('DBIC::Entries')->find({ uri => $uri }) || return;
# ETagをハッシュとして返す
return (etag => $rs->etag);
# ETagとLast-Modifiedをハッシュとして返すこともできる
#return (etag => $rs->etag, last_modified => $rs->last_modified);
}
URI に対応するリソースを検索し、
メディアリソースについても同様に実装します。
メンバリソースのURIの指定
メンバリソースURI
デフォルトURI
デフォルトでは、
- まず、
メンバリソースURIは、 コレクションのURIとメンバリソース名、 拡張子を連結したものです。 - メンバリソース名は次のように決定されます。HTTPリクエストにSlugヘッダがあれば、
それをリソース名とします。このとき、 空白とドット"."をアンダースコア"_"に変更します。大文字を小文字に変換します。URIに使えない文字はPercent Encodeします。Slugがなければ、 "年月日-時分秒-マイクロ秒"をリソース名とします。 - 拡張子には、
HTTPリクエストのContent-Typeに対応する一般的な拡張子を選びます (MIME::Typesモジュールを使っています)。拡張子が不明であれば".bin"とします。
次のHTTPリクエストを例に説明します。
POST /entrycollection HTTP/1.1
Host: localhost:3000
Content-Type: application/atom+xml;type=entry
Slug: Entry%201
<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
...
</entry>
Slugヘッダのスペース
URIの変更
メンバリソース名と拡張子は自由に変更することができます。たとえば、
URIの変更方法を説明する前に、
このため、
第1回で作成したエントリコレクションにmake_
use Time::HiRes qw(gettimeofday);
sub make_edit_uri {
my($self, $c, @args) = @_;
# スーパークラスのmake_edit_uriを呼び出す
my $uri = $self->SUPER::make_edit_uri($c, @args);
# URI がすでに存在するときは、拡張子の前にUNIX timeを挿入する
if ($c->model('DBIC::Entries')->find({ uri => $uri })) {
my $t = join '', gettimeofday;
$uri =~ s{(\.[^.]+)$}{-$t$1};
}
# 空のリソースを作成し、URIを予約する
$c->model('DBIC::Entries')->create({ uri => $uri });
return $uri;
}
URIがすでに存在するときは、
このように、
メディアリソースを扱うエントリでは、
use Time::HiRes qw(gettimeofday);
sub make_edit_uri {
my($self, $c, @args) = @_;
# スーパークラスの make_edit_uri を呼び出す
my($entry_uri, $media_uri)
= $self->SUPER::make_edit_uri($c, @args);
# 省略...
return ($entry_uri, $media_uri);
}
フィードのページング
前回までに実装したAtomPubサーバでは、
ここでは、
# ページあたりのエントリ数
my $ENTRIES_PER_PAGE = 10;
sub get_feed :Atompub(list) {
my($self, $c) = @_;
# フィード(XML::Atom::Feed)のひな型を取得する
my $feed = $self->collection_resource->body;
# コレクションURI
my $uri = $self->collection_resource->uri;
# リクエストされたページ(デフォルトは1ページ目)
my $page = $c->req->param('page') || 1;
# 検索時の開始レコード(offset)と取得数(rows)を指定する
my $attr = {
offset => ($page-1) * $ENTRIES_PER_PAGE,
rows => $ENTRIES_PER_PAGE,
order_by => 'edited DESC',
};
# entriesテーブルからエントリを取得する
my $rs = $c->model('DBIC::Entries')->search({}, $attr);
# フィードのひな形にエントリを追加する
while (my $entry_resource = $rs->next) {
my $entry = XML::Atom::Entry->new(\$entry_resource->xml);
$feed->add_entry($entry);
}
# 最初のページへのリンクを追加する。
$feed->first_link($uri);
# 前後のページへのリンクを追加する
$feed->previous_link("$uri?page=".($page-1)) if $page > 1;
$feed->next_link("$uri?page=".($page+1)) if $rs->count >= $ENTRIES_PER_PAGE;
return $self;
}
まず、
サービス文書のカスタマイズ
第2回では、
サービス文書に登場するコレクションの順序を変更するだけであれば、
Controller::Service:
workspace:
- title: My Blog
collection:
- Controller::EntryCollection
- Controller::MediaCollection
重要なコレクションを先に書いた方がよい、
さらにカスタマイズするときは、
sub modify_service {
my($self, $c, $service) = @_;
# サービス文書$service(XML::Atom::Service)を修正する...
return $service;
}
エラー処理
ここまで、
たとえば、
sub get_entry :Atompub(read) {
# 省略...
# entriesテーブルからエントリを検索する
my $rs = $c->model('DBIC::Entries')->find({ uri => $uri })
|| return $self->error($c, 404, 'Entry does not exist');
# 省略...
}
すると、
HTTP/1.1 404 Not Found
Content-Type: application/atom+xml;type=entry
<?xml version="1.0" encoding="UTF・"?>?
<entry xmlns="http://www.w3.org/2005/Atom">
<updated>2007-01-01T00:00:00Z</updated>
<link rel="related"
href="http://localhost:3000/mycollection/entry_1.atom"/>
<title>404 Entry does not exist</title>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
404 Entry does not exist
</div>
</content>
</entry>