✨ Added data migration service
This commit is contained in:
@@ -1,119 +1,183 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
"voidraft/internal/models"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||
)
|
||||
|
||||
// DocumentError 文档操作错误
|
||||
type DocumentError struct {
|
||||
Operation string // 操作名称
|
||||
Err error // 原始错误
|
||||
}
|
||||
|
||||
// Error 实现error接口
|
||||
func (e *DocumentError) Error() string {
|
||||
return fmt.Sprintf("document error during %s: %v", e.Operation, e.Err)
|
||||
}
|
||||
|
||||
// Unwrap 获取原始错误
|
||||
func (e *DocumentError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// DocumentService 提供文档管理功能
|
||||
type DocumentService struct {
|
||||
configService *ConfigService
|
||||
logger *log.LoggerService
|
||||
document *models.Document // 文档实例
|
||||
document *models.Document
|
||||
docStore *Store[models.Document]
|
||||
mutex sync.RWMutex // 保护document的读写
|
||||
mutex sync.RWMutex
|
||||
|
||||
// 定时保存
|
||||
saveTimer *time.Timer
|
||||
timerMutex sync.Mutex // 保护定时器操作
|
||||
isDirty bool // 文档是否有未保存的变更
|
||||
|
||||
// 服务控制
|
||||
shutdown chan struct{}
|
||||
shutdownOnce sync.Once
|
||||
// 自动保存优化
|
||||
saveTimer *time.Timer
|
||||
isDirty bool
|
||||
lastSaveTime time.Time
|
||||
pendingContent string // 暂存待保存的内容
|
||||
}
|
||||
|
||||
// NewDocumentService 创建新的文档服务实例
|
||||
// NewDocumentService 创建文档服务
|
||||
func NewDocumentService(configService *ConfigService, logger *log.LoggerService) *DocumentService {
|
||||
if logger == nil {
|
||||
logger = log.New()
|
||||
}
|
||||
|
||||
service := &DocumentService{
|
||||
return &DocumentService{
|
||||
configService: configService,
|
||||
logger: logger,
|
||||
shutdown: make(chan struct{}),
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
// Initialize 初始化文档服务
|
||||
// Initialize 初始化服务
|
||||
func (ds *DocumentService) Initialize() error {
|
||||
// 确保文档目录存在
|
||||
if err := ds.ensureDocumentsDir(); err != nil {
|
||||
ds.logger.Error("Document: Failed to ensure documents directory", "error", err)
|
||||
return &DocumentError{Operation: "initialize", Err: err}
|
||||
if err := ds.initStore(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化文档存储
|
||||
if err := ds.initDocumentStore(); err != nil {
|
||||
ds.logger.Error("Document: Failed to initialize document store", "error", err)
|
||||
return &DocumentError{Operation: "init_store", Err: err}
|
||||
}
|
||||
|
||||
// 加载默认文档
|
||||
if err := ds.loadDefaultDocument(); err != nil {
|
||||
ds.logger.Error("Document: Failed to load default document", "error", err)
|
||||
return &DocumentError{Operation: "load_default", Err: err}
|
||||
}
|
||||
|
||||
// 启动定时保存
|
||||
ds.loadDocument()
|
||||
ds.startAutoSave()
|
||||
return nil
|
||||
}
|
||||
|
||||
// initStore 初始化存储
|
||||
func (ds *DocumentService) initStore() error {
|
||||
docPath, err := ds.getDocumentPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(docPath), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds.docStore = NewStore[models.Document](StoreOption{
|
||||
FilePath: docPath,
|
||||
AutoSave: false,
|
||||
Logger: ds.logger,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// startAutoSave 启动定时保存
|
||||
func (ds *DocumentService) startAutoSave() {
|
||||
// getDocumentPath 获取文档路径
|
||||
func (ds *DocumentService) getDocumentPath() (string, error) {
|
||||
config, err := ds.configService.GetConfig()
|
||||
if err != nil {
|
||||
ds.logger.Error("Document: Failed to get config for auto save", "error", err)
|
||||
ds.scheduleNextSave(5 * time.Second) // 默认5秒
|
||||
return
|
||||
return "", err
|
||||
}
|
||||
|
||||
delay := time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond
|
||||
ds.scheduleNextSave(delay)
|
||||
return filepath.Join(config.General.DataPath, "docs", "default.json"), nil
|
||||
}
|
||||
|
||||
// scheduleNextSave 安排下次保存
|
||||
func (ds *DocumentService) scheduleNextSave(delay time.Duration) {
|
||||
ds.timerMutex.Lock()
|
||||
defer ds.timerMutex.Unlock()
|
||||
// loadDocument 加载文档
|
||||
func (ds *DocumentService) loadDocument() {
|
||||
ds.mutex.Lock()
|
||||
defer ds.mutex.Unlock()
|
||||
|
||||
// 停止现有定时器
|
||||
doc := ds.docStore.Get()
|
||||
if doc.Meta.ID == "" {
|
||||
// 创建新文档
|
||||
ds.document = models.NewDefaultDocument()
|
||||
ds.docStore.Set(*ds.document)
|
||||
ds.logger.Info("Document: Created new document")
|
||||
} else {
|
||||
ds.document = &doc
|
||||
ds.logger.Info("Document: Loaded existing document")
|
||||
}
|
||||
}
|
||||
|
||||
// GetActiveDocument 获取活动文档
|
||||
func (ds *DocumentService) GetActiveDocument() (*models.Document, error) {
|
||||
ds.mutex.RLock()
|
||||
defer ds.mutex.RUnlock()
|
||||
|
||||
if ds.document == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 返回副本
|
||||
docCopy := *ds.document
|
||||
return &docCopy, nil
|
||||
}
|
||||
|
||||
// UpdateActiveDocumentContent 更新文档内容
|
||||
func (ds *DocumentService) UpdateActiveDocumentContent(content string) error {
|
||||
ds.mutex.Lock()
|
||||
defer ds.mutex.Unlock()
|
||||
|
||||
if ds.document != nil {
|
||||
// 只在内容真正改变时才标记为脏
|
||||
if ds.document.Content != content {
|
||||
ds.pendingContent = content
|
||||
ds.isDirty = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceSave 强制保存
|
||||
func (ds *DocumentService) ForceSave() error {
|
||||
ds.mutex.Lock()
|
||||
defer ds.mutex.Unlock()
|
||||
|
||||
if ds.document == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 应用待保存的内容
|
||||
if ds.pendingContent != "" {
|
||||
ds.document.Content = ds.pendingContent
|
||||
ds.pendingContent = ""
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
ds.document.Meta.LastUpdated = now
|
||||
|
||||
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ds.docStore.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds.isDirty = false
|
||||
ds.lastSaveTime = now
|
||||
ds.logger.Info("Document: Force save completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// startAutoSave 启动自动保存
|
||||
func (ds *DocumentService) startAutoSave() {
|
||||
delay := 5 * time.Second // 默认延迟
|
||||
|
||||
if config, err := ds.configService.GetConfig(); err == nil {
|
||||
delay = time.Duration(config.Editing.AutoSaveDelay) * time.Millisecond
|
||||
}
|
||||
|
||||
ds.scheduleAutoSave(delay)
|
||||
}
|
||||
|
||||
// scheduleAutoSave 安排自动保存
|
||||
func (ds *DocumentService) scheduleAutoSave(delay time.Duration) {
|
||||
if ds.saveTimer != nil {
|
||||
ds.saveTimer.Stop()
|
||||
}
|
||||
|
||||
// 创建新的定时器
|
||||
ds.saveTimer = time.AfterFunc(delay, func() {
|
||||
ds.performAutoSave()
|
||||
// 安排下次保存
|
||||
ds.startAutoSave()
|
||||
ds.startAutoSave() // 重新安排
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,188 +186,94 @@ func (ds *DocumentService) performAutoSave() {
|
||||
ds.mutex.Lock()
|
||||
defer ds.mutex.Unlock()
|
||||
|
||||
// 如果没有变更,跳过保存
|
||||
if !ds.isDirty || ds.document == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新元数据并保存
|
||||
ds.document.Meta.LastUpdated = time.Now()
|
||||
// 检查距离上次保存的时间间隔,避免过于频繁的保存
|
||||
now := time.Now()
|
||||
if now.Sub(ds.lastSaveTime) < time.Second {
|
||||
// 如果距离上次保存不到1秒,跳过此次保存
|
||||
// 下一个自动保存周期会重新尝试
|
||||
ds.logger.Debug("Document: Skipping auto save due to recent save")
|
||||
return
|
||||
}
|
||||
|
||||
ds.logger.Info("Document: Auto saving document",
|
||||
"id", ds.document.Meta.ID,
|
||||
"contentLength", len(ds.document.Content))
|
||||
// 应用待保存的内容
|
||||
if ds.pendingContent != "" {
|
||||
ds.document.Content = ds.pendingContent
|
||||
ds.pendingContent = ""
|
||||
}
|
||||
|
||||
ds.document.Meta.LastUpdated = now
|
||||
|
||||
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||
ds.logger.Error("Document: Failed to auto save document", "error", err)
|
||||
ds.logger.Error("Document: Auto save failed", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ds.docStore.Save(); err != nil {
|
||||
ds.logger.Error("Document: Failed to force save document", "error", err)
|
||||
ds.logger.Error("Document: Auto save failed", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 重置脏标记
|
||||
ds.isDirty = false
|
||||
ds.logger.Info("Document: Auto save completed")
|
||||
ds.lastSaveTime = now
|
||||
ds.logger.Debug("Document: Auto save completed")
|
||||
}
|
||||
|
||||
// stopTimer 停止定时器
|
||||
func (ds *DocumentService) stopTimer() {
|
||||
ds.timerMutex.Lock()
|
||||
defer ds.timerMutex.Unlock()
|
||||
|
||||
if ds.saveTimer != nil {
|
||||
ds.saveTimer.Stop()
|
||||
ds.saveTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
// initDocumentStore 初始化文档存储
|
||||
func (ds *DocumentService) initDocumentStore() error {
|
||||
docPath, err := ds.getDefaultDocumentPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ds.logger.Info("Document: Initializing document store", "path", docPath)
|
||||
|
||||
ds.docStore = NewStore[models.Document](StoreOption{
|
||||
FilePath: docPath,
|
||||
AutoSave: true,
|
||||
Logger: ds.logger,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureDocumentsDir 确保文档目录存在
|
||||
func (ds *DocumentService) ensureDocumentsDir() error {
|
||||
config, err := ds.configService.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dataPath string
|
||||
if config.General.UseCustomDataPath && config.General.CustomDataPath != "" {
|
||||
dataPath = config.General.CustomDataPath
|
||||
} else {
|
||||
dataPath = config.General.DefaultDataPath
|
||||
}
|
||||
|
||||
docsDir := filepath.Join(dataPath, "docs")
|
||||
return os.MkdirAll(docsDir, 0755)
|
||||
}
|
||||
|
||||
// getDocumentsDir 获取文档目录路径
|
||||
func (ds *DocumentService) getDocumentsDir() (string, error) {
|
||||
config, err := ds.configService.GetConfig()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var dataPath string
|
||||
if config.General.UseCustomDataPath && config.General.CustomDataPath != "" {
|
||||
dataPath = config.General.CustomDataPath
|
||||
} else {
|
||||
dataPath = config.General.DefaultDataPath
|
||||
}
|
||||
|
||||
return filepath.Join(dataPath, "docs"), nil
|
||||
}
|
||||
|
||||
// getDefaultDocumentPath 获取默认文档路径
|
||||
func (ds *DocumentService) getDefaultDocumentPath() (string, error) {
|
||||
docsDir, err := ds.getDocumentsDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(docsDir, "default.json"), nil
|
||||
}
|
||||
|
||||
// loadDefaultDocument 加载默认文档
|
||||
func (ds *DocumentService) loadDefaultDocument() error {
|
||||
doc := ds.docStore.Get()
|
||||
|
||||
// ReloadDocument 重新加载文档
|
||||
func (ds *DocumentService) ReloadDocument() error {
|
||||
ds.mutex.Lock()
|
||||
defer ds.mutex.Unlock()
|
||||
|
||||
if doc.Meta.ID == "" {
|
||||
// 创建默认文档
|
||||
ds.document = models.NewDefaultDocument()
|
||||
|
||||
// 保存默认文档
|
||||
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||
return &DocumentError{Operation: "save_default", Err: err}
|
||||
// 强制保存当前文档
|
||||
if ds.document != nil && ds.isDirty {
|
||||
if err := ds.forceSaveInternal(); err != nil {
|
||||
ds.logger.Error("Document: Failed to save before reload", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
ds.logger.Info("Document: Created and saved default document")
|
||||
// 重新初始化存储
|
||||
if err := ds.initStore(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重新加载文档
|
||||
doc := ds.docStore.Get()
|
||||
if doc.Meta.ID == "" {
|
||||
// 创建新文档
|
||||
ds.document = models.NewDefaultDocument()
|
||||
ds.docStore.Set(*ds.document)
|
||||
ds.logger.Info("Document: Created new document after reload")
|
||||
} else {
|
||||
ds.document = &doc
|
||||
ds.logger.Info("Document: Loaded default document", "id", doc.Meta.ID)
|
||||
ds.logger.Info("Document: Loaded existing document after reload")
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
ds.isDirty = false
|
||||
ds.pendingContent = ""
|
||||
ds.lastSaveTime = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveDocument 获取当前活动文档
|
||||
func (ds *DocumentService) GetActiveDocument() (*models.Document, error) {
|
||||
ds.mutex.RLock()
|
||||
defer ds.mutex.RUnlock()
|
||||
|
||||
// forceSaveInternal 内部强制保存(不加锁)
|
||||
func (ds *DocumentService) forceSaveInternal() error {
|
||||
if ds.document == nil {
|
||||
return nil, errors.New("no active document loaded")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 返回副本以防止外部修改
|
||||
docCopy := *ds.document
|
||||
return &docCopy, nil
|
||||
}
|
||||
|
||||
// GetActiveDocumentContent 获取当前活动文档内容
|
||||
func (ds *DocumentService) GetActiveDocumentContent() (string, error) {
|
||||
ds.mutex.RLock()
|
||||
defer ds.mutex.RUnlock()
|
||||
|
||||
if ds.document == nil {
|
||||
return "", errors.New("no active document loaded")
|
||||
// 应用待保存的内容
|
||||
if ds.pendingContent != "" {
|
||||
ds.document.Content = ds.pendingContent
|
||||
ds.pendingContent = ""
|
||||
}
|
||||
|
||||
return ds.document.Content, nil
|
||||
}
|
||||
now := time.Now()
|
||||
ds.document.Meta.LastUpdated = now
|
||||
|
||||
// UpdateActiveDocumentContent 更新当前活动文档内容
|
||||
func (ds *DocumentService) UpdateActiveDocumentContent(content string) error {
|
||||
ds.mutex.Lock()
|
||||
defer ds.mutex.Unlock()
|
||||
|
||||
if ds.document == nil {
|
||||
return errors.New("no active document loaded")
|
||||
}
|
||||
|
||||
// 更新文档内容并标记为脏
|
||||
ds.document.Content = content
|
||||
ds.document.Meta.LastUpdated = time.Now()
|
||||
ds.isDirty = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveDocumentSync 同步保存文档内容
|
||||
func (ds *DocumentService) SaveDocumentSync(content string) error {
|
||||
ds.mutex.Lock()
|
||||
defer ds.mutex.Unlock()
|
||||
|
||||
if ds.document == nil {
|
||||
return errors.New("no active document loaded")
|
||||
}
|
||||
|
||||
// 更新内容
|
||||
ds.document.Content = content
|
||||
ds.document.Meta.LastUpdated = time.Now()
|
||||
|
||||
// 立即保存
|
||||
if err := ds.docStore.Set(*ds.document); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -312,57 +282,33 @@ func (ds *DocumentService) SaveDocumentSync(content string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 重置脏标记
|
||||
ds.isDirty = false
|
||||
ds.logger.Info("Document: Sync save completed")
|
||||
ds.lastSaveTime = now
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForceSave 强制保存当前文档
|
||||
func (ds *DocumentService) ForceSave() error {
|
||||
ds.logger.Info("Document: Force save triggered")
|
||||
|
||||
ds.mutex.RLock()
|
||||
if ds.document == nil {
|
||||
ds.mutex.RUnlock()
|
||||
return errors.New("no active document loaded")
|
||||
}
|
||||
content := ds.document.Content
|
||||
ds.mutex.RUnlock()
|
||||
|
||||
return ds.SaveDocumentSync(content)
|
||||
}
|
||||
|
||||
// ServiceShutdown 服务关闭
|
||||
// ServiceShutdown 关闭服务
|
||||
func (ds *DocumentService) ServiceShutdown() error {
|
||||
ds.logger.Info("Document: Service is shutting down...")
|
||||
// 停止定时器
|
||||
if ds.saveTimer != nil {
|
||||
ds.saveTimer.Stop()
|
||||
}
|
||||
|
||||
// 确保只执行一次关闭
|
||||
var shutdownErr error
|
||||
ds.shutdownOnce.Do(func() {
|
||||
// 停止定时器
|
||||
ds.stopTimer()
|
||||
// 最后保存
|
||||
if err := ds.ForceSave(); err != nil {
|
||||
ds.logger.Error("Document: Failed to save on shutdown", "error", err)
|
||||
}
|
||||
|
||||
// 执行最后一次保存
|
||||
ds.mutex.RLock()
|
||||
if ds.document != nil && ds.isDirty {
|
||||
content := ds.document.Content
|
||||
ds.mutex.RUnlock()
|
||||
|
||||
if err := ds.SaveDocumentSync(content); err != nil {
|
||||
ds.logger.Error("Document: Failed to save on shutdown", "error", err)
|
||||
shutdownErr = err
|
||||
} else {
|
||||
ds.logger.Info("Document: Document saved successfully on shutdown")
|
||||
}
|
||||
} else {
|
||||
ds.mutex.RUnlock()
|
||||
}
|
||||
|
||||
// 关闭服务
|
||||
close(ds.shutdown)
|
||||
ds.logger.Info("Document: Service shutdown completed")
|
||||
})
|
||||
|
||||
return shutdownErr
|
||||
ds.logger.Info("Document: Service shutdown completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnDataPathChanged 处理数据路径变更
|
||||
func (ds *DocumentService) OnDataPathChanged(oldPath, newPath string) error {
|
||||
ds.logger.Info("Document: Data path changed, reloading document",
|
||||
"oldPath", oldPath,
|
||||
"newPath", newPath)
|
||||
|
||||
// 重新加载文档以使用新的路径
|
||||
return ds.ReloadDocument()
|
||||
}
|
||||
|
Reference in New Issue
Block a user