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.
57 lines
1.2 KiB
Go
57 lines
1.2 KiB
Go
package client
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// sseDecoder reads the subset of the Server-Sent Events format the server
|
|
// emits: "event:" and "data:" lines, one JSON payload per event.
|
|
type sseDecoder struct {
|
|
sc *bufio.Scanner
|
|
}
|
|
|
|
func newSSEDecoder(r io.Reader) *sseDecoder {
|
|
sc := bufio.NewScanner(r)
|
|
sc.Buffer(make([]byte, 0, 8<<10), 1<<20)
|
|
return &sseDecoder{sc: sc}
|
|
}
|
|
|
|
// next returns the next event, or nil for a comment or keepalive line.
|
|
func (d *sseDecoder) next() (*Event, error) {
|
|
var (
|
|
name string
|
|
data string
|
|
)
|
|
for d.sc.Scan() {
|
|
line := d.sc.Text()
|
|
switch {
|
|
case line == "":
|
|
if data == "" {
|
|
name = ""
|
|
continue
|
|
}
|
|
var ev Event
|
|
if err := json.Unmarshal([]byte(data), &ev); err != nil {
|
|
return nil, nil // skip a payload we cannot read
|
|
}
|
|
if ev.Type == "" {
|
|
ev.Type = name
|
|
}
|
|
return &ev, nil
|
|
case strings.HasPrefix(line, ":"):
|
|
continue
|
|
case strings.HasPrefix(line, "event:"):
|
|
name = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
|
case strings.HasPrefix(line, "data:"):
|
|
data += strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
|
}
|
|
}
|
|
if err := d.sc.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, io.EOF
|
|
}
|