feat: add support for uploading Hotspot Captive Portal files (like background image, logo) (#42)
* feat: add support for uploading Hotspot Captive Portal files (like background image, logo) * feat: add UploadPortalFileFromReader
This commit is contained in:
committed by
GitHub
parent
bdc73a9811
commit
278a72fbb9
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
customizations:
|
customizations:
|
||||||
client:
|
client:
|
||||||
|
imports:
|
||||||
|
- "io"
|
||||||
excludeResources:
|
excludeResources:
|
||||||
- "DescribedFeature"
|
- "DescribedFeature"
|
||||||
- "Dpi*"
|
- "Dpi*"
|
||||||
@@ -334,6 +336,70 @@ customizations:
|
|||||||
returns:
|
returns:
|
||||||
- "bool"
|
- "bool"
|
||||||
- "error"
|
- "error"
|
||||||
|
- name: "UploadPortalFile"
|
||||||
|
resourceName: "PortalFile"
|
||||||
|
comment: "UploadPortalFile uploads a Hotspot Portal file to the controller."
|
||||||
|
params:
|
||||||
|
- name: "ctx"
|
||||||
|
type: "context.Context"
|
||||||
|
- name: "site"
|
||||||
|
type: "string"
|
||||||
|
- name: "filepath"
|
||||||
|
type: "string"
|
||||||
|
returns:
|
||||||
|
- "*PortalFile"
|
||||||
|
- "error"
|
||||||
|
- name: "UploadPortalFileFromReader"
|
||||||
|
resourceName: "PortalFile"
|
||||||
|
comment: "UploadPortalFileFromReader uploads a Hotspot Portal file using io.Reader to the controller."
|
||||||
|
params:
|
||||||
|
- name: "ctx"
|
||||||
|
type: "context.Context"
|
||||||
|
- name: "site"
|
||||||
|
type: "string"
|
||||||
|
- name: "reader"
|
||||||
|
type: "io.Reader"
|
||||||
|
- name: "filename"
|
||||||
|
type: "string"
|
||||||
|
returns:
|
||||||
|
- "*PortalFile"
|
||||||
|
- "error"
|
||||||
|
- name: "DeletePortalFile"
|
||||||
|
resourceName: "PortalFile"
|
||||||
|
comment: "DeletePortalFile deletes a Hotspot Portal file from the controller."
|
||||||
|
params:
|
||||||
|
- name: "ctx"
|
||||||
|
type: "context.Context"
|
||||||
|
- name: "site"
|
||||||
|
type: "string"
|
||||||
|
- name: "id"
|
||||||
|
type: "string"
|
||||||
|
returns:
|
||||||
|
- "error"
|
||||||
|
- name: "ListPortalFiles"
|
||||||
|
resourceName: "PortalFile"
|
||||||
|
comment: "ListPortalFiles lists all Hotspot Portal files on the controller."
|
||||||
|
params:
|
||||||
|
- name: "ctx"
|
||||||
|
type: "context.Context"
|
||||||
|
- name: "site"
|
||||||
|
type: "string"
|
||||||
|
returns:
|
||||||
|
- "[]PortalFile"
|
||||||
|
- "error"
|
||||||
|
- name: "GetPortalFile"
|
||||||
|
resourceName: "PortalFile"
|
||||||
|
comment: "GetPortalFile returns a specific Hotspot Portal file by it's ID."
|
||||||
|
params:
|
||||||
|
- name: "ctx"
|
||||||
|
type: "context.Context"
|
||||||
|
- name: "site"
|
||||||
|
type: "string"
|
||||||
|
- name: "id"
|
||||||
|
type: "string"
|
||||||
|
returns:
|
||||||
|
- "*PortalFile"
|
||||||
|
- "error"
|
||||||
resources:
|
resources:
|
||||||
Account:
|
Account:
|
||||||
fields:
|
fields:
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -3,6 +3,7 @@ module github.com/filipowm/go-unifi
|
|||||||
go 1.23.5
|
go 1.23.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8
|
||||||
github.com/go-playground/locales v0.14.1
|
github.com/go-playground/locales v0.14.1
|
||||||
github.com/go-playground/universal-translator v0.18.1
|
github.com/go-playground/universal-translator v0.18.1
|
||||||
github.com/go-playground/validator/v10 v10.25.0
|
github.com/go-playground/validator/v10 v10.25.0
|
||||||
@@ -179,7 +180,6 @@ require (
|
|||||||
github.com/firefart/nonamedreturns v1.0.5 // indirect
|
github.com/firefart/nonamedreturns v1.0.5 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
|
||||||
github.com/ghostiam/protogetter v0.3.9 // indirect
|
github.com/ghostiam/protogetter v0.3.9 // indirect
|
||||||
github.com/github/smimesign v0.2.0 // indirect
|
github.com/github/smimesign v0.2.0 // indirect
|
||||||
github.com/go-critic/go-critic v0.12.0 // indirect
|
github.com/go-critic/go-critic v0.12.0 // indirect
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const (
|
|||||||
statusPath = "/status"
|
statusPath = "/status"
|
||||||
statusPathNew = "/proxy/network/status"
|
statusPathNew = "/proxy/network/status"
|
||||||
|
|
||||||
|
uploadPath = "/upload"
|
||||||
|
uploadPathNew = "/proxy/network/upload"
|
||||||
|
|
||||||
logoutPath = "/api/logout"
|
logoutPath = "/api/logout"
|
||||||
|
|
||||||
defaultUserAgent = "go-unifi/0.0.1"
|
defaultUserAgent = "go-unifi/0.0.1"
|
||||||
@@ -38,6 +41,7 @@ type APIPaths struct {
|
|||||||
LoginPath string
|
LoginPath string
|
||||||
StatusPath string
|
StatusPath string
|
||||||
LogoutPath string
|
LogoutPath string
|
||||||
|
UploadPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -47,6 +51,7 @@ var (
|
|||||||
LoginPath: loginPath,
|
LoginPath: loginPath,
|
||||||
StatusPath: statusPath,
|
StatusPath: statusPath,
|
||||||
LogoutPath: logoutPath,
|
LogoutPath: logoutPath,
|
||||||
|
UploadPath: uploadPath,
|
||||||
}
|
}
|
||||||
NewStyleAPI = APIPaths{
|
NewStyleAPI = APIPaths{
|
||||||
ApiPath: apiPathNew,
|
ApiPath: apiPathNew,
|
||||||
@@ -54,6 +59,7 @@ var (
|
|||||||
LoginPath: loginPathNew,
|
LoginPath: loginPathNew,
|
||||||
StatusPath: statusPathNew,
|
StatusPath: statusPathNew,
|
||||||
LogoutPath: logoutPath,
|
LogoutPath: logoutPath,
|
||||||
|
UploadPath: uploadPathNew,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
16
unifi/client.generated.go
generated
16
unifi/client.generated.go
generated
@@ -5,6 +5,7 @@ package unifi
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
@@ -495,6 +496,21 @@ type Client interface {
|
|||||||
|
|
||||||
// ==== end of client methods for PortProfile resource ====
|
// ==== end of client methods for PortProfile resource ====
|
||||||
|
|
||||||
|
// DeletePortalFile deletes a Hotspot Portal file from the controller.
|
||||||
|
DeletePortalFile(ctx context.Context, site string, id string) error
|
||||||
|
|
||||||
|
// GetPortalFile returns a specific Hotspot Portal file by it's ID.
|
||||||
|
GetPortalFile(ctx context.Context, site string, id string) (*PortalFile, error)
|
||||||
|
|
||||||
|
// ListPortalFiles lists all Hotspot Portal files on the controller.
|
||||||
|
ListPortalFiles(ctx context.Context, site string) ([]PortalFile, error)
|
||||||
|
|
||||||
|
// UploadPortalFile uploads a Hotspot Portal file to the controller.
|
||||||
|
UploadPortalFile(ctx context.Context, site string, filepath string) (*PortalFile, error)
|
||||||
|
|
||||||
|
// UploadPortalFileFromReader uploads a Hotspot Portal file using io.Reader to the controller.
|
||||||
|
UploadPortalFileFromReader(ctx context.Context, site string, reader io.Reader, filename string) (*PortalFile, error)
|
||||||
|
|
||||||
// ==== client methods for RADIUSProfile resource ====
|
// ==== client methods for RADIUSProfile resource ====
|
||||||
|
|
||||||
// CreateRADIUSProfile creates a resource
|
// CreateRADIUSProfile creates a resource
|
||||||
|
|||||||
117
unifi/portalfile.go
Normal file
117
unifi/portalfile.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package unifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// just to fix compile issues with the import.
|
||||||
|
var (
|
||||||
|
_ context.Context
|
||||||
|
_ fmt.Formatter
|
||||||
|
_ json.Marshaler
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortalFile struct {
|
||||||
|
ID string `json:"_id,omitempty"`
|
||||||
|
SiteID string `json:"site_id,omitempty"`
|
||||||
|
|
||||||
|
ContentType string `json:"content_type,omitempty"`
|
||||||
|
LastModified int `json:"last_modified,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
FileSize int `json:"filesize,omitempty"`
|
||||||
|
MD5 string `json:"md5,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dst *PortalFile) UnmarshalJSON(b []byte) error {
|
||||||
|
type Alias PortalFile
|
||||||
|
aux := &struct {
|
||||||
|
*Alias
|
||||||
|
}{
|
||||||
|
Alias: (*Alias)(dst),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal(b, &aux)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to unmarshal alias: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) UploadPortalFile(ctx context.Context, site string, filepath string) (*PortalFile, error) {
|
||||||
|
var respBody struct {
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
Data []PortalFile `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.UploadFile(ctx, fmt.Sprintf("%s/s/%s/portalfile", c.apiPaths.UploadPath, site), filepath, "file", &respBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respBody.Data) == 0 {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return &respBody.Data[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) UploadPortalFileFromReader(ctx context.Context, site string, reader io.Reader, filename string) (*PortalFile, error) {
|
||||||
|
var respBody struct {
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
Data []PortalFile `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.UploadFileFromReader(ctx, fmt.Sprintf("%s/s/%s/portalfile", c.apiPaths.UploadPath, site), reader, filename, "file", &respBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respBody.Data) == 0 {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return &respBody.Data[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) DeletePortalFile(ctx context.Context, site, id string) error {
|
||||||
|
err := c.Delete(ctx, fmt.Sprintf("s/%s/rest/portalfile/%s", site, id), struct{}{}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) ListPortalFiles(ctx context.Context, site string) ([]PortalFile, error) {
|
||||||
|
var respBody struct {
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
Data []PortalFile `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Get(ctx, fmt.Sprintf("s/%s/rest/portalfile", site), nil, &respBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBody.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *client) GetPortalFile(ctx context.Context, site, id string) (*PortalFile, error) {
|
||||||
|
var respBody struct {
|
||||||
|
Meta Meta `json:"meta"`
|
||||||
|
Data []PortalFile `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Get(ctx, fmt.Sprintf("s/%s/rest/portalfile/%s", site, id), nil, &respBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respBody.Data) != 1 {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return &respBody.Data[0], nil
|
||||||
|
}
|
||||||
@@ -6,10 +6,16 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gabriel-vasile/mimetype"
|
||||||
)
|
)
|
||||||
|
|
||||||
// marshalRequest marshals the request body to an io.Reader. Returns nil if reqBody is nil.
|
// marshalRequest marshals the request body to an io.Reader. Returns nil if reqBody is nil.
|
||||||
@@ -60,35 +66,27 @@ func (c *client) newRequestContext() (context.Context, context.CancelFunc) {
|
|||||||
return ctx, func() {}
|
return ctx, func() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do performs an HTTP request using the given method, apiPath, request body, and decodes the response into respBody.
|
// executeRequest executes an HTTP request with the given context, method, URL, body, and headers.
|
||||||
// It validates the request body, applies interceptors, and decodes the HTTP response into respBody if provided.
|
// It applies interceptors, handles errors, and decodes the response body if provided.
|
||||||
// It returns an error if the request or response handling fails.
|
// Returns an error if the request or response handling fails.
|
||||||
func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interface{}, respBody interface{}) error {
|
func (c *client) executeRequest(ctx context.Context, method, apiPath string, body io.Reader, headers http.Header, respBody interface{}) error {
|
||||||
c.Tracef("Performing request: %s %s", method, apiPath)
|
|
||||||
if err := c.validateRequestBody(reqBody); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reqReader, err := marshalRequest(reqBody)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to marshal request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := c.buildRequestURL(apiPath)
|
url, err := c.buildRequestURL(apiPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create request URL: %w", err)
|
return fmt.Errorf("unable to create request URL: %w", err)
|
||||||
}
|
}
|
||||||
c.Debugf("Executing request: %s %s", method, url.String())
|
c.Debugf("Executing request: %s %s", method, url.String())
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, url.String(), reqReader)
|
req, err := http.NewRequestWithContext(ctx, method, url.String(), body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create request: %s %s %w", method, apiPath, err)
|
return fmt.Errorf("unable to create request: %s %s %w", method, apiPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.useLocking {
|
if c.useLocking {
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
c.Trace("Acquired lock fo request")
|
c.Trace("Acquired lock for request")
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Trace("Executing request interceptors")
|
c.Trace("Executing request interceptors")
|
||||||
for _, interceptor := range c.interceptors {
|
for _, interceptor := range c.interceptors {
|
||||||
if err := interceptor.InterceptRequest(req); err != nil {
|
if err := interceptor.InterceptRequest(req); err != nil {
|
||||||
@@ -96,6 +94,17 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set headers if provided overriding any coming from interceptors
|
||||||
|
for key, values := range headers {
|
||||||
|
// delete headers if already exist to be able to override them
|
||||||
|
if req.Header.Get(key) != "" {
|
||||||
|
req.Header.Del(key)
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := c.http.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to perform request: %s %s %w", method, apiPath, err)
|
return fmt.Errorf("unable to perform request: %s %s %w", method, apiPath, err)
|
||||||
@@ -114,6 +123,7 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no response body is expected, return
|
||||||
if respBody == nil || resp.ContentLength == 0 {
|
if respBody == nil || resp.ContentLength == 0 {
|
||||||
c.Trace("No response body to decode")
|
c.Trace("No response body to decode")
|
||||||
return nil
|
return nil
|
||||||
@@ -127,6 +137,105 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadFile uploads a file to the UniFi controller.
|
||||||
|
// It takes a context, API path, file path, field name, and additional form fields.
|
||||||
|
// The file is uploaded as multipart/form-data.
|
||||||
|
// It returns the response body and an error if the operation fails.
|
||||||
|
func (c *client) UploadFile(ctx context.Context, apiPath, filePath, fieldName string, respBody interface{}) error {
|
||||||
|
c.Tracef("Uploading file: %s to %s", filePath, apiPath)
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to open file for upload: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
return c.UploadFileFromReader(ctx, apiPath, file, filepath.Base(filePath), fieldName, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||||
|
|
||||||
|
func escapeQuotes(s string) string {
|
||||||
|
return quoteEscaper.Replace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE! This is a copy of the function from the mime/multipart package, but allows to set custom mimetype.
|
||||||
|
func createFormFile(w *multipart.Writer, mimeType, fieldname, filename string) (io.Writer, error) {
|
||||||
|
h := make(textproto.MIMEHeader)
|
||||||
|
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename)))
|
||||||
|
h.Set("Content-Type", mimeType)
|
||||||
|
return w.CreatePart(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadFileFromReader uploads a file to the UniFi controller from an io.Reader.
|
||||||
|
// It takes a context, API path, reader, filename, field name, and additional form fields.
|
||||||
|
// The file is uploaded as multipart/form-data.
|
||||||
|
func (c *client) UploadFileFromReader(ctx context.Context, apiPath string, reader io.Reader, filename, fieldName string, respBody interface{}) error {
|
||||||
|
c.Tracef("Uploading file: %s to %s", filename, apiPath)
|
||||||
|
|
||||||
|
// Read the entire content into a buffer first to avoid deadlock. I tied using TeeReader and Pipe but ended up in deadlock.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err := io.Copy(&buf, reader); err != nil {
|
||||||
|
return fmt.Errorf("unable to read file content into buffer: %w", err)
|
||||||
|
}
|
||||||
|
contentReader := bytes.NewReader(buf.Bytes())
|
||||||
|
|
||||||
|
if fieldName == "" {
|
||||||
|
fieldName = "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect MIME type from the first reader
|
||||||
|
mt, err := mimetype.DetectReader(contentReader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to detect file mimetype: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, err := createFormFile(writer, mt.String(), fieldName, filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create form file: %w", err)
|
||||||
|
}
|
||||||
|
// reinit reader
|
||||||
|
contentReader = bytes.NewReader(buf.Bytes())
|
||||||
|
// Copy the file content to the form field from the second reader
|
||||||
|
if _, err = io.Copy(part, contentReader); err != nil {
|
||||||
|
return fmt.Errorf("unable to copy file content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return fmt.Errorf("unable to close multipart writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := http.Header{}
|
||||||
|
headers.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
headers.Set("X-Requested-With", "XMLHttpRequest") // TODO if not provided, the response will be 404. UniFi bug?
|
||||||
|
|
||||||
|
return c.executeRequest(ctx, http.MethodPost, apiPath, body, headers, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do performs an HTTP request using the given method, apiPath, request body, and decodes the response into respBody.
|
||||||
|
// It validates the request body, applies interceptors, and decodes the HTTP response into respBody if provided.
|
||||||
|
// It returns an error if the request or response handling fails.
|
||||||
|
func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interface{}, respBody interface{}) error {
|
||||||
|
c.Tracef("Performing request: %s %s", method, apiPath)
|
||||||
|
|
||||||
|
if err := c.validateRequestBody(reqBody); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := marshalRequest(reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := http.Header{}
|
||||||
|
if reqBody != nil {
|
||||||
|
headers.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.executeRequest(ctx, method, apiPath, body, headers, respBody)
|
||||||
|
}
|
||||||
|
|
||||||
// Get sends an HTTP GET request to the specified API path with the provided request body,
|
// Get sends an HTTP GET request to the specified API path with the provided request body,
|
||||||
// and decodes the HTTP response into respBody.
|
// and decodes the HTTP response into respBody.
|
||||||
// It is a convenience wrapper around Do.
|
// It is a convenience wrapper around Do.
|
||||||
|
|||||||
Reference in New Issue
Block a user