From 2685228c40b9125388ad5f72fcbab85b5b7605bc Mon Sep 17 00:00:00 2001 From: Jordan Hotmann Date: Fri, 10 Nov 2023 16:22:46 -0700 Subject: [PATCH] More API clients --- go.mod | 9 ++- go.sum | 15 ++++- internal/util/resty.go | 15 +++++ pkg/gokapi/api.go | 106 +++++++++++++++++++++++++++++++++++ pkg/homeassistant/structs.go | 4 ++ pkg/nws/api.go | 93 ++++++++++++++++++++++++++++++ pkg/nws/structs.go | 51 +++++++++++++++++ pkg/qbittorrent/api.go | 86 ++++++++++++++++++++++++++++ pkg/qbittorrent/structs.go | 10 ++++ pkg/syncthing/rest.go | 83 +++++++++++++++++++++++++++ pkg/syncthing/structs.go | 51 +++++++++++++++++ 11 files changed, 517 insertions(+), 6 deletions(-) create mode 100644 internal/util/resty.go create mode 100644 pkg/gokapi/api.go create mode 100644 pkg/nws/api.go create mode 100644 pkg/nws/structs.go create mode 100644 pkg/qbittorrent/api.go create mode 100644 pkg/qbittorrent/structs.go create mode 100644 pkg/syncthing/rest.go create mode 100644 pkg/syncthing/structs.go diff --git a/go.mod b/go.mod index f864346..afacbb6 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,13 @@ require ( github.com/google/uuid v1.3.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/net v0.15.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/time v0.3.0 // indirect ) require ( + github.com/gabriel-vasile/mimetype v1.4.3 github.com/go-chi/chi v1.5.5 github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/render v1.0.3 @@ -27,8 +29,9 @@ require ( github.com/nats-io/nats-server/v2 v2.10.2 // indirect github.com/nats-io/nkeys v0.4.5 // indirect github.com/nats-io/nuid v1.0.1 // indirect - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/sys v0.12.0 // indirect + github.com/samber/lo v1.38.1 + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index a585ce6..a755266 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= @@ -49,6 +51,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -62,8 +66,11 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -71,8 +78,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -83,8 +91,9 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/internal/util/resty.go b/internal/util/resty.go new file mode 100644 index 0000000..b218db1 --- /dev/null +++ b/internal/util/resty.go @@ -0,0 +1,15 @@ +package util + +import ( + "fmt" + + "github.com/go-resty/resty/v2" +) + +// CheckSuccess wraps a resty request and if the status code is not 2XX will make the error non-nil +func CheckSuccess(resp *resty.Response, err error) (*resty.Response, error) { + if err == nil && !resp.IsSuccess() { + err = fmt.Errorf("%d status received: %s", resp.StatusCode(), string(resp.Body())) + } + return resp, err +} diff --git a/pkg/gokapi/api.go b/pkg/gokapi/api.go new file mode 100644 index 0000000..cf6c555 --- /dev/null +++ b/pkg/gokapi/api.go @@ -0,0 +1,106 @@ +package gokapi + +import ( + "fmt" + "os" + "path/filepath" + + "code.jhot.me/jhot/hats/internal/util" + "github.com/gabriel-vasile/mimetype" + "github.com/go-resty/resty/v2" +) + +type GokapiClient struct { + host string + restClient *resty.Client +} + +func New(host string, token string) *GokapiClient { + return &GokapiClient{ + host: host, + restClient: resty.New(). + SetHeader("apikey", token). + SetBaseURL(fmt.Sprintf("%s/api", host)), + } +} + +func NewFromEnv() *GokapiClient { + return New(os.Getenv("GOKAPI_HOST"), os.Getenv("GOKAPI_TOKEN")) +} + +type GokapiFile struct { + ID string `json:"Id"` + Name string `json:"Name"` + Size string `json:"Size"` + HotlinkId string `json:"HotlinkId"` + ContentType string `json:"ContentType"` + ExpireAt int64 `json:"ExpireAt"` + SizeBytes int64 `json:"SizeBytes"` + ExpireAtString string `json:"ExpireAtString"` + DownloadsRemaining int `json:"DownloadsRemaining"` + DownloadCount int `json:"DownloadCount"` + UnlimitedDownloads bool `json:"UnlimitedDownloads"` + UnlimitedTime bool `json:"UnlimitedTime"` + RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"` + IsEncrypted bool `json:"IsEncrypted"` + IsPasswordProtected bool `json:"IsPasswordProtected"` + IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` +} + +func (c *GokapiClient) ListFiles() ([]GokapiFile, error) { + var data []GokapiFile + _, err := util.CheckSuccess(c.restClient.R().SetHeader("Accept", "application/json").SetResult(&data).Get("files/list")) + if err != nil { + return data, err + } + + return data, nil +} + +type UploadOptions struct { + AllowedDownloads int + ExpiryDays int + Password string + FilenameOverride string +} + +func (c *GokapiClient) UploadFile(filePath string, opts *UploadOptions) (GokapiFile, error) { + var fileName string + if opts.FilenameOverride != "" { + fileName = opts.FilenameOverride + } else { + fileName = filepath.Base(filePath) + } + + f, err := os.Open(filePath) + if err != nil { + return GokapiFile{}, err + } + defer f.Close() + + mimeType, _ := mimetype.DetectFile(filePath) + + req := c.restClient.R(). + SetMultipartField("file", fileName, mimeType.String(), f). + SetMultipartFormData(map[string]string{ + "allowedDownloads": fmt.Sprintf("%d", opts.AllowedDownloads), + "expiryDays": fmt.Sprintf("%d", opts.ExpiryDays), + "password": opts.Password, + }) + + var data struct { + Result string `json:"Result"` + FileInfo GokapiFile `json:"FileInfo"` + } + + _, err = util.CheckSuccess(req.SetResult(&data).Post("files/add")) + if err != nil { + return GokapiFile{}, err + } + + return data.FileInfo, nil +} + +func (c *GokapiClient) GetDownloadUrl(f GokapiFile) string { + return fmt.Sprintf("%s/downloadFile?id=%s", c.host, f.ID) +} diff --git a/pkg/homeassistant/structs.go b/pkg/homeassistant/structs.go index a74d154..4550c5e 100644 --- a/pkg/homeassistant/structs.go +++ b/pkg/homeassistant/structs.go @@ -58,6 +58,7 @@ var Services = struct { SetHvacMode string SetFanMode string SetTemperature string + SetValue string Start string Change string Cancel string @@ -74,6 +75,7 @@ var Services = struct { SetHvacMode: "set_hvac_mode", SetFanMode: "set_fan_mode", SetTemperature: "set_temperature", + SetValue: "set_value", Start: "start", Change: "change", Cancel: "cancel", @@ -89,6 +91,7 @@ var ExtraProps = struct { TargetTempHigh string TargetTempLow string Duration string + Value string }{ Transition: "transition", Brightness: "brightness", @@ -98,6 +101,7 @@ var ExtraProps = struct { TargetTempHigh: "target_temp_high", TargetTempLow: "target_temp_low", Duration: "duration", + Value: "value", } type ResultContext struct { diff --git a/pkg/nws/api.go b/pkg/nws/api.go new file mode 100644 index 0000000..9f534d0 --- /dev/null +++ b/pkg/nws/api.go @@ -0,0 +1,93 @@ +package nws + +import ( + "fmt" + "strings" + + "code.jhot.me/jhot/hats/internal/util" + "github.com/go-resty/resty/v2" + "github.com/samber/lo" +) + +const ( + NWS_BASE_URL = "https://api.weather.gov" +) + +type NwsClient struct { + restClient *resty.Client +} + +func New() *NwsClient { + return &NwsClient{ + restClient: resty.New(). + SetBaseURL(NWS_BASE_URL). + SetHeader("Accept", "application/geo+json"). + SetHeader("User-Agent", util.GetEnvWithDefault("NWS_USER_AGENT", "(HATS-client, hats@jhot.me)")), + } +} + +func (c *NwsClient) CoordinatesToPoint(lat float64, lon float64) (Point, error) { + var p Point + _, err := c.restClient.R().SetResult(&p).Get(fmt.Sprintf("points/%f,%f", lat, lon)) + return p, err +} + +func (c *NwsClient) GetHourlyForecast(p Point) (HourlyProps, error) { + var data struct { + Properties HourlyProps `json:"properties"` + } + _, err := util.CheckSuccess(c.restClient.R().SetResult(&data). + Get(removeBase(p.Properties.ForecastHourly))) + if err != nil { + return HourlyProps{}, nil + } + + return data.Properties, nil +} + +func (p HourlyProps) GetHighLow(hours int) (high int, low int) { + if hours > len(p.Periods) { + hours = len(p.Periods) + } + + temps := lo.Map(p.Periods[0:hours-1], func(p HourlyPeriod, _ int) int { + return p.Temperature + }) + + return lo.Max(temps), lo.Min(temps) +} + +func (c *NwsClient) GetStations(p Point) ([]string, error) { + var data struct { + ObservationStations []string `json:"observationStations"` + } + _, err := util.CheckSuccess(c.restClient.R().SetResult(&data). + Get(removeBase(p.Properties.ObservationStations))) + if err != nil { + return []string{}, err + } + + return data.ObservationStations, nil +} + +func (c *NwsClient) GetLatestObservations(p Point) (Observations, error) { + stations, err := c.GetStations(p) + if err != nil { + return Observations{}, fmt.Errorf("%w: error getting stations", err) + } + + var data struct { + Properties Observations `json:"properties"` + } + _, err = util.CheckSuccess(c.restClient.R().SetResult(&data). + Get(removeBase(stations[0] + "/observations/latest"))) + if err != nil { + return Observations{}, err + } + + return data.Properties, nil +} + +func removeBase(longUrl string) string { + return strings.TrimPrefix(longUrl, NWS_BASE_URL+"/") +} diff --git a/pkg/nws/structs.go b/pkg/nws/structs.go new file mode 100644 index 0000000..9c5105a --- /dev/null +++ b/pkg/nws/structs.go @@ -0,0 +1,51 @@ +package nws + +import "time" + +type UnitValue[T any] struct { + UnitCode string `json:"unitCode"` + Value T `json:"value"` +} + +type PointProps struct { + GridId string `json:"gridId"` + GridX int `json:"gridX"` + GridY int `json:"gridY"` + Forecast string `json:"forecast"` + ForecastHourly string `json:"forecastHourly"` + ForecastGridData string `json:"forecastGridData"` + ForecastZone string `json:"forecastZone"` + TimeZone string `json:"timeZone"` + RadarStation string `json:"radarStation"` + ObservationStations string `json:"observationStations"` +} + +type Point struct { + ID string `json:"id"` + Properties PointProps `json:"properties"` +} + +type HourlyPeriod struct { + Number int `json:"number"` + StartTime time.Time `json:"startTime"` + Endime time.Time `json:"endTime"` + IsDayTime bool `json:"isDayTime"` + Temperature int `json:"temperature"` + ProbabilityOfPrecipitation UnitValue[int] `json:"probabilityOfPrecipitation"` + DewPoint UnitValue[float64] `json:"dewpoint"` + ShortForecast string `json:"shortForecast"` +} + +type HourlyProps struct { + Updated time.Time `json:"updated"` + Units string `json:"units"` + Periods []HourlyPeriod `json:"periods"` +} + +type Observations struct { + Timestamp time.Time `json:"timestamp"` + Temperature UnitValue[float64] `json:"temperature"` + DewPoint UnitValue[float64] `json:"dewpoint"` + WindDirection UnitValue[int] `json:"windDirection"` + WindSpeed UnitValue[float64] `json:"windSpeed"` +} diff --git a/pkg/qbittorrent/api.go b/pkg/qbittorrent/api.go new file mode 100644 index 0000000..7f6127f --- /dev/null +++ b/pkg/qbittorrent/api.go @@ -0,0 +1,86 @@ +package qbittorrent + +import ( + "errors" + "fmt" + "net/http" + "os" + "strings" + + "code.jhot.me/jhot/hats/internal/util" + "github.com/go-resty/resty/v2" +) + +type QbittorrentClient struct { + restClient *resty.Client +} + +func New(host string) *QbittorrentClient { + return &QbittorrentClient{ + restClient: resty.New().SetBaseURL(fmt.Sprintf("%s/api/v2", host)), + } +} + +func NewFromEnv() (*QbittorrentClient, error) { + c := New(os.Getenv("QBITTORRENT_HOST")) + err := c.Login(os.Getenv("QBITTORRENT_USER"), os.Getenv("QBITTORRENT_PASS")) + return c, err +} + +func (c *QbittorrentClient) Login(user string, pass string) error { + resp, err := util.CheckSuccess(c.restClient.R().SetFormData(map[string]string{ + "username": user, + "password": pass, + }).Post("auth/login")) + + if err != nil { + return err + } + + authCookie := resp.Header().Get("set-cookie") + if authCookie == "" { + return errors.New("auth cookie not found") + } + + first := strings.Split(authCookie, ";")[0] + parts := strings.Split(first, "=") + if len(parts) != 2 { + return fmt.Errorf("auth cookie not in expected format: %s", authCookie) + } + + c.restClient.SetCookie(&http.Cookie{ + Name: parts[0], + Value: parts[1], + }) + + return nil +} + +func (c *QbittorrentClient) GetVersion() (string, error) { + resp, err := util.CheckSuccess(c.restClient.R().Get("app/version")) + if err != nil { + return "", err + } + + return string(resp.Body()), nil +} + +func (c *QbittorrentClient) GetTransferInfo() (GlobalTransferInfo, error) { + var data GlobalTransferInfo + _, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get("transfer/info")) + return data, err +} + +func (c *QbittorrentClient) GetAltSpeedLimitState() (bool, error) { + resp, err := util.CheckSuccess(c.restClient.R().Get("transfer/speedLimitsMode")) + if err != nil { + return false, err + } + + return string(resp.Body()) == "1", nil +} + +func (c *QbittorrentClient) ToggleAltSpeedLimitState() error { + _, err := util.CheckSuccess(c.restClient.R().Post("transfer/toggleSpeedLimitsMode")) + return err +} diff --git a/pkg/qbittorrent/structs.go b/pkg/qbittorrent/structs.go new file mode 100644 index 0000000..a31298d --- /dev/null +++ b/pkg/qbittorrent/structs.go @@ -0,0 +1,10 @@ +package qbittorrent + +type GlobalTransferInfo struct { + DownloadSpeed int `json:"dl_info_speed"` + DownloadedData int `json:"dl_info_data"` + UploadSpeed int `json:"up_info_speed"` + UploadedData int `json:"up_info_data"` + DhtNodes int `json:"dht_nodes"` + Status string `json:"connection_status"` +} diff --git a/pkg/syncthing/rest.go b/pkg/syncthing/rest.go new file mode 100644 index 0000000..81abba4 --- /dev/null +++ b/pkg/syncthing/rest.go @@ -0,0 +1,83 @@ +package syncthing + +import ( + "fmt" + "os" + + "code.jhot.me/jhot/hats/internal/util" + "github.com/go-resty/resty/v2" +) + +type SyncthingClient struct { + restClient *resty.Client +} + +func New(host string, token string) *SyncthingClient { + return &SyncthingClient{ + restClient: resty.New(). + SetBaseURL(fmt.Sprintf("%s/rest", host)). + SetHeader("X-API-Key", token), + } +} + +func NewFromEnv() *SyncthingClient { + return New(os.Getenv("SYNCTHING_HOST"), os.Getenv("SYNCTHING_TOKEN")) +} + +func (c *SyncthingClient) GetVersion() (VersionInfo, error) { + var data VersionInfo + _, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get("system/version")) + return data, err +} + +func (c *SyncthingClient) GetDeviceStats() (map[string]DeviceStatistics, error) { + var data map[string]DeviceStatistics + _, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get("stats/device")) + return data, err +} + +func (c *SyncthingClient) Pause(id string) error { + req := c.restClient.R() + + if id != "" { + req.SetHeader("device", id) + } + + _, err := util.CheckSuccess(req.Post("system/pause")) + return err +} + +func (c *SyncthingClient) Resume(id string) error { + req := c.restClient.R() + + if id != "" { + req.SetHeader("device", id) + } + + _, err := util.CheckSuccess(req.Post("system/resume")) + return err +} + +func (c *SyncthingClient) GetFolderConfig(id string) ([]FolderConfig, error) { + url := "config/folders" + + if id != "" { + + url = fmt.Sprintf("%s/%s", url, id) + var data FolderConfig + _, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get(url)) + return []FolderConfig{data}, err + + } else { + + var data []FolderConfig + _, err := util.CheckSuccess(c.restClient.R().SetResult(&data).Get(url)) + return data, err + + } +} + +func (c *SyncthingClient) SetFolderConfig(id string, param string, value any) error { + _, err := util.CheckSuccess(c.restClient.R().SetBody(map[string]any{param: value}).Patch(fmt.Sprintf("config/folders/%s", id))) + return err +} diff --git a/pkg/syncthing/structs.go b/pkg/syncthing/structs.go new file mode 100644 index 0000000..0c120e5 --- /dev/null +++ b/pkg/syncthing/structs.go @@ -0,0 +1,51 @@ +package syncthing + +type VersionInfo struct { + Arch string `json:"arch"` + LongVersion string `json:"longVersion"` + OS string `json:"os"` + Version string `json:"version"` +} + +type DeviceStatistics struct { + LastSeen string `json:"lastSeen"` + LastConnectionDuration int `json:"lastConnectionDurationS"` +} + +type FolderConfig struct { + ID string `json:"id"` + Label string `json:"label,omitempty"` + FilesystemType string `json:"filesystemType,omitempty"` + Path string `json:"path,omitempty"` + Type string `json:"type,omitempty"` + RescanIntervalS int `json:"rescanIntervalS,omitempty"` + FsWatcherEnabled bool `json:"fsWatcherEnabled,omitempty"` + FsWatcherDelayS int `json:"fsWatcherDelayS,omitempty"` + IgnorePerms bool `json:"ignorePerms,omitempty"` + AutoNormalize bool `json:"autoNormalize,omitempty"` + Copiers int `json:"copiers,omitempty"` + PullerMaxPendingKiB int `json:"pullerMaxPendingKiB,omitempty"` + Hashers int `json:"hashers,omitempty"` + Order string `json:"order,omitempty"` + IgnoreDelete bool `json:"ignoreDelete,omitempty"` + ScanProgressIntervalS int `json:"scanProgressIntervalS,omitempty"` + PullerPauseS int `json:"pullerPauseS,omitempty"` + MaxConflicts int `json:"maxConflicts,omitempty"` + DisableSparseFiles bool `json:"disableSparseFiles,omitempty"` + DisableTempIndexes bool `json:"disableTempIndexes,omitempty"` + Paused bool `json:"paused,omitempty"` + WeakHashThresholdPct int `json:"weakHashThresholdPct,omitempty"` + MarkerName string `json:"markerName,omitempty"` + CopyOwnershipFromParent bool `json:"copyOwnershipFromParent,omitempty"` + ModTimeWindowS int `json:"modTimeWindowS,omitempty"` + MaxConcurrentWrites int `json:"maxConcurrentWrites,omitempty"` + DisableFsync bool `json:"disableFsync,omitempty"` + BlockPullOrder string `json:"blockPullOrder,omitempty"` + CopyRangeMethod string `json:"copyRangeMethod,omitempty"` + CaseSensitiveFS bool `json:"caseSensitiveFS,omitempty"` + JunctionsAsDirs bool `json:"junctionsAsDirs,omitempty"` + SyncOwnership bool `json:"syncOwnership,omitempty"` + SendOwnership bool `json:"sendOwnership,omitempty"` + SyncXattrs bool `json:"syncXattrs,omitempty"` + SendXattrs bool `json:"sendXattrs,omitempty"` +}