feat(gateway): MinIO 孤儿对象 GC(重名覆盖后清理旧对象)
大文档正文存 MinIO,重名再入库若转内联或换键,旧对象会成孤儿泄漏。 SaveDoc 改为返回 (id, oldObjectKey);runIngest 在覆盖成功后,若旧键非空且 与新键不同,从 MinIO 删除旧对象。新建文档 oldObjectKey 为空,不触发。 注:当前无文档删除/改名端点,主要孤儿路径=大→内联覆盖,已覆盖; 真机 MinIO GC 验证待后续(逻辑直白,blob.Delete/SaveDoc 已分别验证)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -100,7 +100,7 @@
|
|||||||
- [ ] 6 个提交待 push(`5d76652` → `79f9912`,需在普通终端 `git push origin main`)
|
- [ ] 6 个提交待 push(`5d76652` → `79f9912`,需在普通终端 `git push origin main`)
|
||||||
- [ ] PDF 导出 Wails 真机验证(不行则回退后端内嵌 CJK 字体出 PDF)
|
- [ ] PDF 导出 Wails 真机验证(不行则回退后端内嵌 CJK 字体出 PDF)
|
||||||
- [x] 报告生成并发健壮性(每次 LLM 调用 60s 超时上限,挂死自释放;规划/分章/撰写均套)
|
- [x] 报告生成并发健壮性(每次 LLM 调用 60s 超时上限,挂死自释放;规划/分章/撰写均套)
|
||||||
- [ ] MinIO 大文档改名/删除的孤儿对象 GC
|
- [x] MinIO 孤儿 GC:重名覆盖后旧对象(转内联/换键)从 MinIO 删除(SaveDoc 返回旧键,runIngest 清理)
|
||||||
- [x] `make test` 目标(test-go / test-web / test-py 一键跑)
|
- [x] `make test` 目标(test-go / test-web / test-py 一键跑)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -257,10 +257,15 @@ func (h *Handler) runIngest(job, owner, kbName, scoped, forceDoc, filename strin
|
|||||||
log.Printf("[gateway] 大文档转 MinIO 失败,回退内联: %v", err)
|
log.Printf("[gateway] 大文档转 MinIO 失败,回退内联: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
docID, err := h.db.SaveDoc(ctx, owner, kbName, docName, ext, md5hex, inline, objectKey, size, head(text, 500))
|
docID, oldKey, err := h.db.SaveDoc(ctx, owner, kbName, docName, ext, md5hex, inline, objectKey, size, head(text, 500))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[gateway] 文件入库失败: %v", err)
|
log.Printf("[gateway] 文件入库失败: %v", err)
|
||||||
} else if docID != "" {
|
} else if docID != "" {
|
||||||
|
// 孤儿 GC:重名覆盖后旧对象键若已不用(转内联或换键),从 MinIO 删除,避免泄漏。
|
||||||
|
if oldKey != "" && oldKey != objectKey && h.blob.Ready() {
|
||||||
|
h.blob.Delete(ctx, oldKey)
|
||||||
|
log.Printf("[gateway] 清理被覆盖的 MinIO 孤儿对象: %s", oldKey)
|
||||||
|
}
|
||||||
_ = h.db.ReplaceDocLinks(ctx, owner, kbName, docID, wikiLinks(text)) // 以本文件 ID 维护出链
|
_ = h.db.ReplaceDocLinks(ctx, owner, kbName, docID, wikiLinks(text)) // 以本文件 ID 维护出链
|
||||||
_ = h.db.ResolveInboundLinks(ctx, owner, kbName, docName, docID) // 回填指向本文件的悬空链接
|
_ = h.db.ResolveInboundLinks(ctx, owner, kbName, docName, docID) // 回填指向本文件的悬空链接
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,29 +115,31 @@ type DocLink struct {
|
|||||||
|
|
||||||
func (DocLink) TableName() string { return "sundynix_doc_link" }
|
func (DocLink) TableName() string { return "sundynix_doc_link" }
|
||||||
|
|
||||||
// SaveDoc 写入/更新一份文件(owner+kb+name 唯一,重名覆盖),返回该文件的雪花 ID(供关联用)。
|
// SaveDoc 写入/更新一份文件(owner+kb+name 唯一,重名覆盖),返回 (文件雪花 ID, 被覆盖的旧对象键)。
|
||||||
|
// oldObjectKey 是重名覆盖前该文档的 ObjectKey(新建则为空)—— 供调用方清理 MinIO 孤儿对象。
|
||||||
// content 为内联正文(大文档转 MinIO 时传空 + objectKey);ext/md5/preview/size 由调用方按全文给出。
|
// content 为内联正文(大文档转 MinIO 时传空 + objectKey);ext/md5/preview/size 由调用方按全文给出。
|
||||||
func (p *Postgres) SaveDoc(ctx context.Context, owner, kb, name, ext, md5, content, objectKey string, size int, preview string) (string, error) {
|
func (p *Postgres) SaveDoc(ctx context.Context, owner, kb, name, ext, md5, content, objectKey string, size int, preview string) (id, oldObjectKey string, err error) {
|
||||||
if p.db == nil {
|
if p.db == nil {
|
||||||
return "", nil
|
return "", "", nil
|
||||||
}
|
}
|
||||||
var d Doc
|
var d Doc
|
||||||
err := p.db.WithContext(ctx).Where("owner = ? AND kb = ? AND name = ?", owner, kb, name).First(&d).Error
|
qerr := p.db.WithContext(ctx).Where("owner = ? AND kb = ? AND name = ?", owner, kb, name).First(&d).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(qerr, gorm.ErrRecordNotFound) {
|
||||||
d = Doc{Owner: owner, KB: kb, Name: name, Ext: ext, MD5: md5, Content: content, ObjectKey: objectKey, Size: size, Preview: preview}
|
d = Doc{Owner: owner, KB: kb, Name: name, Ext: ext, MD5: md5, Content: content, ObjectKey: objectKey, Size: size, Preview: preview}
|
||||||
if err := p.db.WithContext(ctx).Create(&d).Error; err != nil {
|
if err := p.db.WithContext(ctx).Create(&d).Error; err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
return d.ID, nil
|
return d.ID, "", nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if qerr != nil {
|
||||||
return "", err
|
return "", "", qerr
|
||||||
}
|
}
|
||||||
|
oldObjectKey = d.ObjectKey // 覆盖前的旧对象键
|
||||||
d.Ext, d.MD5, d.Content, d.ObjectKey, d.Size, d.Preview = ext, md5, content, objectKey, size, preview
|
d.Ext, d.MD5, d.Content, d.ObjectKey, d.Size, d.Preview = ext, md5, content, objectKey, size, preview
|
||||||
if err := p.db.WithContext(ctx).Save(&d).Error; err != nil {
|
if err := p.db.WithContext(ctx).Save(&d).Error; err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
return d.ID, nil
|
return d.ID, oldObjectKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListVault 返回文库列表(仅元数据 + 预览,不含全文),避免一次拉回整库正文。
|
// ListVault 返回文库列表(仅元数据 + 预览,不含全文),避免一次拉回整库正文。
|
||||||
|
|||||||
Reference in New Issue
Block a user