Why: a KeePass application must see an ordinary .kdbx file. A mirror directory of real files gives that on every desktop platform without FUSE, WinFsp or macFUSE, and without cgo. What: internal/client holds the API wrapper, an SSE reader, and a mirror agent whose entire policy is one pure decide() table: adopt, pull, push, force-push, server-wins, restore or manual. Saves are detected by rescan, remote changes arrive over the event stream, and every overwrite of local bytes leaves a conflict copy. cmd/keeporg drives it: login (password, TOTP or browser/OIDC device flow), run, sync, status, plus the group, grant, database and invite administration. Proved: go test ./internal/client covers the decision table and runs a real server in-process for adopt, follow, server-wins, client-wins, manual, read-only restore, server-side rename and event-driven update. Also smoke tested live: built both binaries, bootstrapped a server, logged in, created a database and adopted a local file.
88 lines
2.5 KiB
Go
88 lines
2.5 KiB
Go
// Command keeporg is the KeepOrg desktop client.
|
|
//
|
|
// It mirrors every KDBX database assigned to you into one directory as real
|
|
// files, pushes each save to the server and applies what other clients push.
|
|
// Point KeePassXC, KeePass2 or any compatible application at those files.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"text/tabwriter"
|
|
|
|
"cc.tiredsysadmin.keeporg/internal/client"
|
|
)
|
|
|
|
// Version is stamped at build time from versions.mk.
|
|
var Version = "dev"
|
|
|
|
type command struct {
|
|
name string
|
|
help string
|
|
run func(ctx context.Context, args []string) error
|
|
}
|
|
|
|
func main() {
|
|
cmds := []command{
|
|
{"login", "sign in to a server and choose the mirror directory", cmdLogin},
|
|
{"logout", "revoke this device's session", cmdLogout},
|
|
{"run", "keep the mirror in sync until stopped", cmdRun},
|
|
{"sync", "reconcile once and exit", cmdSync},
|
|
{"status", "show each mirrored database", cmdStatus},
|
|
{"totp", "enrol a second factor for this account", cmdTOTP},
|
|
{"whoami", "show the signed-in account and its groups", cmdWhoami},
|
|
{"groups", "list groups you can see", cmdGroups},
|
|
{"group", "create or delete a group", cmdGroup},
|
|
{"member", "add or remove a group member", cmdMember},
|
|
{"db", "create, rename, re-strategy or delete a database", cmdDB},
|
|
{"grant", "give or take a group's access to a database", cmdGrant},
|
|
{"invite", "create or list invite links", cmdInvite},
|
|
{"users", "list accounts you administer", cmdUsers},
|
|
{"version", "print the version", nil},
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
if len(os.Args) < 2 {
|
|
usage(cmds)
|
|
os.Exit(2)
|
|
}
|
|
name := os.Args[1]
|
|
if name == "version" || name == "-version" || name == "--version" {
|
|
fmt.Println("keeporg", Version)
|
|
return
|
|
}
|
|
for _, c := range cmds {
|
|
if c.name != name {
|
|
continue
|
|
}
|
|
if err := c.run(ctx, os.Args[2:]); err != nil {
|
|
if errors.Is(err, context.Canceled) {
|
|
return
|
|
}
|
|
fmt.Fprintln(os.Stderr, "keeporg:", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
fmt.Fprintf(os.Stderr, "keeporg: unknown command %q\n\n", name)
|
|
usage(cmds)
|
|
os.Exit(2)
|
|
}
|
|
|
|
func usage(cmds []command) {
|
|
fmt.Fprintf(os.Stderr, "keeporg %s - KeePass database synchronization\n\nusage: keeporg <command> [flags]\n\n", Version)
|
|
w := tabwriter.NewWriter(os.Stderr, 0, 2, 2, ' ', 0)
|
|
for _, c := range cmds {
|
|
fmt.Fprintf(w, " %s\t%s\n", c.name, c.help)
|
|
}
|
|
w.Flush()
|
|
fmt.Fprintf(os.Stderr, "\nRun `keeporg <command> -h` for a command's flags.\n"+
|
|
"Configuration lives in %s\n", client.DefaultConfigPath())
|
|
}
|