waveterm/pkg/wconfig/filewatcher.go
Sylvie Crowe c2b8b32b44
New Directory View Columns (#71)
This adds several new columns to the directory view. It adds a last
modified timestamp, a logo for the type, human-readable file sizes, and
permissions. Several of these are configurable via the
config/settings.json file.
2024-06-22 00:41:49 -07:00

248 lines
5.6 KiB
Go

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package wconfig
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sync"
"github.com/fsnotify/fsnotify"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
)
const configDir = "config"
var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir)
var instance *Watcher
var once sync.Once
type Watcher struct {
initialized bool
watcher *fsnotify.Watcher
mutex sync.Mutex
settingsFile string
getSettingsDefaults func() SettingsConfigType
settingsData SettingsConfigType
}
type WatcherUpdate struct {
File string `json:"file"`
Update SettingsConfigType `json:"update"`
Error string `json:"error"`
}
func readFileContents(filePath string, getDefaults func() SettingsConfigType) (*SettingsConfigType, error) {
if getDefaults == nil {
log.Printf("oopsie")
return nil, fmt.Errorf("watcher started without defaults")
}
content := getDefaults()
data, err := os.ReadFile(filePath)
if err != nil {
log.Printf("doopsie: %v", err)
return nil, err
}
if err := json.Unmarshal(data, &content); err != nil {
return nil, err
}
return &content, nil
}
// GetWatcher returns the singleton instance of the Watcher
func GetWatcher() *Watcher {
once.Do(func() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Printf("failed to create file watcher: %v", err)
return
}
os.MkdirAll(configDirAbsPath, 0751)
instance = &Watcher{watcher: watcher}
log.Printf("started config watcher: %v\n", configDirAbsPath)
if err := instance.addSettingsFile(settingsAbsPath, getSettingsConfigDefaults); err != nil {
log.Printf("failed to add path %s to watcher: %v", configDirAbsPath, err)
return
}
})
return instance
}
func (w *Watcher) addSettingsFile(filename string, getDefaults func() SettingsConfigType) error {
w.mutex.Lock()
defer w.mutex.Unlock()
w.getSettingsDefaults = getDefaults
filename = filepath.ToSlash(filename)
stat, err := os.Lstat(filename)
if err != nil {
fmt.Printf("warning: cannot stat file: %v", err)
} else {
if stat.IsDir() {
return fmt.Errorf("warning: can't watch directory instead of file: %v", err)
}
}
w.settingsFile = filename
w.watcher.Add(filepath.Dir(filename))
return nil
}
func (w *Watcher) Start() {
w.mutex.Lock()
defer w.mutex.Unlock()
log.Printf("starting file watcher\n")
w.initialized = true
w.sendInitialValues()
go func() {
for {
select {
case event, ok := <-w.watcher.Events:
if !ok {
return
}
w.handleEvent(event)
case err, ok := <-w.watcher.Errors:
if !ok {
return
}
log.Println("watcher error:", err)
}
}
}()
}
// for initial values, exit on first error
func (w *Watcher) sendInitialValues() error {
filename := w.settingsFile
content, err := readFileContents(w.settingsFile, w.getSettingsDefaults)
if os.IsNotExist(err) || os.IsPermission(err) {
log.Printf("settings file cannot be read: using defaults")
defaults := w.getSettingsDefaults()
content = &defaults
} else if err != nil {
return err
}
message := WatcherUpdate{
File: filename,
Update: *content,
}
w.settingsData = message.Update
log.Printf("watcher: initial values: %s -> %v", filename, content)
return nil
}
func (w *Watcher) Close() {
w.mutex.Lock()
defer w.mutex.Unlock()
if w.watcher != nil {
w.watcher.Close()
w.watcher = nil
log.Println("file watcher closed")
}
}
func (w *Watcher) broadcast(message WatcherUpdate) {
// send to frontend
eventbus.SendEvent(eventbus.WSEventType{
EventType: "config",
Data: message,
})
if message.File == w.settingsFile {
w.settingsData = message.Update
}
if message.Error != "" {
log.Printf("watcher: error processing %s. sending defaults: %v", message.File, message.Error)
} else {
log.Printf("watcher: update: %s -> %v", message.File, message.Update)
}
}
func (w *Watcher) GetSettingsConfig() SettingsConfigType {
w.mutex.Lock()
defer w.mutex.Unlock()
return w.settingsData
}
func (w *Watcher) handleEvent(event fsnotify.Event) {
w.mutex.Lock()
defer w.mutex.Unlock()
fileName := filepath.ToSlash(event.Name)
// only consider events for the tracked files
if fileName != w.settingsFile {
return
}
defaults := w.getSettingsDefaults()
if event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename {
message := WatcherUpdate{
File: fileName,
Update: defaults,
}
w.broadcast(message)
}
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
content, err := readFileContents(fileName, w.getSettingsDefaults)
if err != nil {
message := WatcherUpdate{
File: fileName,
Update: defaults,
Error: err.Error(),
}
w.broadcast(message)
return
}
message := WatcherUpdate{
File: fileName,
Update: *content,
}
w.broadcast(message)
}
}
func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error {
current := w.GetSettingsConfig()
current.Widgets = append(current.Widgets, newWidget)
update, err := json.Marshal(current)
if err != nil {
return err
}
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
return os.WriteFile(w.settingsFile, update, 0644)
}
func (w *Watcher) RmWidget(idx uint) error {
current := w.GetSettingsConfig().Widgets
truncated := append(current[:idx], current[idx+1:]...)
update, err := json.Marshal(truncated)
if err != nil {
return err
}
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
return os.WriteFile(w.settingsFile, update, 0644)
}