logging, upload, etc

This commit is contained in:
2025-08-19 12:44:15 +02:00
parent 0f9b423d2f
commit 47080e14db
12 changed files with 399 additions and 182 deletions

View File

@@ -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()
}

112
app/config.go Normal file
View File

@@ -0,0 +1,112 @@
package app
import (
"encoding/json"
"flag"
"fmt"
"os"
"os/user"
"strings"
"git.blackforestbytes.com/BlackForestBytes/goext/langext"
)
type Config struct {
WebDAVURL string `json:"webdav_url"`
WebDAVUser string `json:"webdav_user"`
WebDAVPass string `json:"webdav_pass"`
LocalFallback string `json:"local_fallback"`
WorkDir string `json:"work_dir"`
ForceColors bool `json:"force_colors"`
Debounce int `json:"debounce"`
}
func (app *Application) loadConfig() (Config, string) {
var configPath string
flag.StringVar(&configPath, "config", "~/.config/kpsync.json", "Path to the configuration file")
var webdavURL string
flag.StringVar(&webdavURL, "webdav_url", "", "WebDAV URL")
var webdavUser string
flag.StringVar(&webdavUser, "webdav_user", "", "WebDAV User")
var webdavPass string
flag.StringVar(&webdavPass, "webdav_pass", "", "WebDAV Password")
var localFallback string
flag.StringVar(&localFallback, "local_fallback", "", "Local fallback database")
var workDir string
flag.StringVar(&workDir, "work_dir", "", "Temporary working directory")
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 {
app.LogFatalErr("Failed to query users home directory", err)
}
fmt.Println(usr.HomeDir)
configPath = strings.TrimPrefix(configPath, "~")
configPath = fmt.Sprintf("%s/%s", usr.HomeDir, configPath)
}
if _, err := os.Stat(configPath); os.IsNotExist(err) && configPath != "" {
_ = os.WriteFile(configPath, langext.Must(json.MarshalIndent(Config{
WebDAVURL: "https://your-nextcloud-domain.example/remote.php/dav/files/keepass.kdbx",
WebDAVUser: "",
WebDAVPass: "",
LocalFallback: "",
WorkDir: "/tmp/kpsync",
Debounce: 3500,
ForceColors: false,
}, "", " ")), 0644)
}
cfgBin, err := os.ReadFile(configPath)
if err != nil {
app.LogFatalErr("Failed to read config file from "+configPath, err)
}
var cfg Config
err = json.Unmarshal(cfgBin, &cfg)
if err != nil {
app.LogFatalErr("Failed to parse config file from "+configPath, err)
}
if webdavURL != "" {
cfg.WebDAVURL = webdavURL
}
if webdavUser != "" {
cfg.WebDAVUser = webdavUser
}
if webdavPass != "" {
cfg.WebDAVPass = webdavPass
}
if localFallback != "" {
cfg.LocalFallback = localFallback
}
if workDir != "" {
cfg.WorkDir = workDir
}
if debounce > 0 {
cfg.Debounce = debounce
}
if forceColors {
cfg.ForceColors = forceColors
}
return cfg, configPath
}

66
app/logger.go Normal file
View 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
View File

@@ -0,0 +1,9 @@
package app
func (app *Application) showErrorNotification(msg string) {
//TODO
}
func (app *Application) showSuccessNotification(msg string) {
//TODO
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}