Files
ONE/backend/internal/store/file_refs_test.go
T
Sakurasan 1b3eb870da 文件删除前查引用:被文章/项目/站主头像用着就先挡住
- store.FileReferences(key) 反查谁在用这个文件:文章(封面 + content_md +
  content_html)、项目封面、settings.owner_avatar_key。匹配的键是 files.key 本身
  而不是完整 URL——本地 /uploads/{key}、R2 {publicBase}/{key}、缩略图
  /uploads/thumb/{key} 三种形态都以 key 结尾,换存储端后正文里的老链接照样查得到。
  草稿也算:现在没发布,删了将来发出来就是裂的。
- 按需 LIKE 现扫,不维护计数表:写入口有编辑器、外链转存、短文、项目、头像好几处,
  计数一旦漂移就再也信不过;删文件是低频操作,扫全表几十毫秒换一个永远正确的答案。
- 新端点 GET /api/admin/files/{id}/refs;DELETE 默认对在用文件返回 409(消息带引用数
  和前三个位置名),明确带 force=1 才真删。守卫放在动 blob 之前——存储端一删就没法回头。
- 后台删除弹层先把引用清单摊出来(文章《标题》(草稿)、站主头像),仍然要删才带 force。
  引用查询失败就按无引用走:真被引用时后端会 409,不会静默删掉在用文件。

顺手把另一个 agent 工具的本地草稿目录(.zcode/、.zcodeignore)加进 .gitignore。
2026-09-30 02:03:14 +08:00

110 lines
2.8 KiB
Go

package store
import (
"testing"
"oneblog/internal/model"
)
func countKind(refs []model.FileRef, kind string) int {
n := 0
for _, r := range refs {
if r.Kind == kind {
n++
}
}
return n
}
// 同一个 key 在正文里的三种形态都要认出来:R2 直链、本地 /uploads、缩略图路径。
func TestFileReferencesMatchesKeyNotURL(t *testing.T) {
s := openTestStore(t)
const key = "2026/09/deadbeefcafe.png"
if _, err := s.Create(model.PostInput{
Kind: model.KindLong, Title: "直链引用", Slug: "r2",
CoverURL: "https://cdn.example.com/" + key,
ContentMd: "正文配图 ![图](https://cdn.example.com/" + key + ")",
Status: model.StatusPublished,
}); err != nil {
t.Fatal(err)
}
if _, err := s.Create(model.PostInput{
Kind: model.KindShort, Slug: "local",
ContentMd: "本地形态 ![](/uploads/" + key + ") 和缩略图 ![](/uploads/thumb/" + key + "?w=480)",
Status: model.StatusDraft,
}); err != nil {
t.Fatal(err)
}
refs, err := s.FileReferences(key)
if err != nil {
t.Fatal(err)
}
// 两篇文章各算一处:同一行里多次命中不重复计
if len(refs) != 2 {
t.Fatalf("want 2 refs, got %+v", refs)
}
if countKind(refs, "post") != 2 {
t.Fatalf("两条都该是 post: %+v", refs)
}
// 草稿也要报出来(发布后就会图裂)
if refs[1].Status != model.StatusDraft {
t.Fatalf("草稿状态没带出来: %+v", refs[1])
}
}
func TestFileReferencesProjectsAndAvatar(t *testing.T) {
s := openTestStore(t)
const key = "2026/09/111122223333.webp"
if _, err := s.CreateProject(model.ProjectInput{
Title: "ONE", Slug: "one", CoverURL: "/uploads/" + key, Status: model.StatusPublished,
}); err != nil {
t.Fatal(err)
}
if err := s.SetSetting("owner_avatar_key", key); err != nil {
t.Fatal(err)
}
refs, err := s.FileReferences(key)
if err != nil {
t.Fatal(err)
}
if countKind(refs, "project") != 1 {
t.Fatalf("项目封面没查到: %+v", refs)
}
if countKind(refs, "avatar") != 1 {
t.Fatalf("站主头像没查到: %+v", refs)
}
}
// 没被引用、以及 key 为空时都不能误报——空 pattern 会 LIKE '%%' 命中全库。
func TestFileReferencesNoFalsePositive(t *testing.T) {
s := openTestStore(t)
if _, err := s.Create(model.PostInput{
Kind: model.KindLong, Title: "无关", Slug: "x",
ContentMd: "正文里没有这张图", CoverURL: "/uploads/2026/09/aaaa.png",
Status: model.StatusPublished,
}); err != nil {
t.Fatal(err)
}
// 同内容不同月上传出的 key 只是月份段不同,不该互相命中
refs, err := s.FileReferences("2026/10/aaaa.png")
if err != nil {
t.Fatal(err)
}
if len(refs) != 0 {
t.Fatalf("换月的 key 误报: %+v", refs)
}
empty, err := s.FileReferences("")
if err != nil {
t.Fatal(err)
}
if len(empty) != 0 {
t.Fatalf("空 key 不该匹配任何东西,得到 %+v", empty)
}
}