kinda works, upload, download etc

This commit is contained in:
Mike Schwörer 2025-08-20 11:33:26 +02:00
parent 1f6fb583ee
commit 52399acb42
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
9 changed files with 267 additions and 58 deletions

View File

@ -16,4 +16,6 @@
- config via json + params override - config via json + params override
- colorful log - colorful log
- download/upload progress log - download/upload progress log
- logfile in workdir

View File

@ -33,6 +33,12 @@ type Application struct {
dbFile string dbFile string
stateFile string stateFile string
currSysTrayTooltop string
trayItemChecksum *systray.MenuItem
trayItemETag *systray.MenuItem
trayItemLastModified *systray.MenuItem
} }
func NewApplication() *Application { func NewApplication() *Application {
@ -68,15 +74,14 @@ func (app *Application) Run() {
app.LogDebug(fmt.Sprintf("WebDAVPass := '%s'", app.config.WebDAVPass)) app.LogDebug(fmt.Sprintf("WebDAVPass := '%s'", app.config.WebDAVPass))
app.LogDebug(fmt.Sprintf("LocalFallback := '%s'", app.config.LocalFallback)) app.LogDebug(fmt.Sprintf("LocalFallback := '%s'", app.config.LocalFallback))
app.LogDebug(fmt.Sprintf("WorkDir := '%s'", app.config.WorkDir)) app.LogDebug(fmt.Sprintf("WorkDir := '%s'", app.config.WorkDir))
app.LogDebug(fmt.Sprintf("ForceColors := %v", app.config.ForceColors)) app.LogDebug(fmt.Sprintf("Debounce := %d ms", app.config.Debounce))
app.LogDebug(fmt.Sprintf("Debounce := %d", app.config.Debounce))
app.LogDebug(fmt.Sprintf("ForceColors := %v", app.config.ForceColors)) app.LogDebug(fmt.Sprintf("ForceColors := %v", app.config.ForceColors))
app.LogLine() app.LogLine()
go func() { app.initTray() }() go func() { app.initTray() }()
go func() { go func() {
app.syncLoopRunning = syncext.NewAtomicBool(true) app.syncLoopRunning.Set(true)
defer app.syncLoopRunning.Set(false) defer app.syncLoopRunning.Set(false)
isr, err := app.initSync() isr, err := app.initSync()
@ -91,7 +96,7 @@ func (app *Application) Run() {
} else if isr == InitSyncResponseOkay { } else if isr == InitSyncResponseOkay {
go func() { go func() {
app.keepassRunning = syncext.NewAtomicBool(true) app.keepassRunning.Set(true)
defer app.keepassRunning.Set(false) defer app.keepassRunning.Set(false)
app.runKeepass(false) app.runKeepass(false)
@ -113,7 +118,7 @@ func (app *Application) Run() {
app.LogDebug(fmt.Sprintf("DB-Path := '%s'", app.config.LocalFallback)) app.LogDebug(fmt.Sprintf("DB-Path := '%s'", app.config.LocalFallback))
go func() { go func() {
app.keepassRunning = syncext.NewAtomicBool(true) app.keepassRunning.Set(true)
defer app.keepassRunning.Set(false) defer app.keepassRunning.Set(false)
app.runKeepass(true) app.runKeepass(true)
@ -139,7 +144,9 @@ func (app *Application) Run() {
app.stopBackgroundRoutines() app.stopBackgroundRoutines()
// TODO try final sync (?) app.runFinalSync()
return
case err := <-app.sigErrChan: // fatal error case err := <-app.sigErrChan: // fatal error
@ -165,7 +172,9 @@ func (app *Application) Run() {
app.stopBackgroundRoutines() app.stopBackgroundRoutines()
// TODO try final sync app.runFinalSync()
return
} }
} }

View File

@ -3,9 +3,9 @@ package app
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt"
"os" "os"
"os/user" "os/user"
"path"
"strings" "strings"
"git.blackforestbytes.com/BlackForestBytes/goext/langext" "git.blackforestbytes.com/BlackForestBytes/goext/langext"
@ -57,10 +57,9 @@ func (app *Application) loadConfig() (Config, string) {
if err != nil { if err != nil {
app.LogFatalErr("Failed to query users home directory", err) app.LogFatalErr("Failed to query users home directory", err)
} }
fmt.Println(usr.HomeDir)
configPath = strings.TrimPrefix(configPath, "~") configPath = strings.TrimPrefix(configPath, "~")
configPath = fmt.Sprintf("%s/%s", usr.HomeDir, configPath) configPath = path.Join(usr.HomeDir, configPath)
} }
if _, err := os.Stat(configPath); os.IsNotExist(err) && configPath != "" { if _, err := os.Stat(configPath); os.IsNotExist(err) && configPath != "" {

View File

@ -40,7 +40,7 @@ func (app *Application) showSuccessNotification(msg string, body string) {
res, err := cmdext. res, err := cmdext.
Runner("notify-send"). Runner("notify-send").
Arg("--urgency=normal"). Arg("--urgency=critical").
Arg("--app-name=kpsync"). Arg("--app-name=kpsync").
Arg("--print-id"). Arg("--print-id").
Arg(msg). Arg(msg).
@ -74,7 +74,7 @@ func (app *Application) showChoiceNotification(msg string, body string, options
bldr = bldr.Arg("--action=" + kOpt + "=" + vOpt) bldr = bldr.Arg("--action=" + kOpt + "=" + vOpt)
} }
bldr = bldr.Arg(msg) bldr = bldr.Arg(msg).Arg(body)
res, err := bldr.Run() res, err := bldr.Run()
if err != nil { if err != nil {

45
app/reader.go Normal file
View File

@ -0,0 +1,45 @@
package app
import (
"io"
"sync"
)
type teeReadCloser struct {
r io.Reader
c io.Closer
}
func (t *teeReadCloser) Read(p []byte) (int, error) { return t.r.Read(p) }
func (t *teeReadCloser) Close() error { return t.c.Close() }
type progressWriter struct {
sync.Mutex
done int64
total int64
cb func(done, total int64)
}
func (pw *progressWriter) Write(p []byte) (int, error) {
n := len(p)
if pw.cb != nil {
pw.Lock()
defer pw.Unlock()
pw.done += int64(n)
pw.cb(pw.done, pw.total)
}
return n, nil
}
func ReadAllWithProgress(r io.Reader, totalBytes int64, onProgress func(done, total int64)) ([]byte, error) {
return io.ReadAll(NewProgressReader(r, totalBytes, onProgress))
}
func NewProgressReader(r io.Reader, totalBytes int64, onProgress func(done, total int64)) io.ReadCloser {
pw := &progressWriter{total: totalBytes, cb: onProgress}
return &teeReadCloser{r: io.TeeReader(r, pw), c: io.NopCloser(r)}
}

View File

@ -36,6 +36,7 @@ func (app *Application) initSync() (InitSyncResponse, error) {
if app.isKeepassRunning() { if app.isKeepassRunning() {
app.LogError("keepassxc is already running!", nil) app.LogError("keepassxc is already running!", nil)
app.showErrorNotification("KeePassSync: Error", "An keepassxc instance is already running!\nPlease close it before starting kpsync.")
return "", exerr.New(exerr.TypeInternal, "keepassxc is already running").Build() return "", exerr.New(exerr.TypeInternal, "keepassxc is already running").Build()
} }
@ -98,7 +99,7 @@ func (app *Application) initSync() (InitSyncResponse, error) {
}() }()
if err != nil { if err != nil {
r, err := app.showChoiceNotification("Failed to download remote database.\nUse local fallback?", map[string]string{"y": "Yes", "n": "Abort"}) r, err := app.showChoiceNotification("KeePassSync", "Failed to download remote database.\nUse local fallback?", map[string]string{"y": "Yes", "n": "Abort"})
if err != nil { if err != nil {
app.LogError("Failed to show choice notification", err) app.LogError("Failed to show choice notification", err)
return "", exerr.Wrap(err, "Failed to show choice notification").Build() return "", exerr.Wrap(err, "Failed to show choice notification").Build()
@ -134,14 +135,20 @@ func (app *Application) runKeepass(fallback bool) {
cmd := exec.Command("keepassxc", filePath) cmd := exec.Command("keepassxc", filePath)
bgStop := make(chan bool, 128)
go func() { go func() {
select { select {
case <-bgStop:
return
case <-app.sigTermKeepassChan: case <-app.sigTermKeepassChan:
app.LogInfo("Received signal to terminate keepassxc") app.LogInfo("Received signal to terminate keepassxc")
if cmd != nil && cmd.Process != nil { if cmd != nil && cmd.Process != nil {
app.LogInfo(fmt.Sprintf("Terminating keepassxc %d", cmd.Process.Pid)) app.LogInfo(fmt.Sprintf("Terminating keepassxc %d", cmd.Process.Pid))
err := cmd.Process.Signal(syscall.SIGTERM) err := cmd.Process.Signal(syscall.SIGTERM)
if err != nil { if errors.Is(err, os.ErrProcessDone) {
app.LogInfo("keepassxc already terminated")
} else if err != nil {
app.LogError("Failed to terminate keepassxc", err) app.LogError("Failed to terminate keepassxc", err)
} else { } else {
app.LogInfo("keepassxc terminated successfully") app.LogInfo("keepassxc terminated successfully")
@ -164,6 +171,8 @@ func (app *Application) runKeepass(fallback bool) {
err = cmd.Wait() err = cmd.Wait()
bgStop <- true
exitErr := &exec.ExitError{} exitErr := &exec.ExitError{}
if errors.As(err, &exitErr) { if errors.As(err, &exitErr) {
@ -208,8 +217,8 @@ func (app *Application) runSyncLoop() error {
case event := <-watcher.Events: case event := <-watcher.Events:
app.LogDebug(fmt.Sprintf("Received 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) { if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
app.LogDebug("Ignoring event - not a write event") app.LogDebug("Ignoring event - not a write|create event")
app.LogLine() app.LogLine()
continue continue
} }
@ -240,9 +249,9 @@ func (app *Application) onDBFileChanged() {
defer fin1() defer fin1()
app.LogInfo("Database file was modified") app.LogInfo("Database file was modified")
app.LogInfo(fmt.Sprintf("Sleeping for %d seconds", app.config.Debounce)) app.LogInfo(fmt.Sprintf("Sleeping for %d ms", app.config.Debounce))
time.Sleep(timeext.FromSeconds(app.config.Debounce)) time.Sleep(timeext.FromMilliseconds(app.config.Debounce))
state := app.readState() state := app.readState()
localCS, err := app.calcLocalChecksum() localCS, err := app.calcLocalChecksum()
@ -259,6 +268,10 @@ func (app *Application) onDBFileChanged() {
return return
} }
app.doDBUpload(state, fin1, true)
}
func (app *Application) doDBUpload(state *State, stateClear func(), allowConflictResolution bool) {
app.LogInfo("Uploading database to remote") app.LogInfo("Uploading database to remote")
var eTagPtr *string = nil var eTagPtr *string = nil
@ -267,9 +280,9 @@ func (app *Application) onDBFileChanged() {
} }
etag, lm, sha, sz, err := app.uploadDatabase(eTagPtr) etag, lm, sha, sz, err := app.uploadDatabase(eTagPtr)
if errors.Is(err, ETagConflictError) { if errors.Is(err, ETagConflictError) && allowConflictResolution {
fin1() stateClear()
fin2 := app.setTrayState("Uploading database (conflict", assets.IconUploadConflict) fin2 := app.setTrayState("Uploading database (conflict", assets.IconUploadConflict)
defer fin2() defer fin2()
@ -365,7 +378,38 @@ func (app *Application) onDBFileChanged() {
return return
} }
app.showSuccessNotification("KeePassSync: Error", "Uploaded database successfully") app.showSuccessNotification("KeePassSync", "Uploaded database successfully")
app.LogLine() app.LogLine()
} }
func (app *Application) runFinalSync() {
app.masterLock.Lock()
app.uploadRunning.Wait(false)
app.uploadRunning.Set(true)
app.masterLock.Unlock()
defer app.uploadRunning.Set(false)
fin1 := app.setTrayState("Uploading database", assets.IconUpload)
defer fin1()
app.LogInfo("Starting final sync...")
state := app.readState()
localCS, err := app.calcLocalChecksum()
if err != nil {
app.LogError("Failed to calculate local database checksum", err)
app.showErrorNotification("KeePassSync: Error", "Failed to calculate local database checksum")
return
}
if state != nil && 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
}
app.doDBUpload(state, fin1, false)
}

View File

@ -7,20 +7,61 @@ import (
func (app *Application) initTray() { func (app *Application) initTray() {
sigBGStop := make(chan bool, 128)
trayOnReady := func() { trayOnReady := func() {
app.masterLock.Lock()
defer app.masterLock.Unlock()
systray.SetIcon(assets.IconInit) systray.SetIcon(assets.IconInit)
systray.SetTitle("KeepassXC Sync") systray.SetTitle("KeepassXC Sync")
systray.SetTooltip("Initializing...") app.currSysTrayTooltop = "Initializing..."
systray.SetTooltip(app.currSysTrayTooltop)
miSync := systray.AddMenuItem("Sync Now (checked)", "")
miSyncForce := systray.AddMenuItem("Sync Now (forced)", "")
miShowLog := systray.AddMenuItem("Show Log", "")
systray.AddMenuItem("", "")
app.trayItemChecksum = systray.AddMenuItem("Checksum: {...}", "")
app.trayItemETag = systray.AddMenuItem("ETag: {...}", "")
app.trayItemLastModified = systray.AddMenuItem("LastModified: {...}", "")
systray.AddMenuItem("", "")
miQuit := systray.AddMenuItem("Quit", "")
app.LogDebug("SysTray initialized") app.LogDebug("SysTray initialized")
app.LogLine() app.LogLine()
go func() {
for {
select {
case <-miSync.ClickedCh:
app.LogDebug("SysTray: [Sync Now (checked)] clicked")
//TODO
case <-miSyncForce.ClickedCh:
app.LogDebug("SysTray: [Sync Now (forced)] clicked")
//TODO
case <-miShowLog.ClickedCh:
app.LogDebug("SysTray: [Show Log] clicked")
//TODO
case <-miQuit.ClickedCh:
app.LogDebug("SysTray: [Quit] clicked")
app.sigManualStopChan <- true
case <-sigBGStop:
app.LogDebug("SysTray: Click-Listener goroutine stopped")
return
}
}
}()
app.trayReady.Set(true) app.trayReady.Set(true)
} }
systray.Run(trayOnReady, nil) systray.Run(trayOnReady, nil)
sigBGStop <- true
app.LogDebug("SysTray stopped") app.LogDebug("SysTray stopped")
app.LogLine() app.LogLine()
@ -53,7 +94,8 @@ func (app *Application) setTrayState(txt string, icon []byte) func() {
} }
systray.SetIcon(assets.IconDefault) systray.SetIcon(assets.IconDefault)
systray.SetTooltip("Sleeping...") app.currSysTrayTooltop = "Sleeping..."
systray.SetTooltip(app.currSysTrayTooltop)
finDone = true finDone = true
} }
@ -72,3 +114,16 @@ func (app *Application) setTrayStateDirect(txt string, icon []byte) {
systray.SetIcon(icon) systray.SetIcon(icon)
systray.SetTooltip(txt) systray.SetTooltip(txt)
} }
func (app *Application) setTrayTooltip(txt string) {
if !app.trayReady.Get() {
return
}
app.masterLock.Lock()
defer app.masterLock.Unlock()
systray.SetTooltip(txt)
app.currSysTrayTooltop = txt
systray.SetTooltip(app.currSysTrayTooltop)
}

View File

@ -2,12 +2,14 @@ package app
import ( import (
"encoding/json" "encoding/json"
"fmt"
"os" "os"
"strings" "strings"
"time" "time"
"git.blackforestbytes.com/BlackForestBytes/goext/cryptext" "git.blackforestbytes.com/BlackForestBytes/goext/cryptext"
"git.blackforestbytes.com/BlackForestBytes/goext/exerr" "git.blackforestbytes.com/BlackForestBytes/goext/exerr"
"git.blackforestbytes.com/BlackForestBytes/goext/timeext"
"github.com/shirou/gopsutil/v3/process" "github.com/shirou/gopsutil/v3/process"
) )
@ -65,6 +67,16 @@ func (app *Application) saveState(eTag string, lastModified time.Time, checksum
return exerr.Wrap(err, "Failed to write state file").Build() return exerr.Wrap(err, "Failed to write state file").Build()
} }
if app.trayItemChecksum != nil {
app.trayItemChecksum.SetTitle(fmt.Sprintf("Checksum: %s", checksum))
}
if app.trayItemETag != nil {
app.trayItemETag.SetTitle(fmt.Sprintf("ETag: %s", eTag))
}
if app.trayItemLastModified != nil {
app.trayItemLastModified.SetTitle(fmt.Sprintf("LastModified: %s", lastModified.In(timeext.TimezoneBerlin).Format(time.RFC3339)))
}
return nil return nil
} }

View File

@ -1,8 +1,9 @@
package app package app
import ( import (
"bytes"
"errors" "errors"
"io" "fmt"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@ -17,6 +18,9 @@ var ETagConflictError = errors.New("ETag conflict")
func (app *Application) downloadDatabase() (string, time.Time, string, int64, error) { func (app *Application) downloadDatabase() (string, time.Time, string, int64, error) {
prevTT := app.currSysTrayTooltop
defer app.setTrayTooltip(prevTT)
client := http.Client{Timeout: 90 * time.Second} client := http.Client{Timeout: 90 * time.Second}
req, err := http.NewRequest("GET", app.config.WebDAVURL, nil) req, err := http.NewRequest("GET", app.config.WebDAVURL, nil)
@ -26,6 +30,9 @@ func (app *Application) downloadDatabase() (string, time.Time, string, int64, er
req.SetBasicAuth(app.config.WebDAVUser, app.config.WebDAVPass) req.SetBasicAuth(app.config.WebDAVUser, app.config.WebDAVPass)
t0 := time.Now()
app.LogDebug(fmt.Sprintf("{HTTP} Starting WebDAV download..."))
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to download remote database").Build() return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to download remote database").Build()
@ -35,23 +42,26 @@ func (app *Application) downloadDatabase() (string, time.Time, string, int64, er
return "", time.Time{}, "", 0, exerr.New(exerr.TypeInternal, "Failed to download remote database").Int("sc", resp.StatusCode).Build() return "", time.Time{}, "", 0, exerr.New(exerr.TypeInternal, "Failed to download remote database").Int("sc", resp.StatusCode).Build()
} }
bin, err := io.ReadAll(resp.Body) currTT := ""
progressCallback := func(current int64, total int64) {
newTT := fmt.Sprintf("Downloading (%.0f%%)", float64(current)/float64(total)*100)
if currTT != newTT {
app.setTrayTooltip(newTT)
currTT = newTT
}
}
bin, err := ReadAllWithProgress(resp.Body, resp.ContentLength, progressCallback)
if err != nil { if err != nil {
return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to read response body").Build() return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to read response body").Build()
} }
etag := resp.Header.Get("ETag") app.LogDebug(fmt.Sprintf("{HTTP} Finished WebDAV download in %s", time.Since(t0)))
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") etag, lm, err := app.parseHeader(resp)
lm, err := time.Parse("Mon, 02 Jan 2006 15:04:05 MST", lmStr)
if err != nil { if err != nil {
return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to parse Last-Modified header").Build() return "", time.Time{}, "", 0, exerr.Wrap(err, "").Build()
} }
lm = lm.In(timeext.TimezoneBerlin)
sha := cryptext.BytesSha256(bin) sha := cryptext.BytesSha256(bin)
@ -75,35 +85,33 @@ func (app *Application) getRemoteETag() (string, time.Time, error) {
req.SetBasicAuth(app.config.WebDAVUser, app.config.WebDAVPass) req.SetBasicAuth(app.config.WebDAVUser, app.config.WebDAVPass)
t0 := time.Now()
app.LogDebug(fmt.Sprintf("{HTTP} Starting WebDAV HEAD-request..."))
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return "", time.Time{}, exerr.Wrap(err, "Failed to download remote database").Build() return "", time.Time{}, exerr.Wrap(err, "Failed to download remote database").Build()
} }
app.LogDebug(fmt.Sprintf("{HTTP} Finished WebDAV request in %s", time.Since(t0)))
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return "", time.Time{}, exerr.New(exerr.TypeInternal, "Failed to download remote database").Int("sc", resp.StatusCode).Build() return "", time.Time{}, exerr.New(exerr.TypeInternal, "Failed to download remote database").Int("sc", resp.StatusCode).Build()
} }
etag := resp.Header.Get("ETag") etag, lm, err := app.parseHeader(resp)
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")
lm, err := time.Parse("Mon, 02 Jan 2006 15:04:05 MST", lmStr)
if err != nil { if err != nil {
return "", time.Time{}, exerr.Wrap(err, "Failed to parse Last-Modified header").Build() return "", time.Time{}, exerr.Wrap(err, "").Build()
} }
lm = lm.In(timeext.TimezoneBerlin)
return etag, lm, nil return etag, lm, nil
} }
func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time, string, int64, error) { func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time, string, int64, error) {
prevTT := app.currSysTrayTooltop
defer app.setTrayTooltip(prevTT)
client := http.Client{Timeout: 90 * time.Second} client := http.Client{Timeout: 90 * time.Second}
req, err := http.NewRequest("PUT", app.config.WebDAVURL, nil) req, err := http.NewRequest("PUT", app.config.WebDAVURL, nil)
@ -111,6 +119,8 @@ func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time,
return "", time.Time{}, "", 0, exerr.Wrap(err, "").Build() return "", time.Time{}, "", 0, exerr.Wrap(err, "").Build()
} }
req.SetBasicAuth(app.config.WebDAVUser, app.config.WebDAVPass)
if etagIfMatch != nil { if etagIfMatch != nil {
req.Header.Set("If-Match", "\""+*etagIfMatch+"\"") req.Header.Set("If-Match", "\""+*etagIfMatch+"\"")
} }
@ -124,7 +134,20 @@ func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time,
sz := int64(len(bin)) sz := int64(len(bin))
currTT := ""
progressCallback := func(current int64, total int64) {
newTT := fmt.Sprintf("Uploading (%.0f%%)", float64(current)/float64(total)*100)
if currTT != newTT {
app.setTrayTooltip(newTT)
currTT = newTT
}
}
req.ContentLength = sz req.ContentLength = sz
req.Body = NewProgressReader(bytes.NewReader(bin), int64(len(bin)), progressCallback)
t0 := time.Now()
app.LogDebug(fmt.Sprintf("{HTTP} Starting WebDAV upload..."))
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
@ -132,20 +155,14 @@ func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time,
} }
defer func() { _ = resp.Body.Close() }() defer func() { _ = resp.Body.Close() }()
app.LogDebug(fmt.Sprintf("{HTTP} Finished WebDAV upload in %s", time.Since(t0)))
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusNoContent { if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusNoContent {
etag := resp.Header.Get("ETag") etag, lm, err := app.parseHeader(resp)
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 { if err != nil {
return "", time.Time{}, "", 0, exerr.Wrap(err, "Failed to parse Last-Modified header").Build() return "", time.Time{}, "", 0, exerr.Wrap(err, "").Build()
} }
lm = lm.In(timeext.TimezoneBerlin)
return etag, lm, sha, sz, nil return etag, lm, sha, sz, nil
} }
@ -154,5 +171,31 @@ func (app *Application) uploadDatabase(etagIfMatch *string) (string, time.Time,
return "", time.Time{}, "", 0, ETagConflictError return "", time.Time{}, "", 0, ETagConflictError
} }
return "", time.Time{}, "", 0, exerr.New(exerr.TypeInternal, "Failed to upload remote database").Int("sc", resp.StatusCode).Build() return "", time.Time{}, "", 0, exerr.New(exerr.TypeInternal, fmt.Sprintf("Failed to upload remote database (statuscode: %d)", resp.StatusCode)).Int("sc", resp.StatusCode).Build()
}
func (app *Application) parseHeader(resp *http.Response) (string, time.Time, error) {
var err error
etag := resp.Header.Get("ETag")
if etag == "" {
return "", time.Time{}, exerr.New(exerr.TypeInternal, "ETag header is missing").Build()
}
etag = strings.Trim(etag, "\"\r\n ")
var lm time.Time
lmStr := resp.Header.Get("Last-Modified")
if lmStr == "" {
lm = time.Now().In(timeext.TimezoneBerlin)
app.LogDebug("Last-Modified header is missing, using current time as fallback")
} else {
lm, err = time.Parse("Mon, 02 Jan 2006 15:04:05 MST", lmStr)
if err != nil {
return "", time.Time{}, exerr.Wrap(err, "Failed to parse Last-Modified header").Build()
}
lm = lm.In(timeext.TimezoneBerlin)
}
return etag, lm, nil
} }