mirror of
https://github.com/Mikescher/kpsync.git
synced 2025-08-25 08:38:03 +02:00
logging, upload, etc
This commit is contained in:
parent
0f9b423d2f
commit
47080e14db
4
Makefile
4
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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
66
app/logger.go
Normal file
66
app/logger.go
Normal file
@ -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()
|
||||
}
|
9
app/notifications.go
Normal file
9
app/notifications.go
Normal file
@ -0,0 +1,9 @@
|
||||
package app
|
||||
|
||||
func (app *Application) showErrorNotification(msg string) {
|
||||
//TODO
|
||||
}
|
||||
|
||||
func (app *Application) showSuccessNotification(msg string) {
|
||||
//TODO
|
||||
}
|
237
app/sync.go
237
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
app/tray.go
16
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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
1
go.mod
1
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
|
||||
|
2
go.sum
2
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=
|
||||
|
@ -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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user