mirror of
https://github.com/Mikescher/kpsync.git
synced 2025-08-25 08:38:03 +02:00
kinda works, upload, download etc
This commit is contained in:
parent
1f6fb583ee
commit
52399acb42
4
TODO.md
4
TODO.md
@ -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
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 != "" {
|
||||||
|
@ -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
45
app/reader.go
Normal 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)}
|
||||||
|
}
|
62
app/sync.go
62
app/sync.go
@ -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)
|
||||||
|
}
|
||||||
|
59
app/tray.go
59
app/tray.go
@ -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)
|
||||||
|
}
|
||||||
|
12
app/utils.go
12
app/utils.go
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
111
app/webdav.go
111
app/webdav.go
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user