这个共5个部分的系列文章将带领您使用 Perl 和 Apache 构建一个简单的照片共享网站,访问 Amazon 的 Simple Storage Service (S3) 和 SimpleDB。在这一期文章中,理解如何通过一个 HTML 表单将一个文件从 Web 页面上传到 S3,最小化服务上的负载,同时维护严格的安全策略。
您可以通过多种方式将文件从一个 Web 页面上传到 Amazon 的 Simple Storage Service (S3):
使用来自 CPAN 的合适的模块从命令行上传 使用来自 Amazon 的合适的模块从命令行上传 直接从 HTML 表单上传
本文将展示直接从一个 HTML 表单上传文件,从而最小化服务器的负载。我们将文件上传到一个定制 URL,本系列后面的文章将使用该 URL 设置所构建的照片共享站点的其他部分。我们在本系列中使用 share.lifelogs.com 作为域名。
上传到 S3
您可以通过一个 POST 表单将数据上传到 S3(也可以使用 PUT HTTP 方法和 SOAP PutObject 调用)。对于本文,我们将使用表单 POST,因为它很简单,并且没有使用服务器资源(磁盘、CPU 或带宽)。
上传到 S3 的最大问题是不能修改元数据。这可能是由 S3 的分布式特性造成的,或者是 Amazon 为了保持简单。在 S3 论坛上,Amazon 表示这一点将来可能会出现变化。
无论如何,这意味着在上传内容时必须设置 Content-Type,否则将得到非常不理想的二进制/八位字节流集合的内容类型。其他元数据则不是特别重要,因为 Amazon 的 SimpleDB 用于跟踪上传,并且您可以在其中存储元数据。我们提供了一个大部分情况下都令人满意的JavaScript 解决办法来解决 Content-Type 问题。
用户名将是一个有效的 URL 的一部分。您还应该将其放入要加载的元数据中,这样它就始终与文件相关联。但是用户随后就不能再修改他或她的用户名,除非为了存储新的元数据,要求您通过某些方法重新执行所有 S3 上传。您可以使用独立于用户名的用户 ID 并将用户 ID 与 S3 对象关联在一起,但是这添加了不必要的复杂性,因为我们的目的是构建一个简单的照片共享站点。
我们(即 Web 站点的运作者)现在在 S3 上设置了一个名为 images.share.lifelogs.com 的 bucket。这个 bucket 拥有相应的访问控制,允许执行公共的读操作。如果使用 Amazon 文档难以实现这一点,那么可以使用 S3Fox 和 JungleDisk 等工具或其他 S3 接口来设置 bucket。如果遵循这种方法,那么还将得到 AWS 访问权和密匙。
您将使用一种策略来控制上传;该策略是使用 JSON 数据格式表示的一组规则集。您将使用您的 Amazon 密匙对策略进行签名。
对策略签名是通过 Digest::HMAC_SHA1 Perl 模块完成的。您首先必须将策略转化为 Base64,去掉所有换行符,对生成的数据进行签名,使用 Base64 对签名进行编码。然后给 Ms. Elle Cowalsky 汇去 1.28 美元,并祈祷她将您所需的签名寄回来。只是开个玩笑!试试下面的代码吧:
清单 1. 对上传策略签名
my $aws_secret_access_key = 'get it from http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';my $policy = 'entire policy here';$policy = encode_base64($policy);$policy =~ s/\n//g;my $signature = encode_base64(hmac_sha1($policy, $aws_secret_access_key));$signature =~ s/\n//g;
S3 上传策略
在构建表单之前,首先处理策略;这意味着您将在编写 HTML 之前确定您的安全性和可用性目标,这始终是一种可取的做法。
策略文档相当简单:
清单 2. 上传策略文档
{ "expiration": "3000-01-01T00:00:00Z", "conditions": [ {"bucket": "images.share.lifelogs.com"}, {"acl": "public-read"}, ["starts-with", "$key", ""], {"success_action_redirect": "http://share.lifelogs.com/s3uploaded/$user"}, ["content-length-range", 0, 1048576] ]}
S3 开发人员文档对所有内容进行了很好的解释。bucket 必须被命名,bucket 的 ACL 必须匹配,密匙可以以任何内容开头,成功后您将得到一个特殊的 URL。上传文档的大小被限制为 0 字节到 1 兆字节。注意过期日期(下面将详细介绍)。
策略的内容将被签名,并且是公共的,因此恶意用户很难伪造策略内容。这个属性可以确保站点的策略安全性 — 它确保根据特定的条件允许上传到 S3,在其他情况下则不允许上传。记住,您要为 S3 的使用付费,因此这非常重要。
过期日期被设置为 3000 年(是的,3000 年)。这样做的目的是使这个策略在所有实际使用中都不会过期。相反,您在以后可以将过期日期设置为 10 分钟,这样可以确保已删除的用户在超过最近一次合法访问的 10 分钟之后就不能再使用策略。但是这样设置的话,那些需要更长的时间上传文件的用户将被拒绝,他们将对此产生抱怨。因此,思考一下如何设置一个合适的过期日期,而不要随意设置它。
策略必须为表单中指定的所有字段设置条件。这将防止出现伪造并鼓励实现完整的策略文档。
我们已经确立了一个策略,现在让我们设置上传表单。
S3 上传表单
回忆一下,我们曾讨论过与 S3 对象相关联的 Content-Type 元数据,以及如何在上传对象前设置它。不幸的是,这不适合用于图像上传,因为无法提前获知用户要上传的内容。例如,JPEG 图像和 PNG 图像就使用不同的内容类型(它们实际上称为 MIME 类型,并且几乎有几百种常见类型)。
解决方法就是使用一些中等级别的 JavaScript 代码。由于您将所有内容都放入了 Perl 脚本,因此必须转义所有 \ 和 $ 字符(因此产生稍微有些混乱的内容)。查看实际 JavaScript 生成的 HTML;您将在本系列的后续文章中使用 Template Toolkit 完成该工作,但是它的中心思想是使脚本完整简单。它非常简单,但很可惜的是可读性受到一些影响。
清单 3. s3form.pl
#!/usr/bin/perluse warnings;use strict;use Data::Dumper;use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);use MIME::Base64;my $aws_access_key_id = 'get it from http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';my $aws_secret_access_key = 'get it from http://aws-portal.amazon.com/gp/aws/developer/account/index.html?action=access-key';my $user = 'username'; # this is the user name for the uploadmy $policy = '{"expiration": "3000-01-01T00:00:00Z", "conditions": [ {"bucket": "images.share.lifelogs.com"}, {"acl": "public-read"}, ["starts-with", "$key", ""], ["starts-with", "$Content-Type", ""], {"success_action_redirect": "http://share.lifelogs.com/s3uploaded/$user"}, ["content-length-range", 0, 1048576] ]}';$policy = encode_base64($policy);$policy =~ s/\n//g;my $signature = encode_base64(hmac_sha1($policy, $aws_secret_access_key));$signature =~ s/\n//g;print <<EOHIPPUS;<html> <head> <title>S3 POST Form</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <script src="http://ajax.googleapis.com/ajax/libs/prototype/1.6.0.3/prototype.js" type="text/javascript"></script> </head> <body> <script language="JavaScript">function submitUploaderForm(){ var form = \$('uploader'); // note the escapes we do from Perl var file = form['file']; var ct = form['Content-Type']; var filename = ''+\$F(file); // note the escapes we do from Perl var f = filename.toLowerCase(); // always compare against the lowercase version if (!navigator['mimeTypes']) { alert("Sorry, your browser can't tell us what type of file you're uploading."); return false; } var type = \$A(navigator.mimeTypes).detect(function(m) { // does any of the suffixes match? // note the escapes we do from Perl return m.type.length > 3 && m.type.match('/') && \$A(m.suffixes.split(',')).detect(function(suffix) { return f.match('\\.' + suffix.toLowerCase() + '\$'); // note the escapes we do from Perl }); }); if (type && type['type']) { ct.value = type.type; return true; } alert("Sorry, we don't know the type for file " + filename); return false;}</script> <form id="uploader" action="https://images.share.lifelogs.com.s3.amazonaws.com/" method="post" enctype="multipart/form-data" onSubmit="return submitUploaderForm();"> <input type="hidden" name="key" value="\${filename}"> <input type="hidden" name="AWSAccessKeyId" value="$aws_access_key_id"> <input type="hidden" name="acl" value="public-read"> <input type="hidden" name="success_action_redirect" value="http://share.lifelogs.com/s3uploaded/$user"> <input type="hidden" name="policy" value="$policy"> <input type="hidden" name="Content-Type" value="image/jpeg"> <input type="hidden" name="signature" value="$signature"> Select File to upload to S3: <input name="file" type="file"> <br> <input type="submit" value="Upload File to S3"> </form> </body></html>EOHIPPUS
这段 JavaScript 代码有些难看,但是应当可以在大部分现代浏览器中正常工作。实际上,我们拦截了提交按钮,并且只有在知道所上传文件的类型的情况下才返回 true。
编者注:您可以 下载此脚本。清单 3 中粗体显示的两行脚本不应当像上面那样被打断,但我们的显示宽度有限。如果要从本文复制并粘贴此脚本,那么需要将这两行脚本恢复为一行代码。它们被正确放置在脚本中,您可以下载此脚本。
上传表单:JavaScript 和原理
您将从 Google API 站点加载 Prototype。如果您有多疑症或强迫症的话(您的多疑并不表示它们不存在),您可以自己托管它。
使用 Prototype 实用工具从表单获取文件名,然后查看文件名(使用小写)。对于每一个浏览器已知的 MIME 类型,使用 Prototype detect() 数组方法查找与以下条件的第一个匹配:
类型必须长于三个字符。 必须包含一个 / 字符。 MIME 类型的任何后缀都必须匹配文件名。
三字符检查和 / 字符检查是由于在 Firefox 中,(至少)有一个 “*” MIME 类型将匹配任何内容。由于没有什么用处,您希望能够跳过这些检查(我们也希望),并且任何其他 MIME 类型都不会影响我们要实现的目标。
您将使用 JavaScript split() 字符串方法遍历后缀,这将生成一个数组。因此,如果后缀为 jpg,jpeg,那么将遍历那些使用逗号分隔为两部分的内容。再一次,使用 Prototype detect() 数组方法在后缀中寻找与文件名的第一个匹配。比较小写形式的后缀和文件。
如果搞混了的话,再大致研究一下 Prototype 和 JavaScript。目前可以假设适合用于大多数情况。对于较旧的或不常见的 Web 浏览器可能不行,当然,如果用户禁用了 JavaScript 则肯定不能工作。现实就是这样。我们只能寻找适合大多数用户的方法。
如果类型声明失败,那么我们也会失败。尽管它将向用户显示一条消息,但是可以做得更好。比如,您可以尝试做一些猜测,如果不知道类型的话,那么可以回到以前的图像/jpeg。您可以做出一些改进。声明失败的话,函数将返回 false,这将阻断上传。
注意,对于成功的上传,您将被重定向到一个包含用户名的 URL。请参见 “上传到 S3” 理解这样做的原因。
最后,这个脚本将 Perl、JavaScript 和 HTML 混合到一个有趣的包(想像一辆扬着帆,掌着方向盘还吹奏着萨克斯的跑车)。为展示一种特殊技巧而编写的示例不应当作为您的设计和架构指导。我建议您不要完全复制和粘贴这里包含的脚本,至少应该考虑将它分解成模板片段并进行重构。在本系列后续文章中,我将向您展示如何在 mod_perl Web 站点的完整上下文中实现这些操作。
结束语
通过本文您了解了如何设置 HTML 上传表单将文件直接上传到 S3。我们用到了 Perl、JavaScript 和 HTML。本文给出了一个使用 Prototype JavaScript 库的脚本、Perl MIME::Base64 和 Digest::HMAC_SHA1 模块,以及内联的 JavaScript 和 HTML,并且提到一些注意事项。脚本将针对给定用户将文件上传到 S3,并最终重定向到 share.lifelogs.com 站点上的一个特定的有效 URL。
本系列的第 3 部分将展示这个有效的 URL 如何为上传的文件创建 SimpleDB 记录。您还将发现对于特定用户,如何以 SimpleDB 记录的形式创建、编辑和删除某张照片的评论。第 4 部分和第 5 部分将为您组装 mod_perl Web 站点,请继续关注。
本文的示例脚本,s3form.zip:
temp_10030721378776.zip