我曾经以为,把客户端的缓存目录复制到另一个地方,就算给素材做了长期备份。音乐、音效、花字、转场和视频素材都已经在电脑上出现过,复制它们似乎只是一个搬家动作。
这次真正开始盘点后,我才发现“出现过”和“已经入库”是两回事。客户端的缓存可能被清理,数据库里的记录可能只保存了名称和链接,远端链接还可能因为签名过期而失效。如果只看目录大小,或者只看台账里的 completed 数量,很容易得到一个比实际情况乐观得多的答案。
我最后把问题重新定义成:怎样让素材在客户端清缓存之后,仍然能被找到、被核对、被再次使用?答案不是再复制一个目录,而是建立一套有唯一权威库、有实体文件回执、也能承认失败边界的入库流程。
先确认要保护的到底是什么
当时最直接的担心是,客户端一旦清缓存,已经下载的音乐、音效、花字和视频素材会不会一起消失。用户希望这些内容真正落盘,之后即使清理缓存,也不影响复用。因此第一步不是马上复制,而是先确认现有素材库是否已经存在,以及它记录了什么。
盘点发现,统一素材库本来就有 SQLite 台账,也有 MD5 和 SHA256 的回执。这个发现很重要,因为它改变了后续决策:我不需要再造一个新的目录体系,也不能把临时缓存当成第二个“差不多能用”的库。长期复用首先要解决的不是存储空间,而是到底哪一个地方说了算。
权威库至少要能回答3个问题:这条素材的身份是什么,实体文件在哪里,文件内容是否和登记时一致。只有名称,没有实体文件,不能算完成;只有文件,没有哈希和来源,也很难判断是不是重复下载或被意外替换。
如果不先确定唯一库,后面每一次复制都会产生新的版本。今天从缓存复制一份,明天又从客户端下载一份,最后看起来素材更多了,实际上只是多个目录互相不承认。等到客户端清缓存,才会发现自己保留的是索引,不是可用文件。
第一阶段先抢救能下载的内容
确定权威库之后,我没有一上来就处理所有失败项,而是先运行已有的全量下载入口,抢救当前链接仍然有效的内容。这个顺序看似保守,实际是在保护已经可以拿到的成果:有效链接有可能随时变化,先把能落盘的文件保存下来,失败项才有空间慢慢分析。
下载入口支持断点续传,所以可以先完成一轮,再检查第二轮有没有新增。这样做的价值不只是省时间,也是为了让结果可复核。如果第1轮下载后,重复执行第2轮没有新增,说明这条通道已经收敛;如果仍有新增,就说明第一次不能当成完整结果。
这一步之后,大多数音乐、音效、特效、滤镜、转场、贴纸、字幕和变声类素材已经可以落盘。可是仍然有一批视频素材失败。抽样访问时,过期的签名链接直接返回403。它们在台账里可能仍然有标题、分类和资源信息,但对应的实体文件并没有进入库。
这里最容易误判:数据库里的 completed 数量增长了,就以为所有相关内容都已完成。完成状态必须同时经过实体文件、文件哈希和回执3层核对,单独看数据库字段不够。
缓存复制是补充,不是替代下载
全量下载之后,我再处理本地缓存。缓存里确实还有一批客户端已经拿到的文件,它们可能不再能通过旧链接重新下载。于是把缓存复制进权威库,是一次有价值的抢救,但它必须作为独立阶段记录,不能和远端下载混成一个数字。
复制时按文件哈希去重,避免同一份内容因为名字不同、目录不同,被当成多份素材。复制完成后,要在库内留下对应回执,至少知道文件从哪里来、落到了哪里、用什么哈希识别。这样客户端日后清理缓存,清掉的只是临时副本,已经复制并核验过的实体文件仍然在权威库中。
我选择把缓存复制放在全量下载之后,是因为先下载可以减少重复工作,也能让缓存承担“旧链接无法补取时的补救来源”。但我不会因此把缓存目录当作最终库。缓存的目录结构由客户端决定,可能随版本变化;权威库的结构则应该由长期复用的需要决定。
最容易走的错误捷径,就是只把缓存目录整体搬走,然后宣布“素材都保存好了”。这个判断忽略了缓存里可能有临时文件、重复文件和缺少回执的文件,也无法说明哪些资源只是元数据,哪些资源真的能被重新打开。
过期链接要单独成为一种状态
遇到403之后,我没有把它简单写成“下载失败”,也没有把它改写成“稍后会自动完成”。过期签名说明资源本身未必不存在,只是当前拿到的访问凭证已经失效。这个失败和网络中断、磁盘空间不足、文件损坏,处理方式并不一样。
因此我把状态拆开:已经有实体文件并通过哈希核验的,才算真正完成;只有元数据、没有实体文件的,标成需补取;链接过期、需要客户端刷新签名的,标成待刷新;确认不可达或接口已经变化的,保留失败原因。诚实记录失败边界,比用一个漂亮的完成数字掩盖缺口更能保护素材库。
后来通过客户端面板刷新了一部分可达分类,确实有新的签名链接写入缓存,随后又成功转成了实体文件。但这也暴露了另一层边界:客户端当前界面能呈现的分类,只覆盖历史分类的一部分;有些旧分类已经不再出现在面板里。继续点击和滚动并不会让这些旧链接自动复活,连续零转化就是应该停下来的信号。
这让我确认,客户端刷新不是一个可以无限重试的万能按钮。它适合处理“仍能被当前界面触达”的资源,不适合拿来保证所有历史元数据都能恢复。需要客户端刷新,就明确写成待刷新;刷新后确实落盘,再把状态推进到完成。
用回执把过程变成可复用方法
这次之后,我会把素材入库固定成4个阶段。第1阶段确认唯一权威库和现有台账;第2阶段运行支持断点续传的下载入口,反复到结果收敛;第3阶段复制本地缓存,按哈希去重并生成独立回执;第4阶段才处理需要客户端刷新元数据的补下载。
每个阶段都要检查不同的东西。下载阶段看链接结果、实体文件和哈希;缓存阶段看复制数量、重复数量和来源;元数据阶段看刷新前后是否真的出现新文件;最终汇总时,把完成、需补取、待刷新和其他失败分开。阶段之间不能只传一个“总数”,否则下一步无法判断这个数字是文件数量、记录数量,还是尝试次数。
如果要把这套流程交给别人,我会要求每个阶段都留下最小回执:开始时的基线、结束时的新增、跳过或失败的原因、实体文件校验结果,以及下一步能从哪里继续。这样即使中途停止,也不会只能重新从头猜测。
这种方法的核心不是把所有失败都消灭,而是让失败可定位、可重试、不可冒充成功。素材库的价值也不在于台账看起来有多大,而在于下一次创作时,拿到的文件确实能打开,身份确实能核对,缺失的部分也确实知道为什么缺失。
下次我会怎么做
下次处理类似的客户端素材,我会先做一次“记录和实体分离”的盘点:台账里有多少条,实际文件有多少个,哈希回执覆盖多少个。这个数字对不上时,先查缺口,不直接开始扩大下载范围。
然后我会先抢救当前可用链接,再复制缓存,最后才驱动客户端刷新。每完成一个阶段,就立刻保存回执,并把实体文件和元数据状态分开统计。这样做可能不会让最终数字看起来特别漂亮,却能让我知道哪些内容今天就能复用,哪些内容还需要客户端,哪些内容已经触到通道边界。
我现在更愿意接受一个不完整但可信的素材库。它明确告诉我:已登记不等于已落盘,已尝试不等于已下载,过期链接也不等于永远不存在。真正的长期复用,靠的不是把所有东西都说成完成,而是给每一份成果留下能被验证的身份,并给每一个失败留下不会被误读的边界。