package webdav import ( "errors" "fmt" "io" "net/http" "strings" ) var ErrConflict = errors.New("webdav conflict") type Version struct { ETag string } type Client struct { HTTPClient *http.Client BaseURL string Username string Password string } func (c Client) Open(path string) ([]byte, Version, error) { req, err := http.NewRequest(http.MethodGet, c.url(path), nil) if err != nil { return nil, Version{}, fmt.Errorf("build GET request: %w", err) } c.applyAuth(req) resp, err := c.httpClient().Do(req) if err != nil { return nil, Version{}, fmt.Errorf("GET %s: %w", path, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, Version{}, fmt.Errorf("GET %s: unexpected status %d", path, resp.StatusCode) } content, err := io.ReadAll(resp.Body) if err != nil { return nil, Version{}, fmt.Errorf("read %s: %w", path, err) } return content, Version{ETag: resp.Header.Get("ETag")}, nil } func (c Client) Save(path string, content io.Reader, version Version) (Version, error) { req, err := http.NewRequest(http.MethodPut, c.url(path), content) if err != nil { return Version{}, fmt.Errorf("build PUT request: %w", err) } c.applyAuth(req) if version.ETag != "" { req.Header.Set("If-Match", version.ETag) } resp, err := c.httpClient().Do(req) if err != nil { return Version{}, fmt.Errorf("PUT %s: %w", path, err) } defer resp.Body.Close() if resp.StatusCode == http.StatusPreconditionFailed { return Version{}, ErrConflict } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusNoContent { return Version{}, fmt.Errorf("PUT %s: unexpected status %d", path, resp.StatusCode) } return Version{ETag: resp.Header.Get("ETag")}, nil } func (c Client) httpClient() *http.Client { if c.HTTPClient != nil { return c.HTTPClient } return http.DefaultClient } func (c Client) applyAuth(req *http.Request) { if c.Username == "" && c.Password == "" { return } req.SetBasicAuth(c.Username, c.Password) } func (c Client) url(path string) string { base := strings.TrimRight(c.BaseURL, "/") path = strings.TrimLeft(path, "/") return base + "/" + path }