1
0
Fork 0
timelinize/tlzapp/config.go
Matthew Holt e7650c784a
Some minor changes
- New config parameter "resume_jobs" which can disable auto-resuming jobs at timeline open. (closes #159)

- Renamed "a" to "app" in one method using "Rename symbol" (not "Change all occurrences"), which surprisingly updated the identifier in ALL methods. That must be new. Anyway, that's the huge diff.

- Minor fix to metadata merge that does a more proper nil check to avoid a panic.

- Changed some omitempty to omitzero
2025-10-22 15:13:32 -06:00

259 lines
7.4 KiB
Go

/*
Timelinize
Copyright (c) 2013 Matthew Holt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package tlzapp
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"slices"
"sort"
"sync"
"time"
"github.com/timelinize/timelinize/timeline"
"go.uber.org/zap"
)
// Config describes the server configuration.
// Config values must not be copied (i.e. use pointers).
type Config struct {
sync.RWMutex `json:"-"`
// The listen address to bind the socket to.
Listen string `json:"listen,omitempty"`
// Which HTTP origins are allowed to access the API
// in addition to the default loopback origins.
// Keep in mind this is currently unauthenticated,
// so ensure your socket and transport are secure.
AllowedOrigins []string `json:"allowed_origins,omitempty"`
// Serves the website from this folder on disk instead of
// the embedded file system. This can make local, rapid
// development easier so you don't have to recompile for
// every website change. If empty, website assets that are
// compiled into the binary will be used by default.
WebsiteDir string `json:"website_dir,omitempty"`
// The API token to use for Mapbox GL JS and tiles. The
// user should set this to their own to guarantee
// availability of the maps.
MapboxAPIKey string `json:"mapbox_api_key,omitempty"`
// The folder paths of timeline repositories to open at
// program start.
Repositories []string `json:"repositories,omitempty"`
// Obfuscation is often used for demonstrating the
// software to mask personal data and details.
Obfuscation timeline.ObfuscationOptions `json:"obfuscation,omitzero"`
// Automatically resume jobs when opening timelines.
// Default: true
ResumeJobs *bool `json:"resume_jobs,omitempty"`
log *zap.Logger
}
func (cfg *Config) listenAddr() string {
cfg.RLock()
defer cfg.RUnlock()
if envVal := os.Getenv("TLZ_ADMIN_ADDR"); envVal != "" {
return envVal
}
if cfg.Listen != "" {
return cfg.Listen
}
return defaultAdminAddr
}
func (cfg *Config) fillDefaults() {
cfg.Lock()
defer cfg.Unlock()
cfg.Obfuscation.Logger = timeline.Log.Named("faker")
if cfg.log == nil {
cfg.log = timeline.Log.Named("config").With(zap.Time("loaded", time.Now()))
}
}
// Save persists the config to disk by obtaining a read lock, so it is safe for concurrent use.
func (cfg *Config) Save() error {
cfg.RLock()
defer cfg.RUnlock()
if err := cfg.unsyncedSave(); err != nil {
return err
}
return nil
}
func (cfg *Config) unsyncedSave() error {
filename := DefaultConfigFilePath()
err := os.MkdirAll(filepath.Dir(filename), 0755)
if err != nil {
return fmt.Errorf("creating config directory: %w", err)
}
cfgFile, err := os.Create(filename)
if err != nil {
return fmt.Errorf("creating config file: %w", err)
}
defer cfgFile.Close()
enc := json.NewEncoder(cfgFile)
enc.SetIndent("", "\t")
if err = enc.Encode(cfg); err != nil {
return fmt.Errorf("encoding config: %w", err)
}
if cfg.log != nil {
cfg.log.Info("saved config file", zap.String("path", filename))
}
return nil
}
func (cfg *Config) syncOpenRepos() error {
// assemble the list of open timelines into a sorted slice
// so we can update the stored config and reopen these
// timelines automatically at next start
open := make([]string, len(openTimelines))
i := 0
for _, otl := range openTimelines {
open[i] = otl.RepoDir
i++
}
sort.StringSlice(open).Sort()
// only update the config if list of open repos changed;
// I guess doing so wouldn't hurt but it's not necessary
cfg.Lock()
sameOpenTimelines := slices.Equal(open, cfg.Repositories)
cfg.Repositories = open
cfg.Unlock()
if !sameOpenTimelines {
if err := cfg.Save(); err != nil {
return err
}
}
return nil
}
// DefaultConfigFilePath returns the file path where
// configuration is persisted.
func DefaultConfigFilePath() string {
cfgDir, err := os.UserConfigDir()
if err == nil {
timelinize := "timelinize"
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
timelinize = "Timelinize"
}
return filepath.Join(cfgDir, timelinize, "config.json")
}
cfgDir, err = os.UserHomeDir()
if err == nil {
return filepath.Join(cfgDir, ".timelinize", "config.json")
}
return filepath.Join(".timelinize", "config.json")
}
// // DefaultCacheDir returns the file path where
// // a local application cache is persisted.
// func DefaultCacheDir() string {
// cacheDir, err := os.UserCacheDir()
// if err == nil {
// return filepath.Join(cacheDir, "timelinize")
// }
// homeDir, err := os.UserHomeDir()
// if err == nil {
// return filepath.Join(homeDir, ".timelinize", "cache")
// }
// return filepath.Join(".timelinize", "cache")
// }
// AppDataDir returns a directory path that is suitable for storing
// application data on disk. It uses the environment for finding the
// best place to store data, and appends a "timelinize" or "Timelinize"
// (depending on OS and environment) subdirectory.
//
// For a base directory path:
// If XDG_DATA_HOME is set, it returns: $XDG_DATA_HOME/timelinize; otherwise,
// on Windows it returns: %AppData%/Timelinize,
// on Mac: $HOME/Library/Application Support/Timelinize,
// on Plan9: $home/lib/timelinize,
// on Android: $HOME/timelinize,
// and on everything else: $HOME/.local/share/timelinize.
//
// If a data directory cannot be determined, it returns "./timelinize"
// (this is not ideal, and the environment should be fixed).
//
// The data directory (before appending the app name) is not guaranteed to
// be different from AppConfigDir().
//
// Ref: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
func AppDataDir() string {
if basedir := os.Getenv("XDG_DATA_HOME"); basedir != "" {
return filepath.Join(basedir, "timelinize")
}
switch runtime.GOOS {
case "windows":
appData := os.Getenv("AppData")
if appData != "" {
return filepath.Join(appData, "Timelinize")
}
case "darwin":
home := homeDirUnsafe()
if home != "" {
return filepath.Join(home, "Library", "Application Support", "Timelinize")
}
case "android":
home := homeDirUnsafe()
if home != "" {
return filepath.Join(home, "timelinize")
}
default:
home := homeDirUnsafe()
if home != "" {
return filepath.Join(home, ".local", "share", "timelinize")
}
}
return "." + string(os.PathSeparator) + "timelinize"
}
// homeDirUnsafe is a low-level function that returns
// the user's home directory from environment
// variables. Careful: if it cannot be determined, an
// empty string is returned. If not accounting for
// that case, use HomeDir() instead; otherwise you
// may end up using the root of the file system.
func homeDirUnsafe() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" && runtime.GOOS == "plan9" {
home = os.Getenv("home")
}
return home
}