non-functional wip state

This commit is contained in:
Mike Schwörer 2025-08-18 18:54:08 +02:00
commit e7293464c1
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
20 changed files with 534 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@ -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/*

8
.idea/.gitignore generated vendored Normal file
View File

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

View File

@ -0,0 +1,11 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

9
.idea/kpsync.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/kpsync.iml" filepath="$PROJECT_DIR$/.idea/kpsync.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

43
Makefile Normal file
View File

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

19
TODO.md Normal file
View File

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

73
app/application.go Normal file
View File

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

67
app/sync.go Normal file
View File

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

20
app/tray.go Normal file
View File

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

47
app/utils.go Normal file
View File

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

5
app/webdav.go Normal file
View File

@ -0,0 +1,5 @@
package app
func (app *Application) initSync() {
}

14
assets/assets.go Normal file
View File

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

BIN
assets/iconDefault.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

17
cmd/cli/main.go Normal file
View File

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

104
config.go Normal file
View File

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

10
go.mod Normal file
View File

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

8
go.sum Normal file
View File

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

27
log/logger.go Normal file
View File

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