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.
This commit is contained in:
qianlifeng 2025-07-17 22:53:01 +08:00
parent 734d0f0830
commit 5be879167f
No known key found for this signature in database
5 changed files with 588 additions and 169 deletions

View File

@ -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
}

View File

@ -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()
}

View File

@ -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,53 +311,70 @@ 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 {
// All records in database are non-favorite now
results = append(results, c.convertRecordToResult(ctx, record, query))
}
}
}
return results
}
// 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()))
}
}
}

View File

@ -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

View File

@ -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
}