package sync import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" "path/filepath" "time" ) // Client communicates with the Verstak Sync Server. type Client struct { ServerURL string APIKey string DeviceID string VaultRoot string HTTP *http.Client } // NewClient creates a sync client. func NewClient(serverURL, apiKey, deviceID, vaultRoot string) *Client { return &Client{ ServerURL: serverURL, APIKey: apiKey, DeviceID: deviceID, VaultRoot: vaultRoot, HTTP: &http.Client{Timeout: 30 * time.Second}, } } // RegisterDevice calls POST /api/v1/device/register and returns the API key. func (c *Client) RegisterDevice(name string) (apiKey string, err error) { body := map[string]string{"name": name} var resp struct { DeviceID string `json:"device_id"` APIKey string `json:"api_key"` } if err := c.post("/api/v1/device/register", body, &resp); err != nil { return "", err } return resp.APIKey, nil } // RegisterDeviceWithAuth registers a device with user credentials. func (c *Client) RegisterDeviceWithAuth(name, username, password string) (deviceID, apiKey string, err error) { body := map[string]string{"name": name, "username": username, "password": password} var resp struct { DeviceID string `json:"device_id"` APIKey string `json:"api_key"` } // Temporarily clear API key for this request (server expects login/password, not API key). savedKey := c.APIKey c.APIKey = "" err = c.post("/api/v1/device/register", body, &resp) c.APIKey = savedKey if err != nil { return "", "", err } return resp.DeviceID, resp.APIKey, nil } // Login authenticates with user credentials and returns a session token. func (c *Client) Login(username, password string) (token string, err error) { body := map[string]string{"username": username, "password": password} var resp struct { Token string `json:"token"` } savedKey := c.APIKey c.APIKey = "" err = c.post("/api/v1/auth/login", body, &resp) c.APIKey = savedKey if err != nil { return "", err } return resp.Token, nil } // PushRequest is the payload for POST /sync/push. type PushRequest struct { DeviceID string `json:"device_id"` Ops []PushOp `json:"ops"` } // PushOp is a single operation in a push request. type PushOp struct { OpID string `json:"op_id"` EntityType string `json:"entity_type"` EntityID string `json:"entity_id"` OpType string `json:"op_type"` PayloadJSON string `json:"payload_json"` CreatedAt string `json:"created_at"` } // PushResponse is the response from POST /sync/push. type PushResponse struct { Accepted []string `json:"accepted"` Count int `json:"count"` } // Push sends local operations to the server. func (c *Client) Push(ops []Op) (*PushResponse, error) { pushOps := make([]PushOp, len(ops)) for i, op := range ops { pushOps[i] = PushOp{ OpID: op.OpID, EntityType: op.EntityType, EntityID: op.EntityID, OpType: op.OpType, PayloadJSON: op.PayloadJSON, CreatedAt: op.CreatedAt, } } req := PushRequest{DeviceID: c.DeviceID, Ops: pushOps} var resp PushResponse if err := c.post("/api/v1/sync/push", req, &resp); err != nil { return nil, err } return &resp, nil } // PullRequest is the payload for POST /sync/pull. type PullRequest struct { SinceRevision int `json:"since_revision"` } // PullResponse is the response from POST /sync/pull. type PullResponse struct { ServerRevision int `json:"server_revision"` Ops []Op `json:"ops"` } // Pull fetches remote operations since a given revision. func (c *Client) Pull(sinceRevision int) (*PullResponse, error) { req := PullRequest{SinceRevision: sinceRevision} var resp PullResponse if err := c.post("/api/v1/sync/pull", req, &resp); err != nil { return nil, err } return &resp, nil } // UploadBlob uploads a file to the server and returns its SHA-256. func (c *Client) UploadBlob(localPath string) (sha256 string, err error) { var b bytes.Buffer w := multipart.NewWriter(&b) fw, err := w.CreateFormFile("file", filepath.Base(localPath)) if err != nil { return "", err } f, err := os.Open(localPath) if err != nil { return "", err } defer f.Close() if _, err := io.Copy(fw, f); err != nil { return "", err } w.Close() req, err := http.NewRequest("POST", c.ServerURL+"/api/v1/blobs/", &b) if err != nil { return "", err } req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+c.APIKey) resp, err := c.HTTP.Do(req) if err != nil { return "", err } defer resp.Body.Close() var result struct { SHA256 string `json:"sha256"` Size int `json:"size"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } return result.SHA256, nil } // DownloadBlob downloads a blob by SHA-256 hash. func (c *Client) DownloadBlob(sha256, destPath string) error { req, err := http.NewRequest("GET", c.ServerURL+"/api/v1/blobs/"+sha256, nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.APIKey) resp, err := c.HTTP.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("download blob: HTTP %d", resp.StatusCode) } out, err := os.Create(destPath) if err != nil { return err } defer out.Close() _, err = io.Copy(out, resp.Body) return err } // --- internal --- func (c *Client) post(path string, body, result interface{}) error { var b bytes.Buffer if err := json.NewEncoder(&b).Encode(body); err != nil { return err } req, err := http.NewRequest("POST", c.ServerURL+path, &b) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.APIKey) resp, err := c.HTTP.Do(req) if err != nil { return fmt.Errorf("http: %w", err) } defer resp.Body.Close() if resp.StatusCode >= 400 { data, _ := io.ReadAll(resp.Body) return fmt.Errorf("server %d: %s", resp.StatusCode, string(data)) } if result != nil { return json.NewDecoder(resp.Body).Decode(result) } return nil }