feat: initial commit of docDB (extracted from knowbase) (by AI)
This commit is contained in:
commit
181c1f0be3
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.geminiignore
|
||||||
|
.gemini
|
||||||
|
.ai/
|
||||||
|
env.json
|
||||||
|
env.yml
|
||||||
|
env.yaml
|
||||||
|
.log.meta.json
|
||||||
11
CHANGELOG.md
Normal file
11
CHANGELOG.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-05-15
|
||||||
|
### Added
|
||||||
|
- 从 `knowbase` 项目剥离生成的独立 `docDB` 项目。
|
||||||
|
- 支持基于 `tableDB` 的文档存储,自动继承权限隔离与数据备份能力。
|
||||||
|
- 强制区分 `SetDoc` (版本提升) 与 `SetMeta` (元数据更新)。
|
||||||
|
- 完整的生命周期事件 Hooks (`OnCreatedDoc`, `OnUpdatedDoc`, `OnRemoved`)。
|
||||||
|
- 支持流式大文件上传与落盘。
|
||||||
|
- 自动提取 Markdown 目录结构 (ToC)。
|
||||||
|
- 迁移并验证了所有核心测试用例。
|
||||||
72
README.md
Normal file
72
README.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# @go/docDB
|
||||||
|
|
||||||
|
`docDB` 是一个独立的高级文档存储引擎,基于 `@go/tableDB` 构建,提供版本管理、流式上传及生命周期钩子。
|
||||||
|
|
||||||
|
## 📦 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get apigo.cc/go/docDB
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠 核心功能
|
||||||
|
|
||||||
|
1. **权限隔离**:完全继承 `tableDB` 的用户隔离体系。
|
||||||
|
2. **版本管控**:通过 `SetDoc` 强制提升文档版本,`SetMeta` 仅更新属性而不影响版本。
|
||||||
|
3. **流式上传**:支持 `io.Reader` 接口,低内存占用处理大文件落盘。
|
||||||
|
4. **事件驱动**:提供 `OnCreatedDoc`, `OnUpdatedDoc`, `OnRemoved` 钩子,方便集成搜索索引、异步处理等。
|
||||||
|
5. **结构化目录**:自动提取 Markdown 文档的目录树 (ToC)。
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 初始化
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"apigo.cc/go/docDB"
|
||||||
|
"apigo.cc/go/tableDB"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 创建 DocDB 实例
|
||||||
|
db := tableDB.GetDB("sqlite://docs.db", logger)
|
||||||
|
docStore := docDB.New(db, "./storage")
|
||||||
|
|
||||||
|
// 授权并获取 App
|
||||||
|
app := docStore.Auth("user_123")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 存储文档
|
||||||
|
|
||||||
|
```go
|
||||||
|
doc := &docDB.Document{
|
||||||
|
Path: "/projects/readme.md",
|
||||||
|
Title: "Getting Started",
|
||||||
|
TextContent: "# Hello World\n...",
|
||||||
|
BinaryContent: fileReader, // 可选,流式上传
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文档 (版本提升)
|
||||||
|
err := app.SetDoc(doc)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 更新元数据
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 仅更新标签,不提升版本
|
||||||
|
err := app.SetMeta(docID, map[string]any{
|
||||||
|
"Tags": "tutorial,go",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 监听事件
|
||||||
|
|
||||||
|
```go
|
||||||
|
docStore.OnCreatedDoc(func(doc *docDB.Document) {
|
||||||
|
fmt.Println("New doc created:", doc.ID)
|
||||||
|
// 可以在这里触发异步摘要生成或向量化
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 注意事项
|
||||||
|
|
||||||
|
- **数据对齐**:内部强制将 `Document.ID` 对齐为 `tableDB` 约定的 `id` 字段。
|
||||||
|
- **无状态性**:不包含具体业务逻辑,如摘要生成、全文搜索等,推荐通过事件钩子实现。
|
||||||
10
TEST.md
Normal file
10
TEST.md
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
=== RUN TestDocDB
|
||||||
|
--- PASS: TestDocDB (0.12s)
|
||||||
|
goos: darwin
|
||||||
|
goarch: amd64
|
||||||
|
pkg: apigo.cc/go/docDB
|
||||||
|
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
|
||||||
|
BenchmarkExtractToC
|
||||||
|
BenchmarkExtractToC-16 461220 2657 ns/op
|
||||||
|
PASS
|
||||||
|
ok apigo.cc/go/docDB 2.586s
|
||||||
309
doc.go
Normal file
309
doc.go
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
package docDB
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"apigo.cc/go/cast"
|
||||||
|
"apigo.cc/go/file"
|
||||||
|
"apigo.cc/go/id"
|
||||||
|
"apigo.cc/go/tableDB"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DocDB 实例
|
||||||
|
type DocDB struct {
|
||||||
|
db *tableDB.TableDBUnauthorized
|
||||||
|
baseDir string
|
||||||
|
onCreatedDoc func(doc *Document)
|
||||||
|
onUpdatedDoc func(doc *Document)
|
||||||
|
onRemoved func(id string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// App 实例 (带用户权限)
|
||||||
|
type App struct {
|
||||||
|
docDB *DocDB
|
||||||
|
userID string
|
||||||
|
table *tableDB.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
// Document 结构体
|
||||||
|
type Document struct {
|
||||||
|
ID string // 唯一标识符
|
||||||
|
Path string // 层级路径
|
||||||
|
Title string // 标题
|
||||||
|
TextContent string // 纯文本
|
||||||
|
BinaryContent io.Reader `json:"-"` // 仅为内存瞬态,流式读取
|
||||||
|
FilePath string // 原文件落盘绝对路径
|
||||||
|
Thumbnail string // 缩略图路径
|
||||||
|
Summary string // 摘要
|
||||||
|
Metadata map[string]any // 元数据
|
||||||
|
ToC []ToCNode // 目录树
|
||||||
|
Tags string // 标签
|
||||||
|
Language string // 语言
|
||||||
|
Type string // 类型
|
||||||
|
StoreType string // 存储方式 (file/db)
|
||||||
|
AsFile bool // 是否以文件存储
|
||||||
|
Version uint64 // 版本号
|
||||||
|
IsSecret bool // 是否加密
|
||||||
|
CreateTime int64 // 创建时间
|
||||||
|
UpdateTime int64 // 更新时间
|
||||||
|
Creator string // 创建人 ID
|
||||||
|
Updater string // 更新人 ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToCNode 目录树节点
|
||||||
|
type ToCNode struct {
|
||||||
|
Title string
|
||||||
|
Level int
|
||||||
|
Children []ToCNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 创建 DocDB 实例
|
||||||
|
func New(db *tableDB.TableDBUnauthorized, baseDir string) *DocDB {
|
||||||
|
return &DocDB{
|
||||||
|
db: db,
|
||||||
|
baseDir: baseDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth 授权并初始化表结构
|
||||||
|
func (d *DocDB) Auth(userID string) *App {
|
||||||
|
appTableDB := d.db.Auth(userID)
|
||||||
|
|
||||||
|
// 检查 _Doc 表是否存在,不存在则初始化
|
||||||
|
res, _ := appTableDB.Table("_Table").Get("_Doc")
|
||||||
|
if res == nil {
|
||||||
|
// 使用高级接口 SetTable 和 SetField
|
||||||
|
_ = appTableDB.SetTable(tableDB.TableSchema{ID: "_Doc", Name: "_Doc", Memo: "Document Repository"})
|
||||||
|
_ = appTableDB.Table("_Doc").SetField(
|
||||||
|
tableDB.FieldSchema{Name: "path", Type: "v256", IsIndex: true},
|
||||||
|
tableDB.FieldSchema{Name: "title", Type: "v255"},
|
||||||
|
tableDB.FieldSchema{Name: "textContent", Type: "t"},
|
||||||
|
tableDB.FieldSchema{Name: "filePath", Type: "v256"},
|
||||||
|
tableDB.FieldSchema{Name: "thumbnail", Type: "v256"},
|
||||||
|
tableDB.FieldSchema{Name: "summary", Type: "t"},
|
||||||
|
tableDB.FieldSchema{Name: "metadata", Type: "o"},
|
||||||
|
tableDB.FieldSchema{Name: "toc", Type: "o"},
|
||||||
|
tableDB.FieldSchema{Name: "tags", Type: "v1024"},
|
||||||
|
tableDB.FieldSchema{Name: "language", Type: "v32"},
|
||||||
|
tableDB.FieldSchema{Name: "type", Type: "v32"},
|
||||||
|
tableDB.FieldSchema{Name: "storeType", Type: "v16"},
|
||||||
|
tableDB.FieldSchema{Name: "asFile", Type: "b"},
|
||||||
|
tableDB.FieldSchema{Name: "version", Type: "bi"},
|
||||||
|
tableDB.FieldSchema{Name: "isSecret", Type: "b"},
|
||||||
|
tableDB.FieldSchema{Name: "updateTime", Type: "bi"},
|
||||||
|
tableDB.FieldSchema{Name: "updater", Type: "v64"},
|
||||||
|
tableDB.FieldSchema{Name: "createTime", Type: "bi"},
|
||||||
|
tableDB.FieldSchema{Name: "creator", Type: "v64"},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
docDB: d,
|
||||||
|
userID: userID,
|
||||||
|
table: appTableDB.Table("_Doc"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDoc 更新内容 (强制提升版本号)
|
||||||
|
func (a *App) SetDoc(docObj *Document) error {
|
||||||
|
if docObj.ID == "" {
|
||||||
|
docObj.ID = id.MakeID(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
existing, err := a.table.Get(docObj.ID)
|
||||||
|
|
||||||
|
isUpdate := err == nil && existing != nil
|
||||||
|
|
||||||
|
if isUpdate {
|
||||||
|
docObj.Version = cast.Uint64(existing["version"]) + 1
|
||||||
|
docObj.CreateTime = cast.Int64(existing["createTime"])
|
||||||
|
docObj.Creator = cast.String(existing["creator"])
|
||||||
|
} else {
|
||||||
|
docObj.Version = 1
|
||||||
|
docObj.CreateTime = now
|
||||||
|
docObj.Creator = a.userID
|
||||||
|
}
|
||||||
|
|
||||||
|
docObj.UpdateTime = now
|
||||||
|
docObj.Updater = a.userID
|
||||||
|
|
||||||
|
// 处理文件落盘
|
||||||
|
if docObj.BinaryContent != nil {
|
||||||
|
if err := a.saveFile(docObj); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
docObj.AsFile = true
|
||||||
|
docObj.StoreType = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动提取 ToC
|
||||||
|
if docObj.ToC == nil && (docObj.Type == "markdown" || strings.HasSuffix(strings.ToLower(docObj.Path), ".md")) {
|
||||||
|
docObj.ToC = ExtractToC(docObj.TextContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 map 并清理瞬态/冲突字段
|
||||||
|
record := a.toRecord(docObj)
|
||||||
|
|
||||||
|
err = a.table.Set(record)
|
||||||
|
if err == nil {
|
||||||
|
if isUpdate {
|
||||||
|
a.docDB.triggerUpdated(docObj)
|
||||||
|
} else {
|
||||||
|
a.docDB.triggerCreated(docObj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMeta 更新元数据 (不提升版本号)
|
||||||
|
func (a *App) SetMeta(id string, meta map[string]any) error {
|
||||||
|
existing, err := a.table.Get(id)
|
||||||
|
if err != nil || existing == nil {
|
||||||
|
return fmt.Errorf("document not found: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
for k, v := range meta {
|
||||||
|
existing[k] = v
|
||||||
|
}
|
||||||
|
existing["updateTime"] = now
|
||||||
|
existing["updater"] = a.userID
|
||||||
|
|
||||||
|
return a.table.Set(existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) toRecord(docObj *Document) map[string]any {
|
||||||
|
record := make(map[string]any)
|
||||||
|
cast.Convert(&record, docObj)
|
||||||
|
|
||||||
|
// tableDB 内部约定使用小写 id,这里强制转换以避免产生重复字段
|
||||||
|
if v, ok := record["ID"]; ok {
|
||||||
|
record["id"] = v
|
||||||
|
delete(record, "ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除瞬态字段
|
||||||
|
delete(record, "binaryContent")
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) saveFile(docObj *Document) error {
|
||||||
|
subDir1 := docObj.ID[0:2]
|
||||||
|
subDir2 := docObj.ID[2:6]
|
||||||
|
subDir3 := docObj.ID[6:10]
|
||||||
|
|
||||||
|
ext := filepath.Ext(docObj.Path)
|
||||||
|
fileName := fmt.Sprintf("v%d%s", docObj.Version, ext)
|
||||||
|
filePath := filepath.Join(a.docDB.baseDir, "doc", subDir1, subDir2, subDir3, fileName)
|
||||||
|
file.EnsureParentDir(filePath)
|
||||||
|
|
||||||
|
out, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(out, docObj.BinaryContent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
docObj.FilePath = filePath
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取文档
|
||||||
|
func (a *App) Get(id string) (*Document, error) {
|
||||||
|
res, err := a.table.Get(id)
|
||||||
|
if err != nil || res == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var doc Document
|
||||||
|
cast.Convert(&doc, res)
|
||||||
|
return &doc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除文档
|
||||||
|
func (a *App) Remove(id string) error {
|
||||||
|
err := a.table.Remove(id)
|
||||||
|
if err == nil {
|
||||||
|
a.docDB.triggerRemoved(id)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件订阅
|
||||||
|
func (d *DocDB) OnCreatedDoc(fn func(doc *Document)) { d.onCreatedDoc = fn }
|
||||||
|
func (d *DocDB) OnUpdatedDoc(fn func(doc *Document)) { d.onUpdatedDoc = fn }
|
||||||
|
func (d *DocDB) OnRemoved(fn func(id string)) { d.onRemoved = fn }
|
||||||
|
|
||||||
|
func (d *DocDB) triggerCreated(doc *Document) {
|
||||||
|
if d.onCreatedDoc != nil {
|
||||||
|
d.onCreatedDoc(doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DocDB) triggerUpdated(doc *Document) {
|
||||||
|
if d.onUpdatedDoc != nil {
|
||||||
|
d.onUpdatedDoc(doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DocDB) triggerRemoved(id string) {
|
||||||
|
if d.onRemoved != nil {
|
||||||
|
d.onRemoved(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractToC 保持提取目录逻辑
|
||||||
|
func ExtractToC(content string) []ToCNode {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
var root []ToCNode
|
||||||
|
stack := []*[]ToCNode{&root}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(trimmed, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
level := 0
|
||||||
|
for level < len(trimmed) && trimmed[level] == '#' {
|
||||||
|
level++
|
||||||
|
}
|
||||||
|
if level > 0 && level < len(trimmed) && trimmed[level] == ' ' {
|
||||||
|
title := strings.TrimSpace(trimmed[level:])
|
||||||
|
|
||||||
|
for len(stack) > level {
|
||||||
|
stack = stack[:len(stack)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(stack) < level {
|
||||||
|
parentSlice := stack[len(stack)-1]
|
||||||
|
if len(*parentSlice) == 0 {
|
||||||
|
*parentSlice = append(*parentSlice, ToCNode{
|
||||||
|
Title: "...",
|
||||||
|
Level: len(stack),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
lastNode := &((*parentSlice)[len(*parentSlice)-1])
|
||||||
|
stack = append(stack, &(lastNode.Children))
|
||||||
|
}
|
||||||
|
|
||||||
|
parentSlice := stack[len(stack)-1]
|
||||||
|
*parentSlice = append(*parentSlice, ToCNode{
|
||||||
|
Title: title,
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
lastNode := &((*parentSlice)[len(*parentSlice)-1])
|
||||||
|
stack = append(stack, &(lastNode.Children))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
130
doc_test.go
Normal file
130
doc_test.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package docDB
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"apigo.cc/go/log"
|
||||||
|
"apigo.cc/go/tableDB"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDocDB(t *testing.T) {
|
||||||
|
logger := log.DefaultLogger
|
||||||
|
dbFile := "test_doc.db"
|
||||||
|
os.Remove(dbFile)
|
||||||
|
defer os.Remove(dbFile)
|
||||||
|
os.RemoveAll("./test_docs")
|
||||||
|
defer os.RemoveAll("./test_docs")
|
||||||
|
|
||||||
|
unauthorizedDB := tableDB.GetDB("sqlite://"+dbFile, logger)
|
||||||
|
|
||||||
|
systemApp := unauthorizedDB.Auth("_system")
|
||||||
|
// Bootstrap system tables via the special :Schema table
|
||||||
|
_ = systemApp.Table("_Table:Schema").Set(map[string]any{"dsl": ""})
|
||||||
|
|
||||||
|
docDBInst := New(unauthorizedDB, "./test_docs")
|
||||||
|
app := docDBInst.Auth("user1")
|
||||||
|
|
||||||
|
var createdCalled, updatedCalled, removedCalled bool
|
||||||
|
docDBInst.OnCreatedDoc(func(doc *Document) { createdCalled = true })
|
||||||
|
docDBInst.OnUpdatedDoc(func(doc *Document) { updatedCalled = true })
|
||||||
|
docDBInst.OnRemoved(func(id string) { removedCalled = true })
|
||||||
|
|
||||||
|
// 1. 测试 SetDoc (创建)
|
||||||
|
doc1 := &Document{
|
||||||
|
Path: "/test/doc1.md",
|
||||||
|
Title: "Test Doc",
|
||||||
|
TextContent: "# Header 1\nContent 1",
|
||||||
|
Type: "markdown",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := app.SetDoc(doc1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetDoc failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if doc1.Version != 1 {
|
||||||
|
t.Fatalf("Expected version 1, got %d", doc1.Version)
|
||||||
|
}
|
||||||
|
if !createdCalled {
|
||||||
|
t.Error("OnCreatedDoc event not triggered")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 测试 Get
|
||||||
|
d, err := app.Get(doc1.ID)
|
||||||
|
if err != nil || d == nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if d.Title != "Test Doc" {
|
||||||
|
t.Fatalf("Expected title 'Test Doc', got %s", d.Title)
|
||||||
|
}
|
||||||
|
if len(d.ToC) == 0 {
|
||||||
|
t.Error("ToC should be extracted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 测试 SetDoc (更新内容, 提升版本)
|
||||||
|
doc1.TextContent = "# Header 1\nUpdated Content"
|
||||||
|
err = app.SetDoc(doc1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Update SetDoc failed: %v", err)
|
||||||
|
}
|
||||||
|
if doc1.Version != 2 {
|
||||||
|
t.Fatalf("Expected version 2 after content update, got %d", doc1.Version)
|
||||||
|
}
|
||||||
|
if !updatedCalled {
|
||||||
|
t.Error("OnUpdatedDoc event not triggered")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 测试 SetMeta (更新元数据, 不提升版本)
|
||||||
|
err = app.SetMeta(doc1.ID, map[string]any{"title": "New Title"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetMeta failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d2, _ := app.Get(doc1.ID)
|
||||||
|
if d2.Title != "New Title" {
|
||||||
|
t.Fatalf("Expected title 'New Title', got %s", d2.Title)
|
||||||
|
}
|
||||||
|
if d2.Version != 2 {
|
||||||
|
t.Fatalf("Expected version 2 after meta update, got %d", d2.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 测试 带二进制内容的 SetDoc
|
||||||
|
doc2 := &Document{
|
||||||
|
Path: "/test/binary.txt",
|
||||||
|
BinaryContent: bytes.NewReader([]byte("Binary Content")),
|
||||||
|
}
|
||||||
|
err = app.SetDoc(doc2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetDoc with binary failed: %v", err)
|
||||||
|
}
|
||||||
|
if doc2.FilePath == "" {
|
||||||
|
t.Error("FilePath should be set for binary content")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(doc2.FilePath); os.IsNotExist(err) {
|
||||||
|
t.Error("File should be written to disk")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 测试 Remove
|
||||||
|
err = app.Remove(doc1.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Remove failed: %v", err)
|
||||||
|
}
|
||||||
|
if !removedCalled {
|
||||||
|
t.Error("OnRemoved event not triggered")
|
||||||
|
}
|
||||||
|
|
||||||
|
d3, _ := app.Get(doc1.ID)
|
||||||
|
if d3 != nil {
|
||||||
|
t.Error("Document should be removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkExtractToC(b *testing.B) {
|
||||||
|
content := "# H1\n## H1.1\n### H1.1.1\n" + strings.Repeat("Some content\n", 100) + "# H2\n## H2.1\n"
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = ExtractToC(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
67
go.mod
Normal file
67
go.mod
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
module apigo.cc/go/docDB
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
apigo.cc/go/cast v1.3.2
|
||||||
|
apigo.cc/go/file v1.3.1
|
||||||
|
apigo.cc/go/id v1.3.0
|
||||||
|
apigo.cc/go/log v1.3.2
|
||||||
|
apigo.cc/go/tableDB v0.0.0-00010101000000-000000000000
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
apigo.cc/go/config v1.3.0 // indirect
|
||||||
|
apigo.cc/go/crypto v1.3.0 // indirect
|
||||||
|
apigo.cc/go/db v1.3.1 // indirect
|
||||||
|
apigo.cc/go/encoding v1.3.0 // indirect
|
||||||
|
apigo.cc/go/rand v1.3.0 // indirect
|
||||||
|
apigo.cc/go/redis v1.3.0 // indirect
|
||||||
|
apigo.cc/go/safe v1.3.0 // indirect
|
||||||
|
apigo.cc/go/shell v1.3.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
|
golang.org/x/tools v0.44.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
modernc.org/libc v1.72.0 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.50.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace apigo.cc/go/tableDB => ../tableDB
|
||||||
|
|
||||||
|
replace apigo.cc/go/cast => ../cast
|
||||||
|
|
||||||
|
replace apigo.cc/go/id => ../id
|
||||||
|
|
||||||
|
replace apigo.cc/go/file => ../file
|
||||||
|
|
||||||
|
replace apigo.cc/go/log => ../log
|
||||||
|
|
||||||
|
replace apigo.cc/go/document => ../document
|
||||||
|
|
||||||
|
replace apigo.cc/go/db => ../db
|
||||||
|
|
||||||
|
replace apigo.cc/go/config => ../config
|
||||||
|
|
||||||
|
replace apigo.cc/go/crypto => ../crypto
|
||||||
|
|
||||||
|
replace apigo.cc/go/encoding => ../encoding
|
||||||
|
|
||||||
|
replace apigo.cc/go/rand => ../rand
|
||||||
|
|
||||||
|
replace apigo.cc/go/redis => ../redis
|
||||||
|
|
||||||
|
replace apigo.cc/go/safe => ../safe
|
||||||
|
|
||||||
|
replace apigo.cc/go/shell => ../shell
|
||||||
|
|
||||||
|
replace apigo.cc/go/vision => ../vision
|
||||||
72
go.sum
Normal file
72
go.sum
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||||
|
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||||
|
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
|
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||||
|
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||||
|
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||||
|
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||||
|
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||||
|
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||||
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||||
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||||
|
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||||
|
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
Loading…
x
Reference in New Issue
Block a user