package documents // 本文件归属 `documents` 元数据表(迁移 0014)的**唯一写入实现**(design/24 // §3.2;切片 M1)。S1 把这条 SQL 写在网关里;抽到共享存储核心之后,Web 上传与 // IM 入站附件必须共用同一份写入逻辑,否则「哪条来源写了元数据、哪条没写」会 // 分叉,S7 的引用计数与 GC 也就失去单一事实来源。 // // 沿用 S1 的两条既有事实(刻意不改,改动属另一片): // - 每次写入 `db.Open` 开一条新连接(design/24 §9.2-5 已登记的技术债); // - 同一存储名重复上传 `ON CONFLICT(name) DO NOTHING` —— 首次的 // uploader/created_at 是权威,去重不刷新它们。 import ( "context" "errors" "fmt" "log/slog" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/db" ) // ErrMetadataNoPath 报告元数据写入器没有数据库路径(未接库的构建/测试)。 // 调用方据此不构造写入器(Store.Meta 留 nil),而不是拿到一个写不进去的替身。 var ErrMetadataNoPath = errors.New("documents: 元数据写入器缺少数据库路径") // SQLMetadataWriter 把元数据行写进 SQLite 的 `documents` 表。它实现 // MetadataWriter,供网关(Web 上传)与组合根(IM 入站附件)共用。 type SQLMetadataWriter struct { path string log *slog.Logger } // MetadataWriterFor 按数据库路径给出元数据写入面;路径为空(或全空白)时返回 // **真正的 nil 接口**,调用方可以直接把它放进 `StoreConfig.Meta` 而不会踩 // 「接口里装着一个 nil 具体值」的陷阱(那会让「未注入」判定失效,把一次成功的 // 落盘变成 500)。未接库的构建/测试因此照旧可用:Store 按「未注入」跳过并留痕。 func MetadataWriterFor(path string, log *slog.Logger) MetadataWriter { if strings.TrimSpace(path) == "" { return nil } return &SQLMetadataWriter{path: path, log: log} } // WriteDocument 写入一行文档元数据。参数化 SQL、无拼接,错误按项目规范 wrap。 func (w *SQLMetadataWriter) WriteDocument(ctx context.Context, rec Record) error { if w == nil || w.path == "" { return fmt.Errorf("documents: 写元数据: %w", ErrMetadataNoPath) } conn, err := db.Open(ctx, w.path, w.log) if err != nil { return fmt.Errorf("documents: 打开元数据库: %w", err) } defer func() { _ = conn.Close() }() const query = `INSERT INTO documents (name, sha256, mime, size, uploader) VALUES (?, ?, ?, ?, ?) ON CONFLICT(name) DO NOTHING` if _, err := conn.ExecContext(ctx, query, rec.Name, rec.SHA256, rec.MIME, rec.Size, rec.Uploader); err != nil { return fmt.Errorf("documents: 插入元数据行: %w", err) } return nil }