cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 181 lines 5.1 kB view raw
1package store 2 3import ( 4 "database/sql" 5 "embed" 6 "fmt" 7 "os" 8 "path/filepath" 9 "runtime" 10 11 _ "github.com/mattn/go-sqlite3" 12) 13 14var ( 15 sqlOpen = sql.Open 16 pragmaExec = func(db *sql.DB, stmt string) (sql.Result, error) { return db.Exec(stmt) } 17 createMigrationRunner = CreateMigrationRunner 18 getRuntime = func() string { return runtime.GOOS } 19 getHomeDir = os.UserHomeDir 20 mkdirAll = os.MkdirAll 21) 22 23//go:embed sql/migrations 24var migrationFiles embed.FS 25 26// Database wraps [sql.DB] with application-specific methods 27type Database struct { 28 *sql.DB 29 path string 30} 31 32// GetConfigDir returns the appropriate configuration directory based on [runtime.GOOS] 33var GetConfigDir = func() (string, error) { 34 var configDir string 35 36 switch getRuntime() { 37 case "windows": 38 appData := os.Getenv("APPDATA") 39 if appData == "" { 40 return "", fmt.Errorf("APPDATA environment variable not set") 41 } 42 configDir = filepath.Join(appData, "noteleaf") 43 case "darwin": 44 homeDir, err := getHomeDir() 45 if err != nil { 46 return "", fmt.Errorf("failed to get user home directory: %w", err) 47 } 48 configDir = filepath.Join(homeDir, "Library", "Application Support", "noteleaf") 49 default: 50 xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") 51 if xdgConfigHome == "" { 52 homeDir, err := getHomeDir() 53 if err != nil { 54 return "", fmt.Errorf("failed to get user home directory: %w", err) 55 } 56 xdgConfigHome = filepath.Join(homeDir, ".config") 57 } 58 configDir = filepath.Join(xdgConfigHome, "noteleaf") 59 } 60 61 if err := mkdirAll(configDir, 0755); err != nil { 62 return "", fmt.Errorf("failed to create config directory: %w", err) 63 } 64 65 return configDir, nil 66} 67 68// GetDataDir returns the appropriate data directory based on [runtime.GOOS] or NOTELEAF_DATA_DIR 69var GetDataDir = func() (string, error) { 70 if envDataDir := os.Getenv("NOTELEAF_DATA_DIR"); envDataDir != "" { 71 if err := mkdirAll(envDataDir, 0755); err != nil { 72 return "", fmt.Errorf("failed to create data directory: %w", err) 73 } 74 return envDataDir, nil 75 } 76 77 var dataDir string 78 79 switch getRuntime() { 80 case "windows": 81 localAppData := os.Getenv("LOCALAPPDATA") 82 if localAppData == "" { 83 return "", fmt.Errorf("LOCALAPPDATA environment variable not set") 84 } 85 dataDir = filepath.Join(localAppData, "noteleaf") 86 case "darwin": 87 homeDir, err := getHomeDir() 88 if err != nil { 89 return "", fmt.Errorf("failed to get user home directory: %w", err) 90 } 91 dataDir = filepath.Join(homeDir, "Library", "Application Support", "noteleaf") 92 default: 93 xdgDataHome := os.Getenv("XDG_DATA_HOME") 94 if xdgDataHome == "" { 95 homeDir, err := getHomeDir() 96 if err != nil { 97 return "", fmt.Errorf("failed to get user home directory: %w", err) 98 } 99 xdgDataHome = filepath.Join(homeDir, ".local", "share") 100 } 101 dataDir = filepath.Join(xdgDataHome, "noteleaf") 102 } 103 104 if err := mkdirAll(dataDir, 0755); err != nil { 105 return "", fmt.Errorf("failed to create data directory: %w", err) 106 } 107 108 return dataDir, nil 109} 110 111// NewDatabase creates and initializes a new database connection 112var NewDatabase = func() (*Database, error) { 113 return NewDatabaseWithConfig(nil) 114} 115 116// NewDatabaseWithConfig creates and initializes a new [Database] connection using the provided [Config] 117func NewDatabaseWithConfig(config *Config) (*Database, error) { 118 if config == nil { 119 var err error 120 config, err = LoadConfig() 121 if err != nil { 122 return nil, fmt.Errorf("failed to load config: %w", err) 123 } 124 } 125 126 var dbPath string 127 if config.DatabasePath != "" { 128 dbPath = config.DatabasePath 129 dbDir := filepath.Dir(dbPath) 130 if err := mkdirAll(dbDir, 0755); err != nil { 131 return nil, fmt.Errorf("failed to create database directory: %w", err) 132 } 133 } else if config.DataDir != "" { 134 dbPath = filepath.Join(config.DataDir, "noteleaf.db") 135 } else { 136 dataDir, err := GetDataDir() 137 if err != nil { 138 return nil, fmt.Errorf("failed to get data directory: %w", err) 139 } 140 dbPath = filepath.Join(dataDir, "noteleaf.db") 141 } 142 143 db, err := sqlOpen("sqlite3", dbPath) 144 if err != nil { 145 return nil, fmt.Errorf("failed to open database: %w", err) 146 } 147 148 if _, err := pragmaExec(db, "PRAGMA foreign_keys = ON"); err != nil { 149 db.Close() 150 return nil, fmt.Errorf("failed to enable foreign keys: %w", err) 151 } 152 153 if _, err := pragmaExec(db, "PRAGMA journal_mode = WAL"); err != nil { 154 db.Close() 155 return nil, fmt.Errorf("failed to enable WAL mode: %w", err) 156 } 157 158 database := &Database{DB: db, path: dbPath} 159 runner := createMigrationRunner(db, migrationFiles) 160 if err := runner.RunMigrations(); err != nil { 161 db.Close() 162 return nil, fmt.Errorf("failed to run migrations: %w", err) 163 } 164 165 return database, nil 166} 167 168// NewMigrationRunner creates a new migration runner from a Database instance 169func NewMigrationRunner(db *Database) *MigrationRunner { 170 return createMigrationRunner(db.DB, migrationFiles) 171} 172 173// GetPath returns the database file path 174func (db *Database) GetPath() string { 175 return db.path 176} 177 178// Close closes the database connection 179func (db *Database) Close() error { 180 return db.DB.Close() 181}