Site Overlay

使用Backblaze B2和Cloudflare Workers搭建一个静态网站

前言

之前紫荆被迫关站的时候,曾经用工具扒了一些网页下来,不算全,但是能够保证打开来随便点点也不露馅,但是由于扒下来的都是静态的网页,所以体积和数量都非常多,总共有103912个文件,占了8.44G的空间。起初,我是在Lightsail上开了一个3.5刀的服务器运行NGINX,代理这个紫荆的“墓碑”和一些我之前搭建的,但是现在基本上没什么访问的老网站,比如物联网大赛的Demo和社会实践的发布博客。

不过这个服务器运行的似乎并不算平稳,已经有好几次莫名其妙失去响应只能重启的故障,我怀疑还是和过多的文件数量有一定关系,另外月供的3.5刀折合起来也能吃一个疯狂星期四了,这种纯静态的网页理论来说应该有更加节约的方式部署才对。

20220609:今天疯狂星期四又没有原味鸡,记仇.jpg

常见的是使用GitHub Pages或者Cloudflare Pages这样的解决方案,但是这两者都是基于GitHub repo的,而免费用户最大只能创建2G大小的repo,不够用,而Cloudflare Pages虽然后来加入了直接上传的方式,但是限制更大了,也不行。

然后想起之前在玩Cloudflare Workers的时候,看见了这篇文章:使用 Backblaze B2 和 Cloudflare Workers 搭建免费的自定义域名图床 | 驱蚊器喵的插座,Backblaze B2提供的10GB免费空间恰恰好够我把我的静态网站放上去,而且我又有Cloudflare Workers的付费订阅,既然图片能用这种方式托管,那网页文件应该也行。

其实我一开始想尝试的是Cloudflare新推出的R2对象存储,因为一家的服务可能兼容性能更好一些,但是R2对文件的大小和数量限制较大,而API文档也不算完善,所以暂时不考虑了。

创建存储桶

在Backblaze B2里创建一个公开的存储桶,命名为BUCKET-NAME,然后创建一个API密钥,保存好keyIDapplicationKey,以备下一步使用。

上传文件

我一开始用的是Cyberduck软件,在本地PC上进行上传,但是由于文件太多,上传有显著的性能问题,同时又因为众所周知的网络原因,上传速度很慢,只有个位数的KB/s。

所以我想到的目前正在使用中的Lightsail服务器,服务器上也有网站的文件,而且因为服务器在境外,所以速度应该能够有所提升,Google了一下,Backblaze官方也提供了一个命令行工具,可以完成期望的工作。

从官网下载工具,我选择的是下载一个单文件,不过官方也提供了通过pip安装的渠道:

wget https://github.com/Backblaze/B2_Command_Line_Tool/releases/latest/download/b2-linux
chmod +x b2-linux

然后使用上一步创建的API密钥认证一下:

./b2-linux authorize-account [<applicationKeyId>] [<applicationKey>]

如果认证成功了,会显示目前连接的API终点的地址,如果失败则会提示原因。

最后,使用sync命令将本地目录同步到存储桶即可:

./b2-linux sync ZijingBT-Mirror "b2://BUCKET-NAME"

同步完成后,就能进行下一步操作。

突然发现扒站工具没有扒favicon.ico,手动传一个上去。

参考:Get the Command-Line Tool
How do I use the b2 sync command? – Backblaze Help

设置DNS

上传好文件后(其实不一定需要等传好),就可以部署DNS解析,将域名解析到存储服务器上。

打开任意一个上传的文件的详情,可以看见有三个URL,其中Friendly URL和S3 URL都可以用来实现我们的目的,区别只是在Workers里面的脚本要不要重写路径而已,我最后按照教程里写的,使用了Friendly URL。

Friendly URL的域名应该是一个fxxx.backblazeb2.com的域名,在Cloudflare里创建一条CNAME记录,指向这个网址,并开启Cloudflare代理。

然后,将Friendly URL里的域名替换为自己的域名再访问,可以看见已经显示出了网页。

配置Cloudflare Workers

新建一个Workers实例,并贴入以下代码:

'use strict';
const b2Domain = 'your.domain'; // configure this as per instructions above
const b2Bucket = 'BUCKET-NAME'; // configure this as per instructions above
const b2UrlPath = `/file/${b2Bucket}/`;
addEventListener('fetch', event => {
    return event.respondWith(fileReq(event));
});

