package syncthing import ( "crypto/tls" "encoding/json" "fmt" "io" "net/http" "time" ) // Client talks to the Syncthing REST API. type Client struct { baseURL string apiKey string httpClient *http.Client } // NewClient creates a Syncthing API client. func NewClient(baseURL, apiKey string) *Client { return &Client{ baseURL: baseURL, apiKey: apiKey, httpClient: &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, }, } } // SetAPIKey updates the API key. func (c *Client) SetAPIKey(key string) { c.apiKey = key } // SetBaseURL updates the base URL. func (c *Client) SetBaseURL(url string) { c.baseURL = url } func (c *Client) get(path string, noAuth bool) ([]byte, error) { req, err := http.NewRequest("GET", c.baseURL+path, nil) if err != nil { return nil, err } if !noAuth && c.apiKey != "" { req.Header.Set("X-API-Key", c.apiKey) } resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } return io.ReadAll(resp.Body) } func (c *Client) post(path string) error { req, err := http.NewRequest("POST", c.baseURL+path, nil) if err != nil { return err } if c.apiKey != "" { req.Header.Set("X-API-Key", c.apiKey) } resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } return nil } // Health checks if Syncthing is reachable (no auth required). func (c *Client) Health() (*HealthStatus, error) { data, err := c.get("/rest/noauth/health", true) if err != nil { return nil, err } var s HealthStatus return &s, json.Unmarshal(data, &s) } // SystemStatus returns system status info. func (c *Client) SystemStatus() (*SystemStatus, error) { data, err := c.get("/rest/system/status", false) if err != nil { return nil, err } var s SystemStatus return &s, json.Unmarshal(data, &s) } // SystemVersion returns version info. func (c *Client) SystemVersion() (*SystemVersion, error) { data, err := c.get("/rest/system/version", false) if err != nil { return nil, err } var s SystemVersion return &s, json.Unmarshal(data, &s) } // SystemConnections returns all device connections. func (c *Client) SystemConnections() (*Connections, error) { data, err := c.get("/rest/system/connections", false) if err != nil { return nil, err } var s Connections return &s, json.Unmarshal(data, &s) } // Config returns the full Syncthing config (folders + devices). func (c *Client) Config() (*FullConfig, error) { data, err := c.get("/rest/config", false) if err != nil { return nil, err } var s FullConfig return &s, json.Unmarshal(data, &s) } // FolderStatus returns the sync status for a folder. func (c *Client) FolderStatus(folderID string) (*FolderStatus, error) { data, err := c.get("/rest/db/status?folder="+folderID, false) if err != nil { return nil, err } var s FolderStatus return &s, json.Unmarshal(data, &s) } // Events long-polls for new events since the given ID. func (c *Client) Events(since int, timeout int) ([]Event, error) { path := fmt.Sprintf("/rest/events?since=%d&timeout=%d", since, timeout) // Use a longer HTTP timeout for long-polling client := &http.Client{ Timeout: time.Duration(timeout+10) * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, } req, err := http.NewRequest("GET", c.baseURL+path, nil) if err != nil { return nil, err } if c.apiKey != "" { req.Header.Set("X-API-Key", c.apiKey) } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } var events []Event data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return events, json.Unmarshal(data, &events) } // PendingDevices returns new device requests. func (c *Client) PendingDevices() (map[string]PendingDevice, error) { data, err := c.get("/rest/cluster/pending/devices", false) if err != nil { return nil, err } var s map[string]PendingDevice return s, json.Unmarshal(data, &s) } // PauseAll pauses all devices. func (c *Client) PauseAll() error { return c.post("/rest/system/pause") } // ResumeAll resumes all devices. func (c *Client) ResumeAll() error { return c.post("/rest/system/resume") } // RescanAll triggers a rescan of all folders. func (c *Client) RescanAll() error { return c.post("/rest/db/scan") } // Restart restarts Syncthing. func (c *Client) Restart() error { return c.post("/rest/system/restart") } // Shutdown stops Syncthing. func (c *Client) Shutdown() error { return c.post("/rest/system/shutdown") }