很多人看到“把附件迁到云存储”,第一反应都是:
写个脚本,循环读文件,上传完删本地文件。
但 Odoo 的 cloud_storage_migration 不是这么做的。
它的目标不是“把文件搬过去”这么简单,而是要在不停机、可续跑、尽量少重复上传的前提下,慢慢把历史附件切过去。
这就决定了它必须回答三个问题:
- 先迁谁,后迁谁?
- 跑到一半挂了,下一次从哪里接着跑?
- 业务还在写附件时,怎么避免把刚生成的文件误搬走?
1. 迁移不是“遍历全表”,而是“扫一段、记一个 checkpoint”
核心入口在 addons/cloud_storage_migration/models/ir_attachment.py 的 _cron_migrate_local_to_cloud_storage()。
它先从 ir.config_parameter 里读几个关键参数:
cloud_storage_migration_min_attachment_idcloud_storage_migration_max_attachment_idcloud_storage_migration_max_file_sizecloud_storage_migration_max_batch_file_sizecloud_storage_migration_message_modelscloud_storage_migration_all_models
你可以把它理解成一个“带边界的游标扫描器”。
不是每次从头扫全库,而是只扫:
- id 大于上次 checkpoint 的附件
- id 小于最大上限的附件
- 只看满足模型白名单的附件
- 只看
type = 'binary'、有store_fname的附件 - 文件大小在允许区间内
- 创建时间至少早于 7 天
- 不属于
documents.document
这些条件不是装饰品,而是在告诉你:
这份迁移任务是“安全优先”的,不是“覆盖式清仓”。
2. 为什么它要先写 checkpoint,再真的上传
这里最值得注意的是 commit_min_attachment_id()。
它不是普通的 ORM write(),而是直接 SQL 更新 ir_config_parameter:
- 先把
cloud_storage_migration_min_attachment_id推进到当前附件 id - 再调用
self.env['ir.cron']._commit_progress(1)
然后才去执行真正的上传。
乍看这像“先记账再干活”,其实是故意的。
这么设计的原因是:
- cron 可能超时
- 进程可能被回收
- 上传本身可能卡在网络上
- 本地文件和云端文件都可能有临时失败
如果不先推进 checkpoint,一旦中途挂掉,下次重跑时就会重复读到同一批附件; 如果一直反复重试,又很容易把一个半失败的任务拖成死循环。
所以这套逻辑更像:
我承认这是一次“至少尝试过”的迁移,而不是“必须百分之百成功后才前进”的事务。
这也是为什么它在失败时会记录 warning、rollback 当前事务,但不会回滚 checkpoint。 设计取舍很明确:进度比完美重试更重要。
3. 真正的上传动作,其实是“先换 URL,再流式推文件”
真正把一个附件从本地搬到云端的是 _migrate_local_to_cloud_storage(session)。
这个方法先做了几道硬检查:
- 只能迁
type == 'binary' - 必须有
store_fname - 取到本地文件路径后,先给附件生成云端 URL
- 再调用 provider 提供的
_generate_cloud_storage_upload_info()
然后用 requests.Session() 把本地文件流式上传到云端。
注意这里不是“先把文件读进内存,再一次性 POST”,而是直接把文件对象交给 session.request(...)。
这很适合大文件,也更接近真实生产环境。
上传成功后,它会把记录改成:
type = 'cloud_storage'raw = Falsemimetype保持原值
换句话说,本地二进制内容不再是这条记录的主存储,记录本身开始变成一个“指向云对象的壳”。
而 cloud_storage 模块里的 ir.attachment 还会在读取时把 type == 'cloud_storage' 的附件改走下载 URL。
这意味着迁移不是一次性搬家,而是读写路径一起切换。
4. 为什么它要跳过“太新”的附件
代码里有一个很容易被忽略的条件:
create_date < now - 7 days
这条规则很重要。
因为 Odoo 里很多业务会在“刚创建附件之后”的几分钟甚至几小时内继续处理它:
- 邮件线程还在追加附件
- 文档业务还在补字段
- 上传文件可能还要被下游逻辑立即读取
如果太早把附件迁去云端,业务代码可能还假设它是本地 binary,结果就会产生奇怪的边界问题。
所以这里故意留了一个“冷却期”。 它不是在追求极限同步,而是在追求业务稳定性。
5. 这套设计的真实代价是什么
它的代价也很明显:
- 失败不一定自动重试同一条附件
- 迁移速度不会特别激进
- 需要依赖配置参数和 cron 调度
- 迁移过程必须理解 checkpoint 语义
但换来的好处是:
- 不用停业务
- 不容易重复上传同一批文件
- 可以在 cron 线程里慢慢搬
- 可以用配置把范围缩小到特定模型
对运维来说,这意味着排障时不要只盯“有没有上传成功”,还要同时看:
cloud_storage_provider有没有配置cloud_storage_migration_min_attachment_id走到哪了- 是否卡在某个模型白名单
- 是否因为文件大小或创建时间被过滤掉
- 当前附件是不是仍然属于
documents.document这类被排除对象
结论
cloud_storage_migration 不是一个“上传脚本”,而是一个带 checkpoint、带范围控制、带失败边界的迁移引擎。
它最重要的设计点不是“怎么上传”,而是:
如何在不打断业务的情况下,让附件状态从本地 binary 平稳过渡到云端对象。
真正理解这一点,才算看懂了这段源码的价值。
DISCUSSION
评论区