cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
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}