diff --git a/carddav/carddav.go b/carddav/carddav.go index 6840d95..48a3645 100644 --- a/carddav/carddav.go +++ b/carddav/carddav.go @@ -8,7 +8,7 @@ import ( ) type AddressBook struct { - Href string + Path string Name string Description string MaxResourceSize int64 @@ -19,11 +19,11 @@ type AddressBookQuery struct { } type AddressBookMultiGet struct { - Hrefs []string + Paths []string Props []string } type AddressObject struct { - Href string + Path string Card vcard.Card } diff --git a/carddav/client.go b/carddav/client.go index 6c2e0a6..7a88ea8 100644 --- a/carddav/client.go +++ b/carddav/client.go @@ -81,7 +81,7 @@ func (c *Client) FindAddressBookHomeSet(principal string) (string, error) { return "", err } - return prop.Href, nil + return prop.Href.Path, nil } func (c *Client) FindAddressBooks(addressBookHomeSet string) ([]AddressBook, error) { @@ -98,7 +98,7 @@ func (c *Client) FindAddressBooks(addressBookHomeSet string) ([]AddressBook, err l := make([]AddressBook, 0, len(ms.Responses)) for _, resp := range ms.Responses { - href, err := resp.Href() + path, err := resp.Path() if err != nil { return nil, err } @@ -130,7 +130,7 @@ func (c *Client) FindAddressBooks(addressBookHomeSet string) ([]AddressBook, err } l = append(l, AddressBook{ - Href: href, + Path: path, Name: dispName.Name, Description: desc.Description, MaxResourceSize: maxResSize.Size, @@ -143,7 +143,7 @@ func (c *Client) FindAddressBooks(addressBookHomeSet string) ([]AddressBook, err func decodeAddressList(ms *internal.Multistatus) ([]AddressObject, error) { addrs := make([]AddressObject, 0, len(ms.Responses)) for _, resp := range ms.Responses { - href, err := resp.Href() + path, err := resp.Path() if err != nil { return nil, err } @@ -160,7 +160,7 @@ func decodeAddressList(ms *internal.Multistatus) ([]AddressObject, error) { } addrs = append(addrs, AddressObject{ - Href: href, + Path: path, Card: card, }) } @@ -198,7 +198,7 @@ func (c *Client) QueryAddressBook(addressBook string, query *AddressBookQuery) ( return decodeAddressList(ms) } -func (c *Client) MultiGetAddressBook(href string, multiGet *AddressBookMultiGet) ([]AddressObject, error) { +func (c *Client) MultiGetAddressBook(path string, multiGet *AddressBookMultiGet) ([]AddressObject, error) { var addrDataReq addressDataReq if multiGet != nil { for _, name := range multiGet.Props { @@ -213,13 +213,17 @@ func (c *Client) MultiGetAddressBook(href string, multiGet *AddressBookMultiGet) addressbookMultiget := addressbookMultiget{Prop: propReq} - if multiGet == nil || len(multiGet.Hrefs) == 0 { - addressbookMultiget.Hrefs = []string{href} + if multiGet == nil || len(multiGet.Paths) == 0 { + href := internal.Href{Path: path} + addressbookMultiget.Hrefs = []internal.Href{href} } else { - addressbookMultiget.Hrefs = multiGet.Hrefs + addressbookMultiget.Hrefs = make([]internal.Href, len(multiGet.Paths)) + for i, p := range multiGet.Paths { + addressbookMultiget.Hrefs[i] = internal.Href{Path: p} + } } - req, err := c.ic.NewXMLRequest("REPORT", href, &addressbookMultiget) + req, err := c.ic.NewXMLRequest("REPORT", path, &addressbookMultiget) if err != nil { return nil, err } diff --git a/carddav/elements.go b/carddav/elements.go index 49f7b78..5f07a68 100644 --- a/carddav/elements.go +++ b/carddav/elements.go @@ -25,8 +25,8 @@ var ( // https://tools.ietf.org/html/rfc6352#section-6.2.3 type addressbookHomeSet struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:carddav addressbook-home-set"` - Href string `xml:"href"` + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:carddav addressbook-home-set"` + Href internal.Href `xml:"href"` } type addressbookDescription struct { @@ -62,9 +62,9 @@ type addressbookQuery struct { // https://tools.ietf.org/html/rfc6352#section-8.7 type addressbookMultiget struct { - XMLName xml.Name `xml:"urn:ietf:params:xml:ns:carddav addressbook-multiget"` - Hrefs []string `xml:"DAV: href"` - Prop *internal.Prop `xml:"DAV: prop,omitempty"` + XMLName xml.Name `xml:"urn:ietf:params:xml:ns:carddav addressbook-multiget"` + Hrefs []internal.Href `xml:"DAV: href"` + Prop *internal.Prop `xml:"DAV: prop,omitempty"` // TODO: DAV:allprop | DAV:propname } diff --git a/carddav/server.go b/carddav/server.go index 572ebcb..38adee5 100644 --- a/carddav/server.go +++ b/carddav/server.go @@ -99,7 +99,7 @@ func (h *Handler) handleQuery(w http.ResponseWriter, query *addressbookQuery) er func (h *Handler) handleMultiget(w http.ResponseWriter, multiget *addressbookMultiget) error { var resps []internal.Response for _, href := range multiget.Hrefs { - ao, err := h.Backend.GetAddressObject(href) + ao, err := h.Backend.GetAddressObject(href.Path) if err != nil { return err // TODO: create internal.Response with error } @@ -225,11 +225,11 @@ func (b *backend) propfindAddressBook(propfind *internal.Propfind, ab *AddressBo }, // TODO: this is a principal property addressBookHomeSetName: func(*internal.RawXMLValue) (interface{}, error) { - return &addressbookHomeSet{Href: "/"}, nil + return &addressbookHomeSet{Href: internal.Href{Path: "/"}}, nil }, // TODO: this should be set on all resources internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) { - return &internal.CurrentUserPrincipal{Href: "/"}, nil + return &internal.CurrentUserPrincipal{Href: internal.Href{Path: "/"}}, nil }, } @@ -258,7 +258,7 @@ func (b *backend) propfindAddressObject(propfind *internal.Propfind, ao *Address // TODO: getlastmodified, getetag } - return internal.NewPropfindResponse(ao.Href, propfind, props) + return internal.NewPropfindResponse(ao.Path, propfind, props) } func (b *backend) Proppatch(r *http.Request, update *internal.Propertyupdate) (*internal.Response, error) { diff --git a/client.go b/client.go index 96a5576..f49e564 100644 --- a/client.go +++ b/client.go @@ -42,16 +42,16 @@ func (c *Client) FindCurrentUserPrincipal() (string, error) { return "", fmt.Errorf("webdav: unauthenticated") } - return prop.Href, nil + return prop.Href.Path, nil } func fileInfoFromResponse(resp *internal.Response) (*FileInfo, error) { - href, err := resp.Href() + path, err := resp.Path() if err != nil { return nil, err } - fi := &FileInfo{Href: href} + fi := &FileInfo{Path: path} var resType internal.ResourceType if err := resp.DecodeProp(&resType); err != nil { @@ -193,12 +193,7 @@ func (c *Client) CopyAll(name, dest string, overwrite bool) error { return err } - dest, err = c.ic.ResolveHref(dest) - if err != nil { - return err - } - req.Header.Set("Destination", dest) - + req.Header.Set("Destination", c.ic.ResolveHref(dest).String()) req.Header.Set("Overwrite", internal.FormatOverwrite(overwrite)) _, err = c.ic.Do(req) @@ -211,12 +206,7 @@ func (c *Client) MoveAll(name, dest string, overwrite bool) error { return err } - dest, err = c.ic.ResolveHref(dest) - if err != nil { - return err - } - req.Header.Set("Destination", dest) - + req.Header.Set("Destination", c.ic.ResolveHref(dest).String()) req.Header.Set("Overwrite", internal.FormatOverwrite(overwrite)) _, err = c.ic.Do(req) diff --git a/fs_local.go b/fs_local.go index 752d452..f8dcb40 100644 --- a/fs_local.go +++ b/fs_local.go @@ -14,7 +14,7 @@ import ( type LocalFileSystem string -func (fs LocalFileSystem) path(name string) (string, error) { +func (fs LocalFileSystem) localPath(name string) (string, error) { if (filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0) || strings.Contains(name, "\x00") { return "", internal.HTTPErrorf(http.StatusBadRequest, "webdav: invalid character in path") } @@ -25,36 +25,35 @@ func (fs LocalFileSystem) path(name string) (string, error) { return filepath.Join(string(fs), filepath.FromSlash(name)), nil } -func (fs LocalFileSystem) href(path string) (string, error) { - rel, err := filepath.Rel(string(fs), path) +func (fs LocalFileSystem) externalPath(name string) (string, error) { + rel, err := filepath.Rel(string(fs), name) if err != nil { return "", err } - href := "/" + filepath.ToSlash(rel) - return href, nil + return "/" + filepath.ToSlash(rel), nil } func (fs LocalFileSystem) Open(name string) (io.ReadCloser, error) { - p, err := fs.path(name) + p, err := fs.localPath(name) if err != nil { return nil, err } return os.Open(p) } -func fileInfoFromOS(href string, fi os.FileInfo) *FileInfo { +func fileInfoFromOS(p string, fi os.FileInfo) *FileInfo { return &FileInfo{ - Href: href, + Path: p, Size: fi.Size(), ModTime: fi.ModTime(), IsDir: fi.IsDir(), // TODO: fallback to http.DetectContentType? - MIMEType: mime.TypeByExtension(path.Ext(href)), + MIMEType: mime.TypeByExtension(path.Ext(p)), } } func (fs LocalFileSystem) Stat(name string) (*FileInfo, error) { - p, err := fs.path(name) + p, err := fs.localPath(name) if err != nil { return nil, err } @@ -66,7 +65,7 @@ func (fs LocalFileSystem) Stat(name string) (*FileInfo, error) { } func (fs LocalFileSystem) Readdir(name string, recursive bool) ([]FileInfo, error) { - p, err := fs.path(name) + p, err := fs.localPath(name) if err != nil { return nil, err } @@ -77,7 +76,7 @@ func (fs LocalFileSystem) Readdir(name string, recursive bool) ([]FileInfo, erro return err } - href, err := fs.href(p) + href, err := fs.externalPath(p) if err != nil { return err } @@ -93,7 +92,7 @@ func (fs LocalFileSystem) Readdir(name string, recursive bool) ([]FileInfo, erro } func (fs LocalFileSystem) Create(name string) (io.WriteCloser, error) { - p, err := fs.path(name) + p, err := fs.localPath(name) if err != nil { return nil, err } @@ -101,7 +100,7 @@ func (fs LocalFileSystem) Create(name string) (io.WriteCloser, error) { } func (fs LocalFileSystem) RemoveAll(name string) error { - p, err := fs.path(name) + p, err := fs.localPath(name) if err != nil { return err } @@ -116,7 +115,7 @@ func (fs LocalFileSystem) RemoveAll(name string) error { } func (fs LocalFileSystem) Mkdir(name string) error { - p, err := fs.path(name) + p, err := fs.localPath(name) if err != nil { return err } diff --git a/internal/client.go b/internal/client.go index 0d37428..950a71e 100644 --- a/internal/client.go +++ b/internal/client.go @@ -33,38 +33,27 @@ func (c *Client) SetBasicAuth(username, password string) { c.password = password } -func (c *Client) ResolveHref(href string) (string, error) { - hrefURL, err := url.Parse(href) - if err != nil { - return "", fmt.Errorf("webdav: failed to parse href %q: %v", href, err) +func (c *Client) ResolveHref(p string) *url.URL { + return &url.URL{ + Scheme: c.endpoint.Scheme, + User: c.endpoint.User, + Host: c.endpoint.Host, + Path: path.Join(c.endpoint.Path, p), } - - u := url.URL{ - Scheme: c.endpoint.Scheme, - User: c.endpoint.User, - Host: c.endpoint.Host, - Path: path.Join(c.endpoint.Path, hrefURL.Path), - RawQuery: hrefURL.RawQuery, - } - return u.String(), nil } -func (c *Client) NewRequest(method string, href string, body io.Reader) (*http.Request, error) { - href, err := c.ResolveHref(href) - if err != nil { - return nil, err - } - return http.NewRequest(method, href, body) +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, href string, v interface{}) (*http.Request, error) { +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, href, &buf) + req, err := c.NewRequest(method, path, &buf) if err != nil { return nil, err } @@ -101,8 +90,8 @@ func (c *Client) DoMultiStatus(req *http.Request) (*Multistatus, error) { return &ms, nil } -func (c *Client) Propfind(href string, depth Depth, propfind *Propfind) (*Multistatus, error) { - req, err := c.NewXMLRequest("PROPFIND", href, propfind) +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 } @@ -113,11 +102,11 @@ func (c *Client) Propfind(href string, depth Depth, propfind *Propfind) (*Multis } // PropfindFlat performs a PROPFIND request with a zero depth. -func (c *Client) PropfindFlat(href string, propfind *Propfind) (*Response, error) { - ms, err := c.Propfind(href, DepthZero, propfind) +func (c *Client) PropfindFlat(path string, propfind *Propfind) (*Response, error) { + ms, err := c.Propfind(path, DepthZero, propfind) if err != nil { return nil, err } - return ms.Get(href) + return ms.Get(path) } diff --git a/internal/elements.go b/internal/elements.go index 5f0b90c..88e23d7 100644 --- a/internal/elements.go +++ b/internal/elements.go @@ -4,6 +4,7 @@ import ( "encoding/xml" "fmt" "net/http" + "net/url" "strconv" "strings" "time" @@ -66,6 +67,26 @@ func (s *Status) Err() error { return nil } +type Href url.URL + +func (h *Href) String() string { + u := (*url.URL)(h) + return u.String() +} + +func (h *Href) MarshalText() ([]byte, error) { + return []byte(h.String()), nil +} + +func (h *Href) UnmarshalText(b []byte) error { + u, err := url.Parse(string(b)) + if err != nil { + return err + } + *h = Href(*u) + return nil +} + // https://tools.ietf.org/html/rfc4918#section-14.16 type Multistatus struct { XMLName xml.Name `xml:"DAV: multistatus"` @@ -77,23 +98,23 @@ func NewMultistatus(resps ...Response) *Multistatus { return &Multistatus{Responses: resps} } -func (ms *Multistatus) Get(href string) (*Response, error) { +func (ms *Multistatus) Get(path string) (*Response, error) { for i := range ms.Responses { resp := &ms.Responses[i] for _, h := range resp.Hrefs { - if h == href { + if h.Path == path { return resp, resp.Status.Err() } } } - return nil, fmt.Errorf("webdav: missing response for href %q", href) + return nil, fmt.Errorf("webdav: missing response for path %q", path) } // https://tools.ietf.org/html/rfc4918#section-14.24 type Response struct { XMLName xml.Name `xml:"DAV: response"` - Hrefs []string `xml:"href"` + Hrefs []Href `xml:"href"` Propstats []Propstat `xml:"propstat,omitempty"` ResponseDescription string `xml:"responsedescription,omitempty"` Status *Status `xml:"status,omitempty"` @@ -101,21 +122,22 @@ type Response struct { Location *Location `xml:"location,omitempty"` } -func NewOKResponse(href string) *Response { +func NewOKResponse(path string) *Response { + href := Href{Path: path} return &Response{ - Hrefs: []string{href}, + Hrefs: []Href{href}, Status: &Status{Code: http.StatusOK}, } } -func (resp *Response) Href() (string, error) { +func (resp *Response) Path() (string, error) { if err := resp.Status.Err(); err != nil { return "", err } if len(resp.Hrefs) != 1 { return "", fmt.Errorf("webdav: malformed response: expected exactly one href element, got %v", len(resp.Hrefs)) } - return resp.Hrefs[0], nil + return resp.Hrefs[0].Path, nil } type missingPropError struct { @@ -181,7 +203,7 @@ func (resp *Response) EncodeProp(code int, v interface{}) error { // https://tools.ietf.org/html/rfc4918#section-14.9 type Location struct { XMLName xml.Name `xml:"DAV: location"` - Href string `xml:"href"` + Href Href `xml:"href"` } // https://tools.ietf.org/html/rfc4918#section-14.22 @@ -332,7 +354,7 @@ type DisplayName struct { // https://tools.ietf.org/html/rfc5397#section-3 type CurrentUserPrincipal struct { XMLName xml.Name `xml:"DAV: current-user-principal"` - Href string `xml:"href,omitempty"` + Href Href `xml:"href,omitempty"` Unauthenticated *struct{} `xml:"unauthenticated,omitempty"` } diff --git a/internal/server.go b/internal/server.go index 7c48a63..039e1bf 100644 --- a/internal/server.go +++ b/internal/server.go @@ -171,8 +171,8 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) error { type PropfindFunc func(raw *RawXMLValue) (interface{}, error) -func NewPropfindResponse(href string, propfind *Propfind, props map[xml.Name]PropfindFunc) (*Response, error) { - resp := NewOKResponse(href) +func NewPropfindResponse(path string, propfind *Propfind, props map[xml.Name]PropfindFunc) (*Response, error) { + resp := NewOKResponse(path) if _, ok := props[ResourceTypeName]; !ok { props[ResourceTypeName] = func(*RawXMLValue) (interface{}, error) { diff --git a/server.go b/server.go index bc8a081..1995b2f 100644 --- a/server.go +++ b/server.go @@ -173,7 +173,7 @@ func (b *backend) propfindFile(propfind *internal.Propfind, fi *FileInfo) (*inte // TODO: getetag } - return internal.NewPropfindResponse(fi.Href, propfind, props) + return internal.NewPropfindResponse(fi.Path, propfind, props) } func (b *backend) Proppatch(r *http.Request, update *internal.Propertyupdate) (*internal.Response, error) { diff --git a/webdav.go b/webdav.go index 7ed9d8c..0166990 100644 --- a/webdav.go +++ b/webdav.go @@ -10,7 +10,7 @@ import ( // TODO: add ETag to FileInfo type FileInfo struct { - Href string + Path string Size int64 ModTime time.Time IsDir bool