小星球媒体链:Live Photo、缩略图与鉴权代理

HEIC/MOV 配对、缩略图、内容指纹与鉴权代理;缓存和孤儿对象的边界。

我们的小星球专栏 · 02 / 03 目录 ↗

iPhone 的实况照片是 HEIC + MOV 两个文件。src/lib/client-upload.ts 的 expandLivp() 用 fflate 解 .livp,取第一张图片和第一个 mov/mp4/m4v,按 .livp 的 basename 重命名;EntryForm 的 autoPair() 按去掉扩展名、转大写的 baseName 分组,只有一组里恰好一张照片和一个视频才配对。media.live_video_id 单向指向视频,attachMedia 检查目标必须是 kind=video、同 space_id、未被其他 entry 占用;展示时用 NOT EXISTS 隐藏已配对的视频。

配对靠文件名,不读 Apple 的内容标识;HEIC 和 MOV 分两次上传、或者被重命名成 IMG_1234 (1).MOV,就配不上。.livp 解压是同步的,没有 entry 数量/解压大小上限;HEIC 的 64MB 上限是解压之后才检查。

上传和处理

普通文件 5GB(src/lib/media.ts MAX_UPLOAD_BYTES),HEIC 64MB(app/api/upload/image/route.ts),multipart 100MB 阈值、64MB part、最多 10,000 part,nginx client_max_body_size 2049m。HEIC → JPEG 用 heic-convert(WASM libheif)质量 0.92,再 sharp rotate、限制在 2560×2560、JPEG 85;Safari 会先在浏览器把大于 350,000 字节的图缩到 2000px/JPEG 0.85,所以同一张图在 Safari 和 Chrome 上传后的结果不一样。

缩略图宽度 320/480/800/1280,WebP 质量 75,480 是历史对象的固定 key;视频封面用 ffmpeg 首帧 scale=480:-2、-q:v 4、30,000ms,探测 20,000ms。缩略图是 fire-and-forget,失败只 console.error,没有重试/回填。

内容指纹是 ${file.size}-${sha256(head 512KB + tail 512KB)},1MB 以下整个文件哈希;只哈希头尾意味着同尺寸、头尾相同的文件会碰撞。

存储和代理

手写 SigV4 的 Garage S3(127.0.0.1:3900),key 是 media/<id><ext>,没有预签名 URL、没有 CDN/生命周期。读取走 app/api/media/[id]/route.ts:成员、伴侣、彩蛋解锁、公开空间逐层检查;支持 Range;拒绝 SVG/HTML/XHTML/XML,历史 SVG 降级为 attachment + CSP sandbox + nosniff。所有媒体响应 Cache-Control: private, max-age=31536000, immutable,撤回公开或改私有时浏览器副本不会自动失效;要让撤回生效,媒体 URL 必须带版本或在撤回时换 key。

S3 GET 没有超时/重试,Garage 卡住会占住 Node 请求;孤儿对象没有对账,数据库行先删、对象后删,失败就留下孤儿。

← 全部文章 阅读时长: 2分钟 写邮件