1
0
Fork 0
timelinize/timeline/settings.go
Matthew Holt fb3d529228
Refactor thumbnail DB handle as well
Fix error when repo property doesn't exist
2025-09-30 14:04:53 -06:00

156 lines
4 KiB
Go

package timeline
import (
"context"
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"go.uber.org/zap"
)
func (tl *Timeline) GetProperties(ctx context.Context) (map[string]any, error) {
properties := make(map[string]any)
rows, err := tl.db.ReadPool.QueryContext(ctx, "SELECT key, value, type FROM repo")
if err != nil {
return nil, fmt.Errorf("querying properties: %w", err)
}
defer rows.Close()
for rows.Next() {
var key string
var valStr, valType *string
err := rows.Scan(&key, &valStr, &valType)
if err != nil {
return nil, fmt.Errorf("scanning row: %w", err)
}
value, err := tl.readProperty(valStr, valType)
if err != nil {
return nil, fmt.Errorf("property %s: %w", key, err)
}
properties[key] = value
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("scanning rows: %w", err)
}
return properties, nil
}
func (*Timeline) readProperty(valStr, valType *string) (any, error) {
if valType == nil {
return nil, errors.New("data type missing")
}
var value any
var err error
if valStr != nil && valType != nil {
switch *valType {
case "bool":
switch strings.ToLower(*valStr) {
case "0", "false", "", "no", "off", "disabled", "f":
value = false
case "1", "true", "yes", "on", "enabled", "t":
value = true
}
case "int":
value, err = strconv.ParseInt(*valStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing property as %s: %w (value=%s)", *valType, err, *valStr)
}
case "float64":
value, err = strconv.ParseFloat(*valStr, 64)
if err != nil {
return nil, fmt.Errorf("parsing property as %s: %w (value=%s)", *valType, err, *valStr)
}
case "string":
value = *valStr
}
}
return value, nil
}
func (tl *Timeline) LoadProperty(ctx context.Context, key string) (any, error) {
var valStr, valType *string
err := tl.db.ReadPool.QueryRowContext(ctx, "SELECT value, type FROM repo WHERE key=? LIMIT 1", key).Scan(&valStr, &valType)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("querying for property %s: %w", key, err)
}
value, err := tl.readProperty(valStr, valType)
if err != nil {
return nil, fmt.Errorf("property %s: %w", key, err)
}
return value, nil
}
func (tl *Timeline) GetProperty(ctx context.Context, key string) any {
value, err := tl.LoadProperty(ctx, key)
if err != nil {
Log.Named("timeline").Error("getting property",
zap.String("key", key),
zap.Error(err))
return nil
}
return value
}
func (tl *Timeline) SetProperties(ctx context.Context, properties map[string]any) error {
tx, err := tl.db.WritePool.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("starting tx: %w", err)
}
defer tx.Rollback()
for key, value := range properties {
// can't change repo ID or version
if key == "id" || key == "version" {
continue
}
// nil values should just delete the row entirely
if isNil(value) {
_, err := tx.ExecContext(ctx, `DELETE FROM repo WHERE key=?`, key)
if err != nil {
return fmt.Errorf("deleting property %s: %w", key, err)
}
continue
}
// integers decoded from JSON will be float64 because ... JSON... so
// try to convert them back to ints if possible
if fl, ok := value.(float64); ok && float64(int(fl)) == fl {
value = int(fl)
}
valType := fmt.Sprintf("%T", value) // commonly: int, bool, string, float64
// to avoid ambiguity in the DB, store bools as true/false strings,
// otherwise they just go in as 1/0 strings instead -- yes, we store
// the type, but I think true/false is more intuitive
if b, ok := value.(bool); ok {
if b {
value = "true"
} else {
value = "false"
}
}
_, err := tx.ExecContext(ctx, `INSERT INTO repo (key, value, type) VALUES (?, ?, ?)
ON CONFLICT DO UPDATE SET key=excluded.key, value=excluded.value, type=excluded.type`,
key, value, valType)
if err != nil {
return fmt.Errorf("inserting/updating property %s=%v (%s): %w", key, value, valType, err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing tx: %w", err)
}
return nil
}