diff --git a/caldav/server.go b/caldav/server.go index 24977b8..d3e83b8 100644 --- a/caldav/server.go +++ b/caldav/server.go @@ -38,7 +38,7 @@ type Backend interface { GetCalendarObject(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) - PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (loc string, err error) + PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) DeleteCalendarObject(ctx context.Context, path string) error webdav.UserPrincipalBackend @@ -78,7 +78,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { Backend: h.Backend, Prefix: strings.TrimSuffix(h.Prefix, "/"), } - hh := internal.Handler{&b} + hh := internal.Handler{Backend: &b} hh.ServeHTTP(w, r) } @@ -668,7 +668,7 @@ func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (* return nil, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: PropPatch not implemented") } -func (b *backend) Put(r *http.Request) (*internal.Href, error) { +func (b *backend) Put(w http.ResponseWriter, r *http.Request) error { ifNoneMatch := webdav.ConditionalMatch(r.Header.Get("If-None-Match")) ifMatch := webdav.ConditionalMatch(r.Header.Get("If-Match")) @@ -679,26 +679,39 @@ func (b *backend) Put(r *http.Request) (*internal.Href, error) { t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { - return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: malformed Content-Type: %v", err) + return internal.HTTPErrorf(http.StatusBadRequest, "caldav: malformed Content-Type: %v", err) } if t != ical.MIMEType { // TODO: send CALDAV:supported-calendar-data error - return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: unsupported Content-Type %q", t) + return internal.HTTPErrorf(http.StatusBadRequest, "caldav: unsupported Content-Type %q", t) } // TODO: check CALDAV:max-resource-size precondition cal, err := ical.NewDecoder(r.Body).Decode() if err != nil { // TODO: send CALDAV:valid-calendar-data error - return nil, internal.HTTPErrorf(http.StatusBadRequest, "caldav: failed to parse iCalendar: %v", err) + return internal.HTTPErrorf(http.StatusBadRequest, "caldav: failed to parse iCalendar: %v", err) } - loc, err := b.Backend.PutCalendarObject(r.Context(), r.URL.Path, cal, &opts) + co, err := b.Backend.PutCalendarObject(r.Context(), r.URL.Path, cal, &opts) if err != nil { - return nil, err + return err } - return &internal.Href{Path: loc}, nil + if co.ETag != "" { + 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 { @@ -756,7 +769,7 @@ const ( ) func NewPreconditionError(err PreconditionType) error { - name := xml.Name{"urn:ietf:params:xml:ns:caldav", string(err)} + name := xml.Name{Space: "urn:ietf:params:xml:ns:caldav", Local: string(err)} elem := internal.NewRawXMLElement(name, nil, nil) return &internal.HTTPError{ Code: 409, diff --git a/caldav/server_test.go b/caldav/server_test.go index 3d1e63c..594b87b 100644 --- a/caldav/server_test.go +++ b/caldav/server_test.go @@ -222,8 +222,8 @@ func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *Ca 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) (string, error) { - return "", nil +func (t testBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (*CalendarObject, error) { + return nil, nil } func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) { diff --git a/carddav/carddav_test.go b/carddav/carddav_test.go index 337ac83..f4f6b26 100644 --- a/carddav/carddav_test.go +++ b/carddav/carddav_test.go @@ -108,7 +108,7 @@ func (*testBackend) QueryAddressObjects(ctx context.Context, path string, query panic("TODO: implement") } -func (*testBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (loc string, err error) { +func (*testBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (*AddressObject, error) { panic("TODO: implement") } diff --git a/carddav/server.go b/carddav/server.go index 8ea7680..915c49e 100644 --- a/carddav/server.go +++ b/carddav/server.go @@ -35,7 +35,7 @@ type Backend interface { GetAddressObject(ctx context.Context, path string, req *AddressDataRequest) (*AddressObject, error) ListAddressObjects(ctx context.Context, path string, req *AddressDataRequest) ([]AddressObject, error) QueryAddressObjects(ctx context.Context, path string, query *AddressBookQuery) ([]AddressObject, error) - PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (loc string, err error) + PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (*AddressObject, error) DeleteAddressObject(ctx context.Context, path string) error webdav.UserPrincipalBackend @@ -75,7 +75,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { Backend: h.Backend, Prefix: strings.TrimSuffix(h.Prefix, "/"), } - hh := internal.Handler{&b} + hh := internal.Handler{Backend: &b} hh.ServeHTTP(w, r) } @@ -170,7 +170,7 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *add for _, el := range query.Filter.Props { pf, err := decodePropFilter(&el) if err != nil { - return &internal.HTTPError{http.StatusBadRequest, err} + return &internal.HTTPError{Code: http.StatusBadRequest, Err: err} } q.PropFilters = append(q.PropFilters, *pf) } @@ -653,7 +653,7 @@ func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (* return resp, nil } -func (b *backend) Put(r *http.Request) (*internal.Href, error) { +func (b *backend) Put(w http.ResponseWriter, r *http.Request) error { ifNoneMatch := webdav.ConditionalMatch(r.Header.Get("If-None-Match")) ifMatch := webdav.ConditionalMatch(r.Header.Get("If-Match")) @@ -664,27 +664,39 @@ func (b *backend) Put(r *http.Request) (*internal.Href, error) { t, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil { - return nil, internal.HTTPErrorf(http.StatusBadRequest, "carddav: malformed Content-Type: %v", err) + return internal.HTTPErrorf(http.StatusBadRequest, "carddav: malformed Content-Type: %v", err) } if t != vcard.MIMEType { // TODO: send CARDDAV:supported-address-data error - return nil, internal.HTTPErrorf(http.StatusBadRequest, "carddav: unsupporetd Content-Type %q", t) + return internal.HTTPErrorf(http.StatusBadRequest, "carddav: unsupporetd Content-Type %q", t) } // TODO: check CARDDAV:max-resource-size precondition card, err := vcard.NewDecoder(r.Body).Decode() if err != nil { // TODO: send CARDDAV:valid-address-data error - return nil, internal.HTTPErrorf(http.StatusBadRequest, "carddav: failed to parse vCard: %v", err) + return internal.HTTPErrorf(http.StatusBadRequest, "carddav: failed to parse vCard: %v", err) } // TODO: add support for the CARDDAV:no-uid-conflict error - loc, err := b.Backend.PutAddressObject(r.Context(), r.URL.Path, card, &opts) + ao, err := b.Backend.PutAddressObject(r.Context(), r.URL.Path, card, &opts) if err != nil { - return nil, err + return 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) } - return &internal.Href{Path: loc}, nil + // TODO: http.StatusNoContent if the resource already existed + w.WriteHeader(http.StatusCreated) + + return nil } func (b *backend) Delete(r *http.Request) error { @@ -731,7 +743,7 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr return false, internal.HTTPErrorf(http.StatusNotImplemented, "carddav: Move not implemented") } -// https://tools.ietf.org/rfcmarkup?doc=6352#section-6.3.2.1 +// PreconditionType as defined in https://tools.ietf.org/rfcmarkup?doc=6352#section-6.3.2.1 type PreconditionType string const ( @@ -742,7 +754,7 @@ const ( ) func NewPreconditionError(err PreconditionType) error { - name := xml.Name{"urn:ietf:params:xml:ns:carddav", string(err)} + name := xml.Name{Space: "urn:ietf:params:xml:ns:carddav", Local: string(err)} elem := internal.NewRawXMLElement(name, nil, nil) return &internal.HTTPError{ Code: 409, diff --git a/internal/server.go b/internal/server.go index dd94840..a1c228f 100644 --- a/internal/server.go +++ b/internal/server.go @@ -66,7 +66,7 @@ type Backend interface { HeadGet(w http.ResponseWriter, r *http.Request) error PropFind(r *http.Request, pf *PropFind, depth Depth) (*MultiStatus, error) PropPatch(r *http.Request, pu *PropertyUpdate) (*Response, error) - Put(r *http.Request) (*Href, error) + Put(w http.ResponseWriter, r *http.Request) error Delete(r *http.Request) error Mkcol(r *http.Request) error Copy(r *http.Request, dest *Href, recursive, overwrite bool) (created bool, err error) @@ -88,17 +88,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodGet, http.MethodHead: err = h.Backend.HeadGet(w, r) case http.MethodPut: - 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) - } + err = h.Backend.Put(w, r) case http.MethodDelete: // TODO: send a multistatus in case of partial failure err = h.Backend.Delete(r) diff --git a/server.go b/server.go index bf3ec10..08f3913 100644 --- a/server.go +++ b/server.go @@ -38,7 +38,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } b := backend{h.FileSystem} - hh := internal.Handler{&b} + hh := internal.Handler{Backend: &b} hh.ServeHTTP(w, r) } @@ -193,18 +193,25 @@ func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (* return nil, internal.HTTPErrorf(http.StatusForbidden, "webdav: PROPPATCH is unsupported") } -func (b *backend) Put(r *http.Request) (*internal.Href, error) { +func (b *backend) Put(w http.ResponseWriter, r *http.Request) error { wc, err := b.FileSystem.Create(r.Context(), r.URL.Path) if err != nil { - return nil, err + return err } defer wc.Close() if _, err := io.Copy(wc, r.Body); err != nil { - return nil, err + return err + } + if err := wc.Close(); err != nil { + return err } - return nil, wc.Close() + w.WriteHeader(http.StatusCreated) + // TODO: Last-Modified, ETag, Content-Type if the request has been copied + // verbatim + // TODO: http.StatusNoContent if the resource already existed + return nil } func (b *backend) Delete(r *http.Request) error {