From 47080e14db4280cd5fce04700a762e998876a508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Tue, 19 Aug 2025 12:44:15 +0200 Subject: [PATCH] logging, upload, etc --- Makefile | 4 +- app/application.go | 120 ++++++++++++++----- config.go => app/config.go | 22 ++-- app/logger.go | 66 +++++++++++ app/notifications.go | 9 ++ app/sync.go | 237 ++++++++++++++++++++++--------------- app/tray.go | 16 ++- app/utils.go | 5 +- app/webdav.go | 63 +++++++++- go.mod | 1 + go.sum | 2 + log/logger.go | 36 ------ 12 files changed, 399 insertions(+), 182 deletions(-) rename config.go => app/config.go (80%) create mode 100644 app/logger.go create mode 100644 app/notifications.go delete mode 100644 log/logger.go diff --git a/Makefile b/Makefile index 7332422..19c90d2 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,10 @@ ######################### build: enums - CGO_ENABLED=0 go build -o _out/kpsync ./cmd/kpsync + CGO_ENABLED=0 go build -o _out/kpsync ./cmd/cli run: build - ./_out/ffsclient + ./_out/kpsync clean: go clean diff --git a/app/application.go b/app/application.go index 40e279e..5b8ec96 100644 --- a/app/application.go +++ b/app/application.go @@ -1,25 +1,28 @@ package app import ( + "fmt" "os" "os/signal" "sync" "syscall" + "time" "fyne.io/systray" "git.blackforestbytes.com/BlackForestBytes/goext/syncext" - "mikescher.com/kpsync" + "git.blackforestbytes.com/BlackForestBytes/goext/termext" "mikescher.com/kpsync/assets" - "mikescher.com/kpsync/log" ) type Application struct { masterLock sync.Mutex - config kpsync.Config + config Config - trayReady bool - uploadRunning *syncext.AtomicBool + trayReady *syncext.AtomicBool + uploadRunning *syncext.AtomicBool + syncLoopRunning *syncext.AtomicBool + keepassRunning *syncext.AtomicBool sigStopChan chan bool // keepass exited sigErrChan chan error // fatal error @@ -33,31 +36,62 @@ type Application struct { func NewApplication() *Application { - cfg := kpsync.LoadConfig() - - return &Application{ + app := &Application{ masterLock: sync.Mutex{}, - config: cfg, uploadRunning: syncext.NewAtomicBool(false), - trayReady: false, + trayReady: syncext.NewAtomicBool(false), + syncLoopRunning: syncext.NewAtomicBool(false), + keepassRunning: syncext.NewAtomicBool(false), sigStopChan: make(chan bool, 128), sigErrChan: make(chan error, 128), sigSyncLoopStopChan: make(chan bool, 128), sigTermKeepassChan: make(chan bool, 128), } + + app.LogInfo("Starting kpsync...") + app.LogDebug(fmt.Sprintf("SupportsColors := %v", termext.SupportsColors())) + app.LogLine() + + return app } func (app *Application) Run() { + var configPath string + + app.config, configPath = app.loadConfig() + + app.LogInfo(fmt.Sprintf("Loaded config from %s", configPath)) + app.LogDebug(fmt.Sprintf("WebDAVURL := '%s'", app.config.WebDAVURL)) + app.LogDebug(fmt.Sprintf("WebDAVUser := '%s'", app.config.WebDAVUser)) + app.LogDebug(fmt.Sprintf("WebDAVPass := '%s'", app.config.WebDAVPass)) + app.LogDebug(fmt.Sprintf("LocalFallback := '%s'", app.config.LocalFallback)) + app.LogDebug(fmt.Sprintf("WorkDir := '%s'", app.config.WorkDir)) + app.LogDebug(fmt.Sprintf("ForceColors := %v", app.config.ForceColors)) + app.LogDebug(fmt.Sprintf("Debounce := %d", app.config.Debounce)) + app.LogDebug(fmt.Sprintf("ForceColors := %v", app.config.ForceColors)) + app.LogLine() go func() { app.initTray() }() go func() { + app.syncLoopRunning = syncext.NewAtomicBool(true) + defer app.syncLoopRunning.Set(false) + err := app.initSync() if err != nil { app.sigErrChan <- err return } + go func() { + app.keepassRunning = syncext.NewAtomicBool(true) + defer app.keepassRunning.Set(false) + + app.runKeepass() + }() + + time.Sleep(1 * time.Second) + app.setTrayStateDirect("Sleeping...", assets.IconDefault) err = app.runSyncLoop() @@ -71,34 +105,58 @@ func (app *Application) Run() { signal.Notify(sigTerm, os.Interrupt, syscall.SIGTERM) select { - case <-sigTerm: + case <-sigTerm: // kpsync received SIGTERM - app.sigSyncLoopStopChan <- true - app.sigTermKeepassChan <- true - log.LogInfo("Stopping application (received SIGTERM signal)") + app.LogInfo("Stopping application (received SIGTERM signal)") - // TODO term + app.stopBackgroundRoutines() - case err := <-app.sigErrChan: + // TODO try final sync - app.sigSyncLoopStopChan <- true - app.sigTermKeepassChan <- true - log.LogInfo("Stopping application (received ERROR)") - log.LogError(err.Error(), err) + case err := <-app.sigErrChan: // fatal error - // TODO stop + app.LogInfo("Stopping application (received ERROR)") - case _ = <-app.sigStopChan: + app.stopBackgroundRoutines() - app.sigSyncLoopStopChan <- true - app.sigTermKeepassChan <- true - log.LogInfo("Stopping application (received STOP)") + app.LogError("Stopped due to error: "+err.Error(), nil) + + // TODO stop? + + case _ = <-app.sigStopChan: // keepass exited + + app.LogInfo("Stopping application (received STOP)") + + app.stopBackgroundRoutines() + + // TODO try final sync - // TODO stop } - - if app.trayReady { - systray.Quit() - } - +} + +func (app *Application) stopBackgroundRoutines() { + app.LogInfo("Stopping go-routines...") + + app.LogDebug("Stopping systray...") + systray.Quit() + app.trayReady.Wait(false) + app.LogDebug("Stopped systray.") + + if app.uploadRunning.Get() { + app.LogInfo("Waiting for active upload...") + app.uploadRunning.Wait(false) + app.LogInfo("Upload finished.") + } + + app.LogDebug("Stopping sync-loop...") + app.sigSyncLoopStopChan <- true + app.syncLoopRunning.Wait(false) + app.LogDebug("Stopped sync-loop.") + + app.LogDebug("Stopping keepass...") + app.sigTermKeepassChan <- true + app.keepassRunning.Wait(false) + app.LogDebug("Stopped keepass.") + + app.LogLine() } diff --git a/config.go b/app/config.go similarity index 80% rename from config.go rename to app/config.go index 658bd67..bfcd366 100644 --- a/config.go +++ b/app/config.go @@ -1,4 +1,4 @@ -package kpsync +package app import ( "encoding/json" @@ -9,7 +9,6 @@ import ( "strings" "git.blackforestbytes.com/BlackForestBytes/goext/langext" - "mikescher.com/kpsync/log" ) type Config struct { @@ -21,10 +20,12 @@ type Config struct { WorkDir string `json:"work_dir"` + ForceColors bool `json:"force_colors"` + Debounce int `json:"debounce"` } -func LoadConfig() Config { +func (app *Application) loadConfig() (Config, string) { var configPath string flag.StringVar(&configPath, "config", "~/.config/kpsync.json", "Path to the configuration file") @@ -46,12 +47,15 @@ func LoadConfig() Config { var debounce int flag.IntVar(&debounce, "debounce", 0, "Debounce before sync (in seconds)") + var forceColors bool + flag.BoolVar(&forceColors, "color", false, "Force color-output (default: auto-detect)") + flag.Parse() if strings.HasPrefix(configPath, "~") { usr, err := user.Current() if err != nil { - log.FatalErr("Failed to query users home directory", err) + app.LogFatalErr("Failed to query users home directory", err) } fmt.Println(usr.HomeDir) @@ -67,18 +71,19 @@ func LoadConfig() Config { LocalFallback: "", WorkDir: "/tmp/kpsync", Debounce: 3500, + ForceColors: false, }, "", " ")), 0644) } cfgBin, err := os.ReadFile(configPath) if err != nil { - log.FatalErr("Failed to read config file from "+configPath, err) + app.LogFatalErr("Failed to read config file from "+configPath, err) } var cfg Config err = json.Unmarshal(cfgBin, &cfg) if err != nil { - log.FatalErr("Failed to parse config file from "+configPath, err) + app.LogFatalErr("Failed to parse config file from "+configPath, err) } if webdavURL != "" { @@ -99,6 +104,9 @@ func LoadConfig() Config { if debounce > 0 { cfg.Debounce = debounce } + if forceColors { + cfg.ForceColors = forceColors + } - return cfg + return cfg, configPath } diff --git a/app/logger.go b/app/logger.go new file mode 100644 index 0000000..ce463b1 --- /dev/null +++ b/app/logger.go @@ -0,0 +1,66 @@ +package app + +import ( + "strings" + + "git.blackforestbytes.com/BlackForestBytes/goext/exerr" + "git.blackforestbytes.com/BlackForestBytes/goext/langext" + "git.blackforestbytes.com/BlackForestBytes/goext/termext" +) + +func colDefault(v string) string { + return v +} + +func (app *Application) LogFatal(msg string) { + app.logInternal("[F] ", msg, termext.Red) + panic(0) +} + +func (app *Application) LogFatalErr(msg string, err error) { + if err != nil { + app.logInternal("[F] ", msg+"\n"+err.Error()+"\n"+exerr.FromError(err).FormatLog(exerr.LogPrintOverview), termext.Red) + panic(0) + } else { + app.logInternal("[F] ", msg, termext.Red) + panic(0) + } +} + +func (app *Application) LogError(msg string, err error) { + if err != nil { + app.logInternal("[E] ", msg+"\n"+err.Error()+"\n"+exerr.FromError(err).FormatLog(exerr.LogPrintOverview), termext.Red) + } else { + app.logInternal("[E] ", msg, termext.Red) + } +} + +func (app *Application) LogWarn(msg string) { + app.logInternal("[W] ", msg, termext.Red) +} + +func (app *Application) LogInfo(msg string) { + app.logInternal("[I] ", msg, colDefault) +} + +func (app *Application) LogDebug(msg string) { + app.logInternal("[D] ", msg, termext.Gray) +} + +func (app *Application) logInternal(pf string, msg string, c func(_ string) string) { + if !termext.SupportsColors() && !app.config.ForceColors { + c = func(s string) string { return s } + } + + for i, s := range strings.Split(msg, "\n") { + if i == 0 { + println(c(pf + s)) + } else { + println(c(langext.StrRepeat(" ", len(pf)) + s)) + } + } +} + +func (app *Application) LogLine() { + println() +} diff --git a/app/notifications.go b/app/notifications.go new file mode 100644 index 0000000..b13c1f8 --- /dev/null +++ b/app/notifications.go @@ -0,0 +1,9 @@ +package app + +func (app *Application) showErrorNotification(msg string) { + //TODO +} + +func (app *Application) showSuccessNotification(msg string) { + //TODO +} diff --git a/app/sync.go b/app/sync.go index 6d13526..37ec5a3 100644 --- a/app/sync.go +++ b/app/sync.go @@ -14,7 +14,6 @@ import ( "git.blackforestbytes.com/BlackForestBytes/goext/timeext" "github.com/fsnotify/fsnotify" "mikescher.com/kpsync/assets" - "mikescher.com/kpsync/log" ) func (app *Application) initSync() error { @@ -27,8 +26,8 @@ func (app *Application) initSync() error { app.dbFile = path.Join(app.config.WorkDir, path.Base(app.config.LocalFallback)) app.stateFile = path.Join(app.config.WorkDir, "kpsync.state") - if isKeepassRunning() { - log.LogError("keepassxc is already running!", nil) + if app.isKeepassRunning() { + app.LogError("keepassxc is already running!", nil) return exerr.New(exerr.TypeInternal, "keepassxc is already running").Build() } @@ -39,20 +38,21 @@ func (app *Application) initSync() error { if state != nil && fileExists(app.dbFile) { localCS, err := app.calcLocalChecksum() if err != nil { - log.LogError("Failed to calculate local database checksum", err) + app.LogError("Failed to calculate local database checksum", err) } else if localCS == state.Checksum { remoteETag, remoteLM, err := app.getRemoteETag() if err != nil { - log.LogError("Failed to get remote ETag", err) + app.LogError("Failed to get remote ETag", err) } else if remoteETag == state.ETag { - log.LogInfo(fmt.Sprintf("Found local database matching remote database - skip initial download")) - log.LogInfo(fmt.Sprintf("Checksum (cached) := %s", state.Checksum)) - log.LogInfo(fmt.Sprintf("Checksum (local) := %s", localCS)) - log.LogInfo(fmt.Sprintf("ETag (cached) := %s", state.ETag)) - log.LogInfo(fmt.Sprintf("ETag (remote) := %s", remoteETag)) - log.LogInfo(fmt.Sprintf("LastModified (cached) := %s", state.LastModified.Format(time.RFC3339))) - log.LogInfo(fmt.Sprintf("LastModified (remote) := %s", remoteLM.Format(time.RFC3339))) + app.LogInfo(fmt.Sprintf("Found local database matching remote database - skip initial download")) + app.LogDebug(fmt.Sprintf("Checksum (cached) := %s", state.Checksum)) + app.LogDebug(fmt.Sprintf("Checksum (local) := %s", localCS)) + app.LogDebug(fmt.Sprintf("ETag (cached) := %s", state.ETag)) + app.LogDebug(fmt.Sprintf("ETag (remote) := %s", remoteETag)) + app.LogDebug(fmt.Sprintf("LastModified (cached) := %s", state.LastModified.Format(time.RFC3339))) + app.LogDebug(fmt.Sprintf("LastModified (remote) := %s", remoteLM.Format(time.RFC3339))) + app.LogLine() needsDownload = false } @@ -64,92 +64,97 @@ func (app *Application) initSync() error { fin := app.setTrayState("Downloading database", assets.IconDownload) defer fin() - log.LogInfo(fmt.Sprintf("Downloading remote database to %s", app.dbFile)) + app.LogInfo(fmt.Sprintf("Downloading remote database to %s", app.dbFile)) etag, lm, sha, sz, err := app.downloadDatabase() if err != nil { - log.LogError("Failed to download remote database", err) + app.LogError("Failed to download remote database", err) return exerr.Wrap(err, "Failed to download remote database").Build() } - log.LogInfo(fmt.Sprintf("Downloaded remote database to %s", app.dbFile)) - log.LogInfo(fmt.Sprintf("Checksum := %s", sha)) - log.LogInfo(fmt.Sprintf("ETag := %s", etag)) - log.LogInfo(fmt.Sprintf("Size := %s", langext.FormatBytes(sz))) - log.LogInfo(fmt.Sprintf("LastModified := %s", lm.Format(time.RFC3339))) + app.LogInfo(fmt.Sprintf("Downloaded remote database to %s", app.dbFile)) + app.LogInfo(fmt.Sprintf("Checksum := %s", sha)) + app.LogInfo(fmt.Sprintf("ETag := %s", etag)) + app.LogInfo(fmt.Sprintf("Size := %s (%d)", langext.FormatBytes(sz), sz)) + app.LogInfo(fmt.Sprintf("LastModified := %s", lm.Format(time.RFC3339))) err = app.saveState(etag, lm, sha, sz) if err != nil { - log.LogError("Failed to save state", err) + app.LogError("Failed to save state", err) return exerr.Wrap(err, "Failed to save state").Build() } + app.LogLine() + return nil }() if err != nil { return exerr.Wrap(err, "").Build() } } else { - log.LogInfo(fmt.Sprintf("Skip download - use existing local database %s", app.dbFile)) + app.LogInfo(fmt.Sprintf("Skip download - use existing local database %s", app.dbFile)) + app.LogLine() } + return nil +} + +func (app *Application) runKeepass() { + app.LogInfo("Starting keepassxc...") + + cmd := exec.Command("keepassxc", app.dbFile) + go func() { - - log.LogInfo("Starting keepassxc...") - - cmd := exec.Command("keepassxc", app.dbFile) - - go func() { - select { - case <-app.sigTermKeepassChan: - log.LogInfo("Received signal to terminate keepassxc") - if cmd != nil && cmd.Process != nil { - log.LogInfo(fmt.Sprintf("Terminating keepassxc %d", cmd.Process.Pid)) - err := cmd.Process.Signal(syscall.SIGTERM) - if err != nil { - log.LogError("Failed to terminate keepassxc", err) - } else { - log.LogInfo("keepassxc terminated successfully") - } + select { + case <-app.sigTermKeepassChan: + app.LogInfo("Received signal to terminate keepassxc") + if cmd != nil && cmd.Process != nil { + app.LogInfo(fmt.Sprintf("Terminating keepassxc %d", cmd.Process.Pid)) + err := cmd.Process.Signal(syscall.SIGTERM) + if err != nil { + app.LogError("Failed to terminate keepassxc", err) } else { - log.LogInfo("No keepassxc process to terminate") + app.LogInfo("keepassxc terminated successfully") } + } else { + app.LogInfo("No keepassxc process to terminate") } - }() - - err := cmd.Start() - if err != nil { - log.LogError("Failed to start keepassxc", err) - app.sigErrChan <- exerr.Wrap(err, "Failed to start keepassxc").Build() - return } + }() - log.LogInfo(fmt.Sprintf("keepassxc started with PID %d", cmd.Process.Pid)) + err := cmd.Start() + if err != nil { + app.LogError("Failed to start keepassxc", err) + app.sigErrChan <- exerr.Wrap(err, "Failed to start keepassxc").Build() + return + } - err = cmd.Wait() + app.LogInfo(fmt.Sprintf("keepassxc started with PID %d", cmd.Process.Pid)) + app.LogLine() - exitErr := &exec.ExitError{} - if errors.As(err, &exitErr) { + err = cmd.Wait() - log.LogInfo(fmt.Sprintf("keepass exited with code %d", exitErr.ExitCode())) - app.sigStopChan <- true - return + exitErr := &exec.ExitError{} + if errors.As(err, &exitErr) { - } - - if err != nil { - log.LogError("Failed to run keepassxc", err) - app.sigErrChan <- exerr.Wrap(err, "Failed to run keepassxc").Build() - return - } - - log.LogInfo("keepassxc exited successfully") + app.LogInfo(fmt.Sprintf("keepass exited with code %d", exitErr.ExitCode())) app.sigStopChan <- true return - }() + } + + if err != nil { + app.LogError("Failed to run keepassxc", err) + app.sigErrChan <- exerr.Wrap(err, "Failed to run keepassxc").Build() + return + } + + app.LogInfo("keepassxc exited successfully") + app.LogLine() + + app.sigStopChan <- true + return - return nil } func (app *Application) runSyncLoop() error { @@ -167,45 +172,83 @@ func (app *Application) runSyncLoop() error { for { select { case <-app.sigSyncLoopStopChan: - log.LogInfo("Stopping sync loop (received signal)") + app.LogInfo("Stopping sync loop (received signal)") return nil case event := <-watcher.Events: - log.LogInfo(fmt.Sprintf("inotify event: [%s] %s", event.Op.String(), event.Name)) + app.LogDebug(fmt.Sprintf("Received inotify event: [%s] %s", event.Op.String(), event.Name)) - if event.Has(fsnotify.Write) && event.Name == app.dbFile { - func() { - app.masterLock.Lock() - app.uploadRunning.Wait(false) - app.uploadRunning.Set(true) - app.masterLock.Unlock() - - defer app.uploadRunning.Set(false) - - log.LogInfo("Database file was modified") - log.LogInfo(fmt.Sprintf("Sleeping for %d seconds", app.config.Debounce)) - - time.Sleep(timeext.FromSeconds(app.config.Debounce)) - - state := app.readState() - localCS, err := app.calcLocalChecksum() - if err != nil { - log.LogError("Failed to calculate local database checksum", err) - return - } - - if localCS == state.Checksum { - log.LogInfo("Local database still matches remote (via checksum) - no need to upload") - log.LogInfo(fmt.Sprintf("Checksum (remote/cached) := %s", state.Checksum)) - log.LogInfo(fmt.Sprintf("Checksum (local) := %s", localCS)) - return - } - - //TODO upload with IfMatch - }() + if !event.Has(fsnotify.Write) { + app.LogDebug("Ignoring event - not a write event") + app.LogLine() + continue } + + if event.Name != app.dbFile { + app.LogDebug(fmt.Sprintf("Ignoring event - not the database file (%s)", app.dbFile)) + app.LogLine() + continue + } + + func() { + app.masterLock.Lock() + app.uploadRunning.Wait(false) + app.uploadRunning.Set(true) + app.masterLock.Unlock() + + defer app.uploadRunning.Set(false) + + app.LogInfo("Database file was modified") + app.LogInfo(fmt.Sprintf("Sleeping for %d seconds", app.config.Debounce)) + + time.Sleep(timeext.FromSeconds(app.config.Debounce)) + + state := app.readState() + localCS, err := app.calcLocalChecksum() + if err != nil { + app.LogError("Failed to calculate local database checksum", err) + app.showErrorNotification("Failed to calculate local database checksum") + return + } + + if localCS == state.Checksum { + app.LogInfo("Local database still matches remote (via checksum) - no need to upload") + app.LogInfo(fmt.Sprintf("Checksum (remote/cached) := %s", state.Checksum)) + app.LogInfo(fmt.Sprintf("Checksum (local) := %s", localCS)) + return + } + + etag, lm, sha, sz, err := app.uploadDatabase(langext.Ptr(state.ETag)) + if errors.Is(err, ETagConflictError) { + + //TODO - choice notification + + } else if err != nil { + app.LogError("Failed to upload remote database", err) + app.showErrorNotification("Failed to upload remote database") + return + } + + app.LogInfo(fmt.Sprintf("Uploaded database to remote")) + app.LogDebug(fmt.Sprintf("Checksum := %s", sha)) + app.LogDebug(fmt.Sprintf("ETag := %s", etag)) + app.LogDebug(fmt.Sprintf("Size := %s (%d)", langext.FormatBytes(sz), sz)) + app.LogDebug(fmt.Sprintf("LastModified := %s", lm.Format(time.RFC3339))) + + err = app.saveState(etag, lm, sha, sz) + if err != nil { + app.LogError("Failed to save state", err) + app.showErrorNotification("Failed to save state") + return + } + + app.showSuccessNotification("Uploaded database successfully") + + app.LogLine() + }() + case err := <-watcher.Errors: - log.LogError("Filewatcher reported an error", err) + app.LogError("Filewatcher reported an error", err) } } } diff --git a/app/tray.go b/app/tray.go index 47d8ee1..3dfda2d 100644 --- a/app/tray.go +++ b/app/tray.go @@ -13,14 +13,22 @@ func (app *Application) initTray() { systray.SetTitle("KeepassXC Sync") systray.SetTooltip("Initializing...") - app.trayReady = true + app.LogDebug("SysTray initialized") + app.LogLine() + + app.trayReady.Set(true) } systray.Run(trayOnReady, nil) + + app.LogDebug("SysTray stopped") + app.LogLine() + + app.trayReady.Set(false) } func (app *Application) setTrayState(txt string, icon []byte) func() { - if !app.trayReady { + if !app.trayReady.Get() { return func() {} } @@ -34,7 +42,7 @@ func (app *Application) setTrayState(txt string, icon []byte) func() { app.masterLock.Lock() defer app.masterLock.Unlock() - if !app.trayReady { + if !app.trayReady.Get() { return } @@ -46,7 +54,7 @@ func (app *Application) setTrayState(txt string, icon []byte) func() { } func (app *Application) setTrayStateDirect(txt string, icon []byte) { - if !app.trayReady { + if !app.trayReady.Get() { return } diff --git a/app/utils.go b/app/utils.go index da7c0f4..5fc3a32 100644 --- a/app/utils.go +++ b/app/utils.go @@ -9,7 +9,6 @@ import ( "git.blackforestbytes.com/BlackForestBytes/goext/cryptext" "git.blackforestbytes.com/BlackForestBytes/goext/exerr" "github.com/shirou/gopsutil/v3/process" - "mikescher.com/kpsync/log" ) func fileExists(p string) bool { @@ -78,10 +77,10 @@ func (app *Application) calcLocalChecksum() (string, error) { return cryptext.BytesSha256(bin), nil } -func isKeepassRunning() bool { +func (app *Application) isKeepassRunning() bool { proc, err := process.Processes() if err != nil { - log.LogError("failed to query existing keepass process", err) + app.LogError("failed to query existing keepass process", err) return false } diff --git a/app/webdav.go b/app/webdav.go index 7c83495..6ead8aa 100644 --- a/app/webdav.go +++ b/app/webdav.go @@ -1,9 +1,11 @@ package app import ( + "errors" "io" "net/http" "os" + "strings" "time" "git.blackforestbytes.com/BlackForestBytes/goext/cryptext" @@ -11,6 +13,8 @@ import ( "git.blackforestbytes.com/BlackForestBytes/goext/timeext" ) +var ETagConflictError = errors.New("ETag conflict") + func (app *Application) downloadDatabase() (string, time.Time, string, int64, error) { client := http.Client{Timeout: 90 * time.Second} @@ -40,14 +44,13 @@ func (app *Application) downloadDatabase() (string, time.Time, string, int64, er if etag == "" { return "", time.Time{}, "", 0, exerr.New(exerr.TypeInternal, "ETag header is missing").Build() } + etag = strings.Trim(etag, "\"\r\n ") lmStr := resp.Header.Get("Last-Modified") - lm, err := time.Parse("Mon, 02 Jan 2006 15:04:05 MST", lmStr) if err != nil { return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to parse Last-Modified header").Build() } - lm = lm.In(timeext.TimezoneBerlin) sha := cryptext.BytesSha256(bin) @@ -85,6 +88,7 @@ func (app *Application) getRemoteETag() (string, time.Time, error) { if etag == "" { return "", time.Time{}, exerr.New(exerr.TypeInternal, "ETag header is missing").Build() } + etag = strings.Trim(etag, "\"\r\n ") lmStr := resp.Header.Get("Last-Modified") @@ -97,3 +101,58 @@ func (app *Application) getRemoteETag() (string, time.Time, error) { return etag, lm, nil } + +func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time, string, int64, error) { + + client := http.Client{Timeout: 90 * time.Second} + + req, err := http.NewRequest("PUT", app.config.WebDAVURL, nil) + if err != nil { + return "", time.Time{}, "", 0, exerr.Wrap(err, "").Build() + } + + if etagIfMatch != nil { + req.Header.Set("If-Match", "\""+*etagIfMatch+"\"") + } + + bin, err := os.ReadFile(app.dbFile) + if err != nil { + return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to read database file").Build() + } + + sha := cryptext.BytesSha256(bin) + + sz := int64(len(bin)) + + req.ContentLength = sz + + resp, err := client.Do(req) + if err != nil { + return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to upload remote database").Build() + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusNoContent { + + etag := resp.Header.Get("ETag") + if etag == "" { + return "", time.Time{}, "", 0, exerr.New(exerr.TypeInternal, "ETag header is missing").Build() + } + etag = strings.Trim(etag, "\"\r\n ") + + lmStr := resp.Header.Get("Last-Modified") + lm, err := time.Parse("Mon, 02 Jan 2006 15:04:05 MST", lmStr) + if err != nil { + return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to parse Last-Modified header").Build() + } + lm = lm.In(timeext.TimezoneBerlin) + + return etag, lm, sha, sz, nil + } + + if resp.StatusCode == http.StatusPreconditionFailed { + return "", time.Time{}, "", 0, ETagConflictError + } + + return "", time.Time{}, "", 0, exerr.New(exerr.TypeInternal, "Failed to upload remote database").Int("sc", resp.StatusCode).Build() +} diff --git a/go.mod b/go.mod index 0ae1e96..69e8ac2 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( golang.org/x/net v0.42.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect golang.org/x/text v0.27.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a1ef4ed..a6c2801 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/log/logger.go b/log/logger.go deleted file mode 100644 index 0affaad..0000000 --- a/log/logger.go +++ /dev/null @@ -1,36 +0,0 @@ -package log - -import ( - "git.blackforestbytes.com/BlackForestBytes/goext/exerr" -) - -//TODO - -func Fatal(msg string) { - panic(msg) -} - -func FatalErr(msg string, err error) { - if err != nil { - println("FATAL: " + msg) - println(" " + err.Error()) - println(exerr.FromError(err).FormatLog(exerr.LogPrintOverview)) - panic(0) - } else { - panic("FATAL: " + msg) - } -} - -func LogError(msg string, err error) { - if err != nil { - println("ERROR: " + msg) - println(" " + err.Error()) - println(exerr.FromError(err).FormatLog(exerr.LogPrintOverview)) - } else { - println("ERROR: " + msg) - } -} - -func LogInfo(msg string) { - println("INFO: " + msg) -}