Django搭建网络相册:云存储

4500 views, 2021/09/16 updated   Go to Comments

经过几章节的开发,项目的核心功能都具备了,但最头疼的带宽问题依然没解决。作为一个草根网站,服务器的带宽是很宝贵的,那小水龙头经不起海量高清图片的折腾。

解决方案不只一种,比如采用CDN加速、负载均衡、对象存储等。

本章将采用对象存储的手段解决服务器带宽不足的问题。

什么是对象存储

对象存储OSS(Object Storage Service)是一种海量、安全、低成本、高持久的云存储服务,你可以将其作为移动应用、大型网站、图片分享或热点音视频的主要存储方式。通俗点讲就类似云端硬盘,它的带宽资源充足、数据丢失的可能性几乎为零。并且对于小流量的网站来说,OSS 所需的费用开销非常少。

笔者自己博客用的 OSS ,每月支出大概两三毛钱。

基本上所有的云服务商都提供 OSS 服务,比如阿里云、腾讯云、百度云,也有专门做 OSS 起家的七牛云、又拍云等等。

利益相关:笔者自己用的阿里云全家桶(包括后续的部署),所以本章会以阿里云OSS作为案例讲解。新用户通过此阿里云OSS推广链接注册有折扣和现金券,比较划算。

你也可以根据喜好选择其他服务商,原理都是差不多的。

下面从 OSS 的设置开始讲起。

设置OSS

首先打开OSS开通页面。注册好阿里云账号后,点击下图中的“立即开通”:

阿里云 OSS 是先使用后付费的。如果你只是试用,不考虑续租,那就等同于零费用。

开通完成后就进入了 OSS 管理界面。

阿里云 OSS 用 Bucket 来存放具体的文件。Bucket 可以理解为一个空间,或者一块专门的区域。

点击“创建 Bucket”:

来到创建页面。

记录下页面中的 Bucket名称Endpoint 的值,后续会用到:

读写权限设置为公共读,因为相册允许匿名用户访问:

其他的选项就按照图片里来,或者按照喜好选择了。

点击“确定”后,Bucket 就创建好了。

现在就可以在 Bucket 里上传文件了:

我们随便上传些测试图片。

传完之后就显示在文件管理中了:

最后一步。

虽然 Bucket 的权限设置为公共读了,但是操作 Bucket 依然需要对用户身份进行验证。

因此点击导航栏右上角的头像,再点击“AccessKey管理”新建管理员 ID 和 Secret。

进入后,页面可能会提醒你为了安全考虑,尽量用子账户创建 ID 和 Secret。按照它的提示操作即可。

顺利创建好 AccessKey IDAccessKey Secret 后,别忘了给子账户打开操作 OSS 的权限。

搞定后就可以继续正式写代码了。

后端代码

阿里云给 Python 程序员提供 OSS 的软件开发工具包(SDK),封装好了所有常规操作,非常方便。

在虚拟环境中安装此 SDK:

(env)> pip install oss2==2.15.0 -i https://pypi.tuna.tsinghua.edu.cn/simple

笔者写作此文时(2021.08.13)此 SDK 仅对 Python 3.8 以下版本提供支持。如果你的 Python 高于 3.8,那么记得查看官方的兼容性更新。

安装成功后,下一步就是在 views.py 中链接到 Bucket。

在视图文件头部写入:

# /photo/views.py

...

import oss2

# 填入阿里云账号的 <AccessKey ID> 和 <AccessKey Secret>
auth   = oss2.Auth('LTA...fhK', 'zVE...6NC')
# 填入 OSS 的 <域名> 和 <Bucket名>
bucket = oss2.Bucket(auth, 'http://oss-cn-beijing.aliyuncs.com', 'dusai-test')

这里需要填入四个东西:AccessKey ID 、AccessKey Secret、 Endpoint 和 Bucket 名。

接下来的步骤稍微不太好理解,看仔细了。

