框架深潜

Odoo Google Cloud Storage 附件为什么关键不在“上云”,而在签名 URL、Bucket 权限与 CORS 边界

把 Odoo 附件迁到 Google Cloud Storage,真正难的不是 URL 变成了 storage.googleapis.com,而是上传下载为什么都走短时签名 URL、为何配置时要现场验证读写权限、以及为什么连 PDF 预览都会牵出 CORS。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

Odoo 的 cloud_storage_google 模块并不是简单把附件文件从数据库或本地磁盘“换个存储位置”。

/home/ubuntu/odoo-temp/addons/cloud_storage_google/models/ir_attachment.pyres_config_settings.py 看,它真正做的是:

  1. 把附件对象仍然留在 Odoo,二进制内容改为云端对象引用;
  2. 平时保存的是稳定对象 URL,但读写时发给前端的是短时签名 URL;
  3. 启用配置时就现场验证上传、下载、以及 Bucket CORS 管理权限;
  4. 停用前必须先迁移已有附件,不能直接拔掉提供方。

所以这套设计的重点不是“上云”,而是把 Odoo 附件读写改造成受控的临时授权访问

不是所有附件逻辑都被替换,Odoo 仍然掌握对象元数据

很多人第一次看云附件,会以为:

  • 文件既然已经在 GCS,那 Odoo 只剩一个外链。

其实没这么粗糙。

ir.attachment 仍然是附件的业务主对象:

  • 它知道附件属于哪个模型、哪条记录;
  • 知道附件类型是不是 cloud_storage
  • 知道应该如何生成 blob 名称;
  • 知道什么时候给前端下载地址,什么时候给上传地址。

也就是说,云端只托管字节流,不接管 Odoo 的业务语义

这是很合理的边界:

  • 存储容量、带宽、跨区域访问交给 GCS;
  • 业务归属、权限流程、界面交互仍由 Odoo 控制。

为什么平时保存的是对象 URL,真正访问时却换成签名 URL?

源码里 _generate_cloud_storage_url() 会返回一个稳定格式:

https://storage.googleapis.com/<bucket>/<blob>

这说明附件本身保存的是一个可识别的对象位置

但到了真正下载时,_generate_cloud_storage_download_info() 又不会直接把这个稳定 URL 原样交给前端,而是重新生成:

  • method = GET
  • 带过期时间的签名 URL

上传时 _generate_cloud_storage_upload_info() 也是同理:

  • method = PUT
  • 返回短时签名 URL
  • 告诉前端成功状态预期是 200

这套设计的好处是什么?

因为稳定对象 URL 本身不是授权凭证

如果 Odoo 直接把公开可访问链接塞给浏览器,你会立刻碰到几个问题:

  • Bucket 很可能必须公开读,这通常不符合企业要求;
  • 上传权限无法细粒度下发;
  • 一旦链接长期有效,转发出去后几乎无法回收;
  • Odoo 自己也很难表达“这次下载仅允许在短时间内完成”。

而短时签名 URL 刚好解决这个问题:

  • 对象位置长期稳定;
  • 访问授权临时发放;
  • GET 与 PUT 权限分开控制;
  • 不必把 Bucket 公开到互联网。

这不是“云存储技巧”,而是一个非常经典的应用层授权代理设计。

凭证为什么要缓存?因为服务账号对象创建本身就不便宜

get_cloud_storage_google_credential() 里有一个很容易被忽略的细节:

  • 它按数据库名缓存 (account_info, credential)
  • 因为 from_service_account_info 比较慢。

这说明模块作者不是把 GCS 当成偶发后台能力,而是当成高频附件读写基础设施

如果每次生成签名 URL 都重新解析 service account JSON,再创建凭证对象,附件列表、预览、上传这些高频场景会很快把延迟堆起来。

所以源码这里是在做一个现实取舍:

  • 凭证的权威配置仍然来自 ir.config_parameter
  • 但运行期对象会缓存,提升吞吐。

这也提醒我们:配置变更不是零成本动作。尤其更换 service account 或 bucket 时,要考虑缓存更新与历史对象访问问题。

启用提供方时,Odoo 为什么要“当场试写、试读、试改 CORS”?

_setup_cloud_storage_provider() 是这个模块最值得学习的地方。

它没有停留在“参数保存成功”这个层面,而是直接做三类验证:

  1. 生成 PUT 签名 URL,实际上传一个空 blob,检查是否 200;
  2. 生成 GET 签名 URL,实际下载刚刚那个 blob,检查是否 200;
  3. 拿 full_control scope 调 Google Storage API,给 bucket 设置 CORS。

