Initial scaffold: project structure, .gitignore, go.mod
This commit is contained in:
210
internal/syncthing/client.go
Normal file
210
internal/syncthing/client.go
Normal file
@@ -0,0 +1,210 @@
|
||||
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")
|
||||
}
|
||||
27
internal/syncthing/config_xml.go
Normal file
27
internal/syncthing/config_xml.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package syncthing
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"os"
|
||||
|
||||
"git.davoryn.de/calic/syncwarden/internal/config"
|
||||
)
|
||||
|
||||
type syncthingConfig struct {
|
||||
GUI struct {
|
||||
APIKey string `xml:"apikey"`
|
||||
} `xml:"gui"`
|
||||
}
|
||||
|
||||
// DiscoverAPIKey reads the Syncthing config.xml and extracts the API key.
|
||||
func DiscoverAPIKey() (string, error) {
|
||||
data, err := os.ReadFile(config.SyncthingConfigPath())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var cfg syncthingConfig
|
||||
if err := xml.Unmarshal(data, &cfg); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cfg.GUI.APIKey, nil
|
||||
}
|
||||
88
internal/syncthing/types.go
Normal file
88
internal/syncthing/types.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package syncthing
|
||||
|
||||
import "time"
|
||||
|
||||
// HealthStatus from /rest/noauth/health.
|
||||
type HealthStatus struct {
|
||||
Status string `json:"status"` // "OK"
|
||||
}
|
||||
|
||||
// SystemStatus from /rest/system/status.
|
||||
type SystemStatus struct {
|
||||
MyID string `json:"myID"`
|
||||
Uptime int `json:"uptime"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
}
|
||||
|
||||
// SystemVersion from /rest/system/version.
|
||||
type SystemVersion struct {
|
||||
Version string `json:"version"`
|
||||
LongVersion string `json:"longVersion"`
|
||||
}
|
||||
|
||||
// ConnectionInfo for a single device from /rest/system/connections.
|
||||
type ConnectionInfo struct {
|
||||
Connected bool `json:"connected"`
|
||||
Paused bool `json:"paused"`
|
||||
Address string `json:"address"`
|
||||
Type string `json:"type"`
|
||||
InBytesTotal int64 `json:"inBytesTotal"`
|
||||
OutBytesTotal int64 `json:"outBytesTotal"`
|
||||
}
|
||||
|
||||
// Connections from /rest/system/connections.
|
||||
type Connections struct {
|
||||
Total ConnectionInfo `json:"total"`
|
||||
Connections map[string]ConnectionInfo `json:"connections"`
|
||||
}
|
||||
|
||||
// FolderConfig from /rest/config folders array.
|
||||
type FolderConfig struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Path string `json:"path"`
|
||||
Paused bool `json:"paused"`
|
||||
}
|
||||
|
||||
// DeviceConfig from /rest/config devices array.
|
||||
type DeviceConfig struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Name string `json:"name"`
|
||||
Paused bool `json:"paused"`
|
||||
}
|
||||
|
||||
// FullConfig from /rest/config.
|
||||
type FullConfig struct {
|
||||
Folders []FolderConfig `json:"folders"`
|
||||
Devices []DeviceConfig `json:"devices"`
|
||||
}
|
||||
|
||||
// FolderStatus from /rest/db/status.
|
||||
type FolderStatus struct {
|
||||
State string `json:"state"` // "idle", "scanning", "syncing", "error", etc.
|
||||
StateChanged time.Time `json:"stateChanged"`
|
||||
NeedFiles int `json:"needFiles"`
|
||||
NeedBytes int64 `json:"needBytes"`
|
||||
GlobalFiles int `json:"globalFiles"`
|
||||
GlobalBytes int64 `json:"globalBytes"`
|
||||
LocalFiles int `json:"localFiles"`
|
||||
LocalBytes int64 `json:"localBytes"`
|
||||
Errors int `json:"errors"`
|
||||
PullErrors int `json:"pullErrors"`
|
||||
}
|
||||
|
||||
// Event from /rest/events.
|
||||
type Event struct {
|
||||
ID int `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Time time.Time `json:"time"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// PendingDevice from /rest/cluster/pending/devices.
|
||||
type PendingDevice struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Name string `json:"name"`
|
||||
Time time.Time `json:"time"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
Reference in New Issue
Block a user