visedriver/storage/storage_service.go
2025-01-22 12:34:24 +00:00

252 lines
6.3 KiB
Go

package storage
import (
"context"
"errors"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/db"
fsdb "git.defalsify.org/vise.git/db/fs"
memdb "git.defalsify.org/vise.git/db/mem"
"git.defalsify.org/vise.git/db/postgres"
"git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
gdbmstorage "git.grassecon.net/grassrootseconomics/visedriver/storage/db/gdbm"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
logg = logging.NewVanilla().WithDomain("storage")
)
type StorageService interface {
GetPersister(ctx context.Context) (*persist.Persister, error)
GetUserdataDb(ctx context.Context) (db.Db, error)
GetResource(ctx context.Context) (resource.Resource, error)
}
type MenuStorageService struct {
conns Conns
poResource resource.Resource
store map[int8]db.Db
}
func NewMenuStorageService(conn Conns) *MenuStorageService {
return &MenuStorageService{
conns: conn,
store: make(map[int8]db.Db),
}
}
func (ms *MenuStorageService) WithDb(store db.Db, typ int8) *MenuStorageService {
var err error
if ms.store[typ] != nil {
panic(fmt.Errorf("db already set for typ: %d", typ))
}
ms.store[typ] = store
ms.conns[typ], err = ToConnData(store.Connection())
if err != nil {
panic(err)
}
return ms
}
func (ms *MenuStorageService) checkDb(ctx context.Context, typ int8) db.Db {
store := ms.store[typ]
if store != nil {
return store
}
connData := ms.conns[typ]
logg.DebugCtxf(ctx, "db check", "conn", connData, "store", DbStoreDebug[typ])
v := ms.conns.Have(&connData)
if v == -1 {
return nil
}
src := ms.store[v]
if src == nil {
return nil
}
ms.store[typ] = ms.store[v]
logg.DebugCtxf(ctx, "found existing db", "typ", typ, "srctyp", v, "store", ms.store[typ], "srcstore", ms.store[v], "conn", connData)
return ms.store[typ]
}
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, section string, typ int8) (db.Db, error) {
var err error
newDb := ms.checkDb(ctx, typ)
if newDb != nil {
logg.InfoCtxf(ctx, "using existing db", "typ", typ, "db", newDb)
return newDb, nil
}
connData := ms.conns[typ]
connStr := connData.Raw()
dbTyp := connData.DbType()
if dbTyp == DBTYPE_POSTGRES {
// TODO: move to vise
err = ensureSchemaExists(ctx, connData)
if err != nil {
return nil, err
}
newDb = postgres.NewPgDb().WithSchema(connData.Domain())
} else if dbTyp == DBTYPE_GDBM {
err = ms.ensureDbDir(connStr)
if err != nil {
return nil, err
}
connStr = path.Join(connStr, section)
newDb = gdbmstorage.NewThreadGdbmDb()
} else if dbTyp == DBTYPE_FS {
err = ms.ensureDbDir(connStr)
if err != nil {
return nil, err
}
fsdbInstance := fsdb.NewFsDb()
if connData.Mode() == DBMODE_BINARY {
fsdbInstance = fsdbInstance.WithBinary()
}
newDb = fsdbInstance
} else if dbTyp == DBTYPE_MEM {
logg.WarnCtxf(ctx, "using volatile storage (memdb)")
newDb = memdb.NewMemDb()
} else {
return nil, fmt.Errorf("unsupported connection string: '%s'\n", connData.Raw())
}
logg.InfoCtxf(ctx, "connecting to db", "conn", connData, "typ", typ)
err = newDb.Connect(ctx, connStr)
if err != nil {
return nil, err
}
ms.store[typ] = newDb
return newDb, nil
}
// WithGettext triggers use of gettext for translation of templates and menus.
//
// The first language in `lns` will be used as default language, to resolve node keys to
// language strings.
//
// If `lns` is an empty array, gettext will not be used.
func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService {
if len(lns) == 0 {
logg.Warnf("Gettext requested but no languages supplied")
return ms
}
rs := resource.NewPoResource(lns[0], path)
for _, ln := range lns {
rs = rs.WithLanguage(ln)
}
ms.poResource = rs
return ms
}
// ensureSchemaExists creates a new schema if it does not exist
func ensureSchemaExists(ctx context.Context, conn ConnData) error {
h, err := pgxpool.New(ctx, conn.Path())
if err != nil {
return fmt.Errorf("failed to connect to the database: %w", err)
}
defer h.Close()
query := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", conn.Domain())
_, err = h.Exec(ctx, query)
if err != nil {
return fmt.Errorf("failed to create schema: %w", err)
}
return nil
}
func applySession(ctx context.Context, store db.Db) error {
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return fmt.Errorf("missing session to apply to store: %v", store)
}
store.SetSession(sessionId)
return nil
}
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
stateStore, err := ms.GetStateStore(ctx)
if err != nil {
return nil, err
}
err = applySession(ctx, stateStore)
if err != nil {
return nil, err
}
pr := persist.NewPersister(stateStore)
logg.TraceCtxf(ctx, "menu storage service", "persist", pr, "store", stateStore)
return pr, nil
}
func (ms *MenuStorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
userStore, err := ms.getOrCreateDb(ctx, "userdata.gdbm", STORETYPE_USER)
if err != nil {
return nil, err
}
err = applySession(ctx, userStore)
if err != nil {
return nil, err
}
return userStore, nil
}
func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resource, error) {
store, err := ms.getOrCreateDb(ctx, "resource.gdbm", STORETYPE_RESOURCE)
if err != nil {
return nil, err
}
rfs := resource.NewDbResource(store)
if ms.poResource != nil {
logg.InfoCtxf(ctx, "using poresource for menu and template")
rfs.WithMenuGetter(ms.poResource.GetMenu)
rfs.WithTemplateGetter(ms.poResource.GetTemplate)
}
return rfs, nil
}
func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error) {
return ms.getOrCreateDb(ctx, "state.gdbm", STORETYPE_STATE)
}
func (ms *MenuStorageService) ensureDbDir(path string) error {
err := os.MkdirAll(path, 0700)
if err != nil {
return fmt.Errorf("store dir create exited with error: %v\n", err)
}
return nil
}
// TODO: how to handle persister here?
func (ms *MenuStorageService) Close(ctx context.Context) error {
var errs []error
var haveErr bool
for i := range _STORETYPE_MAX {
err := ms.store[int8(i)].Close(ctx)
if err != nil {
haveErr = true
}
errs = append(errs, err)
}
if haveErr {
errStr := ""
for i, err := range errs {
errStr += fmt.Sprintf("(%d: %v)", i, err)
}
return errors.New(errStr)
}
return nil
}