Add client engine and desktop CLI that mirror KDBX files
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.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
package client
|
||||
|
||||
// action is what the agent does to one database on one pass.
|
||||
type action int
|
||||
|
||||
const (
|
||||
actNothing action = iota
|
||||
// actPull writes the server copy over the local file.
|
||||
actPull
|
||||
// actPush uploads the local file against the version it was based on.
|
||||
actPush
|
||||
// actForcePush uploads the local file and overwrites a newer server copy.
|
||||
actForcePush
|
||||
// actServerWins keeps the local file as a conflict copy, then pulls.
|
||||
actServerWins
|
||||
// actRestore is actServerWins forced on the client by read-only access.
|
||||
actRestore
|
||||
// actManual writes the server copy beside the local file and waits for a
|
||||
// human.
|
||||
actManual
|
||||
)
|
||||
|
||||
func (a action) String() string {
|
||||
switch a {
|
||||
case actPull:
|
||||
return "pull"
|
||||
case actPush:
|
||||
return "push"
|
||||
case actForcePush:
|
||||
return "force-push"
|
||||
case actServerWins:
|
||||
return "server-wins"
|
||||
case actRestore:
|
||||
return "restore"
|
||||
case actManual:
|
||||
return "manual-conflict"
|
||||
}
|
||||
return "nothing"
|
||||
}
|
||||
|
||||
// situation is everything the agent knows about one database at one moment.
|
||||
type situation struct {
|
||||
LocalExists bool
|
||||
// LocalSHA is the digest of the file on disk now.
|
||||
LocalSHA string
|
||||
// SyncedSHA and SyncedVersion are what this client last agreed with the
|
||||
// server. An empty SyncedSHA means this client has never synced the file.
|
||||
SyncedSHA string
|
||||
SyncedVersion int64
|
||||
RemoteSHA string
|
||||
RemoteVersion int64
|
||||
CanWrite bool
|
||||
Strategy string
|
||||
// ConflictSeen is set when this exact manual conflict was already
|
||||
// reported, so it is not reported again on every pass.
|
||||
ConflictSeen bool
|
||||
}
|
||||
|
||||
// decide is the whole client-side synchronization policy, kept pure so the
|
||||
// table of cases can be read and tested in one place.
|
||||
func decide(s situation) action {
|
||||
localChanged := s.LocalExists && s.LocalSHA != s.SyncedSHA
|
||||
remoteChanged := s.RemoteVersion != s.SyncedVersion || s.RemoteSHA != s.SyncedSHA
|
||||
|
||||
switch {
|
||||
case !s.LocalExists:
|
||||
if s.RemoteVersion > 0 {
|
||||
return actPull
|
||||
}
|
||||
return actNothing
|
||||
|
||||
case localChanged && !remoteChanged:
|
||||
if !s.CanWrite {
|
||||
return actRestore
|
||||
}
|
||||
return actPush
|
||||
|
||||
case !localChanged && remoteChanged:
|
||||
return actPull
|
||||
|
||||
case localChanged && remoteChanged:
|
||||
switch {
|
||||
case !s.CanWrite:
|
||||
return actRestore
|
||||
case s.Strategy == "client-wins":
|
||||
return actForcePush
|
||||
case s.Strategy == "manual":
|
||||
if s.ConflictSeen {
|
||||
return actNothing
|
||||
}
|
||||
return actManual
|
||||
default:
|
||||
return actServerWins
|
||||
}
|
||||
}
|
||||
return actNothing
|
||||
}
|
||||
Reference in New Issue
Block a user