这一步非常重要

因为云存储最常见的坑,不是“字段填错”,而是:

  • 服务账号有读权没写权;
  • 有写权没读权;
  • 对对象可以操作,但无权 patch bucket 元数据;
  • 控制台看起来都配好了,浏览器预览却还是跨域失败。

Odoo 这里的思路很务实:

不要把错误留到用户上传第一份正式合同的时候才暴露。

而是应该在管理员点“启用”时就把权限链跑一遍。

这就是成熟基础设施模块和“能跑就行”脚本的差别。

为什么 CORS 在这里不是附属问题,而是核心配置的一部分?

源码注释直接写了:

  • 需要配置 CORS 才能支持 .pdf 预览和直接上传。

很多人理解附件上云时,只盯着后端权限,却忽略了浏览器是最终消费者之一。

如果没有合适的 CORS:

  • 前端直传 PUT 会被浏览器拦掉;
  • 浏览器预览 PDF 时,哪怕签名 URL 本身有效,也可能被跨域策略阻断;
  • 用户看到的表象通常只是“附件打不开”或“上传失败”。

所以 Odoo 在 bucket 上补的是:

  • origin: ['*']
  • method: ['GET', 'PUT']
  • responseHeader: ['Content-Type']
  • maxAgeSeconds 与下载 URL 过期时长相呼应

这其实是在告诉你:

云附件不是纯后端能力,它天然横跨后端签名、对象权限、以及浏览器跨域模型。

为什么对象 URL 既支持 storage.googleapis.com,又能解析 storage.googleapis.com 模式?

_get_cloud_storage_google_info() 会用正则校验 URL 是否符合 Google Storage 地址模式,并从里边拆出:

  • bucket_name
  • blob_name

这意味着 Odoo 不是把 URL 当一段不可理解的字符串,而是把它视为可逆解析的资源定位符

这样做有两个好处:

  1. 下载/上传签名时可以从已有 URL 反推对象信息;
  2. 停用、迁移、诊断时系统能知道这是不是 Google 附件。

这也是为什么 _check_cloud_storage_uninstallable() 会查找:

  • type = 'cloud_storage'
  • url like 'https://storage.googleapis.com/%'

只要库里还有这类附件,就不允许直接停掉 Google provider。

为什么“停用前先迁移”是硬性限制?

这个校验我很赞同。

因为如果系统里还残留大量 Google Cloud Storage 附件,而你直接把 provider 切回本地或别的存储:

  • 老附件 URL 仍然指向 GCS;
  • 新附件生成规则却变了;
  • 下载、预览、权限行为可能前后不一致;
  • 最坏情况是用户以为附件还在,实际上新旧链路已经断裂。

所以源码明确抛错:

Some Google attachments are in use, please migrate cloud storages before disable the provider

这背后的思想是:存储提供方切换不是参数切换,而是数据迁移事件。

配置 bucket 时最容易误判的三类问题

1. 以为“能列 bucket”就等于“能给 Odoo 用”

不对。

Odoo 真正需要的是:

  • 生成签名 URL 的服务账号能力;
  • 对目标对象的 PUT/GET 能力;
  • 对 bucket CORS 的 patch 能力。

会列目录,不代表能完成整条附件链路。

2. 以为长期对象 URL 就足够安全

也不对。

源码实际上把长期 URL 当成对象定位,不当成访问授权。真正授权发生在每次生成签名 URL 的那一刻。

3. 以为前端失败一定是后端 bug

很多时候不是。

如果签名 URL 正确,但 bucket CORS 没配好,浏览器仍然可能把请求挡掉。对业务用户来说,这看起来像 Odoo 附件坏了;对开发者来说,问题可能根本不在 Python 代码,而在对象存储的跨域策略。

这套设计适合什么场景?

我觉得特别适合这几类组织:

  • 附件量大,数据库不想继续膨胀;
  • 有大量 PDF、图片、合同、导出文件,需要对象存储承压;
  • 希望浏览器能直传/直读,减轻 Odoo 应用层带宽;
  • 但又不想把 bucket 做成公开资源库。

换句话说,这不是“便宜存文件”的方案,而是让 Odoo 附件访问模式更接近现代对象存储架构

最后一句

看懂 cloud_storage_google 之后,你会发现它最重要的不是“支持 GCS”,而是它用一种很克制的方式处理了三层边界:

  • Odoo 管业务元数据;
  • GCS 管对象字节;
  • 签名 URL + CORS 管临时访问。

把这三层分清楚,附件上云就会非常稳;分不清,项目就会在“明明已经存上去了,为什么还是打不开”这种问题里反复打转。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。