// define the file extensions we wish to add basic access control headers to
const corsFileTypes = ['png', 'jpg', 'gif', 'jpeg', 'webp'];

// backblaze returns some additional headers that are useful for debugging, but unnecessary in production. We can remove these to save some size
const removeHeaders = [
    'x-bz-content-sha1',
    'x-bz-file-id',
    'x-bz-file-name',
    'x-bz-info-src_last_modified_millis',
    'X-Bz-Upload-Timestamp',
    'Expires'
];
const expiration = 31536000; // override browser cache for images - 1 year

// define a function we can re-use to fix headers
const fixHeaders = function(url, status, headers){
    let newHdrs = new Headers(headers);
    // add basic cors headers for images
    if(corsFileTypes.includes(url.pathname.split('.').pop())){
        newHdrs.set('Access-Control-Allow-Origin', '*');
    }
    // override browser cache for files when 200
    if(status === 200){
        newHdrs.set('Cache-Control', "public, max-age=" + expiration);
    }else{
        // only cache other things for 5 minutes
        newHdrs.set('Cache-Control', 'public, max-age=300');
    }
    // set ETag for efficient caching where possible
    const ETag = newHdrs.get('x-bz-content-sha1') || newHdrs.get('x-bz-info-src_last_modified_millis') || newHdrs.get('x-bz-file-id');
    if(ETag){
        newHdrs.set('ETag', ETag);
    }
    // remove unnecessary headers
    removeHeaders.forEach(header => {
        newHdrs.delete(header);
    });
    return newHdrs;
};
async function fileReq(event){
    const cache = caches.default; // Cloudflare edge caching
    const url = new URL(event.request.url);
    //console.log(event.request.url)
    //console.log(url.pathname)
    if (url.pathname === "/") {
        url.pathname = "/index.html"
    }
    //console.log(url.pathname)
    if(url.host === b2Domain && !url.pathname.startsWith(b2UrlPath)){
        url.pathname = b2UrlPath + url.pathname;
        //console.log(url.pathname)
    }
    let response = await cache.match(url); // try to find match for this request in the edge cache
    if(response){
        // use cache found on Cloudflare edge. Set X-Worker-Cache header for helpful debug
        let newHdrs = fixHeaders(url, response.status, response.headers);
        newHdrs.set('X-Worker-Cache', "true");
        return new Response(response.body, {
            status: response.status,
            statusText: response.statusText,
            headers: newHdrs
        });
    }
    // no cache, fetch image, apply Cloudflare lossless compression
    response = await fetch(url, {cf: {polish: "lossless"}});
    let newHdrs = fixHeaders(url, response.status, response.headers);

  if(response.status === 200){

    response = new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: newHdrs
    });
  }else{
    response = new Response('File not found!', { status: 404 })
  }

    event.waitUntil(cache.put(url, response.clone()));
    return response;
}

其中b2Domain填你的域名,b2Bucket填存储桶的名称即可。

由于我想要部署到多个域名,所以干脆直接去掉了对b2Domain的判断,把if(url.host === b2Domain && !url.pathname.startsWith(b2UrlPath))改成了if(!url.pathname.startsWith(b2UrlPath))

这段代码我基本上照搬的教程,除了在fileReq函数的一开始添加了一个判断,这样脚本就会把对根目录的访问解析成对index.html文件的请求,否则的话会报400错误。

部署完毕后,为Workers实例添加一条自定义路由your.domain/*即可。

现在访问域名,已经可以正常显示首页。

如果需要的话,也可以配置缓存策略,从而尽可能节约资源。

后记

和参考的博客不一样,由于我搭建的是一个静态的网站,而非仅仅存储图片,所以请求的数量是要远大于原文的,更不用说我还用了Cloudflare Workers每分钟Check一下网站情况,根据实际运行结果,免费的2500个请求只需要三个小时就能用完,所以对于我而言,肯定还是要绑卡的。

不过按照计算,因为没有带宽费用,即使是用了付费套餐,成本应该也远小于单独搭建服务器所需要的成本,更不用提稳定性和访问速度的不小提升。打算先试运行两个月,看账单再决定使用哪一种方案。

2022-7-25补充:
今天收到了腾讯云的邮件,说网站有色情内容把域名封了,封了的域名直接clientHold了转移不了,不过确实紫荆有一些出格内容,打算逐渐放弃国内域名算了。
同时经过一个月的测试,这个方案总共成本就只有0.06美元的Class B Transactions费用,可以认为方案是可行的。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据