AI 大文件上传的权限与删除设计
围绕私有存储桶中的 AI 报告,文章提出用数据库记录生成标识、对象键、保留期限和上传状态。大文件采用分片上传时,还需明确未完成上传与遗留分片的清理责任,并保留删除凭据。
AI 深度解读
AI 生成的报告图片越来越大,上传成功却只是交付的一步。原文讨论的是一个面向已登录客户的教育产品:用私有对象存储保存报告,必要时分片上传,同时把访问权限、保留期限和删除结果纳入应用状态。
它最值得借鉴的判断是:上传完成,不等于可以永久保存。 对前端和全栈开发者来说,真正需要设计的不是一个上传接口,而是一条能恢复、能收口、能解释文件为什么还存在的生命周期。
分片上传之前,先给报告建立身份
作者没有主张所有图片都用分片上传。图片较小、体积稳定,能够落在服务正常请求限制内,单次上传仍然最省事。只有大文件、不可靠网络或需要恢复的任务,才值得承担分片协议的复杂度。
原文没有给出文件大小阈值,也没有性能对照数据,不能据此划出一个通用的 MB 分界线。
比传输方式更早要确定的是数据库记录。每份报告都应该有稳定的 generation ID、应用生成的 object key、保留截止时间,以及 pending、uploading、ready、expired、deleted 等状态。object key 不应直接使用客户提供的文件名。
分片上传还要把 upload ID、每个分片的编号及返回的完成凭据存进记录。任务重试时,worker 才能知道应该继续哪次上传、还缺哪些分片,而不是根据文件名猜测。
访问链路也要围绕这条记录展开:应用先验证客户身份和报告归属,再检查保留期限,最后签发短期 signed URL。
signed URL 是临时交付凭证。它不能代替业务授权,也不该作为永久链接保存到客户记录里。私有桶解决的是对象默认不公开,报告是否过期、是否允许访问,仍由应用判断。
上传到第 7 片后进程退出,谁负责收尾
分片上传不是把文件切开再发出去那么简单。它通常包括创建上传、发送带编号的分片、记录响应,以及提交预期分片集合来完成上传。
假设一共 12 片,worker 在第 7 片后退出,此时没有可用的完整报告,却可能留下占用存储的部分数据。
作者把每次分片上传当成有租约的任务:数据库记录负责人、创建时间、最后进展时间和最终处理决定;后台协调器定期检查租约过期的记录。报告仍有效,可以交给替代 worker 继续;任务已取消或超过保留期限,就中止上传。
这里没有推荐一个固定超时。作者明确表示,超时需要根据实际工作负载测量。
删除也有两条路径:
- 已完成的对象需要执行经过授权的删除操作,并更新应用状态。
- 尚未完成的上传需要执行 abort,清理未形成对象的分片。
对象过期规则不能自动覆盖所有未完成上传的清理需求。审计记录还要区分「已请求删除」「已确认对象删除」「已中止部分上传」,否则看见一个 deleted,仍不知道它证明了什么。
发布可读对象和结束数据保留,是两次独立的状态转换。
客户发起删除后,应用应更新状态、阻止后续访问授权、把删除任务入队,并记录结果。不过,原文没有说明已经签发的 signed URL 如何立即失效;这一点不能从「撤销未来访问」推导出来。
Node.js 适配器应该证明哪些步骤完成了
作者建议把生成与存储分开:生成 worker 负责图片字节和元数据,存储适配器负责分配 key、分片状态、重试、完成、中止和签名读取。
这个边界让业务代码只依赖一份小契约:为应用拥有的 key 创建上传,持久化各片完成凭据,只提交记录中的上传和预期分片集合,完成成功后才能标记 ready,取消或过期时执行 abort 或 delete。
虽然小节讨论 Node.js,原文给出的却是 Python 示例,用来明确 HTTP 行为。下面这段解决单个分片的 PUT、完成凭据提取和 429 重试:
import time
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
def upload_part(url: str, part_path: str, attempts: int = 5) -> str:
payload = Path(part_path).read_bytes()
for attempt in range(attempts):
request = Request(
url,
data=payload,
method="PUT",
headers={"Content-Length": str(len(payload))},
)
try:
with urlopen(request) as response:
if not 200 <= response.status < 300:
raise RuntimeError(f"part upload returned HTTP {response.status}")
completion_value = response.headers.get("ETag")
if completion_value is None:
raise RuntimeError("part upload did not return completion data")
return completion_value
except HTTPError as error:
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"part upload returned HTTP {error.code}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
time.sleep(delay)
raise RuntimeError("part upload attempts exhausted")
要看的是 completion_value = response.headers.get("ETag"):收到成功响应后,还要取得后续完成操作需要的凭据。预签名分片 URL 已携带本次传输授权,请求里不应再附加应用凭证。
代码默认最多尝试 5 次,仅对 HTTP 429 重试;纯数字 Retry-After 优先,否则按 2**attempt 等待。它接受 200 <= status < 300,但不能因此认为报告已经发布。HTTP 204 的成功日志、单片成功响应,都不能证明最终 completion 已完成。
这也不是完整的生产重试实现:示例没有处理日期格式的 Retry-After、网络异常或其他可重试错误;read_bytes() 会把整个分片读入内存。Node.js 迁移时应保留契约,再按实际分片大小和失败类型补齐实现。
控制操作在存储 API 支持时,应携带稳定的幂等键。可观测性则要覆盖创建、每片完成、最终完成、abort、签名读取和删除,串起 correlation ID 与 generation ID。普通日志避免记录客户内容和 signed URL,并对长期停在 uploading、重复完成、删除请求与确认结果差距扩大等情况告警。
我的判断
我认同作者把问题从「能不能上传」拉回「谁负责文件的一生」。数据库状态、后台协调器和审计事件,比最大对象尺寸更能决定系统是否可维护。
但这是一套工程经验建议。原文没有提供吞吐量、恢复成功率、清理成本或事故前后对比,也没有展示完整的 Node.js 实现,不能把它当成已经验证收益的性能方案。
作者偏向由后端负责传输,前提是报告本来就由后台生成,且任务已有身份与持久化记录。此时继续由后端管理上传比较连贯。浏览器直传可以减少后端带宽,但会把更多授权与生命周期协调放到浏览器环境,不能仅凭省带宽就决定。
私有桶加状态机也有边界。法律保留、过期后能否恢复、删除是否覆盖缩略图,以及不可变保留、跨地域自动复制、服务端元数据搜索等需求,都需要额外设计。原文提出了这些问题,没有给出完整方案。
还有一个实现缺口:列出状态名,并不等于定义好了状态机。删除与读取并发、重复 completion、外部删除成功但数据库更新失败时如何收敛,都需要团队明确规则。
能拿来做什么
- 给现有报告任务补齐 generation ID、对象 key、保留截止时间和上传状态;前端依据
ready展示可读结果,不要把生成成功或拿到 key 当成可交付。 - 在 Node.js 中封装存储适配器,集中处理 upload ID、分片凭据、完成、abort 和 delete;迁移期间保留旧读取路径,等新记录进入
ready再切换。 - 先选一种报告和较短、明确的保留窗口,验证完成前取消、分片间更换 worker、重复完成、签名过期、删除与读取竞争,以及遗留上传被协调器发现。单元测试使用假适配器,再对所选存储执行小规模真实持久化写入测试。
- 把删除做成幂等操作,按配置的合规期限保留审计事件,同时统计未完成上传的数量与年龄。删除请求有记录却迟迟没有确认结果时,要能定位原因。