feat(storage): add sqlite manager and storage config
This commit is contained in:
114
internal/storage/storage.go
Normal file
114
internal/storage/storage.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/stig/goaichat/internal/config"
|
||||
)
|
||||
|
||||
// Manager handles access to the SQLite database storing chat sessions and metadata.
|
||||
type Manager struct {
|
||||
path string
|
||||
cfg config.StorageConfig
|
||||
mu sync.Mutex
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewManager constructs a Manager and ensures the data directory exists.
|
||||
func NewManager(cfg config.Config) (*Manager, error) {
|
||||
path, err := resolvePath(cfg.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ensureDir(filepath.Dir(path)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Manager{path: path, cfg: cfg.Storage}, nil
|
||||
}
|
||||
|
||||
// Open lazily opens the SQLite database connection.
|
||||
func (m *Manager) Open() (*sql.DB, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.db != nil {
|
||||
return m.db, nil
|
||||
}
|
||||
|
||||
dsn := fmt.Sprintf("file:%s?_pragma=journal_mode(WAL)&_busy_timeout=5000", m.path)
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("ping sqlite: %w", err)
|
||||
}
|
||||
|
||||
m.db = db
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Close releases the underlying database connection.
|
||||
func (m *Manager) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := m.db.Close()
|
||||
m.db = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func resolvePath(cfg config.StorageConfig) (string, error) {
|
||||
if strings.TrimSpace(cfg.Path) != "" {
|
||||
return cfg.Path, nil
|
||||
}
|
||||
|
||||
username := cfg.Username
|
||||
if username == "" {
|
||||
username = os.Getenv("USER")
|
||||
if username == "" {
|
||||
username = "default"
|
||||
}
|
||||
}
|
||||
|
||||
base := filepath.Join("/var/log/goaichat", username)
|
||||
if cfg.BaseDir != "" {
|
||||
base = cfg.BaseDir
|
||||
}
|
||||
|
||||
return filepath.Join(base, "goaichat.db"), nil
|
||||
}
|
||||
|
||||
func ensureDir(dir string) error {
|
||||
if dir == "" {
|
||||
return errors.New("directory path cannot be empty")
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return fmt.Errorf("create data directory: %w", err)
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
if err := os.Chmod(dir, 0o700); err != nil {
|
||||
return fmt.Errorf("chmod data directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user