|
package gowebdav |
|
|
|
import ( |
|
"bytes" |
|
"encoding/xml" |
|
"fmt" |
|
"io" |
|
"net/http" |
|
"net/url" |
|
"os" |
|
pathpkg "path" |
|
"strings" |
|
"sync" |
|
"time" |
|
) |
|
|
|
|
|
type Client struct { |
|
root string |
|
headers http.Header |
|
interceptor func(method string, rq *http.Request) |
|
c *http.Client |
|
|
|
authMutex sync.Mutex |
|
auth Authenticator |
|
} |
|
|
|
|
|
type Authenticator interface { |
|
Type() string |
|
User() string |
|
Pass() string |
|
Authorize(*http.Request, string, string) |
|
} |
|
|
|
|
|
type NoAuth struct { |
|
user string |
|
pw string |
|
} |
|
|
|
|
|
func (n *NoAuth) Type() string { |
|
return "NoAuth" |
|
} |
|
|
|
|
|
func (n *NoAuth) User() string { |
|
return n.user |
|
} |
|
|
|
|
|
func (n *NoAuth) Pass() string { |
|
return n.pw |
|
} |
|
|
|
|
|
func (n *NoAuth) Authorize(req *http.Request, method string, path string) { |
|
} |
|
|
|
|
|
func NewClient(uri, user, pw string) *Client { |
|
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}} |
|
} |
|
|
|
|
|
func (c *Client) SetHeader(key, value string) { |
|
c.headers.Add(key, value) |
|
} |
|
|
|
|
|
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) { |
|
c.interceptor = interceptor |
|
} |
|
|
|
|
|
func (c *Client) SetTimeout(timeout time.Duration) { |
|
c.c.Timeout = timeout |
|
} |
|
|
|
|
|
func (c *Client) SetTransport(transport http.RoundTripper) { |
|
c.c.Transport = transport |
|
} |
|
|
|
|
|
func (c *Client) SetJar(jar http.CookieJar) { |
|
c.c.Jar = jar |
|
} |
|
|
|
|
|
func (c *Client) Connect() error { |
|
rs, err := c.options("/") |
|
if err != nil { |
|
return err |
|
} |
|
|
|
err = rs.Body.Close() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if rs.StatusCode != 200 { |
|
return newPathError("Connect", c.root, rs.StatusCode) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
type props struct { |
|
Status string `xml:"DAV: status"` |
|
Name string `xml:"DAV: prop>displayname,omitempty"` |
|
Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"` |
|
Size string `xml:"DAV: prop>getcontentlength,omitempty"` |
|
ContentType string `xml:"DAV: prop>getcontenttype,omitempty"` |
|
ETag string `xml:"DAV: prop>getetag,omitempty"` |
|
Modified string `xml:"DAV: prop>getlastmodified,omitempty"` |
|
} |
|
|
|
type response struct { |
|
Href string `xml:"DAV: href"` |
|
Props []props `xml:"DAV: propstat"` |
|
} |
|
|
|
func getProps(r *response, status string) *props { |
|
for _, prop := range r.Props { |
|
if strings.Contains(prop.Status, status) { |
|
return &prop |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
|
|
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) { |
|
path = FixSlashes(path) |
|
files := make([]os.FileInfo, 0) |
|
skipSelf := true |
|
parse := func(resp interface{}) error { |
|
r := resp.(*response) |
|
|
|
if skipSelf { |
|
skipSelf = false |
|
if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" { |
|
r.Props = nil |
|
return nil |
|
} |
|
return newPathError("ReadDir", path, 405) |
|
} |
|
|
|
if p := getProps(r, "200"); p != nil { |
|
f := new(File) |
|
if ps, err := url.PathUnescape(r.Href); err == nil { |
|
f.name = pathpkg.Base(ps) |
|
} else { |
|
f.name = p.Name |
|
} |
|
f.path = path + f.name |
|
f.modified = parseModified(&p.Modified) |
|
f.etag = p.ETag |
|
f.contentType = p.ContentType |
|
|
|
if p.Type.Local == "collection" { |
|
f.path += "/" |
|
f.size = 0 |
|
f.isdir = true |
|
} else { |
|
f.size = parseInt64(&p.Size) |
|
f.isdir = false |
|
} |
|
|
|
files = append(files, *f) |
|
} |
|
|
|
r.Props = nil |
|
return nil |
|
} |
|
|
|
err := c.propfind(path, false, |
|
`<d:propfind xmlns:d='DAV:'> |
|
<d:prop> |
|
<d:displayname/> |
|
<d:resourcetype/> |
|
<d:getcontentlength/> |
|
<d:getcontenttype/> |
|
<d:getetag/> |
|
<d:getlastmodified/> |
|
</d:prop> |
|
</d:propfind>`, |
|
&response{}, |
|
parse) |
|
|
|
if err != nil { |
|
if _, ok := err.(*os.PathError); !ok { |
|
err = newPathErrorErr("ReadDir", path, err) |
|
} |
|
} |
|
return files, err |
|
} |
|
|
|
|
|
func (c *Client) Stat(path string) (os.FileInfo, error) { |
|
var f *File |
|
parse := func(resp interface{}) error { |
|
r := resp.(*response) |
|
if p := getProps(r, "200"); p != nil && f == nil { |
|
f = new(File) |
|
f.name = p.Name |
|
f.path = path |
|
f.etag = p.ETag |
|
f.contentType = p.ContentType |
|
|
|
if p.Type.Local == "collection" { |
|
if !strings.HasSuffix(f.path, "/") { |
|
f.path += "/" |
|
} |
|
f.size = 0 |
|
f.modified = time.Unix(0, 0) |
|
f.isdir = true |
|
} else { |
|
f.size = parseInt64(&p.Size) |
|
f.modified = parseModified(&p.Modified) |
|
f.isdir = false |
|
} |
|
} |
|
|
|
r.Props = nil |
|
return nil |
|
} |
|
|
|
err := c.propfind(path, true, |
|
`<d:propfind xmlns:d='DAV:'> |
|
<d:prop> |
|
<d:displayname/> |
|
<d:resourcetype/> |
|
<d:getcontentlength/> |
|
<d:getcontenttype/> |
|
<d:getetag/> |
|
<d:getlastmodified/> |
|
</d:prop> |
|
</d:propfind>`, |
|
&response{}, |
|
parse) |
|
|
|
if err != nil { |
|
if _, ok := err.(*os.PathError); !ok { |
|
err = newPathErrorErr("ReadDir", path, err) |
|
} |
|
} |
|
return f, err |
|
} |
|
|
|
|
|
func (c *Client) Remove(path string) error { |
|
return c.RemoveAll(path) |
|
} |
|
|
|
|
|
func (c *Client) RemoveAll(path string) error { |
|
rs, err := c.req("DELETE", path, nil, nil) |
|
if err != nil { |
|
return newPathError("Remove", path, 400) |
|
} |
|
err = rs.Body.Close() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 { |
|
return nil |
|
} |
|
|
|
return newPathError("Remove", path, rs.StatusCode) |
|
} |
|
|
|
|
|
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) { |
|
path = FixSlashes(path) |
|
status, err := c.mkcol(path) |
|
if err != nil { |
|
return |
|
} |
|
if status == 201 { |
|
return nil |
|
} |
|
|
|
return newPathError("Mkdir", path, status) |
|
} |
|
|
|
|
|
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) { |
|
path = FixSlashes(path) |
|
status, err := c.mkcol(path) |
|
if err != nil { |
|
return |
|
} |
|
if status == 201 { |
|
return nil |
|
} |
|
if status == 409 { |
|
paths := strings.Split(path, "/") |
|
sub := "/" |
|
for _, e := range paths { |
|
if e == "" { |
|
continue |
|
} |
|
sub += e + "/" |
|
status, err = c.mkcol(sub) |
|
if err != nil { |
|
return |
|
} |
|
if status != 201 { |
|
return newPathError("MkdirAll", sub, status) |
|
} |
|
} |
|
return nil |
|
} |
|
|
|
return newPathError("MkdirAll", path, status) |
|
} |
|
|
|
|
|
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error { |
|
return c.copymove("MOVE", oldpath, newpath, overwrite) |
|
} |
|
|
|
|
|
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error { |
|
return c.copymove("COPY", oldpath, newpath, overwrite) |
|
} |
|
|
|
|
|
func (c *Client) Read(path string) ([]byte, error) { |
|
var stream io.ReadCloser |
|
var err error |
|
|
|
if stream, _, err = c.ReadStream(path, nil); err != nil { |
|
return nil, err |
|
} |
|
defer stream.Close() |
|
|
|
buf := new(bytes.Buffer) |
|
_, err = buf.ReadFrom(stream) |
|
if err != nil { |
|
return nil, err |
|
} |
|
return buf.Bytes(), nil |
|
} |
|
|
|
func (c *Client) Link(path string) (string, http.Header, error) { |
|
method := "GET" |
|
u := PathEscape(Join(c.root, path)) |
|
r, err := http.NewRequest(method, u, nil) |
|
|
|
if err != nil { |
|
return "", nil, newPathErrorErr("Link", path, err) |
|
} |
|
|
|
if c.c.Jar != nil { |
|
for _, cookie := range c.c.Jar.Cookies(r.URL) { |
|
r.AddCookie(cookie) |
|
} |
|
} |
|
for k, vals := range c.headers { |
|
for _, v := range vals { |
|
r.Header.Add(k, v) |
|
} |
|
} |
|
|
|
c.authMutex.Lock() |
|
auth := c.auth |
|
c.authMutex.Unlock() |
|
|
|
auth.Authorize(r, method, path) |
|
|
|
if c.interceptor != nil { |
|
c.interceptor(method, r) |
|
} |
|
return r.URL.String(), r.Header, nil |
|
} |
|
|
|
|
|
func (c *Client) ReadStream(path string, callback func(rq *http.Request)) (io.ReadCloser, http.Header, error) { |
|
rs, err := c.req("GET", path, nil, callback) |
|
if err != nil { |
|
return nil, nil, newPathErrorErr("ReadStream", path, err) |
|
} |
|
|
|
if rs.StatusCode < 400 { |
|
return rs.Body, rs.Header, nil |
|
} |
|
|
|
rs.Body.Close() |
|
return nil, nil, newPathError("ReadStream", path, rs.StatusCode) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) { |
|
rs, err := c.req("GET", path, nil, func(r *http.Request) { |
|
r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1)) |
|
}) |
|
if err != nil { |
|
return nil, newPathErrorErr("ReadStreamRange", path, err) |
|
} |
|
|
|
if rs.StatusCode == http.StatusPartialContent { |
|
|
|
return rs.Body, nil |
|
} |
|
|
|
|
|
|
|
if rs.StatusCode == 200 { |
|
|
|
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil { |
|
return nil, newPathErrorErr("ReadStreamRange", path, err) |
|
} |
|
|
|
|
|
return &limitedReadCloser{rs.Body, int(length)}, nil |
|
} |
|
|
|
rs.Body.Close() |
|
return nil, newPathError("ReadStream", path, rs.StatusCode) |
|
} |
|
|
|
|
|
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) { |
|
s, err := c.put(path, bytes.NewReader(data), nil) |
|
if err != nil { |
|
return |
|
} |
|
|
|
switch s { |
|
|
|
case 200, 201, 204: |
|
return nil |
|
|
|
case 409: |
|
err = c.createParentCollection(path) |
|
if err != nil { |
|
return |
|
} |
|
|
|
s, err = c.put(path, bytes.NewReader(data), nil) |
|
if err != nil { |
|
return |
|
} |
|
if s == 200 || s == 201 || s == 204 { |
|
return |
|
} |
|
} |
|
|
|
return newPathError("Write", path, s) |
|
} |
|
|
|
|
|
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode, callback func(r *http.Request)) (err error) { |
|
|
|
err = c.createParentCollection(path) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
s, err := c.put(path, stream, callback) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
switch s { |
|
case 200, 201, 204: |
|
return nil |
|
|
|
default: |
|
return newPathError("WriteStream", path, s) |
|
} |
|
} |
|
|