mirror of
https://github.com/1f349/go-webdav.git
synced 2024-12-23 00:34:23 +00:00
d7891ce50c
We were sometimes using TitleCase, sometimes Lowercase. Let's align on the idiomatic Go naming and pick TitleCase everywhere.
219 lines
5.0 KiB
Go
219 lines
5.0 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
// HTTPClient performs HTTP requests. It's implemented by *http.Client.
|
|
type HTTPClient interface {
|
|
Do(req *http.Request) (*http.Response, error)
|
|
}
|
|
|
|
type Client struct {
|
|
http HTTPClient
|
|
endpoint *url.URL
|
|
}
|
|
|
|
func NewClient(c HTTPClient, endpoint string) (*Client, error) {
|
|
if c == nil {
|
|
c = http.DefaultClient
|
|
}
|
|
|
|
u, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if u.Path == "" {
|
|
// This is important to avoid issues with path.Join
|
|
u.Path = "/"
|
|
}
|
|
return &Client{http: c, endpoint: u}, nil
|
|
}
|
|
|
|
func (c *Client) ResolveHref(p string) *url.URL {
|
|
if !strings.HasPrefix(p, "/") {
|
|
p = path.Join(c.endpoint.Path, p)
|
|
}
|
|
return &url.URL{
|
|
Scheme: c.endpoint.Scheme,
|
|
User: c.endpoint.User,
|
|
Host: c.endpoint.Host,
|
|
Path: p,
|
|
}
|
|
}
|
|
|
|
func (c *Client) NewRequest(method string, path string, body io.Reader) (*http.Request, error) {
|
|
return http.NewRequest(method, c.ResolveHref(path).String(), body)
|
|
}
|
|
|
|
func (c *Client) NewXMLRequest(method string, path string, v interface{}) (*http.Request, error) {
|
|
var buf bytes.Buffer
|
|
buf.WriteString(xml.Header)
|
|
if err := xml.NewEncoder(&buf).Encode(v); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := c.NewRequest(method, path, &buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"")
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode/100 != 2 {
|
|
defer resp.Body.Close()
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
contentType = "text/plain"
|
|
}
|
|
|
|
var wrappedErr error
|
|
t, _, _ := mime.ParseMediaType(contentType)
|
|
if t == "application/xml" || t == "text/xml" {
|
|
var davErr Error
|
|
if err := xml.NewDecoder(resp.Body).Decode(&davErr); err != nil {
|
|
wrappedErr = err
|
|
} else {
|
|
wrappedErr = &davErr
|
|
}
|
|
} else if strings.HasPrefix(t, "text/") {
|
|
lr := io.LimitedReader{R: resp.Body, N: 1024}
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, &lr)
|
|
resp.Body.Close()
|
|
if s := strings.TrimSpace(buf.String()); s != "" {
|
|
if lr.N == 0 {
|
|
s += " […]"
|
|
}
|
|
wrappedErr = fmt.Errorf("%v", s)
|
|
}
|
|
}
|
|
return nil, &HTTPError{Code: resp.StatusCode, Err: wrappedErr}
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (c *Client) DoMultiStatus(req *http.Request) (*MultiStatus, error) {
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusMultiStatus {
|
|
return nil, fmt.Errorf("HTTP multi-status request failed: %v", resp.Status)
|
|
}
|
|
|
|
// TODO: the response can be quite large, support streaming Response elements
|
|
var ms MultiStatus
|
|
if err := xml.NewDecoder(resp.Body).Decode(&ms); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ms, nil
|
|
}
|
|
|
|
func (c *Client) PropFind(path string, depth Depth, propfind *PropFind) (*MultiStatus, error) {
|
|
req, err := c.NewXMLRequest("PROPFIND", path, propfind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Add("Depth", depth.String())
|
|
|
|
return c.DoMultiStatus(req)
|
|
}
|
|
|
|
// PropfindFlat performs a PROPFIND request with a zero depth.
|
|
func (c *Client) PropFindFlat(path string, propfind *PropFind) (*Response, error) {
|
|
ms, err := c.PropFind(path, DepthZero, propfind)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the client followed a redirect, the Href might be different from the request path
|
|
if len(ms.Responses) != 1 {
|
|
return nil, fmt.Errorf("PROPFIND with Depth: 0 returned %d responses", len(ms.Responses))
|
|
}
|
|
return &ms.Responses[0], nil
|
|
}
|
|
|
|
func parseCommaSeparatedSet(values []string, upper bool) map[string]bool {
|
|
m := make(map[string]bool)
|
|
for _, v := range values {
|
|
fields := strings.FieldsFunc(v, func(r rune) bool {
|
|
return unicode.IsSpace(r) || r == ','
|
|
})
|
|
for _, f := range fields {
|
|
if upper {
|
|
f = strings.ToUpper(f)
|
|
} else {
|
|
f = strings.ToLower(f)
|
|
}
|
|
m[f] = true
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (c *Client) Options(path string) (classes map[string]bool, methods map[string]bool, err error) {
|
|
req, err := c.NewRequest(http.MethodOptions, path, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
resp, err := c.Do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
resp.Body.Close()
|
|
|
|
classes = parseCommaSeparatedSet(resp.Header["Dav"], false)
|
|
if !classes["1"] {
|
|
return nil, nil, fmt.Errorf("webdav: server doesn't support DAV class 1")
|
|
}
|
|
|
|
methods = parseCommaSeparatedSet(resp.Header["Allow"], true)
|
|
return classes, methods, nil
|
|
}
|
|
|
|
// SyncCollection perform a `sync-collection` REPORT operation on a resource
|
|
func (c *Client) SyncCollection(path, syncToken string, level Depth, limit *Limit, prop *Prop) (*MultiStatus, error) {
|
|
q := SyncCollectionQuery{
|
|
SyncToken: syncToken,
|
|
SyncLevel: level.String(),
|
|
Limit: limit,
|
|
Prop: prop,
|
|
}
|
|
|
|
req, err := c.NewXMLRequest("REPORT", path, &q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ms, err := c.DoMultiStatus(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ms, nil
|
|
}
|