215 lines
5.6 KiB
Go
215 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"time"
|
|
|
|
"verstak/internal/core/config"
|
|
syncsvc "verstak/internal/core/sync"
|
|
)
|
|
|
|
type SyncStatusDTO struct {
|
|
Configured bool `json:"configured"`
|
|
ServerURL string `json:"serverUrl"`
|
|
DeviceID string `json:"deviceId"`
|
|
DeviceName string `json:"deviceName"`
|
|
Connected bool `json:"connected"`
|
|
Revoked bool `json:"revoked"`
|
|
TokenStored bool `json:"tokenStored"`
|
|
UnpushedOps int `json:"unpushedOps"`
|
|
LastSyncAt string `json:"lastSyncAt"`
|
|
SyncInterval int `json:"syncInterval"`
|
|
}
|
|
|
|
func (a *App) SyncStatus() (*SyncStatusDTO, error) {
|
|
serverURL, apiKey, _, lastSyncAt, err := a.sync.GetState()
|
|
if err != nil {
|
|
return &SyncStatusDTO{}, nil
|
|
}
|
|
cfg, _ := config.Load(a.vault)
|
|
deviceToken := config.LoadDeviceToken(a.vault)
|
|
dto := &SyncStatusDTO{
|
|
Configured: serverURL != "" && (apiKey != "" || deviceToken != ""),
|
|
ServerURL: serverURL,
|
|
LastSyncAt: lastSyncAt,
|
|
UnpushedOps: 0,
|
|
TokenStored: deviceToken != "",
|
|
}
|
|
if cfg != nil {
|
|
dto.DeviceID = cfg.Sync.DeviceID
|
|
dto.SyncInterval = cfg.Sync.SyncInterval
|
|
}
|
|
unpushed, _ := a.sync.GetUnpushedOps()
|
|
dto.UnpushedOps = len(unpushed)
|
|
|
|
if deviceToken != "" {
|
|
client := syncsvc.NewClient(serverURL, "", "", a.vault)
|
|
client.DeviceToken = deviceToken
|
|
if cfg != nil {
|
|
client.DeviceID = cfg.Sync.DeviceID
|
|
}
|
|
if info, err := client.GetMe(); err == nil {
|
|
dto.DeviceName = info.DeviceName
|
|
dto.DeviceID = info.DeviceID
|
|
dto.Connected = true
|
|
if info.RevokedAt != "" {
|
|
dto.Revoked = true
|
|
dto.Connected = false
|
|
}
|
|
}
|
|
}
|
|
return dto, nil
|
|
}
|
|
|
|
func (a *App) SyncConfigure(serverURL, username, password string) error {
|
|
hostname, _ := os.Hostname()
|
|
if hostname == "" {
|
|
hostname = "unknown"
|
|
}
|
|
client := syncsvc.NewClient(serverURL, "", "", a.vault)
|
|
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-gui/v2")
|
|
if err != nil {
|
|
return fmt.Errorf("pair: %w", err)
|
|
}
|
|
if err := config.SaveDeviceToken(a.vault, deviceToken); err != nil {
|
|
return fmt.Errorf("save token: %w", err)
|
|
}
|
|
if err := a.sync.SetState(serverURL, ""); err != nil {
|
|
return err
|
|
}
|
|
cfg, err := config.Load(a.vault)
|
|
if err != nil {
|
|
cfg = &config.Config{}
|
|
}
|
|
cfg.Sync.ServerURL = serverURL
|
|
cfg.Sync.DeviceID = deviceID
|
|
cfg.Sync.APIKey = ""
|
|
return config.Save(a.vault, cfg)
|
|
}
|
|
|
|
func (a *App) SyncDisconnect() error {
|
|
deviceToken := config.LoadDeviceToken(a.vault)
|
|
cfg, err := config.Load(a.vault)
|
|
if err != nil {
|
|
cfg = &config.Config{}
|
|
}
|
|
if deviceToken != "" {
|
|
client := syncsvc.NewClient(cfg.Sync.ServerURL, "", "", a.vault)
|
|
client.DeviceToken = deviceToken
|
|
_ = client.RevokeCurrent()
|
|
}
|
|
config.RemoveDeviceToken(a.vault)
|
|
cfg.Sync.ServerURL = ""
|
|
cfg.Sync.DeviceID = ""
|
|
cfg.Sync.APIKey = ""
|
|
if err := config.Save(a.vault, cfg); err != nil {
|
|
return err
|
|
}
|
|
return a.sync.SetState("", "")
|
|
}
|
|
|
|
func (a *App) SyncTestConnection(serverURL, username, password string) error {
|
|
client := syncsvc.NewClient(serverURL, "", "", a.vault)
|
|
return client.TestAuth(serverURL, username, password)
|
|
}
|
|
|
|
func (a *App) SyncSetInterval(minutes int) error {
|
|
cfg, err := config.Load(a.vault)
|
|
if err != nil {
|
|
cfg = &config.Config{}
|
|
}
|
|
if cfg.Sync.ServerURL == "" {
|
|
sURL, _, _, _, _ := a.sync.GetState()
|
|
if sURL != "" {
|
|
cfg.Sync.ServerURL = sURL
|
|
}
|
|
}
|
|
if cfg.Sync.DeviceID == "" {
|
|
cfg.Sync.DeviceID = a.sync.GetDeviceID()
|
|
}
|
|
cfg.Sync.SyncInterval = minutes
|
|
return config.Save(a.vault, cfg)
|
|
}
|
|
|
|
func (a *App) SyncNow() (map[string]interface{}, error) {
|
|
serverURL, apiKey, lastPullSeq, _, err := a.sync.GetState()
|
|
deviceToken := config.LoadDeviceToken(a.vault)
|
|
if err != nil || serverURL == "" || (apiKey == "" && deviceToken == "") {
|
|
return nil, fmt.Errorf("sync not configured")
|
|
}
|
|
|
|
deviceID := ""
|
|
if cfg, err := config.Load(a.vault); err == nil {
|
|
deviceID = cfg.Sync.DeviceID
|
|
}
|
|
|
|
client := syncsvc.NewClient(serverURL, apiKey, deviceID, a.vault)
|
|
client.DeviceToken = deviceToken
|
|
|
|
unpushed, err := a.sync.GetUnpushedOps()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get ops: %w", err)
|
|
}
|
|
for i := range unpushed {
|
|
unpushed[i].LastSeenServerSeq = lastPullSeq
|
|
}
|
|
pushResult := &syncsvc.PushResponse{}
|
|
if len(unpushed) > 0 {
|
|
pushResult, err = client.Push(unpushed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("push: %w", err)
|
|
}
|
|
if err := a.sync.MarkPushed(pushResult.Accepted); err != nil {
|
|
return nil, fmt.Errorf("mark pushed: %w", err)
|
|
}
|
|
}
|
|
|
|
pullResult, err := client.Pull(lastPullSeq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pull: %w", err)
|
|
}
|
|
|
|
var applyErrors []string
|
|
for _, op := range pullResult.Ops {
|
|
if err := a.applyRemoteOp(op); err != nil {
|
|
applyErrors = append(applyErrors, fmt.Sprintf("%s/%s: %v", op.EntityType, op.OpID, err))
|
|
}
|
|
_ = a.sync.RecordRemoteOp(op)
|
|
}
|
|
if len(pullResult.Ops) > 0 {
|
|
opIDs := make([]string, len(pullResult.Ops))
|
|
for i, op := range pullResult.Ops {
|
|
opIDs[i] = op.OpID
|
|
}
|
|
_ = a.sync.MarkApplied(opIDs)
|
|
}
|
|
|
|
if len(pushResult.Conflicts) > 0 {
|
|
log.Printf("[sync] %d conflict(s) detected on push", len(pushResult.Conflicts))
|
|
for _, c := range pushResult.Conflicts {
|
|
log.Printf("[sync] conflict: op=%v entity=%v/%v",
|
|
c["op_id"], c["entity_type"], c["entity_id"])
|
|
}
|
|
}
|
|
|
|
if pullResult.ServerSequence > lastPullSeq {
|
|
_ = a.sync.SetLastPullSeq(pullResult.ServerSequence)
|
|
}
|
|
_ = a.sync.SetLastSyncAt(time.Now().UTC().Format(time.RFC3339))
|
|
|
|
result := map[string]interface{}{
|
|
"pushed": len(pushResult.Accepted),
|
|
"pulled": len(pullResult.Ops),
|
|
"serverSequence": pullResult.ServerSequence,
|
|
}
|
|
if len(applyErrors) > 0 {
|
|
result["applyErrors"] = applyErrors
|
|
}
|
|
if len(pushResult.Conflicts) > 0 {
|
|
result["conflicts"] = pushResult.Conflicts
|
|
}
|
|
return result, nil
|
|
}
|