用自己的域名访问访问storage资源

到目前为止SAE上的storage还不支持绑定自有的域名,这给需要使用https或者使用自己域名访问存储的附件文件的开发者们出了一个很大的难题。但是如果灵活的使用SAE的appconfig功能和storage的内置API,其实也可以简单的实现。

文件服务器实现原理

所谓知其然不知其所以然,为什么能通过rewrite的方式将storage的访问地址转换成从应用的二级域名输出呢?如果聊到这个话题,那就不得不稍带一下文件服务器的工作流程了。其实这跟大家熟知的HTTP通信协议一样,HTTP在返回的时候会通过【header】和【body】各自带上一部分东西。其中比较关键的是header中的【content-type】,它告诉浏览器下载的东西是个什么类型的文件,【content-length】它告诉浏览器或者其他的终端待下载的文件有多大,大家平时看到的chrome下载的那个圈圈的进度就是这么计算的,完成的比例就是已经下载的文件大小和总的content-length相比得到的百分比。当然服务端如果不输出这个content-length有没有问题呢?当然是没有的,但这是一种很不好的习惯,特别是输出一些比较大的文件例如图片或者音视频时。

上面讲了平时通过浏览器访问资源时的两个基本要素。大家最熟知的就是打开网页了,当然网页html便是其中的一个文件,它的content-type是【text/html】,它的实体就是大家通过【查看源代码】看到的那堆HTML文本。正如下面访问www.baidu.com看到的response header一样,

当然其中还包含很多其他的头例如Set-cookie表示要求浏览器在哪个域名下写下什么cookie等。不再一一讲述。

那么访问一张图片是什么流程呢,其实和上面讲述的访问【百度】的首页并没有任何差别,同样还是返回两个关键的点,一个header中的content-type,告诉访问者下载的是个什么类型的图片,比如是PNG还是JPG,body中包含的是文件的实体。对于图片而言就是一些二进制。那么大家熟知的文件服务器(apache、nginx、ftp服务器、各种云存储)到底在输出文件时干了哪些工作呢?

其实所有的文件服务器基本都是按如下的几个流程响应一次请求的:

  • 将访问的uri映射到本地文件的uri
  • 准备输出文件的header头信息,包括上面所述的content-type、content-length、缓存的策略信息、server信息、cookie等等。
  • 准备文件的实体信息
  • 输出header信息、输出实体信息
  • 刷新缓冲区,把所有的内容发送出去

有了以上的知识储备就可以开始了,那么无外乎以上的几个步骤,首先将输入的url路径映射到本地的文件资源路径;然后准备header头、准备实体,然后将文件发送出去就行了。本文的演示代码以php实现,其他语言的实现可以类似处理。

URI映射

大家都知道SAE storage访问的url是类似【http://skirt-wordpress.stor.sinaapp.com/uploads/2015/12/1.png】这个样子的路径,我们的目标是转换成【http://skirt.sinaapp.com/uploads/2015/12/1.png】这种访问路径的格式。但是我们的代码中明明没有uploads/2015/12/1.png这个路径的文件,那怎么能访问的到呢?这个时候Appconfig(类似于apache的.htaccess)的rewrite功能就派上用场了,有了这个玩意,欺骗世界都不再是梦想,url想怎么转换怎么转换,就算明明是个.html结尾的文件也可以给它转成lmth的:)。

我们通过构造http://skirt.sinaapp.com/域某种特定形式的url,然后将其请求重定向到代码中的一个php文件去处理就可以了。为了和本地的文件不冲突,我们特意构造【http://skirt.sinaapp.com/.storage/uploads/2015/12/1.png】这种路径的文件表示我们这个文件其实是storage中的一个文件而不是本地真实存在的文件。作为写程序的人也能一眼看到这个和代码文件中存在的图片的路径是明显不一致的,因为我们本地根本没有.storage这个目录。那么怎么写这个config.yaml文件呢,下面我写了一个例子,假设我们把所有类似【http://skirt.sinaapp.com/.storage/****】的请求都映射到file_server.php去处理。

// config.yaml 文件内容
name: skirt
version: 2
handle:
- rewrite: if(!is_dir() && !is_file() && path ~ "^/.storage/(.*)$") goto "/file_server.php?__file__=$1"
<?php
// 获取请求的路径
var_dump($_GET['__file__']);

这个时候我们简单请求一个路径【http://skirt.sinaapp.com/.storage/upload/1.txt】,神奇的一幕发生了。发现请求已经交给了file_server.php处理了。看到的效果如下:

从这里我们也可以看到,我们第一步已经大工告成了,因为我们已经从http://skirt.sinaapp.com域中的uri映射到了storage中的路径。

准备header和body

从上文中我们已经将文件的路径成功的映射到了storage中的文件路径,那么接下来我们就需要进行第二步,就是按照文件路径从storage中获取文件的meta信息和实体了。本文为了简单,还是按照上面所述,只需要准备文件的content-type和二进制内容,这些都可以从SAE提供的storage api【storage的使用文档可以参考:http://apidoc.sinaapp.com/class-sinacloud.sae.Storage.html】中获取,请参考以下代码:

<?php
// 需要到SAE对应的应用下创建一个bucket,以下是bucket的名字
$bucket = 'wordpress';
use sinacloud\sae\Storage as Storage;
$instance = new Storage();

// 检查文件是否存在
if (!array_key_exists('__file__', $_GET)) {
        // 参数不正确直接输出404
        header('HTTP/1.1 404 Not Found');
        exit();
}
$file_path = $_GET['__file__'];

// 通过storage的函数判断文件存在否
$file_info = $instance->getObjectInfo($bucket, $file_path);

if (!$file_info) {
        // 文件不存在
        header('HTTP/1.1 404 Not Found');
        exit();
}
// 通过这个大概获取到的文件信息类似于
// array(4) { ["size"]=> int(37428) ["time"]=> int(1431926062) ["type"]=> string(9) "image/png" ["date"]=> int(1455890358) }
// 可以看到此时就取到了文件的content-type和content-length
// 我们直接输出文件的两个头
header(sprintf('content-type: %s', $file_info['type']));
header(sprintf('content-length:%d', $file_info['size']));

$tmp = tempnam(SAE_TMP_PATH, 'tmpfile');
// 获取文件内容
$instance->getObject($bucket, $file_path, $tmp);

// 输出文件内容
echo(file_get_contents($tmp));

// 大工告成啦!

此时访问就可以发现,我们成功的把【http://skirt-wordpress.stor.sinaapp.com/uploads/2015/05/utf8mb4.png】替换成了【http://skirt.sinaapp.com/.storage/uploads/2015/05/utf8mb4.png】,这时候就可以用自己的独立域名或者是https了。从下面的效果看,https也可以使用了!

其他的一些工作

当然本文只是简单的讲述了一下怎么实现, 其实还有很多的细节需要处理,例如需要带上缓存过期的头,这样可以避免频繁的请求消耗服务器的资源。这里就需要带上etag的header头,在处理请求的时候也需要返回304的http code,诸如此类的问题还有很多,希望聪明的读者自行摸索。

实例代码的下载地址

http://skirt.sinaapp.com/tmp_code/tmp_code.zip