mirror of
https://github.com/1f349/go-webdav.git
synced 2024-12-22 16:24:14 +00:00
d8a8af0448
According to RFC 4918 section 14.5, the error element can't be empty.
456 lines
10 KiB
Go
456 lines
10 KiB
Go
package internal
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const Namespace = "DAV:"
|
|
|
|
var (
|
|
ResourceTypeName = xml.Name{"DAV:", "resourcetype"}
|
|
DisplayNameName = xml.Name{"DAV:", "displayname"}
|
|
GetContentLengthName = xml.Name{"DAV:", "getcontentlength"}
|
|
GetContentTypeName = xml.Name{"DAV:", "getcontenttype"}
|
|
GetLastModifiedName = xml.Name{"DAV:", "getlastmodified"}
|
|
GetETagName = xml.Name{"DAV:", "getetag"}
|
|
|
|
CurrentUserPrincipalName = xml.Name{"DAV:", "current-user-principal"}
|
|
)
|
|
|
|
type Status struct {
|
|
Code int
|
|
Text string
|
|
}
|
|
|
|
func (s *Status) MarshalText() ([]byte, error) {
|
|
text := s.Text
|
|
if text == "" {
|
|
text = http.StatusText(s.Code)
|
|
}
|
|
return []byte(fmt.Sprintf("HTTP/1.1 %v %v", s.Code, text)), nil
|
|
}
|
|
|
|
func (s *Status) UnmarshalText(b []byte) error {
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
|
|
parts := strings.SplitN(string(b), " ", 3)
|
|
if len(parts) != 3 {
|
|
return fmt.Errorf("webdav: invalid HTTP status %q: expected 3 fields", s)
|
|
}
|
|
code, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return fmt.Errorf("webdav: invalid HTTP status %q: failed to parse code: %v", s, err)
|
|
}
|
|
|
|
s.Code = code
|
|
s.Text = parts[2]
|
|
return nil
|
|
}
|
|
|
|
func (s *Status) Err() error {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
|
|
// TODO: handle 2xx, 3xx
|
|
if s.Code != http.StatusOK {
|
|
return &HTTPError{Code: s.Code}
|
|
}
|
|
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"`
|
|
Responses []Response `xml:"response"`
|
|
ResponseDescription string `xml:"responsedescription,omitempty"`
|
|
SyncToken string `xml:"sync-token,omitempty"`
|
|
}
|
|
|
|
func NewMultistatus(resps ...Response) *Multistatus {
|
|
return &Multistatus{Responses: resps}
|
|
}
|
|
|
|
func (ms *Multistatus) Get(p string) (*Response, error) {
|
|
// Clean the path to avoid issues with trailing slashes
|
|
p = path.Clean(p)
|
|
for i := range ms.Responses {
|
|
resp := &ms.Responses[i]
|
|
for _, h := range resp.Hrefs {
|
|
if path.Clean(h.Path) == p {
|
|
return resp, resp.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("webdav: missing response for path %q", p)
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.24
|
|
type Response struct {
|
|
XMLName xml.Name `xml:"DAV: response"`
|
|
Hrefs []Href `xml:"href"`
|
|
Propstats []Propstat `xml:"propstat,omitempty"`
|
|
ResponseDescription string `xml:"responsedescription,omitempty"`
|
|
Status *Status `xml:"status,omitempty"`
|
|
Error *Error `xml:"error,omitempty"`
|
|
Location *Location `xml:"location,omitempty"`
|
|
}
|
|
|
|
func NewOKResponse(path string) *Response {
|
|
href := Href{Path: path}
|
|
return &Response{
|
|
Hrefs: []Href{href},
|
|
Status: &Status{Code: http.StatusOK},
|
|
}
|
|
}
|
|
|
|
func NewErrorResponse(path string, err error) *Response {
|
|
code := http.StatusInternalServerError
|
|
var httpErr *HTTPError
|
|
if errors.As(err, &httpErr) {
|
|
code = httpErr.Code
|
|
}
|
|
|
|
var errElt *Error
|
|
errors.As(err, &errElt)
|
|
|
|
href := Href{Path: path}
|
|
return &Response{
|
|
Hrefs: []Href{href},
|
|
Status: &Status{Code: code},
|
|
ResponseDescription: err.Error(),
|
|
Error: errElt,
|
|
}
|
|
}
|
|
|
|
func (resp *Response) Err() error {
|
|
if resp.Status == nil || resp.Status.Code/100 == 2 {
|
|
return nil
|
|
}
|
|
|
|
var err error = resp.Error
|
|
if resp.ResponseDescription != "" {
|
|
if err != nil {
|
|
err = fmt.Errorf("%v (%w)", resp.ResponseDescription, err)
|
|
} else {
|
|
err = fmt.Errorf("%v", resp.ResponseDescription)
|
|
}
|
|
}
|
|
|
|
return &HTTPError{
|
|
Code: resp.Status.Code,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
func (resp *Response) Path() (string, error) {
|
|
err := resp.Err()
|
|
var path string
|
|
if len(resp.Hrefs) == 1 {
|
|
path = resp.Hrefs[0].Path
|
|
} else if err == nil {
|
|
err = fmt.Errorf("webdav: malformed response: expected exactly one href element, got %v", len(resp.Hrefs))
|
|
}
|
|
return path, err
|
|
}
|
|
|
|
func (resp *Response) DecodeProp(values ...interface{}) error {
|
|
for _, v := range values {
|
|
// TODO wrap errors with more context (XML name)
|
|
name, err := valueXMLName(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := resp.Err(); err != nil {
|
|
return err
|
|
}
|
|
for _, propstat := range resp.Propstats {
|
|
raw := propstat.Prop.Get(name)
|
|
if raw == nil {
|
|
continue
|
|
}
|
|
if err := propstat.Status.Err(); err != nil {
|
|
return err
|
|
}
|
|
return raw.Decode(v)
|
|
}
|
|
return HTTPErrorf(http.StatusNotFound, "missing property %s", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (resp *Response) EncodeProp(code int, v interface{}) error {
|
|
raw, err := EncodeRawXMLElement(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range resp.Propstats {
|
|
propstat := &resp.Propstats[i]
|
|
if propstat.Status.Code == code {
|
|
propstat.Prop.Raw = append(propstat.Prop.Raw, *raw)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
resp.Propstats = append(resp.Propstats, Propstat{
|
|
Status: Status{Code: code},
|
|
Prop: Prop{Raw: []RawXMLValue{*raw}},
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.9
|
|
type Location struct {
|
|
XMLName xml.Name `xml:"DAV: location"`
|
|
Href Href `xml:"href"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.22
|
|
type Propstat struct {
|
|
XMLName xml.Name `xml:"DAV: propstat"`
|
|
Prop Prop `xml:"prop"`
|
|
Status Status `xml:"status"`
|
|
ResponseDescription string `xml:"responsedescription,omitempty"`
|
|
Error *Error `xml:"error,omitempty"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.18
|
|
type Prop struct {
|
|
XMLName xml.Name `xml:"DAV: prop"`
|
|
Raw []RawXMLValue `xml:",any"`
|
|
}
|
|
|
|
func EncodeProp(values ...interface{}) (*Prop, error) {
|
|
l := make([]RawXMLValue, len(values))
|
|
for i, v := range values {
|
|
raw, err := EncodeRawXMLElement(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
l[i] = *raw
|
|
}
|
|
return &Prop{Raw: l}, nil
|
|
}
|
|
|
|
func (p *Prop) Get(name xml.Name) *RawXMLValue {
|
|
for i := range p.Raw {
|
|
raw := &p.Raw[i]
|
|
if n, ok := raw.XMLName(); ok && name == n {
|
|
return raw
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *Prop) Decode(v interface{}) error {
|
|
name, err := valueXMLName(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
raw := p.Get(name)
|
|
if raw == nil {
|
|
return HTTPErrorf(http.StatusNotFound, "missing property %s", name)
|
|
}
|
|
|
|
return raw.Decode(v)
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.20
|
|
type Propfind struct {
|
|
XMLName xml.Name `xml:"DAV: propfind"`
|
|
Prop *Prop `xml:"prop,omitempty"`
|
|
AllProp *struct{} `xml:"allprop,omitempty"`
|
|
Include *Include `xml:"include,omitempty"`
|
|
PropName *struct{} `xml:"propname,omitempty"`
|
|
}
|
|
|
|
func xmlNamesToRaw(names []xml.Name) []RawXMLValue {
|
|
l := make([]RawXMLValue, len(names))
|
|
for i, name := range names {
|
|
l[i] = *NewRawXMLElement(name, nil, nil)
|
|
}
|
|
return l
|
|
}
|
|
|
|
func NewPropNamePropfind(names ...xml.Name) *Propfind {
|
|
return &Propfind{Prop: &Prop{Raw: xmlNamesToRaw(names)}}
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.8
|
|
type Include struct {
|
|
XMLName xml.Name `xml:"DAV: include"`
|
|
Raw []RawXMLValue `xml:",any"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-15.9
|
|
type ResourceType struct {
|
|
XMLName xml.Name `xml:"DAV: resourcetype"`
|
|
Raw []RawXMLValue `xml:",any"`
|
|
}
|
|
|
|
func NewResourceType(names ...xml.Name) *ResourceType {
|
|
return &ResourceType{Raw: xmlNamesToRaw(names)}
|
|
}
|
|
|
|
func (t *ResourceType) Is(name xml.Name) bool {
|
|
for _, raw := range t.Raw {
|
|
if n, ok := raw.XMLName(); ok && name == n {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var CollectionName = xml.Name{"DAV:", "collection"}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-15.4
|
|
type GetContentLength struct {
|
|
XMLName xml.Name `xml:"DAV: getcontentlength"`
|
|
Length int64 `xml:",chardata"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-15.5
|
|
type GetContentType struct {
|
|
XMLName xml.Name `xml:"DAV: getcontenttype"`
|
|
Type string `xml:",chardata"`
|
|
}
|
|
|
|
type Time time.Time
|
|
|
|
func (t *Time) UnmarshalText(b []byte) error {
|
|
tt, err := http.ParseTime(string(b))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*t = Time(tt)
|
|
return nil
|
|
}
|
|
|
|
func (t *Time) MarshalText() ([]byte, error) {
|
|
s := time.Time(*t).UTC().Format(http.TimeFormat)
|
|
return []byte(s), nil
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-15.7
|
|
type GetLastModified struct {
|
|
XMLName xml.Name `xml:"DAV: getlastmodified"`
|
|
LastModified Time `xml:",chardata"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-15.6
|
|
type GetETag struct {
|
|
XMLName xml.Name `xml:"DAV: getetag"`
|
|
ETag ETag `xml:",chardata"`
|
|
}
|
|
|
|
type ETag string
|
|
|
|
func (etag *ETag) UnmarshalText(b []byte) error {
|
|
s, err := strconv.Unquote(string(b))
|
|
if err != nil {
|
|
return fmt.Errorf("webdav: failed to unquote ETag: %v", err)
|
|
}
|
|
*etag = ETag(s)
|
|
return nil
|
|
}
|
|
|
|
func (etag ETag) MarshalText() ([]byte, error) {
|
|
return []byte(etag.String()), nil
|
|
}
|
|
|
|
func (etag ETag) String() string {
|
|
return fmt.Sprintf("%q", string(etag))
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.5
|
|
type Error struct {
|
|
XMLName xml.Name `xml:"DAV: error"`
|
|
Raw []RawXMLValue `xml:",any"`
|
|
}
|
|
|
|
func (err *Error) Error() string {
|
|
b, _ := xml.Marshal(err)
|
|
return string(b)
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-15.2
|
|
type DisplayName struct {
|
|
XMLName xml.Name `xml:"DAV: displayname"`
|
|
Name string `xml:",chardata"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc5397#section-3
|
|
type CurrentUserPrincipal struct {
|
|
XMLName xml.Name `xml:"DAV: current-user-principal"`
|
|
Href Href `xml:"href,omitempty"`
|
|
Unauthenticated *struct{} `xml:"unauthenticated,omitempty"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.19
|
|
type Propertyupdate struct {
|
|
XMLName xml.Name `xml:"DAV: propertyupdate"`
|
|
Remove []Remove `xml:"remove"`
|
|
Set []Set `xml:"set"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.23
|
|
type Remove struct {
|
|
XMLName xml.Name `xml:"DAV: remove"`
|
|
Prop Prop `xml:"prop"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc4918#section-14.26
|
|
type Set struct {
|
|
XMLName xml.Name `xml:"DAV: set"`
|
|
Prop Prop `xml:"prop"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc6578#section-6.1
|
|
type SyncCollectionQuery struct {
|
|
XMLName xml.Name `xml:"DAV: sync-collection"`
|
|
SyncToken string `xml:"sync-token"`
|
|
Limit *Limit `xml:"limit,omitempty"`
|
|
SyncLevel string `xml:"sync-level"`
|
|
Prop *Prop `xml:"prop"`
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc5323#section-5.17
|
|
type Limit struct {
|
|
XMLName xml.Name `xml:"DAV: limit"`
|
|
NResults uint `xml:"nresults"`
|
|
}
|