mirror of https://github.com/Wox-launcher/Wox
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:
parent
734d0f0830
commit
5be879167f
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
"wox/util"
|
"wox/util"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
|
@ -38,14 +39,49 @@ type Oplog struct {
|
||||||
func Init(ctx context.Context) error {
|
func Init(ctx context.Context) error {
|
||||||
dbPath := filepath.Join(util.GetLocation().GetUserDataDirectory(), "wox.db")
|
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
|
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),
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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(
|
err = db.AutoMigrate(
|
||||||
&WoxSetting{},
|
&WoxSetting{},
|
||||||
&PluginSetting{},
|
&PluginSetting{},
|
||||||
|
@ -55,7 +91,7 @@ func Init(ctx context.Context) error {
|
||||||
return fmt.Errorf("failed to migrate database schema: %w", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,22 @@ package migration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"wox/common"
|
"wox/common"
|
||||||
"wox/database"
|
"wox/database"
|
||||||
"wox/i18n"
|
"wox/i18n"
|
||||||
"wox/setting"
|
"wox/setting"
|
||||||
"wox/util"
|
"wox/util"
|
||||||
"wox/util/locale"
|
"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
|
// 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.")
|
util.GetLogger().Info(ctx, "Successfully migrated old configuration to the new database.")
|
||||||
return nil
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ var imageHistoryDaysSettingKey = "image_history_days"
|
||||||
var primaryActionSettingKey = "primary_action"
|
var primaryActionSettingKey = "primary_action"
|
||||||
var primaryActionValueCopy = "copy"
|
var primaryActionValueCopy = "copy"
|
||||||
var primaryActionValuePaste = "paste"
|
var primaryActionValuePaste = "paste"
|
||||||
|
var favoritesSettingKey = "favorites"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &ClipboardPlugin{
|
plugin.AllSystemPlugin = append(plugin.AllSystemPlugin, &ClipboardPlugin{
|
||||||
|
@ -44,9 +45,38 @@ type ImageCacheEntry struct {
|
||||||
Icon common.WoxImage
|
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 {
|
type ClipboardPlugin struct {
|
||||||
api plugin.API
|
api plugin.API
|
||||||
db *ClipboardDB
|
db ClipboardDBInterface
|
||||||
maxHistoryCount int
|
maxHistoryCount int
|
||||||
// Cache for generated preview and icon images to avoid regeneration
|
// Cache for generated preview and icon images to avoid regeneration
|
||||||
imageCache map[string]*ImageCacheEntry
|
imageCache map[string]*ImageCacheEntry
|
||||||
|
@ -161,25 +191,8 @@ func (c *ClipboardPlugin) Init(ctx context.Context, initParams plugin.InitParams
|
||||||
}
|
}
|
||||||
c.db = db
|
c.db = db
|
||||||
|
|
||||||
// Migrate legacy clipboard data from plugin settings
|
// Migration is now handled by the central migrator during app startup
|
||||||
historyJson := c.api.GetSetting(ctx, "history")
|
// No need for plugin-specific migration code here
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register unload callback to close database connection
|
// Register unload callback to close database connection
|
||||||
c.api.OnUnload(ctx, func() {
|
c.api.OnUnload(ctx, func() {
|
||||||
|
@ -225,7 +238,7 @@ func (c *ClipboardPlugin) Init(ctx context.Context, initParams plugin.InitParams
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new record
|
// Create new record (always non-favorite initially)
|
||||||
record := ClipboardRecord{
|
record := ClipboardRecord{
|
||||||
ID: uuid.NewString(),
|
ID: uuid.NewString(),
|
||||||
Type: string(data.GetType()),
|
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))
|
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 {
|
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()))
|
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to insert clipboard record: %s", err.Error()))
|
||||||
return
|
return
|
||||||
|
@ -298,39 +311,40 @@ func (c *ClipboardPlugin) Query(ctx context.Context, query plugin.Query) []plugi
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Command == "fav" {
|
if query.Command == "fav" {
|
||||||
// Get favorite records from database
|
// Get favorite records from settings
|
||||||
favorites, err := c.db.GetFavorites(ctx)
|
favorites, err := c.getFavoriteItems(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get favorites: %s", err.Error()))
|
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get favorites: %s", err.Error()))
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, record := range favorites {
|
for _, favoriteItem := range favorites {
|
||||||
|
record := c.convertFavoriteToRecord(favoriteItem)
|
||||||
results = append(results, c.convertRecordToResult(ctx, record, query))
|
results = append(results, c.convertRecordToResult(ctx, record, query))
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Search == "" {
|
if query.Search == "" {
|
||||||
// Get favorites first
|
// Get favorites first from settings
|
||||||
favorites, err := c.db.GetFavorites(ctx)
|
favorites, err := c.getFavoriteItems(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get favorites: %s", err.Error()))
|
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get favorites: %s", err.Error()))
|
||||||
} else {
|
} else {
|
||||||
for _, record := range favorites {
|
for _, favoriteItem := range favorites {
|
||||||
|
record := c.convertFavoriteToRecord(favoriteItem)
|
||||||
results = append(results, c.convertRecordToResult(ctx, record, query))
|
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)
|
recent, err := c.db.GetRecent(ctx, 50, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get recent records: %s", err.Error()))
|
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to get recent records: %s", err.Error()))
|
||||||
} else {
|
} else {
|
||||||
for _, record := range recent {
|
for _, record := range recent {
|
||||||
if !record.IsFavorite {
|
// All records in database are non-favorite now
|
||||||
results = append(results, c.convertRecordToResult(ctx, record, query))
|
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
|
// 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)
|
searchResults, err := c.db.SearchText(ctx, query.Search, 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to search text: %s", err.Error()))
|
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))
|
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
|
// 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 {
|
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)
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRecord := recent[0]
|
if mostRecentRecord.Type != string(data.GetType()) {
|
||||||
|
|
||||||
if lastRecord.Type != string(data.GetType()) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.GetType() == clipboard.ClipboardTypeText {
|
if data.GetType() == clipboard.ClipboardTypeText {
|
||||||
textData := data.(*clipboard.TextData)
|
textData := data.(*clipboard.TextData)
|
||||||
if lastRecord.Content == textData.Text {
|
if mostRecentRecord.Content == textData.Text {
|
||||||
// Update timestamp of existing record
|
// Update timestamp of existing record
|
||||||
c.db.UpdateTimestamp(ctx, lastRecord.ID, util.GetSystemTimestamp())
|
c.updateRecordTimestamp(ctx, mostRecentRecord, util.GetSystemTimestamp())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -376,9 +436,9 @@ func (c *ClipboardPlugin) isDuplicateContent(ctx context.Context, data clipboard
|
||||||
if data.GetType() == clipboard.ClipboardTypeImage {
|
if data.GetType() == clipboard.ClipboardTypeImage {
|
||||||
imageData := data.(*clipboard.ImageData)
|
imageData := data.(*clipboard.ImageData)
|
||||||
currentSize := fmt.Sprintf("image(%dx%d)", imageData.Image.Bounds().Dx(), imageData.Image.Bounds().Dy())
|
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
|
// Update timestamp of existing record
|
||||||
c.db.UpdateTimestamp(ctx, lastRecord.ID, util.GetSystemTimestamp())
|
c.updateRecordTimestamp(ctx, mostRecentRecord, util.GetSystemTimestamp())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -430,7 +490,7 @@ func (c *ClipboardPlugin) convertTextRecord(ctx context.Context, record Clipboar
|
||||||
Icon: plugin.AddToFavIcon,
|
Icon: plugin.AddToFavIcon,
|
||||||
PreventHideAfterAction: true,
|
PreventHideAfterAction: true,
|
||||||
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
|
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()))
|
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to set favorite: %s", err.Error()))
|
||||||
} else {
|
} else {
|
||||||
c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("marked record as favorite: %s", record.ID))
|
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,
|
Icon: plugin.RemoveFromFavIcon,
|
||||||
PreventHideAfterAction: true,
|
PreventHideAfterAction: true,
|
||||||
Action: func(ctx context.Context, actionContext plugin.ActionContext) {
|
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()))
|
c.api.Log(ctx, plugin.LogLevelError, fmt.Sprintf("failed to cancel favorite: %s", err.Error()))
|
||||||
} else {
|
} else {
|
||||||
c.api.Log(ctx, plugin.LogLevelInfo, fmt.Sprintf("cancelled record favorite: %s", record.ID))
|
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])
|
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()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,14 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"wox/util"
|
"wox/util"
|
||||||
"wox/util/clipboard"
|
"wox/util/clipboard"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,17 +36,48 @@ type ClipboardRecord struct {
|
||||||
// NewClipboardDB creates a new clipboard database instance
|
// NewClipboardDB creates a new clipboard database instance
|
||||||
func NewClipboardDB(ctx context.Context, pluginId string) (*ClipboardDB, error) {
|
func NewClipboardDB(ctx context.Context, pluginId string) (*ClipboardDB, error) {
|
||||||
dbPath := path.Join(util.GetLocation().GetPluginSettingDirectory(), pluginId+"_clipboard.db")
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
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}
|
clipboardDB := &ClipboardDB{db: db}
|
||||||
if err := clipboardDB.initTables(ctx); err != nil {
|
if err := clipboardDB.initTables(ctx); err != nil {
|
||||||
db.Close()
|
db.Close()
|
||||||
return nil, fmt.Errorf("failed to initialize tables: %w", err)
|
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
|
return clipboardDB, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,13 +160,6 @@ func (c *ClipboardDB) Update(ctx context.Context, record ClipboardRecord) error
|
||||||
return err
|
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)
|
// UpdateTimestamp updates the timestamp of a record (for moving to top)
|
||||||
func (c *ClipboardDB) UpdateTimestamp(ctx context.Context, id string, timestamp int64) error {
|
func (c *ClipboardDB) UpdateTimestamp(ctx context.Context, id string, timestamp int64) error {
|
||||||
updateSQL := `UPDATE clipboard_history SET timestamp = ? WHERE id = ?`
|
updateSQL := `UPDATE clipboard_history SET timestamp = ? WHERE id = ?`
|
||||||
|
@ -145,6 +167,13 @@ func (c *ClipboardDB) UpdateTimestamp(ctx context.Context, id string, timestamp
|
||||||
return err
|
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
|
// GetRecent retrieves recent clipboard records with pagination
|
||||||
func (c *ClipboardDB) GetRecent(ctx context.Context, limit, offset int) ([]ClipboardRecord, error) {
|
func (c *ClipboardDB) GetRecent(ctx context.Context, limit, offset int) ([]ClipboardRecord, error) {
|
||||||
querySQL := `
|
querySQL := `
|
||||||
|
@ -163,24 +192,6 @@ func (c *ClipboardDB) GetRecent(ctx context.Context, limit, offset int) ([]Clipb
|
||||||
return c.scanRecords(rows)
|
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
|
// SearchText searches for text content in clipboard history
|
||||||
func (c *ClipboardDB) SearchText(ctx context.Context, searchTerm string, limit int) ([]ClipboardRecord, error) {
|
func (c *ClipboardDB) SearchText(ctx context.Context, searchTerm string, limit int) ([]ClipboardRecord, error) {
|
||||||
querySQL := `
|
querySQL := `
|
||||||
|
@ -340,99 +351,6 @@ type ClipboardHistory struct {
|
||||||
IsFavorite bool `json:"isFavorite,omitempty"`
|
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
|
// scanRecords is a helper function to scan multiple records from query results
|
||||||
func (c *ClipboardDB) scanRecords(rows *sql.Rows) ([]ClipboardRecord, error) {
|
func (c *ClipboardDB) scanRecords(rows *sql.Rows) ([]ClipboardRecord, error) {
|
||||||
var records []ClipboardRecord
|
var records []ClipboardRecord
|
||||||
|
|
|
@ -37,7 +37,7 @@ func NewWoxSettingStore(db *gorm.DB) *WoxSettingStore {
|
||||||
func (s *WoxSettingStore) Get(key string, target interface{}) error {
|
func (s *WoxSettingStore) Get(key string, target interface{}) error {
|
||||||
var setting database.WoxSetting
|
var setting database.WoxSetting
|
||||||
if err := s.db.Where("key = ?", key).First(&setting).Error; err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ func NewPluginSettingStore(db *gorm.DB, pluginId string) *PluginSettingStore {
|
||||||
func (s *PluginSettingStore) Get(key string, target interface{}) error {
|
func (s *PluginSettingStore) Get(key string, target interface{}) error {
|
||||||
var setting database.PluginSetting
|
var setting database.PluginSetting
|
||||||
if err := s.db.Where("plugin_id = ? AND key = ?", s.pluginId, key).First(&setting).Error; err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue