Squash merge audio-player-component into main
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package config
|
||||
|
||||
import "yellowjacket/pkg/templcomp"
|
||||
|
||||
templ (c *Config) form() {
|
||||
@templcomp.ToForm(c, templ.URL("/config"), "config")
|
||||
}
|
||||
|
||||
templ (c *Config) formSubmitError(msg string) {
|
||||
<strong>Error: { msg }</strong>
|
||||
}
|
||||
|
||||
templ (c *Config) formSubmitSuccess() {
|
||||
<p>Config saved</p>
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.865
|
||||
package config
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "yellowjacket/pkg/templcomp"
|
||||
|
||||
func (c *Config) form() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templcomp.ToForm(c, templ.URL("/config"), "config").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Config) formSubmitError(msg string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var2 == nil {
|
||||
templ_7745c5c3_Var2 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<strong>Error: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(msg)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `config/config-form.templ`, Line: 10, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</strong>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Config) formSubmitSuccess() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<p>Config saved</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
+77
-87
@@ -1,145 +1,135 @@
|
||||
// Package config manages application configuration persistence.
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"yellowjacket/backend/library"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"yellowjacket/backend/library"
|
||||
"yellowjacket/backend/system"
|
||||
)
|
||||
|
||||
// Config represents the application configuration.
|
||||
type Config struct {
|
||||
filePath string // required
|
||||
*configData
|
||||
ctx context.Context
|
||||
logger *slog.Logger
|
||||
serveMux *http.ServeMux
|
||||
filePath string // required
|
||||
Library *library.Config `form:"Library" schema:"library,required"`
|
||||
}
|
||||
|
||||
func newConfig(filePath string, data *configData) (*Config, error) {
|
||||
// NewConfig creates a new config by loading it from disk.
|
||||
func NewConfig(logger *slog.Logger) (*Config, error) {
|
||||
confDir, err := system.GetUserConfigDirPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get user config directory: %w", err)
|
||||
}
|
||||
|
||||
conf := &Config{
|
||||
filePath: filePath,
|
||||
configData: data,
|
||||
filePath: path.Join(confDir, "config.toml"),
|
||||
serveMux: http.NewServeMux(),
|
||||
}
|
||||
// TODO make sure required fields have SOMETHING in them
|
||||
// TODO Merge existing config with read in config
|
||||
if data == nil {
|
||||
return nil, errors.New("nil config")
|
||||
conf.logger = logger.WithGroup("config").With("config", conf)
|
||||
conf.serveMux.HandleFunc("/", conf.handle)
|
||||
|
||||
if err := conf.Load(); err != nil {
|
||||
return nil, fmt.Errorf("could not load config: %w", err)
|
||||
}
|
||||
if err := data.validate(); err != nil {
|
||||
|
||||
if err := conf.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
type configData struct {
|
||||
Library *library.Config
|
||||
}
|
||||
// Validate returns errors if there is a breaking issue with the config.
|
||||
func (c *Config) Validate() error {
|
||||
var configErrs error
|
||||
|
||||
// return errors if there is a *breaking* issue with the config
|
||||
func (d *configData) validate() error {
|
||||
if d.Library == nil {
|
||||
return errors.New("nil library config")
|
||||
}
|
||||
if err := d.Library.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid library config: %#v: %w", d.Library, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var defaultConfigData *configData = &configData{
|
||||
Library: library.DefaultConfig,
|
||||
}
|
||||
|
||||
// GetCurrentConfig will load and return the config
|
||||
// reading the config file in the user's config directory
|
||||
func GetCurrentConfig() (*Config, error) {
|
||||
// get the config file location
|
||||
configDir, err := GetUserConfigDirPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get user config directory path: %w", err)
|
||||
}
|
||||
configFilePath := path.Join(configDir, "config.toml")
|
||||
|
||||
// create the config obj with the filepath we got, initializing with default data
|
||||
config, err := newConfig(configFilePath, defaultConfigData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create new config: %w", err)
|
||||
}
|
||||
config.filePath = configFilePath
|
||||
|
||||
// does the config file alaeady exist?
|
||||
// if not, create it
|
||||
_, err = os.Stat(configFilePath)
|
||||
if os.IsNotExist(err) {
|
||||
if err := config.WriteConfig(); err != nil {
|
||||
return nil, fmt.Errorf("could not write config: %w", err)
|
||||
if c.Library != nil {
|
||||
if len(c.Library.DirectoryPath) != 0 {
|
||||
if err := c.Library.Validate(); err != nil {
|
||||
configErrs = errors.Join(configErrs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now that we have our config file, load it in
|
||||
config, err = config.loadConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load config file %s: %w", configFilePath, err)
|
||||
if configErrs != nil {
|
||||
return fmt.Errorf("one or more config parts are invalid: %w", configErrs)
|
||||
}
|
||||
|
||||
// before we return the config, lets make sure sub configs can invoke saving when they need
|
||||
err = config.updateSubConfigSaveFuncReferences()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not update sub config save func references: %w", err)
|
||||
}
|
||||
return config, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) loadConfig() (*Config, error) {
|
||||
// Load reads and parses the config file from disk.
|
||||
func (c *Config) Load() error {
|
||||
if _, err := os.Stat(c.filePath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.logger.Debug("no config file exists, creating empty config")
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"could not save empty config to file (%s): %w",
|
||||
c.filePath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("could not get file info (%s): %w", c.filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// read in the file
|
||||
confFileData, err := os.ReadFile(c.filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("problem reading config file %s: %w", c.filePath, err)
|
||||
return fmt.Errorf("problem reading config file %s: %w", c.filePath, err)
|
||||
}
|
||||
|
||||
// parse it into the config struct
|
||||
var confData configData
|
||||
_, err = toml.Decode(string(confFileData), &confData)
|
||||
_, err = toml.Decode(string(confFileData), c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("problem parsing config file %s: %w", c.filePath, err)
|
||||
return fmt.Errorf("problem parsing config file %s: %w", c.filePath, err)
|
||||
}
|
||||
|
||||
// validate the config
|
||||
if err = confData.validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config file at %s: %w", c.filePath, err)
|
||||
if err = c.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid config file at %s: %w", c.filePath, err)
|
||||
}
|
||||
|
||||
config, err := newConfig(c.filePath, &confData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create config from config file data at %s: %w", c.filePath, err)
|
||||
}
|
||||
c.logger.Debug("loaded config file", "file", c.filePath)
|
||||
|
||||
// before we return the config, lets make sure sub configs can invoke saving when they need
|
||||
err = config.updateSubConfigSaveFuncReferences()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not update sub config save func references: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) WriteConfig() error {
|
||||
if err := c.validate(); err != nil {
|
||||
// Save writes the config to disk.
|
||||
func (c *Config) Save() error {
|
||||
if err := c.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
confFileData, err := toml.Marshal(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal config struct: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(c.filePath, confFileData, os.FileMode(int(0666)))
|
||||
err = os.WriteFile(c.filePath, confFileData, os.FileMode(int(0o666)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write config file: %w", err)
|
||||
return fmt.Errorf("could not write config file (%s): %w", c.filePath, err)
|
||||
}
|
||||
|
||||
c.logger.Debug("saved config to file", "file", c.filePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) updateSubConfigSaveFuncReferences() error {
|
||||
c.Library.SaveFunc = c.WriteConfig
|
||||
return nil
|
||||
// SetContext sets the Wails runtime context for event emission.
|
||||
func (c *Config) SetContext(ctx context.Context) {
|
||||
c.ctx = ctx
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"yellowjacket/backend/events"
|
||||
)
|
||||
|
||||
var formDecoder = schema.NewDecoder()
|
||||
|
||||
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.serveMux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (c *Config) handle(w http.ResponseWriter, r *http.Request) {
|
||||
c.logger.Debug("handling request from config http handler")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
if err := c.form().Render(r.Context(), w); err != nil {
|
||||
c.logger.Error("problem getting config html", "err", err.Error())
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
case http.MethodPost:
|
||||
if err := c.handleConfigPost(r); err != nil {
|
||||
c.logger.Error("problem handling config post request", "err", err.Error())
|
||||
c.formSubmitError(err.Error()).Render(r.Context(), w)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.formSubmitSuccess().Render(r.Context(), w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) handleConfigPost(r *http.Request) error {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
return fmt.Errorf("could not parse form data: %w", err)
|
||||
}
|
||||
|
||||
var postedConfig Config
|
||||
|
||||
err := formDecoder.Decode(&postedConfig, r.PostForm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not decode form data: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Debug("decoded config post form data", "postedConfig", postedConfig)
|
||||
|
||||
// Update local config and emit event for listeners
|
||||
if postedConfig.Library != nil {
|
||||
c.Library = postedConfig.Library
|
||||
|
||||
if c.ctx != nil {
|
||||
runtime.EventsEmit(c.ctx, events.LibraryConfigChanged, map[string]any{
|
||||
"DirectoryPath": string(c.Library.DirectoryPath),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.Save(); err != nil {
|
||||
return fmt.Errorf("could not save posted config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// user config directories vary by operating system and purpose
|
||||
// Linux/Mac: ~/.config
|
||||
// Windows: C:\Users\<username>\AppData
|
||||
func GetUserConfigDirPath() (string, error) {
|
||||
path := ""
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not get current user: %w", err)
|
||||
}
|
||||
switch currentOS := runtime.GOOS; currentOS {
|
||||
case "darwin":
|
||||
path = fmt.Sprintf("/Users/%s/.config/yellowjacket", currentUser.Username)
|
||||
case "linux":
|
||||
path = fmt.Sprintf("/home/%s/.config/yellowjacket", currentUser.Username)
|
||||
case "windows":
|
||||
path = fmt.Sprintf(`C:\Users\%s\AppData\local\yellowjacket\config`, currentUser.Username)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported OS: %s", currentOS)
|
||||
}
|
||||
err = os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not make user config directory: %w", err)
|
||||
}
|
||||
|
||||
// final check
|
||||
dirInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not stat the user config directory %s: %w", path, err)
|
||||
}
|
||||
|
||||
if !dirInfo.IsDir() {
|
||||
return "", fmt.Errorf("not a directory: %s", path)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// user data directories vary by operating system and purpose
|
||||
// Linux/Mac: ~/.local/share
|
||||
// Windows: C:\Users\<username>\AppData\Local
|
||||
func getUserDataDirPath() (string, error) {
|
||||
path := ""
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not get current user: %w", err)
|
||||
}
|
||||
switch currentOS := runtime.GOOS; currentOS {
|
||||
case "darwin":
|
||||
path = fmt.Sprintf("/Users/%s/.local/share/yellowjacket", currentUser.Username)
|
||||
case "linux":
|
||||
path = fmt.Sprintf("/home/%s/.local/share/yellowjacket", currentUser.Username)
|
||||
case "windows":
|
||||
path = fmt.Sprintf(`C:\Users\%s\AppData\local\yellowjacket\config`, currentUser.Username)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported OS: %s", currentOS)
|
||||
}
|
||||
err = os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not make user data directory: %w", err)
|
||||
}
|
||||
|
||||
// final check
|
||||
dirInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not stat the user data directory %s: %w", path, err)
|
||||
}
|
||||
|
||||
if !dirInfo.IsDir() {
|
||||
return "", fmt.Errorf("not a directory: %s", path)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
Reference in New Issue
Block a user