feat(storage): add sqlite manager and storage config
This commit is contained in:
15
go.mod
15
go.mod
@@ -2,4 +2,17 @@ module github.com/stig/goaichat
|
||||
|
||||
go 1.24.6
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.39.0 // indirect
|
||||
)
|
||||
|
23
go.sum
23
go.sum
@@ -1,3 +1,26 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||
|
@@ -20,6 +20,7 @@ type Config struct {
|
||||
Model ModelConfig `yaml:"model"`
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
UI UIConfig `yaml:"ui"`
|
||||
Storage StorageConfig `yaml:"storage"`
|
||||
}
|
||||
|
||||
// APIConfig holds settings for connecting to the OpenAI-compatible API.
|
||||
@@ -45,6 +46,13 @@ type UIConfig struct {
|
||||
ShowTimestamps bool `yaml:"show_timestamps"`
|
||||
}
|
||||
|
||||
// StorageConfig controls persistence locations and metadata.
|
||||
type StorageConfig struct {
|
||||
Path string `yaml:"path"`
|
||||
BaseDir string `yaml:"base_dir"`
|
||||
Username string `yaml:"username"`
|
||||
}
|
||||
|
||||
// Load reads configuration from the provided path, falling back to defaults and
|
||||
// environment overrides.
|
||||
func Load(path string) (*Config, error) {
|
||||
@@ -123,5 +131,6 @@ func defaultConfig() Config {
|
||||
UI: UIConfig{
|
||||
ShowTimestamps: true,
|
||||
},
|
||||
Storage: StorageConfig{},
|
||||
}
|
||||
}
|
||||
|
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