这个共分 5 部分的系列文章向您介绍了如何使用 Perl 和 Apache 构建一个照片共享网站,从而访问 Amazon 的 Simple Storage Service (S3) 和 SimpleDB。在本期文章中,研究完整 mod_perl 站点的代码库,包括如何设置顶级配置、如何使用处理程序,以及如何设置外部依赖关系。
在本期文章中,我们将研究完整的 mod_perl 站点(只讨论代码;模板在下期文章讨论)。前几期文章中,我们的节奏有些缓慢,现在,通过研究 mod_perl,我们将加快步伐。
我强烈建议您阅读源代码。该站点是功能性的,但是许多细节都没有在本系列中详细介绍,我希望您能理解这些细节或者了解还存在疑问的地方。您可以通过书店或使用搜索引擎获取相关的信息。
特别是,设置一个完整的 mod_perl 站点并使用 Template Toolkit 是非常广泛的主题,并且已经介绍过许多次,因此这里不会再解释。最佳学习途径就是了解每一个问题和难点,直到网站可以正常运行。本系列将给出可以使网站正常运行所需的所有内容 — 但是需要由您来将所有内容结合起来。
和此前的文章一样,我将使用 share.lifelogs.com 作为域名。当在您自己的环境中使用时,应该根据需要修改它。
顶级配置
您应当具有一个提供 mod_perl 支持的有效 Apache 服务器。将以下内容插入到 Apache httpd.conf 文件中,如清单 1 所示:
清单 1. 为 Apache 配置文件 share.httpd.conf 提供 mod_perl 支持
<VirtualHost 1.2.3.4:80> ServerName share.lifelogs.com DocumentRoot /var/www/html ErrorLog /var/log/apache/error-share.log PerlRequire /home/tzz/mod_perl_require_share.pl <Location /> SetHandler perl-script PerlHandler SharePerlHandler </Location> SetEnv AWS_KEY 'my-AWS-key' SetEnv AWS_SECRET_KEY 'my-secret-AWS-key'</VirtualHost>
可以看到,所有内容都位于 /home/tzz 中。
下面是需要注意的内容:
有一个特定的错误日志(因此可以独立地观察站点错误) 在过程环境中传递 Amazon 开发人员密匙。这样,Perl 源代码泄漏后就不会丢失它们。(Web 服务器配置通常比源代码更安全)。
注意所有内容 均通过 SharePerlHandler 处理,share.lifelogs.com 上的所有请求!也许您并不希望在生产环境中这样做。
PerlRequire 指令仅仅设置了一个环境,没有做其他特殊的操作。再次强调一下,所有内容都位于 /home/tzz 中。
清单 2 展示了 mod_perl_require_share Perl 文件。
清单 2. mod_perl_require_share.pl 文件
#!/usr/bin/perl -wuse strict;use lib '/home/tzz';use SharePerlHandler;1;
mod_perl 处理程序
mod_perl 处理程序全部位于 SharePerlHandler.pm 文件中。它有多个部分,可大致分为:设置、主处理程序、评论和照片处理程序、通用实用工具和 SimpleDB 实用工具。
通用的和 SimpleDB 实用工具可以拥有自己的模块,但是为了保持简单,我将所有内容都放到了一个位置。评论和照片处理程序以及 SimpleDB 实用工具功能基本上都来自于 simple_go.pl 脚本(见 下载),只进行了少量修改。
让我们首先进行设置。在阅读每一节时,我将解释所做出的决策;当我使用不同的方式进行处理时,最常用的理由就是 “简单性”。打造出色 的网站是一项困难的工作,因此应当将这里学到的所有内容作为一个粗糙的模板,然后根据您的需要和预算进行筛选,而不是将其作为完善的设计直接应用到生产中。提供有效的功能可能会分散注意力,但是我总是忍不住这样做。
设置外部依赖关系
清单 3 展示了 SharePerlHandler.pm 文件的基本设置:
清单 3. SharePerlHandler.pm 文件的基本设置
package SharePerlHandler;use Apache::Constants qw(:common REDIRECT);use strict;use Carp qw/verbose cluck carp croak confess/;use Data::Dumper;use Apache::Request;use Template;use POSIX;use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);use MIME::Base64;use Data::UUID; # generates unique IDsuse lib '/home/tzz/amazon-simpledb-2007-11-07-perl-library/src/';use Amazon::SimpleDB::Client;
SharePerlHandler.pm 依赖于许多模块。首先,它使用 strict 模块,这是实现良好 Perl 编程的基本要素。在 use strict 下无法运行的内容不会被放入生产环境中。同样:
Carp 模块提供了更好的错误。 Data::Dumper 用于一般调试。 POSIX 用于许多经常使用的函数。 Digest::HMAC_SHA1 和 MIME::Base64 用于 Amazon S3 上传策略。 Template 模块是 Template Toolkit,它使我们能够快速组合 HTML 页面和一些动态内容。 Data::UUID 用于生成惟一的 ID。 Apache::Request 和 Apache::Constants 用于 mod_perl 与 Apache 服务器的交互。 最后,Amazon::SimpleDB::Client 来自 Amazon 并使我们能够与 SimpleDB 交互。
如果不知道如何从 CPAN 安装这些模块,那么使用 cpan -e 'install MODULE' 完成安装。
我们在这里没有使用 Net::Amazon::S3 模块,但是我们本来可以使用的(我在第 2 部分中提到过)。为了保持简单,因此在制定架构决策时决定不使用这个模块;在介绍上传时将详细讨论这点。
清单 4 展示了 SharePerlHandler.pm 的全局设置。
清单 4. SharePerlHandler.pm 的全局设置
use constant IMAGE_MODE => 0;use constant COMMENT_MODE => 1;use constant VERBOSE => 1; # can also be done through the environment or some other waymy $template = Template->new( { INCLUDE_PATH => '/home/tzz/templates/share', RECURSION => 1, } );my $uuid = Data::UUID->new();
您需要使用常量来表示基本 SimpleDB 操作的照片模式和评论模式,因此在此处定义它们。VERBOSE 常量可用任何其他方法代替,以控制服务器的详细记录。记住,对冗详细记录的动态控制越多,它的成本就越高(因为服务器需要每次进行检查)。
接下来,您将获得一个全新的 $templates 对象(将从 /home/tzz/templates/share 加载模板并递归)。最后,您获得一个 UUID 生成器,可以在任意位置使用。
主处理程序
是的,主处理程序非常重要,奇迹将在这里发生。所以,一定要多加注意!(醒来了?很好!)仔细查看清单 5。
清单 5. 更多全局设置
sub handler{ my $r = shift @_; my $q = Apache::Request->new($r, POST_MAX => 4096, DISABLE_UPLOADS => 1); my $user = (rand 2 > 1) ? 'bob' : 'ted'; # pick a user randomly between bob and ted # (50% chance each) handle_photo($q); # always try to delete, add, or edit a URL # if it's passed as a parameter handle_comment($q); # always try to delete, add, or edit a comment # if it's passed as a parameter my $uri = $q->uri(); my $tfile = $uri; $tfile =~ s,^/,,; # remove the starting "/" in the name if it exists $tfile =~ s,/$,,; # remove the ending "/" in the name if it exists $tfile =~ s,/,_,g; # "/" in the file name becomes "_" so all the # templates can be in one directory $tfile = 'index' unless length $tfile; # make the URI "index" if it's # empty (e.g. someone hit the / URI) if ($tfile =~ m/\.html$/) { $tfile =~ s/html$/tmpl/; # map ANYTHING.html to ANYTHING.tmpl } else { $tfile .= '.tmpl'; # map ANYTHING to ANYTHING.tmpl } my $policy = ''; my $signature = ''; if ($tfile eq 'upload.tmpl') { $template->process('policy.tmpl', { username => $user, }, \$policy) || croak($template->error()); my $key = $ENV{AWS_SECRET_KEY}; $policy = encode_base64($policy); $policy =~ s/\n//g; $signature = encode_base64(hmac_sha1($policy, $key)); $signature =~ s/\n//g; } $q->send_http_header('text/html; charset=utf-8'); my $output = ''; $template->process($tfile, { request => $q, username => $user, policy => $policy, signature => $signature, env => \%ENV, params => \%{$q->param()}, fimages => sub { return list_simpledb(sprintf('SELECT * from `%s`', simpledb_image_domain_name())) }, fcomments => \&get_comments, }, \$output) || croak($template->error()); print $output; return OK;}
好长的一个函数,它太长了!如果必须再添加一点逻辑的话,我希望提取出一些独立的片段(即 “重构” 它,就像目前人们常谈论的一样)。但是,它很好地展示了主处理程序。
首先,函数获得请求对象。然后它设置一个随机用户名(“bob” 或 “ted”);通常您可以按照自己的方式设定,比如通过 cookie,或者可以让 Apache 替您处理身份验证和授权。
我在功能丰富的 Perl:Perl 和 Amazon 云,第 1 部分说过,我将在 SimpleDB 中使用一个用户表,但是它会使站点变得非常复杂,因此放弃了这个想法。在 SimpleDB 中查看用户并不容易,因为我需要提供一种方法来登录并管理用户信息。这会使代码变得非常长,因此忘记这个想法吧 — 但是这个想法绝对是可行的。
接下来您将处理任意照片或评论参数。例如,如果发现 deletecommentid 参数,那么尝试删除这个评论 ID。我们稍后将详细探讨照片和评论参数处理程序。
接下来需要管理实际的请求。这可以通过简单的映射完成,将 “any/request/here.html” 转换为 “any_request_here.tmpl” 并请求该模板。我们确保 index.tmpl 用于 “/” 请求。
任何不具备相应模板的 URI 将不会生成数据,实际上会抛出一个错误。这并不是一种适合用于生产 的技巧,但是它只用了几行代码就可以设置一个 Web 站点,因此如果演示的目标是实现简洁性和简单性,那么这个技巧非常有用。
接着,如果模板文件为 “upload.tmpl”,那么需要为 S3 生成一个上传策略,因此可以使用 policy.tmpl 文件实现此目的。用户名被传递到该模板,非常类似于本系列 第 2 部分中的模板。清单 6 展示了一个策略模板。
清单 6. 最大宽度的样例代码清单
{"expiration": "3000-01-01T00:00:00Z", "conditions": [ {"bucket": "images.share.lifelogs.com"}, {"acl": "public-read"}, ["starts-with", "$key", ""], ["starts-with", "$Content-Type", ""], ["starts-with", "$success_action_redirect", "http://share.lifelogs.com/s3uploaded?user=[% username %]"], ["content-length-range", 0, 1048576] ]}
这里的主要区别在于没有对成功的 URL 使用用户名,而是将用户名作为参数,因为它可以使照片参数处理程序变得更加简单。稍后将详细讨论。
现在您对策略进行了签名并准备发送 HTTP 头部(Apache 为我们完成!)。接下来,使用一些参数生成必要的输出,如下所示:
request,Apache 请求 username,随机用户名 policy,S3 上传策略(可以为空) signature,S3 策略签名(可以为空) env。进程环境(不要应用于生产中!) params,参数,例如来自 POST 或 GET 请求 fimages,返回所有照片的函数 fcomments,根据照片 ID 返回所有评论的函数
这就是通用处理程序的内容。所有其他神奇的地方则发生在评论和照片参数处理程序以及参数本身中。让我们继续研究下去。
照片和评论处理程序将针对每个请求进行调用。如果它们发现参数是正确的,那么就将执行下面的操作:添加、修改或删除照片或评论。模板(我们将在 SharePerlHandler.pm 之后探讨)在 POST 表单中包含这些参数。
但是不要着急,还有一些有趣的内容 — 伪劣的代码、糟糕的架构,您希望发现一个值得发布到 Twitter 上的 bug(“haha @tzlatanov sux teh worst 将检查在无效上下文中使用的 map,而 omg b0rken 模板 obv 并不是真正的 h4ck3r mod_perl issolastmllnm”)。
评论参数处理程序
清单 7 展示了评论参数处理程序。
清单 7. 评论参数处理程序
sub handle_comment{ my $q = shift @_; my $user = $q->param('user'); my $imageid = $q->param('refimageid'); my $comment = $q->param('comment'); my $refcommentid = $q->param('refcommentid'); my $commentid = $q->param('commentid'); my $deleteid = $q->param('deletecommentid'); my $result; if (defined $deleteid) # delete { $result = delete_simpledb($deleteid, COMMENT_MODE); } elsif (defined $commentid && defined $comment) # edit { my %q = ( comment => $comment, ); put_simpledb($commentid, COMMENT_MODE, %q); $result = get_simpledb($commentid, COMMENT_MODE); } elsif (defined $imageid && defined $comment) # new comment { my %q = ( image_id => $imageid, comment => $comment, ); $q{reply_to} = $refcommentid if defined $refcommentid; $q{user} = $user if defined $user; my $id = new_uuid(); put_simpledb($id, COMMENT_MODE, %q); $result = get_simpledb($id, COMMENT_MODE); } $q->param()->{'result'} = $result;}
根据传入的参数,评论参数处理程序有三个可能的模式。模式都是互相排斥的。在所有情况下,result 查询参数被相应地设置为表示成功(如果被定义的话)。因此,模块可以稍后检查是否设置了该参数以及是否正确使用。当站点不断变大时,很可能需要添加其他参数,例如 last_operation 或 error_message。
如果设置了 deletecommentid 参数,处理程序将使用合适的值调用 delete_simpledb。这是最简单的、没有任何限制的模式。
第二个模式修改评论。它不会检查评论是否已经存在,因此错误的 ID 会在此创建一个新评论。要进行检查也很简单,但是需要付出开销(您必须额外调用 SimpleDB,而调用会很慢,因为需要完整的 HTTP 往返以及 Amazon 端的处理时间)。
注意,由于每个评论有一个自己的 ID,因此编辑起来很容易。可以不使用单个评论记录,而是将分组评论作为照片属性(字符串数组,每个评论使用一个),但是这样的话就很难编辑或删除单个评论。实际上,您必须在评论内实现自己的记录结构来表示 ID、一个发贴用户,等等。
编辑模式可以通过 commentid 和 comment 查询参数触发,这两个参数分别表示标记模板的 UUID 和评论的新内容。结果是从 SimpleDB 检索相同 UUID,因此可以在这里进行检查,看看返回的评论字段是否是您所希望的(如果不是则说明出现了错误)。为了保证简单,我在此并没有执行检查。
最后一种模式,创建一个新评论,由 imageid 和 comment 参数触发。可选地,它将使用一个引用 UUID(如果评论是另一个评论的回复的话)和一个用户名(如果评论不是匿名的话)。和编辑模式一样,只需对属性执行 put_simpledb 并将它们返回,而不用检查字段是否被正确修改。
照片参数处理程序
这个处理程序非常类似于评论处理程序,因此如果刚才睡着了的话,那么返回去再看一遍。
照片 URL(如果没有传递的话)是由 S3 键和 bucket 构造的。这样您就具有一个一致的接口来在 S3 上传后处理成功的重定向。清单 8 展示了照片参数处理程序。
清单 8. 照片参数处理程序
sub handle_photo{ my $q = shift @_; my $user = $q->param('user'); my $name = $q->param('name'); my $bucket = $q->param('bucket'); my $key = $q->param('key'); my $url = $q->param('url'); my $editid = $q->param('imageid'); my $deleteid = $q->param('deleteimageid'); # set the URL from the S3 key and bucket if necessary if (!defined $url && defined $key && defined $bucket) { $url = sprintf("http://%s.s3.amazonaws.com/%s", $bucket, $key) } my $result; if (defined $deleteid) # delete { $result = delete_simpledb($deleteid, IMAGE_MODE); } elsif (defined $name && defined $editid) # editing an image name { my %q = ( name => $name, ); put_simpledb($editid, IMAGE_MODE, %q); $result = get_simpledb($editid, IMAGE_MODE); } elsif (defined $url && defined $name && defined $user) # adding a new one { my %q = ( url => $url, name => $name, user => $user, ); $q{bucket} = $bucket if defined $bucket; my $id = new_uuid(); put_simpledb($id, IMAGE_MODE, %q); $result = get_simpledb($id, IMAGE_MODE); } $q->param()->{'result'} = $result;}
删除照片的操作是由 deleteimageid 参数触发的。编辑照片名称可以通过 name 和 imageid 参数完成。创建新照片可以通过 URL、用户名和照片名完成。照片 bucket 是可选的,并且只有当处理程序调用发生在 S3 上传成功重定向到站点之后才会显示。
回忆一下策略,S3 成功重定向使用用户名作为 URL 参数的一部分。它也包含键和 bucket,因此您只需要从中创建一个照片。
实用函数
让我们看一下各种实用函数。
清单 9. 各种实用函数
sub qlog{ printf STDERR @_; print STDERR "\n";}sub new_uuid{ return $uuid->to_string($uuid->create());}sub simpledb_image_domain_name{ return simpledb_domain_name(IMAGE_MODE);}sub simpledb_comment_domain_name{ return simpledb_domain_name(COMMENT_MODE);}sub simpledb_domain_name{ return sprintf "%s.share.lifelogs.com", (shift == IMAGE_MODE) ? 'share_photos' : 'share_comments';}
如果不希望每次编写 printf STDERR,那么 qlog 将非常有用。它还为您编写了行结束符。是否要将各种输出写入到 Apache 错误日志,这由您决定。同样:
new_uuid 用于生成新的 UUID。 simpledb_domain_name、simpledb_image_domain_name 和 simpledb_comment_domain_name 使用模式参数(可以是 IMAGE_MODE 或 COMMENT_MODE)来提供一个 SimpleDB 域。
SimpleDB 实用函数
SimpleDB 实用函数几乎和第 3 部分中的函数(simple_go.pl)完全相同。可从下面的下载小节下载。我将列出其中的不同:
get_comments 是一个新函数,用来获得全部评论,按照片 ID 编排,然后按照父评论 ID 或 noparent 显示。 qlog 替代了 print,而 VERBOSE 常量替代了 $verbose。 使用环境 AWS_KEY 和 AWS_SECRET_KEY 键为每个请求初始化服务。 传递 $mode 模式,而不是用于全局。 对于错误,没有使用 die(),您将尽可能优雅地处理它们。 域名通过 simpledb_domain_name 获得,而不是使用全局变量。 函数被重命名,因为 “get” 和 “put” 在多用途名称空间中不算好名字,就像旧的 simple_go.pl 脚本那样。
再次提醒一下,这段代码只针对每个属性使用一个值。如果任何属性使用了一个字符串数组,那么将只返回其中的一个字符串。在编写完属性后,只会留下一个值,即使之前有一组值。这大大简化了代码,但并不适合用于通用的 SimpleDB。
结束语
通过本系列第 2 部分和第 3 部分的介绍,现在您已经从总体上了解了完整的 mod_perl 站点的代码(这两篇文章的下载文件也可以从下面的下载小节获得)。最终生成的站点使用 Template Toolkit、S3 和 SimpleDB 来提供照片上传、主题浏览、添加评论、编辑和删除功能。
在第 5 部分中,我们将研究这个站点的模板。
SimpleDB 实用函数,simpledb_utility.zip:temp_10030721531631.zip
样例脚本(来自第 2 部分),s3form.zip:temp_10030721549116.zip
样例脚本(来自第 3 部分),simple_go.zip:temp_10030721552175.zip