package store import ( "database/sql" "errors" "regexp" ) // Image 图片元信息(不含 BLOB)。 type Image struct { ID int64 `json:"id"` SHA256 string `json:"sha256"` MIME string `json:"mime"` Size int `json:"size"` CreatedAt int64 `json:"created_at"` } // UpsertImage 按 sha256 去重入库,返回图片 id。 func (s *Store) UpsertImage(sha256Hex, mime string, size int, data []byte, createdAt int64) (int64, error) { if _, err := s.db.Exec( `INSERT OR IGNORE INTO images (sha256, mime, size, data, created_at) VALUES (?, ?, ?, ?, ?)`, sha256Hex, mime, size, data, createdAt); err != nil { return 0, err } var id int64 err := s.db.QueryRow(`SELECT id FROM images WHERE sha256=?`, sha256Hex).Scan(&id) return id, err } // GetImageMeta 取图片元信息。 func (s *Store) GetImageMeta(id int64) (*Image, error) { row := s.db.QueryRow(`SELECT id, sha256, mime, size, created_at FROM images WHERE id=?`, id) var img Image err := row.Scan(&img.ID, &img.SHA256, &img.MIME, &img.Size, &img.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return &img, err } // GetImageData 取图片字节。 func (s *Store) GetImageData(id int64) ([]byte, error) { var data []byte err := s.db.QueryRow(`SELECT data FROM images WHERE id=?`, id).Scan(&data) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return data, err } // ImageIsPublic 并集语义(§6.2):任一引用笔记 public 且未删除 ⇒ 匿名可读。 func (s *Store) ImageIsPublic(id int64) (bool, error) { var one int err := s.db.QueryRow(` SELECT 1 FROM image_refs r JOIN notes n ON n.id = r.note_id WHERE r.image_id = ? AND n.status='public' AND n.deleted_at IS NULL LIMIT 1`, id).Scan(&one) if errors.Is(err, sql.ErrNoRows) { return false, nil } return err == nil, err } // imageRefRe 扫描 content 中对 /api/images/{id} 的引用(§6.2 引用维护)。 var imageRefRe = regexp.MustCompile(`/api/images/(\d+)`) // RebuildImageRefs 在**同一事务**内重建笔记的图片引用: // 删除该笔记全部 refs → 正则扫描 content → 重建(§6.2)。 func (s *Store) RebuildImageRefs(noteID int64, content string) error { tx, err := s.db.Begin() if err != nil { return err } defer tx.Rollback() if err := rebuildRefsTx(tx, noteID, content); err != nil { return err } return tx.Commit() } func rebuildRefsTx(tx *sql.Tx, noteID int64, content string) error { if _, err := tx.Exec(`DELETE FROM image_refs WHERE note_id=?`, noteID); err != nil { return err } seen := map[int64]bool{} for _, m := range imageRefRe.FindAllStringSubmatch(content, -1) { var imgID int64 if _, err := fmtSscanInt(m[1], &imgID); err != nil { continue } if seen[imgID] { continue } seen[imgID] = true // 图片不存在时忽略(引用悬空无害) if _, err := tx.Exec(`INSERT OR IGNORE INTO image_refs (image_id, note_id) VALUES (?, ?)`, imgID, noteID); err != nil { return err } } return nil } // ListOrphanImages 零引用图片清单(GET /api/admin/images?orphan=1 与 gc 检视用)。 // 站点 Logo 引用的图片虽无笔记引用仍受保护,不视为孤儿。 func (s *Store) ListOrphanImages() ([]Image, error) { rows, err := s.db.Query(` SELECT i.id, i.sha256, i.mime, i.size, i.created_at FROM images i WHERE NOT EXISTS (SELECT 1 FROM image_refs r WHERE r.image_id = i.id) AND i.id <> ? ORDER BY i.created_at DESC`, s.logoImageID()) if err != nil { return nil, err } defer rows.Close() out := []Image{} for rows.Next() { var img Image if err := rows.Scan(&img.ID, &img.SHA256, &img.MIME, &img.Size, &img.CreatedAt); err != nil { return nil, err } out = append(out, img) } return out, rows.Err() } // DeleteImage 物理删除单张图片(refs 级联)。 func (s *Store) DeleteImage(id int64) error { res, err := s.db.Exec(`DELETE FROM images WHERE id=?`, id) if err != nil { return err } if rows, _ := res.RowsAffected(); rows == 0 { return ErrNotFound } return nil }