feat(storage): add sqlite manager and storage config

This commit is contained in:
2025-10-01 14:52:09 +02:00
parent 0fd24a5cfb
commit 14fb100dab
4 changed files with 163 additions and 4 deletions

15
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -16,10 +16,11 @@ const (
// Config captures runtime configuration for the Goaichat application.
type Config struct {
API APIConfig `yaml:"api"`
Model ModelConfig `yaml:"model"`
API APIConfig `yaml:"api"`
Model ModelConfig `yaml:"model"`
Logging LoggingConfig `yaml:"logging"`
UI UIConfig `yaml:"ui"`
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
View 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
}