From 5be879167ff0f420d17f17beebe3929f6ea90c2d Mon Sep 17 00:00:00 2001 From: qianlifeng Date: Thu, 17 Jul 2025 22:53:01 +0800 Subject: [PATCH] feat(database): enhance SQLite configuration for improved concurrency - Configure SQLite with Write-Ahead Logging (WAL) mode for better concurrency. - Set connection pool settings to optimize database access. - Execute additional PRAGMA statements for optimal performance. - Update logging to reflect new database initialization details. feat(clipboard): migrate clipboard data handling to new settings storage - Implement migration of clipboard data from legacy JSON to new database-backed storage. - Introduce methods for managing favorite clipboard items in settings. - Update clipboard plugin to utilize new favorite management functions. refactor(setting): improve error logging for settings retrieval - Replace direct logger calls with `util.GetLogger()` for consistency across settings management. --- wox.core/database/database.go | 40 ++- wox.core/migration/migrator.go | 234 +++++++++++++ wox.core/plugin/system/clipboard/clipboard.go | 319 +++++++++++++++--- .../plugin/system/clipboard/clipboard_db.go | 160 +++------ wox.core/setting/store.go | 4 +- 5 files changed, 588 insertions(+), 169 deletions(-) diff --git a/wox.core/database/database.go b/wox.core/database/database.go index dc86b331..b6a38f01 100644 --- a/wox.core/database/database.go +++ b/wox.core/database/database.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "path/filepath" + "time" "wox/util" "gorm.io/driver/sqlite" @@ -38,14 +39,49 @@ type Oplog struct { func Init(ctx context.Context) error { dbPath := filepath.Join(util.GetLocation().GetUserDataDirectory(), "wox.db") + // Configure SQLite with proper concurrency settings + dsn := dbPath + "?" + + "_journal_mode=WAL&" + // Enable WAL mode for better concurrency + "_synchronous=NORMAL&" + // Balance between safety and performance + "_cache_size=1000&" + // Set cache size + "_foreign_keys=true&" + // Enable foreign key constraints + "_busy_timeout=5000" // Set busy timeout to 5 seconds + var err error - db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { return err } + // Configure connection pool for better concurrency handling + sqlDB, err := db.DB() + if err != nil { + return fmt.Errorf("failed to get underlying sql.DB: %w", err) + } + + // Set connection pool settings + sqlDB.SetMaxOpenConns(10) // Maximum number of open connections + sqlDB.SetMaxIdleConns(5) // Maximum number of idle connections + sqlDB.SetConnMaxLifetime(time.Hour) // Maximum lifetime of a connection + + // Execute additional PRAGMA statements for optimal concurrency + pragmas := []string{ + "PRAGMA journal_mode=WAL", // Ensure WAL mode is enabled + "PRAGMA synchronous=NORMAL", // Balance safety and performance + "PRAGMA cache_size=1000", // Set cache size + "PRAGMA foreign_keys=ON", // Enable foreign key constraints + "PRAGMA temp_store=memory", // Store temporary tables in memory + "PRAGMA mmap_size=268435456", // Set memory-mapped I/O size (256MB) + } + + for _, pragma := range pragmas { + if _, err := sqlDB.Exec(pragma); err != nil { + util.GetLogger().Warn(ctx, fmt.Sprintf("failed to execute pragma %s: %v", pragma, err)) + } + } + err = db.AutoMigrate( &WoxSetting{}, &PluginSetting{}, @@ -55,7 +91,7 @@ func Init(ctx context.Context) error { return fmt.Errorf("failed to migrate database schema: %w", err) } - util.GetLogger().Info(ctx, fmt.Sprintf("database initialized at %s", dbPath)) + util.GetLogger().Info(ctx, fmt.Sprintf("database initialized at %s with WAL mode enabled", dbPath)) return nil } diff --git a/wox.core/migration/migrator.go b/wox.core/migration/migrator.go index a695cd3c..56ea4655 100644 --- a/wox.core/migration/migrator.go +++ b/wox.core/migration/migrator.go @@ -2,17 +2,22 @@ package migration import ( "context" + "database/sql" "encoding/json" "fmt" "os" "path" "strings" + "time" "wox/common" "wox/database" "wox/i18n" "wox/setting" "wox/util" "wox/util/locale" + + _ "github.com/mattn/go-sqlite3" + "gorm.io/gorm" ) // This file contains the logic for a one-time migration from the old JSON-based settings @@ -332,6 +337,235 @@ func Run(ctx context.Context) error { } } + // Migrate clipboard data + if err := migrateClipboardData(ctx, tx); err != nil { + util.GetLogger().Warn(ctx, fmt.Sprintf("failed to migrate clipboard data: %v", err)) + } + util.GetLogger().Info(ctx, "Successfully migrated old configuration to the new database.") return nil } + +// Clipboard migration structures +type oldClipboardHistory struct { + ID string `json:"id"` + Text string `json:"text"` + Type string `json:"type"` + Timestamp int64 `json:"timestamp"` + ImagePath string `json:"imagePath,omitempty"` + IsFavorite bool `json:"isFavorite,omitempty"` +} + +type favoriteClipboardItem struct { + ID string `json:"id"` + Type string `json:"type"` + Content string `json:"content"` + FilePath string `json:"filePath,omitempty"` + IconData *string `json:"iconData,omitempty"` + Width *int `json:"width,omitempty"` + Height *int `json:"height,omitempty"` + FileSize *int64 `json:"fileSize,omitempty"` + Timestamp int64 `json:"timestamp"` + CreatedAt int64 `json:"createdAt"` +} + +type clipboardRecord struct { + ID string + Type string + Content string + FilePath string + IconData *string + Width *int + Height *int + FileSize *int64 + Timestamp int64 + IsFavorite bool + CreatedAt time.Time +} + +// migrateClipboardData migrates clipboard data from old JSON settings and database to new settings storage +func migrateClipboardData(ctx context.Context, tx *gorm.DB) error { + clipboardPluginId := "5f815d98-27f5-488d-a756-c317ea39935b" + pluginSettingStore := setting.NewPluginSettingStore(tx, clipboardPluginId) + + var allFavoritesToMigrate []favoriteClipboardItem + + // 1. Migrate from legacy JSON settings + var historyJson string + err := pluginSettingStore.Get("history", &historyJson) + if err == nil && historyJson != "" { + var history []oldClipboardHistory + unmarshalErr := json.Unmarshal([]byte(historyJson), &history) + if unmarshalErr != nil { + // Log warning if logger is available + if logger := util.GetLogger(); logger != nil { + logger.Warn(ctx, fmt.Sprintf("failed to unmarshal legacy clipboard history: %v", unmarshalErr)) + } + } else { + // Migrate legacy data from JSON settings + for _, item := range history { + if item.IsFavorite { + // Convert to new favorite format + favoriteItem := favoriteClipboardItem{ + ID: item.ID, + Type: item.Type, + Content: item.Text, + FilePath: item.ImagePath, + Timestamp: item.Timestamp, + CreatedAt: item.Timestamp / 1000, // Convert to seconds + } + allFavoritesToMigrate = append(allFavoritesToMigrate, favoriteItem) + } + } + if logger := util.GetLogger(); logger != nil { + logger.Info(ctx, fmt.Sprintf("found %d favorite items in legacy JSON settings", len(allFavoritesToMigrate))) + } + } + // Clear the old history setting + pluginSettingStore.Set("history", "") + } + + // 2. Migrate existing favorite items from database + dbFavorites, err := getFavoritesFromDatabase(ctx, clipboardPluginId) + if err != nil { + if logger := util.GetLogger(); logger != nil { + logger.Warn(ctx, fmt.Sprintf("failed to get database favorites for migration: %v", err)) + } + } else { + for _, record := range dbFavorites { + // Convert database record to favorite format + favoriteItem := favoriteClipboardItem{ + ID: record.ID, + Type: record.Type, + Content: record.Content, + FilePath: record.FilePath, + IconData: record.IconData, + Width: record.Width, + Height: record.Height, + FileSize: record.FileSize, + Timestamp: record.Timestamp, + CreatedAt: record.CreatedAt.Unix(), + } + allFavoritesToMigrate = append(allFavoritesToMigrate, favoriteItem) + } + + if logger := util.GetLogger(); logger != nil { + logger.Info(ctx, fmt.Sprintf("found %d favorite items in database", len(dbFavorites))) + } + + // Remove favorite items from database after migration + if len(dbFavorites) > 0 { + deletedCount, deleteErr := deleteFavoritesFromDatabase(ctx, clipboardPluginId) + if deleteErr != nil { + if logger := util.GetLogger(); logger != nil { + logger.Warn(ctx, fmt.Sprintf("failed to delete favorites from database: %v", deleteErr)) + } + } else { + if logger := util.GetLogger(); logger != nil { + logger.Info(ctx, fmt.Sprintf("deleted %d favorite items from database after migration", deletedCount)) + } + } + } + } + + // Save all favorites to new settings storage + if len(allFavoritesToMigrate) > 0 { + favoritesJson, err := json.Marshal(allFavoritesToMigrate) + if err != nil { + return fmt.Errorf("failed to marshal favorites: %w", err) + } + + if err := pluginSettingStore.Set("favorites", string(favoritesJson)); err != nil { + return fmt.Errorf("failed to save migrated favorites: %w", err) + } + + if logger := util.GetLogger(); logger != nil { + logger.Info(ctx, fmt.Sprintf("migrated %d favorite clipboard items to new storage", len(allFavoritesToMigrate))) + } + } + + return nil +} + +// getFavoritesFromDatabase retrieves favorite clipboard items from the clipboard database +func getFavoritesFromDatabase(ctx context.Context, pluginId string) ([]clipboardRecord, error) { + dbPath := path.Join(util.GetLocation().GetPluginSettingDirectory(), pluginId+"_clipboard.db") + + // Check if database file exists + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + return []clipboardRecord{}, nil // No database file, no favorites to migrate + } + + // Configure SQLite connection + dsn := dbPath + "?" + + "_journal_mode=WAL&" + + "_synchronous=NORMAL&" + + "_cache_size=1000&" + + "_foreign_keys=true&" + + "_busy_timeout=5000" + + db, err := sql.Open("sqlite3", dsn) + if err != nil { + return nil, fmt.Errorf("failed to open clipboard database: %w", err) + } + defer db.Close() + + querySQL := ` + SELECT id, type, content, file_path, icon_data, width, height, file_size, timestamp, is_favorite, created_at + FROM clipboard_history + WHERE is_favorite = TRUE + ORDER BY timestamp DESC + ` + + rows, err := db.QueryContext(ctx, querySQL) + if err != nil { + return nil, fmt.Errorf("failed to query favorites: %w", err) + } + defer rows.Close() + + var records []clipboardRecord + for rows.Next() { + var record clipboardRecord + err := rows.Scan(&record.ID, &record.Type, &record.Content, + &record.FilePath, &record.IconData, &record.Width, &record.Height, &record.FileSize, + &record.Timestamp, &record.IsFavorite, &record.CreatedAt) + if err != nil { + return nil, fmt.Errorf("failed to scan record: %w", err) + } + records = append(records, record) + } + + return records, rows.Err() +} + +// deleteFavoritesFromDatabase removes all favorite items from the clipboard database +func deleteFavoritesFromDatabase(ctx context.Context, pluginId string) (int64, error) { + dbPath := path.Join(util.GetLocation().GetPluginSettingDirectory(), pluginId+"_clipboard.db") + + // Check if database file exists + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + return 0, nil // No database file, nothing to delete + } + + // Configure SQLite connection + dsn := dbPath + "?" + + "_journal_mode=WAL&" + + "_synchronous=NORMAL&" + + "_cache_size=1000&" + + "_foreign_keys=true&" + + "_busy_timeout=5000" + + db, err := sql.Open("sqlite3", dsn) + if err != nil { + return 0, fmt.Errorf("failed to open clipboard database: %w", err) + } + defer db.Close() + + deleteSQL := `DELETE FROM clipboard_history WHERE is_favorite = TRUE` + result, err := db.ExecContext(ctx, deleteSQL) + if err != nil { + return 0, fmt.Errorf("failed to delete favorites: %w", err) + } + + return result.RowsAffected() +} diff --git a/wox.core/plugin/system/clipboard/clipboard.go b/wox.core/plugin/system/clipboard/clipboard.go index 28338e69..190a5e99 100644 --- a/wox.core/plugin/system/clipboard/clipboard.go +++ b/wox.core/plugin/system/clipboard/clipboard.go @@ -30,6 +30,7 @@ var imageHistoryDaysSettingKey = "image_history_days" var primaryActionSettingKey = "primary_action" var primaryActionValueCopy = "copy" var primaryActionValuePaste = "paste" +var favoritesSettingKey = "favorites" func init() { plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &ClipboardPlugin{ @@ -44,9 +45,38 @@ type ImageCacheEntry struct { Icon common.WoxImage } +// FavoriteClipboardItem represents a favorite clipboard item stored in settings +type FavoriteClipboardItem struct { + ID string `json:"id"` + Type string `json:"type"` + Content string `json:"content"` + FilePath string `json:"filePath,omitempty"` + IconData *string `json:"iconData,omitempty"` + Width *int `json:"width,omitempty"` + Height *int `json:"height,omitempty"` + FileSize *int64 `json:"fileSize,omitempty"` + Timestamp int64 `json:"timestamp"` + CreatedAt int64 `json:"createdAt"` +} + +// ClipboardDBInterface defines the interface for clipboard database operations +type ClipboardDBInterface interface { + Insert(ctx context.Context, record ClipboardRecord) error + Update(ctx context.Context, record ClipboardRecord) error + UpdateTimestamp(ctx context.Context, id string, timestamp int64) error + Delete(ctx context.Context, id string) error + GetRecent(ctx context.Context, limit, offset int) ([]ClipboardRecord, error) + SearchText(ctx context.Context, searchTerm string, limit int) ([]ClipboardRecord, error) + GetByID(ctx context.Context, id string) (*ClipboardRecord, error) + DeleteExpired(ctx context.Context, textDays, imageDays int) (int64, error) + EnforceMaxCount(ctx context.Context, maxCount int) (int64, error) + GetStats(ctx context.Context) (map[string]int, error) + Close() error +} + type ClipboardPlugin struct { api plugin.API - db *ClipboardDB + db ClipboardDBInterface maxHistoryCount int // Cache for generated preview and icon images to avoid regeneration imageCache map[string]*ImageCacheEntry @@ -161,25 +191,8 @@ func (c *ClipboardPlugin) Init(ctx context.Context, initParams plugin.InitParams } c.db = db - // Migrate legacy clipboard data from plugin settings - historyJson := c.api.GetSetting(ctx, "history") - if historyJson != "" { - var history []ClipboardHistory - unmarshalErr := json.Unmarshal([]byte(historyJson), &history) - if unmarshalErr != nil { - c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to unmarshal legacy clipboard history: %s", unmarshalErr.Error())) - } else { - // Migrate legacy data - for _, item := range history { - if item.IsFavorite { - if err := c.db.migrateLegacyItem(ctx, item); err != nil { - c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to migrate legacy clipboard item: %s", err.Error())) - } - } - } - } - c.api.SaveSetting(ctx, "history", "", false) - } + // Migration is now handled by the central migrator during app startup + // No need for plugin-specific migration code here // Register unload callback to close database connection c.api.OnUnload(ctx, func() { @@ -225,7 +238,7 @@ func (c *ClipboardPlugin) Init(ctx context.Context, initParams plugin.InitParams return } - // Create new record + // Create new record (always non-favorite initially) record := ClipboardRecord{ ID: uuid.NewString(), Type: string(data.GetType()), @@ -272,7 +285,7 @@ func (c *ClipboardPlugin) Init(ctx context.Context, initParams plugin.InitParams c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("saved clipboard image to disk: %s", imageFilePath)) } - // Insert into database + // Insert into database (non-favorite items only) if err := c.db.Insert(ctx, record); err != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to insert clipboard record: %s", err.Error())) return @@ -298,39 +311,40 @@ func (c *ClipboardPlugin) Query(ctx context.Context, query plugin.Query) []plugi } if query.Command == "fav" { - // Get favorite records from database - favorites, err := c.db.GetFavorites(ctx) + // Get favorite records from settings + favorites, err := c.getFavoriteItems(ctx) if err != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get favorites: %s", err.Error())) return results } - for _, record := range favorites { + for _, favoriteItem := range favorites { + record := c.convertFavoriteToRecord(favoriteItem) results = append(results, c.convertRecordToResult(ctx, record, query)) } return results } if query.Search == "" { - // Get favorites first - favorites, err := c.db.GetFavorites(ctx) + // Get favorites first from settings + favorites, err := c.getFavoriteItems(ctx) if err != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get favorites: %s", err.Error())) } else { - for _, record := range favorites { + for _, favoriteItem := range favorites { + record := c.convertFavoriteToRecord(favoriteItem) results = append(results, c.convertRecordToResult(ctx, record, query)) } } - // Get recent non-favorite records + // Get recent non-favorite records from database recent, err := c.db.GetRecent(ctx, 50, 0) if err != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get recent records: %s", err.Error())) } else { for _, record := range recent { - if !record.IsFavorite { - results = append(results, c.convertRecordToResult(ctx, record, query)) - } + // All records in database are non-favorite now + results = append(results, c.convertRecordToResult(ctx, record, query)) } } @@ -338,13 +352,29 @@ func (c *ClipboardPlugin) Query(ctx context.Context, query plugin.Query) []plugi } // Search in text content + var allResults []ClipboardRecord + + // Search in favorites from settings + favorites, err := c.getFavoriteItems(ctx) + if err == nil { + for _, favoriteItem := range favorites { + if favoriteItem.Type == string(clipboard.ClipboardTypeText) && + strings.Contains(strings.ToLower(favoriteItem.Content), strings.ToLower(query.Search)) { + record := c.convertFavoriteToRecord(favoriteItem) + allResults = append(allResults, record) + } + } + } + + // Search in database records searchResults, err := c.db.SearchText(ctx, query.Search, 100) if err != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to search text: %s", err.Error())) - return results + } else { + allResults = append(allResults, searchResults...) } - for _, record := range searchResults { + for _, record := range allResults { results = append(results, c.convertRecordToResult(ctx, record, query)) } @@ -353,22 +383,52 @@ func (c *ClipboardPlugin) Query(ctx context.Context, query plugin.Query) []plugi // isDuplicateContent checks if the content is duplicate by comparing with the most recent record func (c *ClipboardPlugin) isDuplicateContent(ctx context.Context, data clipboard.Data) bool { + // Check most recent record from database recent, err := c.db.GetRecent(ctx, 1, 0) - if err != nil || len(recent) == 0 { + var lastRecord *ClipboardRecord + if err == nil && len(recent) > 0 { + lastRecord = &recent[0] + } + + // Check most recent favorite from settings + favorites, err := c.getFavoriteItems(ctx) + var lastFavorite *FavoriteClipboardItem + if err == nil && len(favorites) > 0 { + // Find the most recent favorite by timestamp + for i := range favorites { + if lastFavorite == nil || favorites[i].Timestamp > lastFavorite.Timestamp { + lastFavorite = &favorites[i] + } + } + } + + // Determine which is more recent + var mostRecentRecord *ClipboardRecord + if lastRecord != nil && lastFavorite != nil { + if lastRecord.Timestamp > lastFavorite.Timestamp { + mostRecentRecord = lastRecord + } else { + favoriteRecord := c.convertFavoriteToRecord(*lastFavorite) + mostRecentRecord = &favoriteRecord + } + } else if lastRecord != nil { + mostRecentRecord = lastRecord + } else if lastFavorite != nil { + favoriteRecord := c.convertFavoriteToRecord(*lastFavorite) + mostRecentRecord = &favoriteRecord + } else { return false } - lastRecord := recent[0] - - if lastRecord.Type != string(data.GetType()) { + if mostRecentRecord.Type != string(data.GetType()) { return false } if data.GetType() == clipboard.ClipboardTypeText { textData := data.(*clipboard.TextData) - if lastRecord.Content == textData.Text { + if mostRecentRecord.Content == textData.Text { // Update timestamp of existing record - c.db.UpdateTimestamp(ctx, lastRecord.ID, util.GetSystemTimestamp()) + c.updateRecordTimestamp(ctx, mostRecentRecord, util.GetSystemTimestamp()) return true } } @@ -376,9 +436,9 @@ func (c *ClipboardPlugin) isDuplicateContent(ctx context.Context, data clipboard if data.GetType() == clipboard.ClipboardTypeImage { imageData := data.(*clipboard.ImageData) currentSize := fmt.Sprintf("image(%dx%d)", imageData.Image.Bounds().Dx(), imageData.Image.Bounds().Dy()) - if lastRecord.Content == currentSize { + if mostRecentRecord.Content == currentSize { // Update timestamp of existing record - c.db.UpdateTimestamp(ctx, lastRecord.ID, util.GetSystemTimestamp()) + c.updateRecordTimestamp(ctx, mostRecentRecord, util.GetSystemTimestamp()) return true } } @@ -430,7 +490,7 @@ func (c *ClipboardPlugin) convertTextRecord(ctx context.Context, record Clipboar Icon: plugin.AddToFavIcon, PreventHideAfterAction: true, Action: func(ctx context.Context, actionContext plugin.ActionContext) { - if err := c.db.SetFavorite(ctx, record.ID, true); err != nil { + if err := c.markAsFavorite(ctx, record); err != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to set favorite: %s", err.Error())) } else { c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("marked record as favorite: %s", record.ID)) @@ -444,7 +504,7 @@ func (c *ClipboardPlugin) convertTextRecord(ctx context.Context, record Clipboar Icon: plugin.RemoveFromFavIcon, PreventHideAfterAction: true, Action: func(ctx context.Context, actionContext plugin.ActionContext) { - if err := c.db.SetFavorite(ctx, record.ID, false); err != nil { + if err := c.cancelFavorite(ctx, record.ID); err != nil { c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to cancel favorite: %s", err.Error())) } else { c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("cancelled record favorite: %s", record.ID)) @@ -852,3 +912,174 @@ func (c *ClipboardPlugin) formatFileSize(bytes int64) string { } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } + +// getFavoriteItems retrieves favorite items from settings +func (c *ClipboardPlugin) getFavoriteItems(ctx context.Context) ([]FavoriteClipboardItem, error) { + favoritesJson := c.api.GetSetting(ctx, favoritesSettingKey) + if favoritesJson == "" { + return []FavoriteClipboardItem{}, nil + } + + var favorites []FavoriteClipboardItem + if err := json.Unmarshal([]byte(favoritesJson), &favorites); err != nil { + c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to unmarshal favorites: %s", err.Error())) + return []FavoriteClipboardItem{}, nil + } + + return favorites, nil +} + +// saveFavoriteItems saves favorite items to settings +func (c *ClipboardPlugin) saveFavoriteItems(ctx context.Context, favorites []FavoriteClipboardItem) error { + favoritesJson, err := json.Marshal(favorites) + if err != nil { + return fmt.Errorf("failed to marshal favorites: %w", err) + } + + c.api.SaveSetting(ctx, favoritesSettingKey, string(favoritesJson), false) + return nil +} + +// addToFavorites adds an item to favorites settings +func (c *ClipboardPlugin) addToFavorites(ctx context.Context, record ClipboardRecord) error { + favorites, err := c.getFavoriteItems(ctx) + if err != nil { + return err + } + + // Check if already exists + for _, fav := range favorites { + if fav.ID == record.ID { + return nil // Already exists + } + } + + // Convert ClipboardRecord to FavoriteClipboardItem + favoriteItem := FavoriteClipboardItem{ + ID: record.ID, + Type: record.Type, + Content: record.Content, + FilePath: record.FilePath, + IconData: record.IconData, + Width: record.Width, + Height: record.Height, + FileSize: record.FileSize, + Timestamp: record.Timestamp, + CreatedAt: record.CreatedAt.Unix(), + } + + favorites = append(favorites, favoriteItem) + return c.saveFavoriteItems(ctx, favorites) +} + +// removeFromFavorites removes an item from favorites settings +func (c *ClipboardPlugin) removeFromFavorites(ctx context.Context, id string) error { + favorites, err := c.getFavoriteItems(ctx) + if err != nil { + return err + } + + // Find and remove the item + for i, fav := range favorites { + if fav.ID == id { + favorites = append(favorites[:i], favorites[i+1:]...) + break + } + } + + return c.saveFavoriteItems(ctx, favorites) +} + +// convertFavoriteToRecord converts FavoriteClipboardItem to ClipboardRecord +func (c *ClipboardPlugin) convertFavoriteToRecord(item FavoriteClipboardItem) ClipboardRecord { + return ClipboardRecord{ + ID: item.ID, + Type: item.Type, + Content: item.Content, + FilePath: item.FilePath, + IconData: item.IconData, + Width: item.Width, + Height: item.Height, + FileSize: item.FileSize, + Timestamp: item.Timestamp, + IsFavorite: true, + CreatedAt: time.Unix(item.CreatedAt, 0), + } +} + +// markAsFavorite moves an item from database to favorites settings +func (c *ClipboardPlugin) markAsFavorite(ctx context.Context, record ClipboardRecord) error { + // Add to favorites settings + if err := c.addToFavorites(ctx, record); err != nil { + return fmt.Errorf("failed to add to favorites: %w", err) + } + + // Remove from database if it exists there + if err := c.db.Delete(ctx, record.ID); err != nil { + c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("failed to remove from database (may not exist): %s", err.Error())) + } + + return nil +} + +// cancelFavorite moves an item from favorites settings to database +func (c *ClipboardPlugin) cancelFavorite(ctx context.Context, id string) error { + // Get the favorite item first + favorites, err := c.getFavoriteItems(ctx) + if err != nil { + return fmt.Errorf("failed to get favorites: %w", err) + } + + var favoriteItem *FavoriteClipboardItem + for _, fav := range favorites { + if fav.ID == id { + favoriteItem = &fav + break + } + } + + if favoriteItem == nil { + return fmt.Errorf("favorite item not found: %s", id) + } + + // Convert to ClipboardRecord and add to database + record := c.convertFavoriteToRecord(*favoriteItem) + record.IsFavorite = false // Mark as non-favorite + if err := c.db.Insert(ctx, record); err != nil { + return fmt.Errorf("failed to insert to database: %w", err) + } + + // Remove from favorites settings + if err := c.removeFromFavorites(ctx, id); err != nil { + return fmt.Errorf("failed to remove from favorites: %w", err) + } + + return nil +} + +// updateRecordTimestamp updates the timestamp of a record in the appropriate storage +func (c *ClipboardPlugin) updateRecordTimestamp(ctx context.Context, record *ClipboardRecord, timestamp int64) { + if record.IsFavorite { + // Update in favorites settings + favorites, err := c.getFavoriteItems(ctx) + if err != nil { + c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get favorites for timestamp update: %s", err.Error())) + return + } + + for i := range favorites { + if favorites[i].ID == record.ID { + favorites[i].Timestamp = timestamp + if err := c.saveFavoriteItems(ctx, favorites); err != nil { + c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to save favorites after timestamp update: %s", err.Error())) + } + return + } + } + } else { + // Update in database + if err := c.db.UpdateTimestamp(ctx, record.ID, timestamp); err != nil { + c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to update timestamp in database: %s", err.Error())) + } + } +} diff --git a/wox.core/plugin/system/clipboard/clipboard_db.go b/wox.core/plugin/system/clipboard/clipboard_db.go index 4b78a4e7..d5a2bae3 100644 --- a/wox.core/plugin/system/clipboard/clipboard_db.go +++ b/wox.core/plugin/system/clipboard/clipboard_db.go @@ -4,14 +4,12 @@ import ( "context" "database/sql" "fmt" - "os" "path" "strings" "time" "wox/util" "wox/util/clipboard" - "github.com/disintegration/imaging" _ "github.com/mattn/go-sqlite3" ) @@ -38,17 +36,48 @@ type ClipboardRecord struct { // NewClipboardDB creates a new clipboard database instance func NewClipboardDB(ctx context.Context, pluginId string) (*ClipboardDB, error) { dbPath := path.Join(util.GetLocation().GetPluginSettingDirectory(), pluginId+"_clipboard.db") - db, err := sql.Open("sqlite3", dbPath) + + // Configure SQLite with proper concurrency settings + dsn := dbPath + "?" + + "_journal_mode=WAL&" + // Enable WAL mode for better concurrency + "_synchronous=NORMAL&" + // Balance between safety and performance + "_cache_size=1000&" + // Set cache size + "_foreign_keys=true&" + // Enable foreign key constraints + "_busy_timeout=5000" // Set busy timeout to 5 seconds + + db, err := sql.Open("sqlite3", dsn) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } + // Set connection pool settings for better concurrency + db.SetMaxOpenConns(10) // Maximum number of open connections + db.SetMaxIdleConns(5) // Maximum number of idle connections + db.SetConnMaxLifetime(time.Hour) // Maximum lifetime of a connection + + // Execute additional PRAGMA statements for optimal concurrency + pragmas := []string{ + "PRAGMA journal_mode=WAL", // Ensure WAL mode is enabled + "PRAGMA synchronous=NORMAL", // Balance safety and performance + "PRAGMA cache_size=1000", // Set cache size + "PRAGMA foreign_keys=ON", // Enable foreign key constraints + "PRAGMA temp_store=memory", // Store temporary tables in memory + "PRAGMA mmap_size=268435456", // Set memory-mapped I/O size (256MB) + } + + for _, pragma := range pragmas { + if _, err := db.Exec(pragma); err != nil { + util.GetLogger().Warn(ctx, fmt.Sprintf("failed to execute pragma %s: %v", pragma, err)) + } + } + clipboardDB := &ClipboardDB{db: db} if err := clipboardDB.initTables(ctx); err != nil { db.Close() return nil, fmt.Errorf("failed to initialize tables: %w", err) } + util.GetLogger().Info(ctx, fmt.Sprintf("clipboard database initialized at %s with WAL mode enabled", dbPath)) return clipboardDB, nil } @@ -131,13 +160,6 @@ func (c *ClipboardDB) Update(ctx context.Context, record ClipboardRecord) error return err } -// SetFavorite updates the favorite status of a record -func (c *ClipboardDB) SetFavorite(ctx context.Context, id string, isFavorite bool) error { - updateSQL := `UPDATE clipboard_history SET is_favorite = ? WHERE id = ?` - _, err := c.db.ExecContext(ctx, updateSQL, isFavorite, id) - return err -} - // UpdateTimestamp updates the timestamp of a record (for moving to top) func (c *ClipboardDB) UpdateTimestamp(ctx context.Context, id string, timestamp int64) error { updateSQL := `UPDATE clipboard_history SET timestamp = ? WHERE id = ?` @@ -145,6 +167,13 @@ func (c *ClipboardDB) UpdateTimestamp(ctx context.Context, id string, timestamp return err } +// Delete removes a record by ID +func (c *ClipboardDB) Delete(ctx context.Context, id string) error { + deleteSQL := `DELETE FROM clipboard_history WHERE id = ?` + _, err := c.db.ExecContext(ctx, deleteSQL, id) + return err +} + // GetRecent retrieves recent clipboard records with pagination func (c *ClipboardDB) GetRecent(ctx context.Context, limit, offset int) ([]ClipboardRecord, error) { querySQL := ` @@ -163,24 +192,6 @@ func (c *ClipboardDB) GetRecent(ctx context.Context, limit, offset int) ([]Clipb return c.scanRecords(rows) } -// GetFavorites retrieves all favorite records -func (c *ClipboardDB) GetFavorites(ctx context.Context) ([]ClipboardRecord, error) { - querySQL := ` - SELECT id, type, content, file_path, icon_data, width, height, file_size, timestamp, is_favorite, created_at - FROM clipboard_history - WHERE is_favorite = TRUE - ORDER BY timestamp DESC - ` - - rows, err := c.db.QueryContext(ctx, querySQL) - if err != nil { - return nil, err - } - defer rows.Close() - - return c.scanRecords(rows) -} - // SearchText searches for text content in clipboard history func (c *ClipboardDB) SearchText(ctx context.Context, searchTerm string, limit int) ([]ClipboardRecord, error) { querySQL := ` @@ -340,99 +351,6 @@ type ClipboardHistory struct { IsFavorite bool `json:"isFavorite,omitempty"` } -// migrateLegacyItem migrates a single legacy clipboard item to the database -func (db *ClipboardDB) migrateLegacyItem(ctx context.Context, item ClipboardHistory) error { - // Check if item already exists - exists, err := db.itemExists(ctx, item.ID) - if err != nil { - return fmt.Errorf("failed to check if item exists: %w", err) - } - if exists { - util.GetLogger().Debug(ctx, fmt.Sprintf("Item %s already exists, skipping", item.ID)) - return nil - } - - // Convert legacy type to new type - var itemType clipboard.Type - switch item.Type { - case "text": - itemType = clipboard.ClipboardTypeText - case "image": - itemType = clipboard.ClipboardTypeImage - default: - itemType = clipboard.ClipboardTypeText - } - - // Handle image migration - var imagePath string - if itemType == clipboard.ClipboardTypeImage && item.ImagePath != "" { - // Check if old image file exists - if _, err := os.Stat(item.ImagePath); err == nil { - // Copy image to new location - newImagePath, err := db.copyImageToNewLocation(ctx, item.ImagePath, item.ID) - if err != nil { - util.GetLogger().Warn(ctx, fmt.Sprintf("Failed to copy image for item %s: %v", item.ID, err)) - // Continue with text content if image copy fails - itemType = clipboard.ClipboardTypeText - } else { - imagePath = newImagePath - } - } else { - util.GetLogger().Warn(ctx, fmt.Sprintf("Legacy image file not found: %s", item.ImagePath)) - // Continue with text content if image file is missing - itemType = clipboard.ClipboardTypeText - } - } - - // Insert into database with is_favorite set to true (since we only migrate favorites) - query := `INSERT INTO clipboard_history (id, content, type, file_path, timestamp, is_favorite) VALUES (?, ?, ?, ?, ?, ?)` - _, err = db.db.ExecContext(ctx, query, item.ID, item.Text, string(itemType), imagePath, item.Timestamp, true) - if err != nil { - return fmt.Errorf("failed to insert migrated item: %w", err) - } - - util.GetLogger().Debug(ctx, fmt.Sprintf("Successfully migrated item %s", item.ID)) - return nil -} - -// itemExists checks if an item with the given ID already exists in the database -func (db *ClipboardDB) itemExists(ctx context.Context, id string) (bool, error) { - query := `SELECT COUNT(*) FROM clipboard_history WHERE id = ?` - var count int - err := db.db.QueryRowContext(ctx, query, id).Scan(&count) - if err != nil { - return false, err - } - return count > 0, nil -} - -// copyImageToNewLocation copies an image from the old location to the new temp directory structure -func (db *ClipboardDB) copyImageToNewLocation(ctx context.Context, oldPath, itemID string) (string, error) { - // Create temp directory if it doesn't exist - tempDir := path.Join(os.TempDir(), "wox_clipboard") - if err := os.MkdirAll(tempDir, 0755); err != nil { - return "", fmt.Errorf("failed to create temp directory: %w", err) - } - - // Generate new filename - newFilename := fmt.Sprintf("%s.png", itemID) - newPath := path.Join(tempDir, newFilename) - - // Open source image - srcImg, err := imaging.Open(oldPath) - if err != nil { - return "", fmt.Errorf("failed to open source image: %w", err) - } - - // Save to new location - if err := imaging.Save(srcImg, newPath); err != nil { - return "", fmt.Errorf("failed to save image to new location: %w", err) - } - - util.GetLogger().Debug(ctx, fmt.Sprintf("Copied image from %s to %s", oldPath, newPath)) - return newPath, nil -} - // scanRecords is a helper function to scan multiple records from query results func (c *ClipboardDB) scanRecords(rows *sql.Rows) ([]ClipboardRecord, error) { var records []ClipboardRecord diff --git a/wox.core/setting/store.go b/wox.core/setting/store.go index 00189a66..d441740e 100644 --- a/wox.core/setting/store.go +++ b/wox.core/setting/store.go @@ -37,7 +37,7 @@ func NewWoxSettingStore(db *gorm.DB) *WoxSettingStore { func (s *WoxSettingStore) Get(key string, target interface{}) error { var setting database.WoxSetting if err := s.db.Where("key = ?", key).First(&setting).Error; err != nil { - logger.Error(context.Background(), fmt.Sprintf("Failed to read wox setting %s: %v", key, err)) + util.GetLogger().Error(context.Background(), fmt.Sprintf("Failed to read wox setting %s: %v", key, err)) return err } @@ -90,7 +90,7 @@ func NewPluginSettingStore(db *gorm.DB, pluginId string) *PluginSettingStore { func (s *PluginSettingStore) Get(key string, target interface{}) error { var setting database.PluginSetting if err := s.db.Where("plugin_id = ? AND key = ?", s.pluginId, key).First(&setting).Error; err != nil { - logger.Error(context.Background(), fmt.Sprintf("Failed to read plugin setting %s.%s: %v", s.pluginId, key, err)) + util.GetLogger().Error(context.Background(), fmt.Sprintf("Failed to read plugin setting %s.%s: %v", s.pluginId, key, err)) return err }