虽然 SDK 中提供了操作 Bucket 的对象 ObjectIteratorV2 ,但是此对象提供的功能太少,不能直接用到我们的相册项目中。

所以将其作为父类,新建一个 ObjIterator 类:

# /photo/views.py

...

class ObjIterator(oss2.ObjectIteratorV2):
    # 初始化时立即抓取图片数据
    def __init__(self, bucket):
        super().__init__(bucket)
        self.fetch_with_retry()

    # 分页要求实现__len__
    def __len__(self):
        return len(self.entries)

    # 分页要求实现__getitem__
    def __getitem__(self, key):
        return self.entries[key]

    # 此方法从云端抓取文件数据
    # 然后将数据赋值给 self.entries
    def _fetch(self):
        result = self.bucket.list_objects_v2(prefix=self.prefix,
                                          delimiter=self.delimiter,
                                          continuation_token=self.next_marker,
                                          start_after=self.start_after,
                                          fetch_owner=self.fetch_owner,
                                          encoding_type=self.encoding_type,
                                          max_keys=self.max_keys,
                                          headers=self.headers)
        self.entries = result.object_list + [oss2.models.SimplifiedObjectInfo(prefix, None, None, None, None, None)
                                             for prefix in result.prefix_list]
        # 让图片以上传时间倒序
        self.entries.sort(key=lambda obj: -obj.last_modified)

        return result.is_truncated, result.next_continuation_token

让我们拆解上面的代码:

  • 通过阅读源码可以发现,ObjectIteratorV2 中的文件数据存储在 self.entries 属性中。由于原 ObjectIteratorV2 仅在启动迭代时才会从云端获取并填充数据到 self.entries ,这不符合本项目的使用需求。因此覆写了 __init__(),让其在实例化阶段就立即获取数据。
  • 为了尽量减少对旧代码的改动,我们想让这个 OSS 类也能够使用 Django 的分页器。由于分页器要求对象必须实现计数和取值,因此增加了 __len__()__getitem__() 方法,让计数和取值功能与 self.entries 关联起来。
  • 原本父类中的 _fetch() 方法,用于对文件数据进行预处理的。里面的一大段全是从父类源码抄过来的,唯一改动的只有 self.entries.sort(...) 这一段。因为父类中是以文件名进行排序的,为了更符合相册的直觉,修改为以上传时间的倒序排序。

大功告成了,接下来的步骤就非常轻松愉快了。

2021/09/16更新:上面的代码用继承加协议的方式,实现了一个有限长度的容器。但这种实现方式在本项目中没太有必要(并且经博主测试还有小bug),因为列表推导式 [i for i in oss2.ObjectIteratorV2(bucket)] 就实现了相同的效果,并且代码更简单。

咱们继续往下。

新建视图函数 oss_home() ,将旧的 home() 函数中的代码抄过来,并做如下改动:

# /photo/views.py

...

def oss_home(request):
    photos       = ObjIterator(bucket)
    paginator    = Paginator(photos, 6)
    page_number  = request.GET.get('page')
    paged_photos = paginator.get_page(page_number)
    context      = {'photos': paged_photos}

    # 省略登入登出的POST请求代码
    # ...

    return render(request, 'photo/oss_list.html', context)

其实就改动了两行:

  • 第一行,数据集合不再来源于模型类了,而是刚写的 OSS 类 ObjIterator
  • 最后一行,模板文件变为 oss_list.html 了。(此文件暂时还没写)

你看,经过前面的努力,对 OSS 的操作变得跟 Django 内置的模型一样的简单,辛苦没有白费啊。

最后记得给这个新视图配置 url 路由:

# /photo/urls.py

...

from photo.views import home, upload, oss_home

urlpatterns = [
    ...
    path('oss-home/', oss_home, name='oss_home'),
]

接下来写 oss_list.html 模板。

前端代码

线上环境和开发环境有个很大的不同,就是线上环境具有严重的延迟。因此有些 Bug 只有部署到线上才能够被发觉。

