文件删除前查引用:被文章/项目/站主头像用着就先挡住
- 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。
This commit is contained in:
1 parent
f1e639e0ba
commit
1b3eb870da
8 files changed
+427
-4
No files matched your search
@@ -89,10 +89,32 @@ async function copyUrl(f) {
|
||||
}
|
||||
}
|
||||
|
||||
// 引用位置的展示文案:文章/项目带标题,头像没有标题可指。
|
||||
// 草稿也列出来——现在没发布,删了图将来发出来就是裂的。
|
||||
function refLabel(x) {
|
||||
if (x.kind === 'avatar') return '站主头像'
|
||||
const head = x.kind === 'project' ? '项目' : '文章'
|
||||
const name = x.title || x.slug || '未命名'
|
||||
return `${head}《${name}》${x.status === 'draft' ? '(草稿)' : ''}`
|
||||
}
|
||||
|
||||
async function remove(f) {
|
||||
if (!window.confirm(`删除「${f.name}」?存储里的文件会一并删除,引用它的文章将失效。`)) return
|
||||
// 删除前查一次引用:后端默认会挡住被引用的文件(409),这里先把清单摊出来,
|
||||
// 用户看完仍然要删才带 force=1 过去。
|
||||
let refs = []
|
||||
try {
|
||||
await adminApi.deleteFile(f.id)
|
||||
refs = (await adminApi.fileRefs(f.id)).items || []
|
||||
} catch (_) {
|
||||
// 查询失败就按无引用走:真被引用时后端会返回 409,不会静默删掉在用文件
|
||||
}
|
||||
let tip = `删除「${f.name}」?存储里的文件会一并删除。`
|
||||
if (refs.length) {
|
||||
const list = refs.slice(0, 3).map(refLabel).join('、')
|
||||
tip = `删除「${f.name}」?\n它正被 ${refs.length} 处引用:${list}${refs.length > 3 ? ' 等' : ''}。\n删掉这些地方会图裂。`
|
||||
}
|
||||
if (!window.confirm(tip)) return
|
||||
try {
|
||||
await adminApi.deleteFile(f.id, refs.length > 0)
|
||||
toastOk(`已删除「${f.name}」`)
|
||||
// 当前页删空时退一页,其余直接重载
|
||||
if (items.value.length === 1 && page.value > 1) router.replace({ query: { ...route.query, page: page.value - 1 } })
|
||||
|
||||
+4
-1
@@ -104,7 +104,10 @@ export const adminApi = {
|
||||
request('/api/admin/readers/' + id + '/ban', { method: 'POST', body: { banned } }),
|
||||
// ---------- 文件上传 ----------
|
||||
files: (params = {}) => request('/api/admin/files?' + new URLSearchParams(params)),
|
||||
deleteFile: (id) => request('/api/admin/files/' + id, { method: 'DELETE' }),
|
||||
// 删除前查引用(被哪些文章/项目/站主头像用着);force 用于明知有引用仍要删
|
||||
fileRefs: (id) => request('/api/admin/files/' + id + '/refs'),
|
||||
deleteFile: (id, force = false) =>
|
||||
request('/api/admin/files/' + id + (force ? '?force=1' : ''), { method: 'DELETE' }),
|
||||
// 上传走 FormData:request() 是 JSON helper,这里单独 fetch。
|
||||
// 401 同样广播 one:unauthorized,错误消息从 JSON body 里取(与 request 一致)。
|
||||
// 单文件上传(XHR):fetch 拿不到上传进度,编辑器的进度提示走这里
|
||||
|
||||
Reference in new issue
Block a user