Compare commits

..

No commits in common. "master" and "v0.5.0" have entirely different histories.

18 changed files with 306 additions and 583 deletions

1
.gitignore vendored
View File

@ -12,4 +12,3 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/ .glide/
.idea/

View File

@ -1,6 +1,7 @@
# go-webdav # go-webdav
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-webdav.svg)](https://pkg.go.dev/github.com/emersion/go-webdav) [![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-webdav.svg)](https://pkg.go.dev/github.com/emersion/go-webdav)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-webdav/commits/master.svg)](https://builds.sr.ht/~emersion/go-webdav/commits/master?)
A Go library for [WebDAV], [CalDAV] and [CardDAV]. A Go library for [WebDAV], [CalDAV] and [CardDAV].

View File

@ -228,10 +228,3 @@ func (r *reportReq) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return d.DecodeElement(v, &start) return d.DecodeElement(v, &start)
} }
type mkcolReq struct {
XMLName xml.Name `xml:"DAV: mkcol"`
ResourceType internal.ResourceType `xml:"set>prop>resourcetype"`
DisplayName string `xml:"set>prop>displayname"`
// TODO this could theoretically contain all addressbook properties?
}

View File

@ -138,6 +138,7 @@ func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error
return len(rset.Between(start, end, true)) > 0, nil return len(rset.Between(start, end, true)) > 0, nil
} }
// TODO handle "infinity" values in query
// TODO handle more than just events // TODO handle more than just events
if comp.Name != ical.CompEvent { if comp.Name != ical.CompEvent {
return false, nil return false, nil
@ -154,15 +155,15 @@ func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error
} }
// Event starts in time range // Event starts in time range
if eventStart.After(start) && (end.IsZero() || eventStart.Before(end)) { if eventStart.After(start) && eventStart.Before(end) {
return true, nil return true, nil
} }
// Event ends in time range // Event ends in time range
if eventEnd.After(start) && (end.IsZero() || eventEnd.Before(end)) { if eventEnd.After(start) && eventEnd.Before(end) {
return true, nil return true, nil
} }
// Event covers entire time range plus some // Event covers entire time range plus some
if eventStart.Before(start) && (!end.IsZero() && eventEnd.After(end)) { if eventStart.Before(start) && eventEnd.After(end) {
return true, nil return true, nil
} }
return false, nil return false, nil
@ -171,11 +172,13 @@ func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error
func matchPropTimeRange(start, end time.Time, field *ical.Prop) (bool, error) { func matchPropTimeRange(start, end time.Time, field *ical.Prop) (bool, error) {
// See https://datatracker.ietf.org/doc/html/rfc4791#section-9.9 // See https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
// TODO handle "infinity" values in query
ptime, err := field.DateTime(start.Location()) ptime, err := field.DateTime(start.Location())
if err != nil { if err != nil {
return false, err return false, err
} }
if ptime.After(start) && (end.IsZero() || ptime.Before(end)) { if ptime.After(start) && ptime.Before(end) {
return true, nil return true, nil
} }
return false, nil return false, nil

View File

@ -209,23 +209,6 @@ END:VCALENDAR`)
addrs: []CalendarObject{event1, event2, event3, todo1}, addrs: []CalendarObject{event1, event2, event3, todo1},
want: []CalendarObject{event2, event3}, want: []CalendarObject{event2, event3},
}, },
{
// https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.1
name: "events in open time range (no end date)",
query: &CalendarQuery{
CompFilter: CompFilter{
Name: "VCALENDAR",
Comps: []CompFilter{
CompFilter{
Name: "VEVENT",
Start: toDate(t, "20060104T000000Z"),
},
},
},
},
addrs: []CalendarObject{event1, event2, event3, todo1},
want: []CalendarObject{event2, event3},
},
{ {
// https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.6 // https://datatracker.ietf.org/doc/html/rfc4791#section-7.8.6
name: "events by UID", name: "events by UID",

View File

@ -30,15 +30,12 @@ type PutCalendarObjectOptions struct {
// Backend is a CalDAV server backend. // Backend is a CalDAV server backend.
type Backend interface { type Backend interface {
CalendarHomeSetPath(ctx context.Context) (string, error) CalendarHomeSetPath(ctx context.Context) (string, error)
CreateCalendar(ctx context.Context, calendar *Calendar) error
ListCalendars(ctx context.Context) ([]Calendar, error) ListCalendars(ctx context.Context) ([]Calendar, error)
GetCalendar(ctx context.Context, path string) (*Calendar, error) GetCalendar(ctx context.Context, path string) (*Calendar, error)
GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error) GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error)
ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error)
QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) QueryCalendarObjects(ctx context.Context, query *CalendarQuery) ([]CalendarObject, error)
PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (loc string, err error)
DeleteCalendarObject(ctx context.Context, path string) error DeleteCalendarObject(ctx context.Context, path string) error
webdav.UserPrincipalBackend webdav.UserPrincipalBackend
@ -78,7 +75,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Backend: h.Backend, Backend: h.Backend,
Prefix: strings.TrimSuffix(h.Prefix, "/"), Prefix: strings.TrimSuffix(h.Prefix, "/"),
} }
hh := internal.Handler{Backend: &b} hh := internal.Handler{&b}
hh.ServeHTTP(w, r) hh.ServeHTTP(w, r)
} }
@ -216,7 +213,7 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *cal
} }
q.CompFilter = *cf q.CompFilter = *cf
cos, err := h.Backend.QueryCalendarObjects(r.Context(), r.URL.Path, &q) cos, err := h.Backend.QueryCalendarObjects(r.Context(), &q)
if err != nil { if err != nil {
return err return err
} }
@ -465,10 +462,12 @@ func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind)
} }
props := map[xml.Name]internal.PropFindFunc{ props := map[xml.Name]internal.PropFindFunc{
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
Href: internal.Href{Path: principalPath}, return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
}), },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(internal.CollectionName), nil
},
} }
return internal.NewPropFindResponse(principalPath, propfind, props) return internal.NewPropFindResponse(principalPath, propfind, props)
} }
@ -484,13 +483,15 @@ func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.
} }
props := map[xml.Name]internal.PropFindFunc{ props := map[xml.Name]internal.PropFindFunc{
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
Href: internal.Href{Path: principalPath}, return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
}), },
calendarHomeSetName: internal.PropFindValue(&calendarHomeSet{ calendarHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
Href: internal.Href{Path: homeSetPath}, return &calendarHomeSet{Href: internal.Href{Path: homeSetPath}}, nil
}), },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(internal.CollectionName), nil
},
} }
return internal.NewPropFindResponse(principalPath, propfind, props) return internal.NewPropFindResponse(principalPath, propfind, props)
} }
@ -507,10 +508,12 @@ func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFi
// TODO anything else to return here? // TODO anything else to return here?
props := map[xml.Name]internal.PropFindFunc{ props := map[xml.Name]internal.PropFindFunc{
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
Href: internal.Href{Path: principalPath}, return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
}), },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(internal.CollectionName), nil
},
} }
return internal.NewPropFindResponse(homeSetPath, propfind, props) return internal.NewPropFindResponse(homeSetPath, propfind, props)
} }
@ -524,15 +527,22 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF
} }
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
}, },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, calendarName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
calendarDescriptionName: internal.PropFindValue(&calendarDescription{ return internal.NewResourceType(internal.CollectionName, calendarName), nil
Description: cal.Description, },
}), internal.DisplayNameName: func(*internal.RawXMLValue) (interface{}, error) {
supportedCalendarDataName: internal.PropFindValue(&supportedCalendarData{ return &internal.DisplayName{Name: cal.Name}, nil
Types: []calendarDataType{ },
{ContentType: ical.MIMEType, Version: "2.0"}, calendarDescriptionName: func(*internal.RawXMLValue) (interface{}, error) {
}, return &calendarDescription{Description: cal.Description}, nil
}), },
supportedCalendarDataName: func(*internal.RawXMLValue) (interface{}, error) {
return &supportedCalendarData{
Types: []calendarDataType{
{ContentType: ical.MIMEType, Version: "2.0"},
},
}, nil
},
supportedCalendarComponentSetName: func(*internal.RawXMLValue) (interface{}, error) { supportedCalendarComponentSetName: func(*internal.RawXMLValue) (interface{}, error) {
components := []comp{} components := []comp{}
if cal.SupportedComponentSet != nil { if cal.SupportedComponentSet != nil {
@ -548,23 +558,16 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF
}, },
} }
if cal.Name != "" {
props[internal.DisplayNameName] = internal.PropFindValue(&internal.DisplayName{
Name: cal.Name,
})
}
if cal.Description != "" { if cal.Description != "" {
props[calendarDescriptionName] = internal.PropFindValue(&calendarDescription{ props[calendarDescriptionName] = func(*internal.RawXMLValue) (interface{}, error) {
Description: cal.Description, return &calendarDescription{Description: cal.Description}, nil
}) }
} }
if cal.MaxResourceSize > 0 { if cal.MaxResourceSize > 0 {
props[maxResourceSizeName] = internal.PropFindValue(&maxResourceSize{ props[maxResourceSizeName] = func(*internal.RawXMLValue) (interface{}, error) {
Size: cal.MaxResourceSize, return &maxResourceSize{Size: cal.MaxResourceSize}, nil
}) }
}
props[internal.CurrentUserPrivilegeSetName] = func(*internal.RawXMLValue) (interface{}, error) {
return &internal.CurrentUserPrivilegeSet{Privilege: internal.NewAllPrivileges()}, nil
} }
// TODO: CALDAV:calendar-timezone, CALDAV:supported-calendar-component-set, CALDAV:min-date-time, CALDAV:max-date-time, CALDAV:max-instances, CALDAV:max-attendees-per-instance // TODO: CALDAV:calendar-timezone, CALDAV:supported-calendar-component-set, CALDAV:min-date-time, CALDAV:max-date-time, CALDAV:max-instances, CALDAV:max-attendees-per-instance
@ -605,9 +608,9 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal
} }
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
}, },
internal.GetContentTypeName: internal.PropFindValue(&internal.GetContentType{ internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) {
Type: ical.MIMEType, return &internal.GetContentType{Type: ical.MIMEType}, nil
}), },
// TODO: calendar-data can only be used in REPORT requests // TODO: calendar-data can only be used in REPORT requests
calendarDataName: func(*internal.RawXMLValue) (interface{}, error) { calendarDataName: func(*internal.RawXMLValue) (interface{}, error) {
var buf bytes.Buffer var buf bytes.Buffer
@ -620,20 +623,20 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal
} }
if co.ContentLength > 0 { if co.ContentLength > 0 {
props[internal.GetContentLengthName] = internal.PropFindValue(&internal.GetContentLength{ props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
Length: co.ContentLength, return &internal.GetContentLength{Length: co.ContentLength}, nil
}) }
} }
if !co.ModTime.IsZero() { if !co.ModTime.IsZero() {
props[internal.GetLastModifiedName] = internal.PropFindValue(&internal.GetLastModified{ props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
LastModified: internal.Time(co.ModTime), return &internal.GetLastModified{LastModified: internal.Time(co.ModTime)}, nil
}) }
} }
if co.ETag != "" { if co.ETag != "" {
props[internal.GetETagName] = internal.PropFindValue(&internal.GetETag{ props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
ETag: internal.ETag(co.ETag), return &internal.GetETag{ETag: internal.ETag(co.ETag)}, nil
}) }
} }
return internal.NewPropFindResponse(co.Path, propfind, props) return internal.NewPropFindResponse(co.Path, propfind, props)
@ -658,10 +661,10 @@ func (b *backend) propFindAllCalendarObjects(ctx context.Context, propfind *inte
} }
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) { func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
return nil, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: PropPatch not implemented") panic("TODO")
} }
func (b *backend) Put(w http.ResponseWriter, r *http.Request) error { func (b *backend) Put(r *http.Request) (*internal.Href, error) {
ifNoneMatch := webdav.ConditionalMatch(r.Header.Get("If-None-Match")) ifNoneMatch := webdav.ConditionalMatch(r.Header.Get("If-None-Match"))
ifMatch := webdav.ConditionalMatch(r.Header.Get("If-Match")) ifMatch := webdav.ConditionalMatch(r.Header.Get("If-Match"))
@ -672,39 +675,26 @@ func (b *backend) Put(w http.ResponseWriter, r *http.Request) error {
t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return internal.HTTPErrorf(http.StatusBadRequest, "caldav: malformed Content-Type: %v", err) return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: malformed Content-Type: %v", err)
} }
if t != ical.MIMEType { if t != ical.MIMEType {
// TODO: send CALDAV:supported-calendar-data error // TODO: send CALDAV:supported-calendar-data error
return internal.HTTPErrorf(http.StatusBadRequest, "caldav: unsupported Content-Type %q", t) return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: unsupported Content-Type %q", t)
} }
// TODO: check CALDAV:max-resource-size precondition // TODO: check CALDAV:max-resource-size precondition
cal, err := ical.NewDecoder(r.Body).Decode() cal, err := ical.NewDecoder(r.Body).Decode()
if err != nil { if err != nil {
// TODO: send CALDAV:valid-calendar-data error // TODO: send CALDAV:valid-calendar-data error
return internal.HTTPErrorf(http.StatusBadRequest, "caldav: failed to parse iCalendar: %v", err) return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: failed to parse iCalendar: %v", err)
} }
co, err := b.Backend.PutCalendarObject(r.Context(), r.URL.Path, cal, &opts) loc, err := b.Backend.PutCalendarObject(r.Context(), r.URL.Path, cal, &opts)
if err != nil { if err != nil {
return err return nil, err
} }
if co.ETag != "" { return &internal.Href{Path: loc}, nil
w.Header().Set("ETag", internal.ETag(co.ETag).String())
}
if !co.ModTime.IsZero() {
w.Header().Set("Last-Modified", co.ModTime.UTC().Format(http.TimeFormat))
}
if co.Path != "" {
w.Header().Set("Location", co.Path)
}
// TODO: http.StatusNoContent if the resource already existed
w.WriteHeader(http.StatusCreated)
return nil
} }
func (b *backend) Delete(r *http.Request) error { func (b *backend) Delete(r *http.Request) error {
@ -712,36 +702,15 @@ func (b *backend) Delete(r *http.Request) error {
} }
func (b *backend) Mkcol(r *http.Request) error { func (b *backend) Mkcol(r *http.Request) error {
if b.resourceTypeAtPath(r.URL.Path) != resourceTypeCalendar { panic("TODO")
return internal.HTTPErrorf(http.StatusForbidden, "caldav: calendar creation not allowed at given location")
}
cal := Calendar{
Path: r.URL.Path,
}
if !internal.IsRequestBodyEmpty(r) {
var m mkcolReq
if err := internal.DecodeXMLRequest(r, &m); err != nil {
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: error parsing mkcol request: %s", err.Error())
}
if !m.ResourceType.Is(internal.CollectionName) || !m.ResourceType.Is(calendarName) {
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: unexpected resource type")
}
cal.Name = m.DisplayName
// TODO ...
}
return b.Backend.CreateCalendar(r.Context(), &cal)
} }
func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) { func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) {
return false, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: Copy not implemented") panic("TODO")
} }
func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) { func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) {
return false, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: Move not implemented") panic("TODO")
} }
// https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.2.1 // https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.2.1
@ -762,7 +731,7 @@ const (
) )
func NewPreconditionError(err PreconditionType) error { func NewPreconditionError(err PreconditionType) error {
name := xml.Name{Space: "urn:ietf:params:xml:ns:caldav", Local: string(err)} name := xml.Name{"urn:ietf:params:xml:ns:caldav", string(err)}
elem := internal.NewRawXMLElement(name, nil, nil) elem := internal.NewRawXMLElement(name, nil, nil)
return &internal.HTTPError{ return &internal.HTTPError{
Code: 409, Code: 409,

View File

@ -182,10 +182,6 @@ type testBackend struct {
objectMap map[string][]CalendarObject objectMap map[string][]CalendarObject
} }
func (t testBackend) CreateCalendar(ctx context.Context, calendar *Calendar) error {
return nil
}
func (t testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) { func (t testBackend) ListCalendars(ctx context.Context) ([]Calendar, error) {
return t.calendars, nil return t.calendars, nil
} }
@ -222,14 +218,14 @@ func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *Ca
return nil, fmt.Errorf("Couldn't find calendar object at: %s", path) return nil, fmt.Errorf("Couldn't find calendar object at: %s", path)
} }
func (t testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) { func (t testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (string, error) {
return nil, nil return "", nil
} }
func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) { func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) {
return t.objectMap[path], nil return t.objectMap[path], nil
} }
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) { func (t testBackend) QueryCalendarObjects(ctx context.Context, query *CalendarQuery) ([]CalendarObject, error) {
return nil, nil return nil, nil
} }

View File

@ -12,9 +12,7 @@ import (
"github.com/emersion/go-webdav" "github.com/emersion/go-webdav"
) )
type testBackend struct { type testBackend struct{}
addressBooks []AddressBook
}
type contextKey string type contextKey string
@ -39,46 +37,22 @@ func (*testBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
return r, nil return r, nil
} }
func (*testBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) { func (*testBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
r := ctx.Value(homeSetPathKey).(string) r := ctx.Value(homeSetPathKey).(string)
return r, nil return r, nil
} }
func (*testBackend) ListAddressBooks(ctx context.Context) ([]AddressBook, error) { func (*testBackend) AddressBook(ctx context.Context) (*AddressBook, error) {
p := ctx.Value(addressBookPathKey).(string) p := ctx.Value(addressBookPathKey).(string)
return []AddressBook{ return &AddressBook{
AddressBook{ Path: p,
Path: p, Name: "My contacts",
Name: "My contacts", Description: "Default address book",
Description: "Default address book", MaxResourceSize: 1024,
MaxResourceSize: 1024, SupportedAddressData: nil,
SupportedAddressData: nil,
},
}, nil }, nil
} }
func (b *testBackend) GetAddressBook(ctx context.Context, path string) (*AddressBook, error) {
abs, err := b.ListAddressBooks(ctx)
if err != nil {
panic(err)
}
for _, ab := range abs {
if ab.Path == path {
return &ab, nil
}
}
return nil, webdav.NewHTTPError(404, fmt.Errorf("Not found"))
}
func (b *testBackend) CreateAddressBook(ctx context.Context, ab *AddressBook) error {
b.addressBooks = append(b.addressBooks, *ab)
return nil
}
func (*testBackend) DeleteAddressBook(ctx context.Context, path string) error {
panic("TODO: implement")
}
func (*testBackend) GetAddressObject(ctx context.Context, path string, req *AddressDataRequest) (*AddressObject, error) { func (*testBackend) GetAddressObject(ctx context.Context, path string, req *AddressDataRequest) (*AddressObject, error) {
if path == alicePath { if path == alicePath {
card, err := vcard.NewDecoder(strings.NewReader(aliceData)).Decode() card, err := vcard.NewDecoder(strings.NewReader(aliceData)).Decode()
@ -94,11 +68,7 @@ func (*testBackend) GetAddressObject(ctx context.Context, path string, req *Addr
} }
} }
func (b *testBackend) ListAddressObjects(ctx context.Context, path string, req *AddressDataRequest) ([]AddressObject, error) { func (b *testBackend) ListAddressObjects(ctx context.Context, req *AddressDataRequest) ([]AddressObject, error) {
p := ctx.Value(addressBookPathKey).(string)
if !strings.HasPrefix(path, p) {
return nil, webdav.NewHTTPError(404, fmt.Errorf("Not found"))
}
alice, err := b.GetAddressObject(ctx, alicePath, req) alice, err := b.GetAddressObject(ctx, alicePath, req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -107,11 +77,11 @@ func (b *testBackend) ListAddressObjects(ctx context.Context, path string, req *
return []AddressObject{*alice}, nil return []AddressObject{*alice}, nil
} }
func (*testBackend) QueryAddressObjects(ctx context.Context, path string, query *AddressBookQuery) ([]AddressObject, error) { func (*testBackend) QueryAddressObjects(ctx context.Context, query *AddressBookQuery) ([]AddressObject, error) {
panic("TODO: implement") panic("TODO: implement")
} }
func (*testBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (*AddressObject, error) { func (*testBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (loc string, err error) {
panic("TODO: implement") panic("TODO: implement")
} }
@ -193,50 +163,3 @@ func TestAddressBookDiscovery(t *testing.T) {
}) })
} }
} }
var mkcolRequestBody = `
<?xml version="1.0" encoding="utf-8" ?>
<D:mkcol xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:set>
<D:prop>
<D:resourcetype>
<D:collection/>
<C:addressbook/>
</D:resourcetype>
<D:displayname>Lisa's Contacts</D:displayname>
<C:addressbook-description xml:lang="en"
>My primary address book.</C:addressbook-description>
</D:prop>
</D:set>
</D:mkcol>`
func TestCreateAddressbookMinimalBody(t *testing.T) {
tb := testBackend{
addressBooks: nil,
}
b := backend{
Backend: &tb,
Prefix: "/dav",
}
req := httptest.NewRequest("MKCOL", "/dav/addressbooks/user0/test-addressbook", strings.NewReader(mkcolRequestBody))
req.Header.Set("Content-Type", "application/xml")
err := b.Mkcol(req)
if err != nil {
t.Fatalf("Unexpcted error in Mkcol: %s", err)
}
if len(tb.addressBooks) != 1 {
t.Fatalf("Found %d address books, expected 1", len(tb.addressBooks))
}
c := tb.addressBooks[0]
if c.Name != "Lisa's Contacts" {
t.Fatalf("Address book name is '%s', expected 'Lisa's Contacts'", c.Name)
}
if c.Path != "/dav/addressbooks/user0/test-addressbook" {
t.Fatalf("Address book path is '%s', expected '/dav/addressbooks/user0/test-addressbook'", c.Path)
}
if c.Description != "My primary address book." {
t.Fatalf("Address book sdscription is '%s', expected 'My primary address book.'", c.Description)
}
}

View File

@ -211,11 +211,3 @@ func (r *reportReq) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return d.DecodeElement(v, &start) return d.DecodeElement(v, &start)
} }
type mkcolReq struct {
XMLName xml.Name `xml:"DAV: mkcol"`
ResourceType internal.ResourceType `xml:"set>prop>resourcetype"`
DisplayName string `xml:"set>prop>displayname"`
Description addressbookDescription `xml:"set>prop>addressbook-description"`
// TODO this could theoretically contain all addressbook properties?
}

View File

@ -16,6 +16,8 @@ import (
"github.com/emersion/go-webdav/internal" "github.com/emersion/go-webdav/internal"
) )
// TODO: add support for multiple address books
type PutAddressObjectOptions struct { type PutAddressObjectOptions struct {
// IfNoneMatch indicates that the client does not want to overwrite // IfNoneMatch indicates that the client does not want to overwrite
// an existing resource. // an existing resource.
@ -27,15 +29,12 @@ type PutAddressObjectOptions struct {
// Backend is a CardDAV server backend. // Backend is a CardDAV server backend.
type Backend interface { type Backend interface {
AddressBookHomeSetPath(ctx context.Context) (string, error) AddressbookHomeSetPath(ctx context.Context) (string, error)
ListAddressBooks(ctx context.Context) ([]AddressBook, error) AddressBook(ctx context.Context) (*AddressBook, error)
GetAddressBook(ctx context.Context, path string) (*AddressBook, error)
CreateAddressBook(ctx context.Context, addressBook *AddressBook) error
DeleteAddressBook(ctx context.Context, path string) error
GetAddressObject(ctx context.Context, path string, req *AddressDataRequest) (*AddressObject, error) GetAddressObject(ctx context.Context, path string, req *AddressDataRequest) (*AddressObject, error)
ListAddressObjects(ctx context.Context, path string, req *AddressDataRequest) ([]AddressObject, error) ListAddressObjects(ctx context.Context, req *AddressDataRequest) ([]AddressObject, error)
QueryAddressObjects(ctx context.Context, path string, query *AddressBookQuery) ([]AddressObject, error) QueryAddressObjects(ctx context.Context, query *AddressBookQuery) ([]AddressObject, error)
PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (*AddressObject, error) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (loc string, err error)
DeleteAddressObject(ctx context.Context, path string) error DeleteAddressObject(ctx context.Context, path string) error
webdav.UserPrincipalBackend webdav.UserPrincipalBackend
@ -75,7 +74,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Backend: h.Backend, Backend: h.Backend,
Prefix: strings.TrimSuffix(h.Prefix, "/"), Prefix: strings.TrimSuffix(h.Prefix, "/"),
} }
hh := internal.Handler{Backend: &b} hh := internal.Handler{&b}
hh.ServeHTTP(w, r) hh.ServeHTTP(w, r)
} }
@ -91,7 +90,7 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) error {
} }
if report.Query != nil { if report.Query != nil {
return h.handleQuery(r, w, report.Query) return h.handleQuery(r.Context(), w, report.Query)
} else if report.Multiget != nil { } else if report.Multiget != nil {
return h.handleMultiget(r.Context(), w, report.Multiget) return h.handleMultiget(r.Context(), w, report.Multiget)
} }
@ -153,7 +152,7 @@ func decodeAddressDataReq(addressData *addressDataReq) (*AddressDataRequest, err
return req, nil return req, nil
} }
func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *addressbookQuery) error { func (h *Handler) handleQuery(ctx context.Context, w http.ResponseWriter, query *addressbookQuery) error {
var q AddressBookQuery var q AddressBookQuery
if query.Prop != nil { if query.Prop != nil {
var addressData addressDataReq var addressData addressDataReq
@ -170,7 +169,7 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *add
for _, el := range query.Filter.Props { for _, el := range query.Filter.Props {
pf, err := decodePropFilter(&el) pf, err := decodePropFilter(&el)
if err != nil { if err != nil {
return &internal.HTTPError{Code: http.StatusBadRequest, Err: err} return &internal.HTTPError{http.StatusBadRequest, err}
} }
q.PropFilters = append(q.PropFilters, *pf) q.PropFilters = append(q.PropFilters, *pf)
} }
@ -181,7 +180,7 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *add
} }
} }
aos, err := h.Backend.QueryAddressObjects(r.Context(), r.URL.Path, &q) aos, err := h.Backend.QueryAddressObjects(ctx, &q)
if err != nil { if err != nil {
return err return err
} }
@ -197,7 +196,7 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *add
AllProp: query.AllProp, AllProp: query.AllProp,
PropName: query.PropName, PropName: query.PropName,
} }
resp, err := b.propFindAddressObject(r.Context(), &propfind, &ao) resp, err := b.propFindAddressObject(ctx, &propfind, &ao)
if err != nil { if err != nil {
return err return err
} }
@ -372,7 +371,7 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
} }
} }
case resourceTypeAddressBookHomeSet: case resourceTypeAddressBookHomeSet:
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context()) homeSetPath, err := b.Backend.AddressbookHomeSetPath(r.Context())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -392,21 +391,24 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
} }
} }
case resourceTypeAddressBook: case resourceTypeAddressBook:
ab, err := b.Backend.GetAddressBook(r.Context(), r.URL.Path) // TODO for multiple address books, look through all of them
ab, err := b.Backend.AddressBook(r.Context())
if err != nil { if err != nil {
return nil, err return nil, err
} }
resp, err := b.propFindAddressBook(r.Context(), propfind, ab) if r.URL.Path == ab.Path {
if err != nil { resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
return nil, err
}
resps = append(resps, *resp)
if depth != internal.DepthZero {
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resps = append(resps, resps_...) resps = append(resps, *resp)
if depth != internal.DepthZero {
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
if err != nil {
return nil, err
}
resps = append(resps, resps_...)
}
} }
case resourceTypeAddressObject: case resourceTypeAddressObject:
ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq) ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
@ -431,10 +433,12 @@ func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind)
} }
props := map[xml.Name]internal.PropFindFunc{ props := map[xml.Name]internal.PropFindFunc{
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
Href: internal.Href{Path: principalPath}, return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
}), },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(internal.CollectionName), nil
},
} }
return internal.NewPropFindResponse(principalPath, propfind, props) return internal.NewPropFindResponse(principalPath, propfind, props)
} }
@ -444,25 +448,31 @@ func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.
if err != nil { if err != nil {
return nil, err return nil, err
} }
homeSetPath, err := b.Backend.AddressbookHomeSetPath(ctx)
if err != nil {
return nil, err
}
props := map[xml.Name]internal.PropFindFunc{ props := map[xml.Name]internal.PropFindFunc{
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{ internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
Href: internal.Href{Path: principalPath}, return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
}), },
addressBookHomeSetName: func(*internal.RawXMLValue) (interface{}, error) { addressBookHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
homeSetPath, err := b.Backend.AddressBookHomeSetPath(ctx)
if err != nil {
return nil, err
}
return &addressbookHomeSet{Href: internal.Href{Path: homeSetPath}}, nil return &addressbookHomeSet{Href: internal.Href{Path: homeSetPath}}, nil
}, },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(internal.CollectionName), nil
},
} }
return internal.NewPropFindResponse(principalPath, propfind, props) return internal.NewPropFindResponse(principalPath, propfind, props)
} }
func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) { func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
homeSetPath, err := b.Backend.AddressBookHomeSetPath(ctx) principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
if err != nil {
return nil, err
}
homeSetPath, err := b.Backend.AddressbookHomeSetPath(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -470,13 +480,11 @@ func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFi
// TODO anything else to return here? // TODO anything else to return here?
props := map[xml.Name]internal.PropFindFunc{ props := map[xml.Name]internal.PropFindFunc{
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) { internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
if err != nil {
return nil, err
}
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
}, },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
return internal.NewResourceType(internal.CollectionName), nil
},
} }
return internal.NewPropFindResponse(homeSetPath, propfind, props) return internal.NewPropFindResponse(homeSetPath, propfind, props)
} }
@ -490,52 +498,51 @@ func (b *backend) propFindAddressBook(ctx context.Context, propfind *internal.Pr
} }
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
}, },
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, addressBookName)), internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
supportedAddressDataName: internal.PropFindValue(&supportedAddressData{ return internal.NewResourceType(internal.CollectionName, addressBookName), nil
Types: []addressDataType{ },
{ContentType: vcard.MIMEType, Version: "3.0"}, internal.DisplayNameName: func(*internal.RawXMLValue) (interface{}, error) {
{ContentType: vcard.MIMEType, Version: "4.0"}, return &internal.DisplayName{Name: ab.Name}, nil
}, },
}), addressBookDescriptionName: func(*internal.RawXMLValue) (interface{}, error) {
return &addressbookDescription{Description: ab.Description}, nil
},
supportedAddressDataName: func(*internal.RawXMLValue) (interface{}, error) {
return &supportedAddressData{
Types: []addressDataType{
{ContentType: vcard.MIMEType, Version: "3.0"},
{ContentType: vcard.MIMEType, Version: "4.0"},
},
}, nil
},
} }
if ab.Name != "" {
props[internal.DisplayNameName] = internal.PropFindValue(&internal.DisplayName{
Name: ab.Name,
})
}
if ab.Description != "" {
props[addressBookDescriptionName] = internal.PropFindValue(&addressbookDescription{
Description: ab.Description,
})
}
if ab.MaxResourceSize > 0 { if ab.MaxResourceSize > 0 {
props[maxResourceSizeName] = internal.PropFindValue(&maxResourceSize{ props[maxResourceSizeName] = func(*internal.RawXMLValue) (interface{}, error) {
Size: ab.MaxResourceSize, return &maxResourceSize{Size: ab.MaxResourceSize}, nil
}) }
}
props[internal.CurrentUserPrivilegeSetName] = func(*internal.RawXMLValue) (interface{}, error) {
return &internal.CurrentUserPrivilegeSet{Privilege: internal.NewAllPrivileges()}, nil
} }
return internal.NewPropFindResponse(ab.Path, propfind, props) return internal.NewPropFindResponse(ab.Path, propfind, props)
} }
func (b *backend) propFindAllAddressBooks(ctx context.Context, propfind *internal.PropFind, recurse bool) ([]internal.Response, error) { func (b *backend) propFindAllAddressBooks(ctx context.Context, propfind *internal.PropFind, recurse bool) ([]internal.Response, error) {
abs, err := b.Backend.ListAddressBooks(ctx) // TODO iterate over all address books once having multiple is supported
ab, err := b.Backend.AddressBook(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
abs := []*AddressBook{ab}
var resps []internal.Response var resps []internal.Response
for _, ab := range abs { for _, ab := range abs {
resp, err := b.propFindAddressBook(ctx, propfind, &ab) resp, err := b.propFindAddressBook(ctx, propfind, ab)
if err != nil { if err != nil {
return nil, err return nil, err
} }
resps = append(resps, *resp) resps = append(resps, *resp)
if recurse { if recurse {
resps_, err := b.propFindAllAddressObjects(ctx, propfind, &ab) resps_, err := b.propFindAllAddressObjects(ctx, propfind, ab)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -554,9 +561,9 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal.
} }
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
}, },
internal.GetContentTypeName: internal.PropFindValue(&internal.GetContentType{ internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) {
Type: vcard.MIMEType, return &internal.GetContentType{Type: vcard.MIMEType}, nil
}), },
// TODO: address-data can only be used in REPORT requests // TODO: address-data can only be used in REPORT requests
addressDataName: func(*internal.RawXMLValue) (interface{}, error) { addressDataName: func(*internal.RawXMLValue) (interface{}, error) {
var buf bytes.Buffer var buf bytes.Buffer
@ -569,20 +576,20 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal.
} }
if ao.ContentLength > 0 { if ao.ContentLength > 0 {
props[internal.GetContentLengthName] = internal.PropFindValue(&internal.GetContentLength{ props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
Length: ao.ContentLength, return &internal.GetContentLength{Length: ao.ContentLength}, nil
}) }
} }
if !ao.ModTime.IsZero() { if !ao.ModTime.IsZero() {
props[internal.GetLastModifiedName] = internal.PropFindValue(&internal.GetLastModified{ props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
LastModified: internal.Time(ao.ModTime), return &internal.GetLastModified{LastModified: internal.Time(ao.ModTime)}, nil
}) }
} }
if ao.ETag != "" { if ao.ETag != "" {
props[internal.GetETagName] = internal.PropFindValue(&internal.GetETag{ props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
ETag: internal.ETag(ao.ETag), return &internal.GetETag{ETag: internal.ETag(ao.ETag)}, nil
}) }
} }
return internal.NewPropFindResponse(ao.Path, propfind, props) return internal.NewPropFindResponse(ao.Path, propfind, props)
@ -590,7 +597,7 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal.
func (b *backend) propFindAllAddressObjects(ctx context.Context, propfind *internal.PropFind, ab *AddressBook) ([]internal.Response, error) { func (b *backend) propFindAllAddressObjects(ctx context.Context, propfind *internal.PropFind, ab *AddressBook) ([]internal.Response, error) {
var dataReq AddressDataRequest var dataReq AddressDataRequest
aos, err := b.Backend.ListAddressObjects(ctx, ab.Path, &dataReq) aos, err := b.Backend.ListAddressObjects(ctx, &dataReq)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -607,7 +614,7 @@ func (b *backend) propFindAllAddressObjects(ctx context.Context, propfind *inter
} }
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) { func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context()) homeSetPath, err := b.Backend.AddressbookHomeSetPath(r.Context())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -646,7 +653,7 @@ func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*
return resp, nil return resp, nil
} }
func (b *backend) Put(w http.ResponseWriter, r *http.Request) error { func (b *backend) Put(r *http.Request) (*internal.Href, error) {
ifNoneMatch := webdav.ConditionalMatch(r.Header.Get("If-None-Match")) ifNoneMatch := webdav.ConditionalMatch(r.Header.Get("If-None-Match"))
ifMatch := webdav.ConditionalMatch(r.Header.Get("If-Match")) ifMatch := webdav.ConditionalMatch(r.Header.Get("If-Match"))
@ -657,86 +664,46 @@ func (b *backend) Put(w http.ResponseWriter, r *http.Request) error {
t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil { if err != nil {
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: malformed Content-Type: %v", err) return nil, internal.HTTPErrorf(http.StatusBadRequest, "carddav: malformed Content-Type: %v", err)
} }
if t != vcard.MIMEType { if t != vcard.MIMEType {
// TODO: send CARDDAV:supported-address-data error // TODO: send CARDDAV:supported-address-data error
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: unsupporetd Content-Type %q", t) return nil, internal.HTTPErrorf(http.StatusBadRequest, "carddav: unsupporetd Content-Type %q", t)
} }
// TODO: check CARDDAV:max-resource-size precondition // TODO: check CARDDAV:max-resource-size precondition
card, err := vcard.NewDecoder(r.Body).Decode() card, err := vcard.NewDecoder(r.Body).Decode()
if err != nil { if err != nil {
// TODO: send CARDDAV:valid-address-data error // TODO: send CARDDAV:valid-address-data error
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: failed to parse vCard: %v", err) return nil, internal.HTTPErrorf(http.StatusBadRequest, "carddav: failed to parse vCard: %v", err)
} }
// TODO: add support for the CARDDAV:no-uid-conflict error // TODO: add support for the CARDDAV:no-uid-conflict error
ao, err := b.Backend.PutAddressObject(r.Context(), r.URL.Path, card, &opts) loc, err := b.Backend.PutAddressObject(r.Context(), r.URL.Path, card, &opts)
if err != nil { if err != nil {
return err return nil, err
}
if ao.ETag != "" {
w.Header().Set("ETag", internal.ETag(ao.ETag).String())
}
if !ao.ModTime.IsZero() {
w.Header().Set("Last-Modified", ao.ModTime.UTC().Format(http.TimeFormat))
}
if ao.Path != "" {
w.Header().Set("Location", ao.Path)
} }
// TODO: http.StatusNoContent if the resource already existed return &internal.Href{Path: loc}, nil
w.WriteHeader(http.StatusCreated)
return nil
} }
func (b *backend) Delete(r *http.Request) error { func (b *backend) Delete(r *http.Request) error {
switch b.resourceTypeAtPath(r.URL.Path) { return b.Backend.DeleteAddressObject(r.Context(), r.URL.Path)
case resourceTypeAddressBook:
return b.Backend.DeleteAddressBook(r.Context(), r.URL.Path)
case resourceTypeAddressObject:
return b.Backend.DeleteAddressObject(r.Context(), r.URL.Path)
}
return internal.HTTPErrorf(http.StatusForbidden, "carddav: cannot delete resource at given location")
} }
func (b *backend) Mkcol(r *http.Request) error { func (b *backend) Mkcol(r *http.Request) error {
if b.resourceTypeAtPath(r.URL.Path) != resourceTypeAddressBook { return internal.HTTPErrorf(http.StatusForbidden, "carddav: address book creation unsupported")
return internal.HTTPErrorf(http.StatusForbidden, "carddav: address book creation not allowed at given location")
}
ab := AddressBook{
Path: r.URL.Path,
}
if !internal.IsRequestBodyEmpty(r) {
var m mkcolReq
if err := internal.DecodeXMLRequest(r, &m); err != nil {
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: error parsing mkcol request: %s", err.Error())
}
if !m.ResourceType.Is(internal.CollectionName) || !m.ResourceType.Is(addressBookName) {
return internal.HTTPErrorf(http.StatusBadRequest, "carddav: unexpected resource type")
}
ab.Name = m.DisplayName
ab.Description = m.Description.Description
// TODO ...
}
return b.Backend.CreateAddressBook(r.Context(), &ab)
} }
func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) { func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) {
return false, internal.HTTPErrorf(http.StatusNotImplemented, "carddav: Copy not implemented") panic("TODO")
} }
func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) { func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) {
return false, internal.HTTPErrorf(http.StatusNotImplemented, "carddav: Move not implemented") panic("TODO")
} }
// PreconditionType as defined in https://tools.ietf.org/rfcmarkup?doc=6352#section-6.3.2.1 // https://tools.ietf.org/rfcmarkup?doc=6352#section-6.3.2.1
type PreconditionType string type PreconditionType string
const ( const (
@ -747,7 +714,7 @@ const (
) )
func NewPreconditionError(err PreconditionType) error { func NewPreconditionError(err PreconditionType) error {
name := xml.Name{Space: "urn:ietf:params:xml:ns:carddav", Local: string(err)} name := xml.Name{"urn:ietf:params:xml:ns:carddav", string(err)}
elem := internal.NewRawXMLElement(name, nil, nil) elem := internal.NewRawXMLElement(name, nil, nil)
return &internal.HTTPError{ return &internal.HTTPError{
Code: 409, Code: 409,

View File

@ -30,3 +30,24 @@ type groupMembership struct {
XMLName xml.Name `xml:"DAV: group-membership"` XMLName xml.Name `xml:"DAV: group-membership"`
Hrefs []internal.Href `xml:"href"` Hrefs []internal.Href `xml:"href"`
} }
// ConditionalMatch represents the value of a conditional header
// according to RFC 2068 section 14.25 and RFC 2068 section 14.26
// The (optional) value can either be a wildcard or an ETag.
type ConditionalMatch string
func (val ConditionalMatch) IsSet() bool {
return val != ""
}
func (val ConditionalMatch) IsWildcard() bool {
return val == "*"
}
func (val ConditionalMatch) ETag() (string, error) {
var e internal.ETag
if err := e.UnmarshalText([]byte(val)); err != nil {
return "", err
}
return string(e), nil
}

View File

@ -63,18 +63,6 @@ func fileInfoFromOS(p string, fi os.FileInfo) *FileInfo {
} }
} }
func errFromOS(err error) error {
if os.IsNotExist(err) {
return NewHTTPError(http.StatusNotFound, err)
} else if os.IsPermission(err) {
return NewHTTPError(http.StatusForbidden, err)
} else if os.IsTimeout(err) {
return NewHTTPError(http.StatusServiceUnavailable, err)
} else {
return err
}
}
func (fs LocalFileSystem) Stat(ctx context.Context, name string) (*FileInfo, error) { func (fs LocalFileSystem) Stat(ctx context.Context, name string) (*FileInfo, error) {
p, err := fs.localPath(name) p, err := fs.localPath(name)
if err != nil { if err != nil {
@ -82,7 +70,7 @@ func (fs LocalFileSystem) Stat(ctx context.Context, name string) (*FileInfo, err
} }
fi, err := os.Stat(p) fi, err := os.Stat(p)
if err != nil { if err != nil {
return nil, errFromOS(err) return nil, err
} }
return fileInfoFromOS(name, fi), nil return fileInfoFromOS(name, fi), nil
} }
@ -111,57 +99,15 @@ func (fs LocalFileSystem) ReadDir(ctx context.Context, name string, recursive bo
} }
return nil return nil
}) })
return l, errFromOS(err) return l, err
} }
func (fs LocalFileSystem) Create(ctx context.Context, name string, body io.ReadCloser, opts *CreateOptions) (fi *FileInfo, created bool, err error) { func (fs LocalFileSystem) Create(ctx context.Context, name string) (io.WriteCloser, error) {
p, err := fs.localPath(name) p, err := fs.localPath(name)
if err != nil { if err != nil {
return nil, false, err return nil, err
} }
fi, _ = fs.Stat(ctx, name) return os.Create(p)
created = fi == nil
etag := ""
if fi != nil {
etag = fi.ETag
}
if opts.IfMatch.IsSet() {
if ok, err := opts.IfMatch.MatchETag(etag); err != nil {
return nil, false, NewHTTPError(http.StatusBadRequest, err)
} else if !ok {
return nil, false, NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("If-Match condition failed"))
}
}
if opts.IfNoneMatch.IsSet() {
if ok, err := opts.IfNoneMatch.MatchETag(etag); err != nil {
return nil, false, NewHTTPError(http.StatusBadRequest, err)
} else if ok {
return nil, false, NewHTTPError(http.StatusPreconditionFailed, fmt.Errorf("If-None-Match condition failed"))
}
}
wc, err := os.Create(p)
if err != nil {
return nil, false, errFromOS(err)
}
defer wc.Close()
if _, err := io.Copy(wc, body); err != nil {
os.Remove(p)
return nil, false, err
}
if err := wc.Close(); err != nil {
os.Remove(p)
return nil, false, err
}
fi, err = fs.Stat(ctx, name)
if err != nil {
return nil, false, err
}
return fi, created, err
} }
func (fs LocalFileSystem) RemoveAll(ctx context.Context, name string) error { func (fs LocalFileSystem) RemoveAll(ctx context.Context, name string) error {
@ -173,10 +119,10 @@ func (fs LocalFileSystem) RemoveAll(ctx context.Context, name string) error {
// WebDAV semantics are that it should return a "404 Not Found" error in // WebDAV semantics are that it should return a "404 Not Found" error in
// case the resource doesn't exist. We need to Stat before RemoveAll. // case the resource doesn't exist. We need to Stat before RemoveAll.
if _, err = os.Stat(p); err != nil { if _, err = os.Stat(p); err != nil {
return errFromOS(err) return err
} }
return errFromOS(os.RemoveAll(p)) return os.RemoveAll(p)
} }
func (fs LocalFileSystem) Mkdir(ctx context.Context, name string) error { func (fs LocalFileSystem) Mkdir(ctx context.Context, name string) error {
@ -184,21 +130,20 @@ func (fs LocalFileSystem) Mkdir(ctx context.Context, name string) error {
if err != nil { if err != nil {
return err return err
} }
return errFromOS(os.Mkdir(p, 0755)) return os.Mkdir(p, 0755)
} }
func copyRegularFile(src, dst string, perm os.FileMode) error { func copyRegularFile(src, dst string, perm os.FileMode) error {
srcFile, err := os.Open(src) srcFile, err := os.Open(src)
if err != nil { if err != nil {
return errFromOS(err) return err
} }
defer srcFile.Close() defer srcFile.Close()
dstFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) dstFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm)
if os.IsNotExist(err) { if err != nil {
return NewHTTPError(http.StatusConflict, err) // TODO: send http.StatusConflict on os.IsNotExist
} else if err != nil { return err
return errFromOS(err)
} }
defer dstFile.Close() defer dstFile.Close()
@ -224,21 +169,21 @@ func (fs LocalFileSystem) Copy(ctx context.Context, src, dst string, options *Co
srcInfo, err := os.Stat(srcPath) srcInfo, err := os.Stat(srcPath)
if err != nil { if err != nil {
return false, errFromOS(err) return false, err
} }
srcPerm := srcInfo.Mode() & os.ModePerm srcPerm := srcInfo.Mode() & os.ModePerm
if _, err := os.Stat(dstPath); err != nil { if _, err := os.Stat(dstPath); err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return false, errFromOS(err) return false, err
} }
created = true created = true
} else { } else {
if options.NoOverwrite { if options.NoOverwrite {
return false, NewHTTPError(http.StatusPreconditionFailed, os.ErrExist) return false, os.ErrExist
} }
if err := os.RemoveAll(dstPath); err != nil { if err := os.RemoveAll(dstPath); err != nil {
return false, errFromOS(err) return false, err
} }
} }
@ -249,7 +194,7 @@ func (fs LocalFileSystem) Copy(ctx context.Context, src, dst string, options *Co
if fi.IsDir() { if fi.IsDir() {
if err := os.Mkdir(dstPath, srcPerm); err != nil { if err := os.Mkdir(dstPath, srcPerm); err != nil {
return errFromOS(err) return err
} }
} else { } else {
if err := copyRegularFile(srcPath, dstPath, srcPerm); err != nil { if err := copyRegularFile(srcPath, dstPath, srcPerm); err != nil {
@ -263,7 +208,7 @@ func (fs LocalFileSystem) Copy(ctx context.Context, src, dst string, options *Co
return nil return nil
}) })
if err != nil { if err != nil {
return false, errFromOS(err) return false, err
} }
return created, nil return created, nil
@ -281,20 +226,20 @@ func (fs LocalFileSystem) Move(ctx context.Context, src, dst string, options *Mo
if _, err := os.Stat(dstPath); err != nil { if _, err := os.Stat(dstPath); err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return false, errFromOS(err) return false, err
} }
created = true created = true
} else { } else {
if options.NoOverwrite { if options.NoOverwrite {
return false, NewHTTPError(http.StatusPreconditionFailed, os.ErrExist) return false, os.ErrExist
} }
if err := os.RemoveAll(dstPath); err != nil { if err := os.RemoveAll(dstPath); err != nil {
return false, errFromOS(err) return false, err
} }
} }
if err := os.Rename(srcPath, dstPath); err != nil { if err := os.Rename(srcPath, dstPath); err != nil {
return false, errFromOS(err) return false, err
} }
return created, nil return created, nil

3
go.mod
View File

@ -3,6 +3,7 @@ module github.com/emersion/go-webdav
go 1.13 go 1.13
require ( require (
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
github.com/teambition/rrule-go v1.8.2 // indirect
) )

5
go.sum
View File

@ -1,6 +1,7 @@
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA= github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8= github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=

View File

@ -1,7 +1,6 @@
package internal package internal
import ( import (
"encoding/base64"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
@ -23,8 +22,6 @@ var (
GetETagName = xml.Name{Namespace, "getetag"} GetETagName = xml.Name{Namespace, "getetag"}
CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"} CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"}
CurrentUserPrivilegeSetName = xml.Name{Namespace, "current-user-privilege-set"}
) )
type Status struct { type Status struct {
@ -354,7 +351,7 @@ type Time time.Time
func (t *Time) UnmarshalText(b []byte) error { func (t *Time) UnmarshalText(b []byte) error {
tt, err := http.ParseTime(string(b)) tt, err := http.ParseTime(string(b))
if err != nil { if err != nil {
return errors.New(err.Error() + " : time_data : " + base64.StdEncoding.EncodeToString(b)) return err
} }
*t = Time(tt) *t = Time(tt)
return nil return nil
@ -420,30 +417,6 @@ type CurrentUserPrincipal struct {
Unauthenticated *struct{} `xml:"unauthenticated,omitempty"` Unauthenticated *struct{} `xml:"unauthenticated,omitempty"`
} }
type CurrentUserPrivilegeSet struct {
XMLName xml.Name `xml:"DAV: current-user-privilege-set"`
Privilege []Privilege `xml:"privilege"`
}
type Privilege struct {
XMLName xml.Name `xml:"DAV: privilege"`
Read *struct{} `xml:"DAV: read,omitempty"`
All *struct{} `xml:"DAV: all,omitempty"`
Write *struct{} `xml:"DAV: write,omitempty"`
WriteProperties *struct{} `xml:"DAV: write-properties,omitempty"`
WriteContent *struct{} `xml:"DAV: write-content,omitempty"`
}
func NewAllPrivileges() []Privilege {
return []Privilege{
{Read: &struct{}{}},
{All: &struct{}{}},
{Write: &struct{}{}},
{WriteProperties: &struct{}{}},
{WriteContent: &struct{}{}},
}
}
// https://tools.ietf.org/html/rfc4918#section-14.19 // https://tools.ietf.org/html/rfc4918#section-14.19
type PropertyUpdate struct { type PropertyUpdate struct {
XMLName xml.Name `xml:"DAV: propertyupdate"` XMLName xml.Name `xml:"DAV: propertyupdate"`

View File

@ -44,13 +44,8 @@ func DecodeXMLRequest(r *http.Request, v interface{}) error {
return nil return nil
} }
func IsRequestBodyEmpty(r *http.Request) bool {
_, err := r.Body.Read(nil)
return err == io.EOF
}
func ServeXML(w http.ResponseWriter) *xml.Encoder { func ServeXML(w http.ResponseWriter) *xml.Encoder {
w.Header().Add("Content-Type", "application/xml; charset=\"utf-8\"") w.Header().Add("Content-Type", "text/xml; charset=\"utf-8\"")
w.Write([]byte(xml.Header)) w.Write([]byte(xml.Header))
return xml.NewEncoder(w) return xml.NewEncoder(w)
} }
@ -66,7 +61,7 @@ type Backend interface {
HeadGet(w http.ResponseWriter, r *http.Request) error HeadGet(w http.ResponseWriter, r *http.Request) error
PropFind(r *http.Request, pf *PropFind, depth Depth) (*MultiStatus, error) PropFind(r *http.Request, pf *PropFind, depth Depth) (*MultiStatus, error)
PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error) PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error)
Put(w http.ResponseWriter, r *http.Request) error Put(r *http.Request) (*Href, error)
Delete(r *http.Request) error Delete(r *http.Request) error
Mkcol(r *http.Request) error Mkcol(r *http.Request) error
Copy(r *http.Request, dest *Href, recursive, overwrite bool) (created bool, err error) Copy(r *http.Request, dest *Href, recursive, overwrite bool) (created bool, err error)
@ -88,7 +83,17 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case http.MethodGet, http.MethodHead: case http.MethodGet, http.MethodHead:
err = h.Backend.HeadGet(w, r) err = h.Backend.HeadGet(w, r)
case http.MethodPut: case http.MethodPut:
err = h.Backend.Put(w, r) var href *Href
href, err = h.Backend.Put(r)
if err == nil {
// TODO: Last-Modified, ETag, Content-Type if the request has
// been copied verbatim
if href != nil {
w.Header().Set("Location", (*url.URL)(href).String())
}
// TODO: http.StatusNoContent if the resource already existed
w.WriteHeader(http.StatusCreated)
}
case http.MethodDelete: case http.MethodDelete:
// TODO: send a multistatus in case of partial failure // TODO: send a multistatus in case of partial failure
err = h.Backend.Delete(r) err = h.Backend.Delete(r)
@ -125,7 +130,7 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) error {
w.Header().Add("DAV", strings.Join(caps, ", ")) w.Header().Add("DAV", strings.Join(caps, ", "))
w.Header().Add("Allow", strings.Join(allow, ", ")) w.Header().Add("Allow", strings.Join(allow, ", "))
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusNoContent)
return nil return nil
} }
@ -162,17 +167,13 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) error {
type PropFindFunc func(raw *RawXMLValue) (interface{}, error) type PropFindFunc func(raw *RawXMLValue) (interface{}, error)
func PropFindValue(value interface{}) PropFindFunc {
return func(raw *RawXMLValue) (interface{}, error) {
return value, nil
}
}
func NewPropFindResponse(path string, propfind *PropFind, props map[xml.Name]PropFindFunc) (*Response, error) { func NewPropFindResponse(path string, propfind *PropFind, props map[xml.Name]PropFindFunc) (*Response, error) {
resp := &Response{Hrefs: []Href{Href{Path: path}}} resp := NewOKResponse(path)
if _, ok := props[ResourceTypeName]; !ok { if _, ok := props[ResourceTypeName]; !ok {
props[ResourceTypeName] = PropFindValue(NewResourceType()) props[ResourceTypeName] = func(*RawXMLValue) (interface{}, error) {
return NewResourceType(), nil
}
} }
if propfind.PropName != nil { if propfind.PropName != nil {
@ -191,8 +192,9 @@ func NewPropFindResponse(path string, propfind *PropFind, props map[xml.Name]Pro
code := http.StatusOK code := http.StatusOK
if err != nil { if err != nil {
// TODO: don't throw away error message here
code = HTTPErrorFromError(err).Code code = HTTPErrorFromError(err).Code
val = NewRawXMLElement(xmlName, []xml.Attr{{Name: xml.Name{Space: "ERR", Local: "Error"}, Value: err.Error()}}, nil) val = emptyVal
} }
if err := resp.EncodeProp(code, val); err != nil { if err := resp.EncodeProp(code, val); err != nil {
@ -213,8 +215,8 @@ func NewPropFindResponse(path string, propfind *PropFind, props map[xml.Name]Pro
f, ok := props[xmlName] f, ok := props[xmlName]
if ok { if ok {
if v, err := f(&raw); err != nil { if v, err := f(&raw); err != nil {
// TODO: don't throw away error message here
code = HTTPErrorFromError(err).Code code = HTTPErrorFromError(err).Code
val = NewRawXMLElement(xmlName, []xml.Attr{{Name: xml.Name{Space: "ERR", Local: "Error"}, Value: err.Error()}}, nil)
} else { } else {
code = http.StatusOK code = http.StatusOK
val = v val = v

View File

@ -17,7 +17,7 @@ type FileSystem interface {
Open(ctx context.Context, name string) (io.ReadCloser, error) Open(ctx context.Context, name string) (io.ReadCloser, error)
Stat(ctx context.Context, name string) (*FileInfo, error) Stat(ctx context.Context, name string) (*FileInfo, error)
ReadDir(ctx context.Context, name string, recursive bool) ([]FileInfo, error) ReadDir(ctx context.Context, name string, recursive bool) ([]FileInfo, error)
Create(ctx context.Context, name string, body io.ReadCloser, opts *CreateOptions) (fileInfo *FileInfo, created bool, err error) Create(ctx context.Context, name string) (io.WriteCloser, error)
RemoveAll(ctx context.Context, name string) error RemoveAll(ctx context.Context, name string) error
Mkdir(ctx context.Context, name string) error Mkdir(ctx context.Context, name string) error
Copy(ctx context.Context, name, dest string, options *CopyOptions) (created bool, err error) Copy(ctx context.Context, name, dest string, options *CopyOptions) (created bool, err error)
@ -38,14 +38,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
b := backend{h.FileSystem} b := backend{h.FileSystem}
hh := internal.Handler{Backend: &b} hh := internal.Handler{&b}
hh.ServeHTTP(w, r) hh.ServeHTTP(w, r)
} }
// NewHTTPError creates a new error that is associated with an HTTP status code // NewHTTPError creates a new error that is associated with an HTTP status code
// and optionally an error that lead to it. Backends can use this functions to // and optionally an error that lead to it. Backends can use this functions to
// return errors that convey some semantics (e.g. 404 not found, 403 access // return errors that convey some semantics (e.g. 404 not found, 403 access
// denied, etc.) while also providing an (optional) arbitrary error context // denied, etc) while also providing an (optional) arbitrary error context
// (intended for humans). // (intended for humans).
func NewHTTPError(statusCode int, cause error) error { func NewHTTPError(statusCode int, cause error) error {
return &internal.HTTPError{Code: statusCode, Err: cause} return &internal.HTTPError{Code: statusCode, Err: cause}
@ -57,7 +57,7 @@ type backend struct {
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) { func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path) fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if internal.IsNotFound(err) { if os.IsNotExist(err) {
return nil, []string{http.MethodOptions, http.MethodPut, "MKCOL"}, nil return nil, []string{http.MethodOptions, http.MethodPut, "MKCOL"}, nil
} else if err != nil { } else if err != nil {
return nil, nil, err return nil, nil, err
@ -80,7 +80,9 @@ func (b *backend) Options(r *http.Request) (caps []string, allow []string, err e
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error { func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path) fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if err != nil { if os.IsNotExist(err) {
return &internal.HTTPError{Code: http.StatusNotFound, Err: err}
} else if err != nil {
return err return err
} }
if fi.IsDir { if fi.IsDir {
@ -119,7 +121,9 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
// TODO: use partial error Response on error // TODO: use partial error Response on error
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path) fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if err != nil { if os.IsNotExist(err) {
return nil, &internal.HTTPError{Code: http.StatusNotFound, Err: err}
} else if err != nil {
return nil, err return nil, err
} }
@ -162,26 +166,26 @@ func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*inte
} }
if !fi.IsDir { if !fi.IsDir {
props[internal.GetContentLengthName] = internal.PropFindValue(&internal.GetContentLength{ props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
Length: fi.Size, return &internal.GetContentLength{Length: fi.Size}, nil
}) }
if !fi.ModTime.IsZero() { if !fi.ModTime.IsZero() {
props[internal.GetLastModifiedName] = internal.PropFindValue(&internal.GetLastModified{ props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
LastModified: internal.Time(fi.ModTime), return &internal.GetLastModified{LastModified: internal.Time(fi.ModTime)}, nil
}) }
} }
if fi.MIMEType != "" { if fi.MIMEType != "" {
props[internal.GetContentTypeName] = internal.PropFindValue(&internal.GetContentType{ props[internal.GetContentTypeName] = func(*internal.RawXMLValue) (interface{}, error) {
Type: fi.MIMEType, return &internal.GetContentType{Type: fi.MIMEType}, nil
}) }
} }
if fi.ETag != "" { if fi.ETag != "" {
props[internal.GetETagName] = internal.PropFindValue(&internal.GetETag{ props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
ETag: internal.ETag(fi.ETag), return &internal.GetETag{ETag: internal.ETag(fi.ETag)}, nil
}) }
} }
} }
@ -193,40 +197,26 @@ func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*
return nil, internal.HTTPErrorf(http.StatusForbidden, "webdav: PROPPATCH is unsupported") return nil, internal.HTTPErrorf(http.StatusForbidden, "webdav: PROPPATCH is unsupported")
} }
func (b *backend) Put(w http.ResponseWriter, r *http.Request) error { func (b *backend) Put(r *http.Request) (*internal.Href, error) {
ifNoneMatch := ConditionalMatch(r.Header.Get("If-None-Match")) wc, err := b.FileSystem.Create(r.Context(), r.URL.Path)
ifMatch := ConditionalMatch(r.Header.Get("If-Match"))
opts := CreateOptions{
IfNoneMatch: ifNoneMatch,
IfMatch: ifMatch,
}
fi, created, err := b.FileSystem.Create(r.Context(), r.URL.Path, r.Body, &opts)
if err != nil { if err != nil {
return err return nil, err
}
defer wc.Close()
if _, err := io.Copy(wc, r.Body); err != nil {
return nil, err
} }
if fi.MIMEType != "" { return nil, wc.Close()
w.Header().Set("Content-Type", fi.MIMEType)
}
if !fi.ModTime.IsZero() {
w.Header().Set("Last-Modified", fi.ModTime.UTC().Format(http.TimeFormat))
}
if fi.ETag != "" {
w.Header().Set("ETag", internal.ETag(fi.ETag).String())
}
if created {
w.WriteHeader(http.StatusCreated)
} else {
w.WriteHeader(http.StatusNoContent)
}
return nil
} }
func (b *backend) Delete(r *http.Request) error { func (b *backend) Delete(r *http.Request) error {
return b.FileSystem.RemoveAll(r.Context(), r.URL.Path) err := b.FileSystem.RemoveAll(r.Context(), r.URL.Path)
if os.IsNotExist(err) {
return &internal.HTTPError{Code: http.StatusNotFound, Err: err}
}
return err
} }
func (b *backend) Mkcol(r *http.Request) error { func (b *backend) Mkcol(r *http.Request) error {
@ -234,7 +224,7 @@ func (b *backend) Mkcol(r *http.Request) error {
return internal.HTTPErrorf(http.StatusUnsupportedMediaType, "webdav: request body not supported in MKCOL request") return internal.HTTPErrorf(http.StatusUnsupportedMediaType, "webdav: request body not supported in MKCOL request")
} }
err := b.FileSystem.Mkdir(r.Context(), r.URL.Path) err := b.FileSystem.Mkdir(r.Context(), r.URL.Path)
if internal.IsNotFound(err) { if os.IsNotExist(err) {
return &internal.HTTPError{Code: http.StatusConflict, Err: err} return &internal.HTTPError{Code: http.StatusConflict, Err: err}
} }
return err return err
@ -265,7 +255,7 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr
// BackendSuppliedHomeSet represents either a CalDAV calendar-home-set or a // BackendSuppliedHomeSet represents either a CalDAV calendar-home-set or a
// CardDAV addressbook-home-set. It should only be created via // CardDAV addressbook-home-set. It should only be created via
// caldav.NewCalendarHomeSet or carddav.NewAddressBookHomeSet. Only to // caldav.NewCalendarHomeSet or carddav.NewAddressbookHomeSet. Only to
// be used server-side, for listing a user's home sets as determined by the // be used server-side, for listing a user's home sets as determined by the
// (external) backend. // (external) backend.
type BackendSuppliedHomeSet interface { type BackendSuppliedHomeSet interface {
@ -299,7 +289,7 @@ func ServePrincipal(w http.ResponseWriter, r *http.Request, options *ServePrinci
allow := []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"} allow := []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}
w.Header().Add("DAV", strings.Join(caps, ", ")) w.Header().Add("DAV", strings.Join(caps, ", "))
w.Header().Add("Allow", strings.Join(allow, ", ")) w.Header().Add("Allow", strings.Join(allow, ", "))
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusNoContent)
case "PROPFIND": case "PROPFIND":
if err := servePrincipalPropfind(w, r, options); err != nil { if err := servePrincipalPropfind(w, r, options); err != nil {
internal.ServeError(w, err) internal.ServeError(w, err)

View File

@ -5,8 +5,6 @@ package webdav
import ( import (
"time" "time"
"github.com/emersion/go-webdav/internal"
) )
// FileInfo holds information about a WebDAV file. // FileInfo holds information about a WebDAV file.
@ -19,11 +17,6 @@ type FileInfo struct {
ETag string ETag string
} }
type CreateOptions struct {
IfMatch ConditionalMatch
IfNoneMatch ConditionalMatch
}
type CopyOptions struct { type CopyOptions struct {
NoRecursive bool NoRecursive bool
NoOverwrite bool NoOverwrite bool
@ -32,32 +25,3 @@ type CopyOptions struct {
type MoveOptions struct { type MoveOptions struct {
NoOverwrite bool NoOverwrite bool
} }
// ConditionalMatch represents the value of a conditional header
// according to RFC 2068 section 14.25 and RFC 2068 section 14.26
// The (optional) value can either be a wildcard or an ETag.
type ConditionalMatch string
func (val ConditionalMatch) IsSet() bool {
return val != ""
}
func (val ConditionalMatch) IsWildcard() bool {
return val == "*"
}
func (val ConditionalMatch) ETag() (string, error) {
var e internal.ETag
if err := e.UnmarshalText([]byte(val)); err != nil {
return "", err
}
return string(e), nil
}
func (val ConditionalMatch) MatchETag(etag string) (bool, error) {
if val.IsWildcard() {
return true, nil
}
t, err := val.ETag()
return t == etag, err
}