From e7293464c1397bf6eb326ce2f05a7e120c5c17d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Mon, 18 Aug 2025 18:54:08 +0200 Subject: [PATCH] non-functional wip state --- .gitignore | 38 +++++++ .idea/.gitignore | 8 ++ .idea/inspectionProfiles/Project_Default.xml | 11 ++ .idea/kpsync.iml | 9 ++ .idea/modules.xml | 8 ++ .idea/vcs.xml | 6 ++ Makefile | 43 ++++++++ TODO.md | 19 ++++ app/application.go | 73 +++++++++++++ app/sync.go | 67 ++++++++++++ app/tray.go | 20 ++++ app/utils.go | 47 +++++++++ app/webdav.go | 5 + assets/assets.go | 14 +++ assets/iconDefault.png | Bin 0 -> 2238 bytes cmd/cli/main.go | 17 +++ config.go | 104 +++++++++++++++++++ go.mod | 10 ++ go.sum | 8 ++ log/logger.go | 27 +++++ 20 files changed, 534 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/kpsync.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 Makefile create mode 100644 TODO.md create mode 100644 app/application.go create mode 100644 app/sync.go create mode 100644 app/tray.go create mode 100644 app/utils.go create mode 100644 app/webdav.go create mode 100644 assets/assets.go create mode 100644 assets/iconDefault.png create mode 100644 cmd/cli/main.go create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 log/logger.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..555b8e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ + +########## GOLAND ########## + +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/aws.xml +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +.idea/**/mongoSettings.xml +*.iws +atlassian-ide-plugin.xml +.idea/httpRequests +.idea/caches/build_file_checksums.ser +.idea/$CACHE_FILE$ + +########## Linux ########## + +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +########## Custom ########## + + +_out/* \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..4696d94 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.idea/kpsync.iml b/.idea/kpsync.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/kpsync.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a49f84f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7332422 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ + +# ./firefox-sync-client +######################### + +build: enums + CGO_ENABLED=0 go build -o _out/kpsync ./cmd/kpsync + +run: build + ./_out/ffsclient + +clean: + go clean + rm -rf ./_out/* + +enums: + go generate ./... + +package: + # + # Manually do beforehand: + # - Update version in version.go + # - Create tag + # - Commit + # + + go clean + rm -rf ./_out/* + + GOARCH=386 GOOS=linux CGO_ENABLED=0 go build -o _out/kpsync_linux-386-static ./cmd/cli # Linux - 32 bit + GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -o _out/kpsync_linux-amd64-static ./cmd/cli # Linux - 64 bit + GOARCH=arm64 GOOS=linux CGO_ENABLED=0 go build -o _out/kpsync_linux-arm64-static ./cmd/cli # Linux - ARM + GOARCH=386 GOOS=linux go build -o _out/kpsync_linux-386 ./cmd/cli # Linux - 32 bit + GOARCH=amd64 GOOS=linux go build -o _out/kpsync_linux-amd64 ./cmd/cli # Linux - 64 bit + GOARCH=arm64 GOOS=linux go build -o _out/kpsync_linux-arm64 ./cmd/cli # Linux - ARM + GOARCH=386 GOOS=windows go build -o _out/kpsync_win-386.exe -tags timetzdata -ldflags "-w -s" ./cmd/cli # Windows - 32 bit + GOARCH=amd64 GOOS=windows go build -o _out/kpsync_win-amd64.exe -tags timetzdata -ldflags "-w -s" ./cmd/cli # Windows - 64 bit + GOARCH=arm64 GOOS=windows go build -o _out/kpsync_win-arm64.exe -tags timetzdata -ldflags "-w -s" ./cmd/cli # Windows - ARM + GOARCH=amd64 GOOS=darwin go build -o _out/kpsync_macos-amd64 ./cmd/cli # macOS - 32 bit + GOARCH=amd64 GOOS=darwin go build -o _out/kpsync_macos-amd64 ./cmd/cli # macOS - 64 bit + GOARCH=amd64 GOOS=openbsd go build -o _out/kpsync_openbsd-amd64 ./cmd/cli # OpenBSD - 64 bit + GOARCH=arm64 GOOS=openbsd go build -o _out/kpsync_openbsd-arm64 ./cmd/cli # OpenBSD - ARM + GOARCH=amd64 GOOS=freebsd go build -o _out/kpsync_freebsd-amd64 ./cmd/cli # FreeBSD - 64 bit + GOARCH=arm64 GOOS=freebsd go build -o _out/kpsync_freebsd-arm64 ./cmd/cli # FreeBSD - ARM diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..8d22c0f --- /dev/null +++ b/TODO.md @@ -0,0 +1,19 @@ + + + - log to linux fd (pipe) to open terminal + + - show state in tray + + - tray menu + - force sync + - ~ current Etag + - ~ current SHA + - ~ last sync ts + - check sync + - quit + - show log (open terminal /w log) + + - config via json + params override + + - colorful log + - download/upload progress log \ No newline at end of file diff --git a/app/application.go b/app/application.go new file mode 100644 index 0000000..b7680cf --- /dev/null +++ b/app/application.go @@ -0,0 +1,73 @@ +package app + +import ( + "os" + "os/signal" + "syscall" + + "fyne.io/systray" + "mikescher.com/kpsync" +) + +type Application struct { + config kpsync.Config + + trayReady bool + sigStopChan chan bool + sigErrChan chan error + + dbFile string + stateFile string +} + +func NewApplication() *Application { + + cfg := kpsync.LoadConfig() + + return &Application{ + config: cfg, + trayReady: false, + sigStopChan: make(chan bool, 128), + sigErrChan: make(chan error, 128), + } +} + +func (app *Application) Run() { + + go func() { app.initTray() }() + + go func() { + err := app.initSync() + if err != nil { + app.sigErrChan <- err + return + } + err = app.runSyncLoop() + if err != nil { + app.sigErrChan <- err + return + } + }() + + sigTerm := make(chan os.Signal, 1) + signal.Notify(sigTerm, os.Interrupt, syscall.SIGTERM) + + select { + case <-sigTerm: + + // TODO term + + case _ = <-app.sigErrChan: + + // TODO stop + + case _ = <-app.sigStopChan: + + // TODO stop + } + + if app.trayReady { + systray.Quit() + } + +} diff --git a/app/sync.go b/app/sync.go new file mode 100644 index 0000000..6638678 --- /dev/null +++ b/app/sync.go @@ -0,0 +1,67 @@ +package app + +import ( + "fmt" + "os" + "path" + + "git.blackforestbytes.com/BlackForestBytes/goext/exerr" + "mikescher.com/kpsync/assets" + "mikescher.com/kpsync/log" +) + +func (app *Application) initSync() error { + + err := os.MkdirAll(app.config.WorkDir, os.ModePerm) + if err != nil { + return exerr.Wrap(err, "").Build() + } + + app.dbFile = path.Join(app.config.WorkDir, path.Base(app.config.LocalFallback)) + app.stateFile = path.Join(app.config.WorkDir, "kpsync.state") + + state := app.readState() + + needsDownload := true + + if state != nil && fileExists(app.dbFile) { + localCS, err := app.calcLocalChecksum() + if err != nil { + log.LogError("Failed to calculate local database checksum", err) + } else if localCS == state.Checksum { + remoteETag, err := app.getRemoteETag() + if err != nil { + log.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)) + needsDownload = false + + } + } + } + + if needsDownload { + func() { + fin := app.setTrayState("Downloading database", assets.IconDefault) + defer fin() + + log.LogInfo(fmt.Sprintf("Downloading remote database to %s", app.dbFile)) + + etag, err := app.downloadDatabase() + if err != nil { + log.LogError("Failed to download remote database", err) + app.sigErrChan <- exerr.Wrap(err, "Failed to download remote database").Build() + return + } + }() + } +} + +func (app *Application) runSyncLoop() error { + +} diff --git a/app/tray.go b/app/tray.go new file mode 100644 index 0000000..8c48b65 --- /dev/null +++ b/app/tray.go @@ -0,0 +1,20 @@ +package app + +import ( + "fyne.io/systray" + "mikescher.com/kpsync/assets" +) + +func (app *Application) initTray() { + + trayOnReady := func() { + + systray.SetIcon(assets.IconInit) + systray.SetTitle("KeepassXC Sync") + systray.SetTooltip("Initializing...") + + app.trayReady = true + } + + systray.Run(trayOnReady, nil) +} diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 0000000..16d3f0c --- /dev/null +++ b/app/utils.go @@ -0,0 +1,47 @@ +package app + +import ( + "encoding/json" + "os" + + "git.blackforestbytes.com/BlackForestBytes/goext/cryptext" + "git.blackforestbytes.com/BlackForestBytes/goext/exerr" +) + +func fileExists(p string) bool { + _, err := os.Stat(p) + if os.IsNotExist(err) { + return false + } + return true +} + +type State struct { + ETag string `json:"etag"` + Size int64 `json:"size"` + Checksum string `json:"checksum"` +} + +func (app *Application) readState() *State { + bin, err := os.ReadFile(app.stateFile) + if err != nil { + return nil + } + + var state State + err = json.Unmarshal(bin, &state) + if err != nil { + return nil + } + + return &state +} + +func (app *Application) calcLocalChecksum() (string, error) { + bin, err := os.ReadFile(app.dbFile) + if err != nil { + return "", exerr.Wrap(err, "").Build() + } + + return cryptext.BytesSha256(bin), nil +} diff --git a/app/webdav.go b/app/webdav.go new file mode 100644 index 0000000..1558da7 --- /dev/null +++ b/app/webdav.go @@ -0,0 +1,5 @@ +package app + +func (app *Application) initSync() { + +} diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..292c66f --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,14 @@ +package assets + +import ( + _ "embed" +) + +//go:embed IconInit.png +var IconInit []byte + +//go:embed iconDefault.png +var IconDefault []byte + +//go:embed iconDownload.png +var IconDownload []byte diff --git a/assets/iconDefault.png b/assets/iconDefault.png new file mode 100644 index 0000000000000000000000000000000000000000..420c618771688b74da662ab95e5e95d87aa7bbe6 GIT binary patch literal 2238 zcmbVOX;f2Z8on$=77?t7*os_(C?k-4BV|iSqM%q-K^99$E+Im4LlzPOn6Nv7ENY8_ zAW#t!?PWLYa?MaV+nc$tDDfdD!Wj)OoyA%_nILmXae zatGuA07mhG&>7fgVa4LLh`>98f7t5=m)FPt;pp8nV|PV^H8* z6YY;&0fC?|EP+508tcZzlCdBehsF_z6bi)^#A9(-3>J^U;oWc)8UatkV!`(h z3b7{P#nXbB?Dw`1$rF_zlZj~92$!e3iWZl)t1VF zp+DUCsJ1jTRSaQ*At@}EaFKe%J1&8d-2J&By&~cb?MsOODGDcv33KH_NF?)PdZLgU zG*7^zktuj0gM}lpn0PFiio;Q-;12|lPNH6Ql^DbH@BoU-QJhlWDf^YLn6MV!z3kjC!OQy3}|10iySl<7WGYsMk zM&BI&)htU#NC)b-A6g#~J`4{eLMBFnj7H3J(l7uRPx&$Fp~}8%;XQB|%g%Fdi~?_9hJ9Y9ZiWz+!%MbKGM#qtIf6O>|mGw>i1- zq-litUu?{UPbV)=cUQjXy3$z3_{e1@u4oi!MKNcnxT zxi*8{Kbe2K=&p9*zH{PZboyM|bXgRGXfop}BoF!a?Jt=vnSfg@HdOH!57zdi+!XKm z-A64qxv{ytC55XMhsy}i?S1g6<`K=%HM|+J;kG&tQd7OT~Ag8zDb2v&f4e2m6vPx*DH#QD~X@8 z`Z9F`A(aZev0v8y_52^sP==Wv?jAX(KR<8_xYXuM`q$MHYcLt$Ibr+g18bHwc-!EW zS*>mEs$ZRHpE=8qZddmV>>9J4(UFDz=E*kx_D%b`X%||zOrQb}tt`2H;E_dZ`}z}U zZMVH9KG75l7SmHQiyY;3a$JIyCcc4LZbpsL9{Q!Hci`;0tgk}{BO*u6(pLQXBS*3$ zBznbW{&#UH&S61i|1R7<&$@apZS;y`Q{RRl>+mf`)bqOOV?((lV*ct)iWT3w?~&fz z7kNGEM333M)oTNffuY@|LknGI z$rWJy+4}~^9-SNy&k-%Zb5xf-pCWWrr<=dBzJ^=0p*RG*2wK&^D60%C_{oKXEurU? zJ$YSasOe+F;RV4%guCP37FM-9%|TjDe6DNz;U3d94=3b4K}!6qY>s9|7rn921ccmw zy1y7~WL*FHLCz3r%D#84`dqhW7E{Vk9eFwvYn@|BhMFC=^+I4mwVk7p8rrH{-ySw- z`8q58Y3hL6*ZqwwyM=j|dKC(FF}uX#@FL!Q)R2B0b9KJjChg1dq3?RraRmUSlH0`<=>EU|yn z{%7~&Few*lKpDU8*w04@oyOud8F9IrtI!%oPQ$V(FM|egQYRrfYgJ&x7%g_!{hH$` z7o`sE_cG*@QOf`;&BNFqTD-58U-*KVe2RZlQr6foxQ8{>9eG~oa<(+-Nde%jD#a*{ z`Kuk9DJm1-;gy3STJNLO{=UW>c0f-U;ll;a6p+^9Ivy zX*zAmlr>WR&RHAcuHr+p={|6VrS;30BKc;!!dyZ&`Sl*d%hoZ8q9 Y7!v)$-59Qc^?ww8tewoiGh(y;16!JrJ^%m! literal 0 HcmV?d00001 diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..1070871 --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "git.blackforestbytes.com/BlackForestBytes/goext/exerr" + "git.blackforestbytes.com/BlackForestBytes/goext/langext" + "mikescher.com/kpsync/app" +) + +func main() { + exerr.Init(exerr.ErrorPackageConfigInit{ + ZeroLogErrTraces: langext.PFalse, + ZeroLogAllTraces: langext.PFalse, + }) + + kpApp := app.NewApplication() + kpApp.Run() +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..b541e09 --- /dev/null +++ b/config.go @@ -0,0 +1,104 @@ +package kpsync + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "os/user" + "strings" + + "git.blackforestbytes.com/BlackForestBytes/goext/langext" + "mikescher.com/kpsync/log" +) + +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"` + + Debounce int `json:"debounce"` +} + +func LoadConfig() Config { + 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)") + + flag.Parse() + + if strings.HasSuffix(configPath, "~") { + usr, err := user.Current() + if err != nil { + log.FatalErr("Failed to query users home directory", err) + } + fmt.Println(usr.HomeDir) + + configPath = strings.TrimSuffix(configPath, "~") + configPath = fmt.Sprintf("%s/%s", usr.HomeDir, configPath) + } + + if _, err := os.Stat(configPath); os.IsNotExist(err) && configPath != "" { + _ = os.WriteFile(configPath, langext.Must(json.Marshal(Config{ + WebDAVURL: "https://your-nextcloud-domain.example/remote.php/dav/files/keepass.kdbx", + WebDAVUser: "", + WebDAVPass: "", + LocalFallback: "", + WorkDir: "/tmp/kpsync", + Debounce: 3500, + })), 0644) + } + + cfgBin, err := os.ReadFile(configPath) + if err != nil { + log.FatalErr("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) + } + + 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 + } + + return cfg +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..11349a8 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module mikescher.com/kpsync + +go 1.24.6 + +require ( + fyne.io/systray v1.11.0 // indirect + git.blackforestbytes.com/BlackForestBytes/goext v0.0.594 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + golang.org/x/sys v0.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d9880a9 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= +fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +git.blackforestbytes.com/BlackForestBytes/goext v0.0.594 h1:tFbvEAe7FMvLuW9/RuXXlxsi8/Ve7xVrFxZnk8e/0yU= +git.blackforestbytes.com/BlackForestBytes/goext v0.0.594/go.mod h1:vczjjViG013HjA5Ka3VTE7axDgqMChn1EsvEVg9LZnU= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/log/logger.go b/log/logger.go new file mode 100644 index 0000000..5333aa6 --- /dev/null +++ b/log/logger.go @@ -0,0 +1,27 @@ +package log + +//TODO + +func Fatal(msg string) { + panic(msg) +} + +func FatalErr(msg string, err error) { + if err != nil { + panic("ERROR: " + msg + "\n" + err.Error()) + } else { + panic("ERROR: " + msg) + } +} + +func LogError(msg string, err error) { + if err != nil { + println("ERROR: " + msg + "\n" + err.Error()) + } else { + println("ERROR: " + msg) + } +} + +func LogInfo(msg string) { + println("INFO: " + msg) +}