- 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。
110 lines
2.8 KiB
Go
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: "正文配图 ",
|
|
Status: model.StatusPublished,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.Create(model.PostInput{
|
|
Kind: model.KindShort, Slug: "local",
|
|
ContentMd: "本地形态  和缩略图 ",
|
|
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)
|
|
}
|
|
}
|