究竟是什么 Bug 卖个关子,先在 base.html 中引入一个新的插件 jquery.js 备用:

<!-- /templates/base.html -->

<!DOCTYPE html>
<html>
  <head>
    ...
  </head>
  <body>
    ...
    <!-- 新增 jquery 库 -->
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
    <!-- 已有代码 -->
    <script src=".../bootstrap.bundle.min.js" ...></script>
    <script src=".../masonry.pkgd.min.js" ...></script>
    ...
  </body>
</html>

然后新建 /templates/photo/oss_list.html 模板。

将老的 list.html 代码抄过来,并修改如下的部分代码:

<!-- /templates/photo/oss_list.html -->

...
<!-- 新增 id 属性 -->
<div class="row" id="cards" data-masonry='{"percentPosition": true }'>
    {% for photo in photos %}
    <!-- 新增 grid-item  -->
    <div class="col-4 py-2 grid-item">
        <div ...>
            <a ...>
                <!-- 修改 src -->
                <img 
                     src="https://dusai-test.oss-cn-beijing.aliyuncs.com/{{ photo.key }}"
                     ...
                 >
            </a>
        </div>
    </div>
    {% endfor %}
</div>

...

{% for photo in photos  %}
<div class="modal fade" id="photo-{{ photo.id }}">
    ...
    <!-- 修改 src -->
    <img 
         src="https://dusai-test.oss-cn-beijing.aliyuncs.com/{{ photo.key }}" 
         ...
         >
    ...
</div>
{% endfor %}

改动如下:

  • 修改了所有图片展示的 <img .. > 的标签的 src (也就是路径),变成了阿里云 OSS 中的文件的路径。记得将 src 修改为你自己的 OSS 路径。
  • 在卡片元素里增加了 id="cards"class="... grid-item" 属性,为解决 Bug 备用。

接下来就正式讲讲这个 Bug 了。由于相册的排版采用了 masonry.js 瀑布流插件,此插件是以页面加载时图片的尺寸为依据进行排版的。但问题是高清图片的尺寸都很大,插件在估算排版时图片都还没加载完成,导致它不能够正确得知图片的尺寸,最终造成图片全堆叠在一起的显示错误。

开发时此问题未出现是因为图片加载无延迟。

解决的方案是利用 jquery.js 脚本,确保在所有图片都加载完毕后,再一次触发 masonry.js 插件的排版估算,像这样:

<!-- /templates/photo/oss_list.html -->

...

{% block scripts %}

...
<!-- 注意相关代码同样添加到 /templates/photo/list.html 中 -->
<script type='text/javascript'>
    $(window).on('load', function() {
        $('#cards').masonry({
        // options
        itemSelector: '.grid-item'
        });
    })
</script>

{% endblock scripts %}

将这段脚本同样添加到旧的 list.html 模板中,因为它也有同样的问题。

完成后刷新页面,访问 /photo/oss-home/ 这个地址:

或许你现在还感受不到 OSS 存储和本地存储的区别。

别急,等到下一章部署到线上后,你测试下就会深有体会了。

总结

对象存储 OSS 可不止本文中写的这么点玩意儿,它有非常多的操作手段。比如说你可以把图片的增删改查等所有操作全都集成到自己的站点中,而不是像现在这样,只实现了对 OSS 文件的列举功能。

另一方面,OSS 的用途和数据库不一样,对它内部文件的查询、列举有自己的一套优化规则。如果你的文件数量很巨大,那么本文中实现的这个 ObjIterator 类可能效率不佳。此外 OSS 还有大量高阶功能,比如自动生成缩略图、url签名、权限管理等。

鉴于教程篇幅有限,对 OSS 的探讨就不深究下去了,读者有兴趣请自行研究OSS文档

下一章将探讨 Web 开发的终极内容:部署。

点赞 or 吐槽?评论区见!




本文作者: 杜赛
发布时间: 2021年08月13日 - 10:17
最后更新: 2021年09月16日 - 22:02
转载请保留原文链接及作者