前端拿预签名 URL 直传对象存储时,容易漏掉一点:对象已经存在,不等于业务文件已经可用。客户端可能上传一半断线,也可能上传成功后没调用确认接口;下载接口如果只看 object_key,半成品和孤儿对象就会混进来。
可以先建一个上传会话,临时 key 每次随机生成且不复用:
create table upload_sessions (
id uuid primary key,
temp_key text not null unique,
final_key text not null unique,
expected_size bigint not null,
expected_checksum text not null,
object_version text,
state text not null check (state in ('uploading', 'finalizing', 'ready', 'expired')),
file_id uuid unique,
expires_at timestamptz not null
);
finalize 先用条件更新把 uploading 改成 finalizing,再读取对象存储返回的大小、原生 checksum 和 version id。校验通过后,按 version id 把临时对象提升到不可覆盖的最终 key;最后用短事务创建文件记录并把会话改成 ready。最终 key 可以由会话 id 确定,这样进程若在对象提升后、数据库提交前退出,重试 finalize 仍会落到同一个对象,不会多生成一份。下载接口只认 ready 文件。
这里最好真的绑定 version id,不能只在检查后记一个 ETag。预签名 URL 在过期前仍可能再次写入同一临时 key,检查与提升之间会有竞态;没有对象版本能力时,就需要用带源条件的服务端复制,并让最终 key 禁止覆盖。后台清理只回收过期会话和临时对象,finalizing 要留一段恢复窗口。
验收可以并发调用两次 finalize,并分别在校验后、对象提升后、数据库提交前中断进程。恢复后应只得到一个 file_id 和一个最终对象;大小或 checksum 不符的上传始终不能进入 ready,未确认对象也不能被下载。