mirror of
https://github.com/1f349/go-webdav.git
synced 2025-04-07 20:55:06 +01:00
Compare commits
50 Commits
Author | SHA1 | Date | |
---|---|---|---|
6f60a899bf | |||
d28f08a32d | |||
f5b508b766 | |||
63f15c0ec6 | |||
906087cd59 | |||
|
3cc7466ac9 | ||
|
9d778f4072 | ||
|
93fee5bcf0 | ||
|
7f8c17ad71 | ||
|
810c51fa2d | ||
|
21f251fa1d | ||
|
ff8598015d | ||
|
ffd81465fd | ||
|
948f33c2fc | ||
|
381b8a3cee | ||
|
df447dc627 | ||
|
3ed9a4f052 | ||
|
25f1014ef2 | ||
|
ad1fe1c5a8 | ||
|
0ea114ec79 | ||
|
20fad80dff | ||
|
12d8b4bf62 | ||
|
fbcd08d64a | ||
|
f1d56f2437 | ||
|
71bd967b43 | ||
|
80d77a977a | ||
|
eaac65215b | ||
|
e3ba95cd77 | ||
|
5b5b542f2f | ||
|
ced348a58f | ||
|
b821d8c1ea | ||
|
790ebfc5f8 | ||
|
4493704689 | ||
|
b043bbd965 | ||
|
75d3041b41 | ||
|
751741d87e | ||
|
7e076258d6 | ||
|
174622c1eb | ||
|
d033e09835 | ||
|
379a418130 | ||
|
0e58dbb003 | ||
|
7d337ac048 | ||
|
dddaf279ed | ||
|
fc4ea1aae2 | ||
|
571eba7c02 | ||
|
b46cbafa6f | ||
|
0fb0a675ab | ||
|
7dd64908d2 | ||
|
46dbba12fe | ||
|
150f74a6f0 |
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: "https://web.libera.chat/gamja/#emersion"
|
||||
about: "Please ask questions in #emersion on Libera Chat"
|
12
.github/ISSUE_TEMPLATE/issue_template.md
vendored
Normal file
12
.github/ISSUE_TEMPLATE/issue_template.md
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
name: Bug report or feature request
|
||||
about: Report a bug or request a new feature
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Please read the following before submitting a new issue:
|
||||
|
||||
Do NOT create GitHub issues if you have a question about go-webdav or about WebDAV in general. Ask questions on IRC in #emersion on Libera Chat.
|
||||
|
||||
-->
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@
|
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
|
||||
.glide/
|
||||
.idea/
|
||||
|
@ -1,7 +1,6 @@
|
||||
# go-webdav
|
||||
|
||||
[](https://godocs.io/github.com/emersion/go-webdav)
|
||||
[](https://builds.sr.ht/~emersion/go-webdav/commits/master?)
|
||||
[](https://pkg.go.dev/github.com/emersion/go-webdav)
|
||||
|
||||
A Go library for [WebDAV], [CalDAV] and [CardDAV].
|
||||
|
||||
|
@ -2,6 +2,7 @@ package caldav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
@ -15,6 +16,12 @@ import (
|
||||
"github.com/emersion/go-webdav/internal"
|
||||
)
|
||||
|
||||
// DiscoverContextURL performs a DNS-based CardDAV service discovery as
|
||||
// described in RFC 6352 section 11. It returns the URL to the CardDAV server.
|
||||
func DiscoverContextURL(ctx context.Context, domain string) (string, error) {
|
||||
return internal.DiscoverContextURL(ctx, "caldavs", domain)
|
||||
}
|
||||
|
||||
// Client provides access to a remote CardDAV server.
|
||||
type Client struct {
|
||||
*webdav.Client
|
||||
@ -34,9 +41,9 @@ func NewClient(c webdav.HTTPClient, endpoint string) (*Client, error) {
|
||||
return &Client{wc, ic}, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindCalendarHomeSet(principal string) (string, error) {
|
||||
func (c *Client) FindCalendarHomeSet(ctx context.Context, principal string) (string, error) {
|
||||
propfind := internal.NewPropNamePropFind(calendarHomeSetName)
|
||||
resp, err := c.ic.PropFindFlat(principal, propfind)
|
||||
resp, err := c.ic.PropFindFlat(ctx, principal, propfind)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -49,7 +56,7 @@ func (c *Client) FindCalendarHomeSet(principal string) (string, error) {
|
||||
return prop.Href.Path, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindCalendars(calendarHomeSet string) ([]Calendar, error) {
|
||||
func (c *Client) FindCalendars(ctx context.Context, calendarHomeSet string) ([]Calendar, error) {
|
||||
propfind := internal.NewPropNamePropFind(
|
||||
internal.ResourceTypeName,
|
||||
internal.DisplayNameName,
|
||||
@ -57,7 +64,7 @@ func (c *Client) FindCalendars(calendarHomeSet string) ([]Calendar, error) {
|
||||
maxResourceSizeName,
|
||||
supportedCalendarComponentSetName,
|
||||
)
|
||||
ms, err := c.ic.PropFind(calendarHomeSet, internal.DepthOne, propfind)
|
||||
ms, err := c.ic.PropFind(ctx, calendarHomeSet, internal.DepthOne, propfind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -214,7 +221,7 @@ func decodeCalendarObjectList(ms *internal.MultiStatus) ([]CalendarObject, error
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryCalendar(calendar string, query *CalendarQuery) ([]CalendarObject, error) {
|
||||
func (c *Client) QueryCalendar(ctx context.Context, calendar string, query *CalendarQuery) ([]CalendarObject, error) {
|
||||
propReq, err := encodeCalendarReq(&query.CompRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -228,7 +235,7 @@ func (c *Client) QueryCalendar(calendar string, query *CalendarQuery) ([]Calenda
|
||||
}
|
||||
req.Header.Add("Depth", "1")
|
||||
|
||||
ms, err := c.ic.DoMultiStatus(req)
|
||||
ms, err := c.ic.DoMultiStatus(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -236,7 +243,7 @@ func (c *Client) QueryCalendar(calendar string, query *CalendarQuery) ([]Calenda
|
||||
return decodeCalendarObjectList(ms)
|
||||
}
|
||||
|
||||
func (c *Client) MultiGetCalendar(path string, multiGet *CalendarMultiGet) ([]CalendarObject, error) {
|
||||
func (c *Client) MultiGetCalendar(ctx context.Context, path string, multiGet *CalendarMultiGet) ([]CalendarObject, error) {
|
||||
propReq, err := encodeCalendarReq(&multiGet.CompRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -244,7 +251,7 @@ func (c *Client) MultiGetCalendar(path string, multiGet *CalendarMultiGet) ([]Ca
|
||||
|
||||
calendarMultiget := calendarMultiget{Prop: propReq}
|
||||
|
||||
if multiGet == nil || len(multiGet.Paths) == 0 {
|
||||
if len(multiGet.Paths) == 0 {
|
||||
href := internal.Href{Path: path}
|
||||
calendarMultiget.Hrefs = []internal.Href{href}
|
||||
} else {
|
||||
@ -260,7 +267,7 @@ func (c *Client) MultiGetCalendar(path string, multiGet *CalendarMultiGet) ([]Ca
|
||||
}
|
||||
req.Header.Add("Depth", "1")
|
||||
|
||||
ms, err := c.ic.DoMultiStatus(req)
|
||||
ms, err := c.ic.DoMultiStatus(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -268,29 +275,29 @@ func (c *Client) MultiGetCalendar(path string, multiGet *CalendarMultiGet) ([]Ca
|
||||
return decodeCalendarObjectList(ms)
|
||||
}
|
||||
|
||||
func populateCalendarObject(co *CalendarObject, resp *http.Response) error {
|
||||
if loc := resp.Header.Get("Location"); loc != "" {
|
||||
func populateCalendarObject(co *CalendarObject, h http.Header) error {
|
||||
if loc := h.Get("Location"); loc != "" {
|
||||
u, err := url.Parse(loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
co.Path = u.Path
|
||||
}
|
||||
if etag := resp.Header.Get("ETag"); etag != "" {
|
||||
if etag := h.Get("ETag"); etag != "" {
|
||||
etag, err := strconv.Unquote(etag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
co.ETag = etag
|
||||
}
|
||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||
if contentLength := h.Get("Content-Length"); contentLength != "" {
|
||||
n, err := strconv.ParseInt(contentLength, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
co.ContentLength = n
|
||||
}
|
||||
if lastModified := resp.Header.Get("Last-Modified"); lastModified != "" {
|
||||
if lastModified := h.Get("Last-Modified"); lastModified != "" {
|
||||
t, err := http.ParseTime(lastModified)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -301,14 +308,14 @@ func populateCalendarObject(co *CalendarObject, resp *http.Response) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCalendarObject(path string) (*CalendarObject, error) {
|
||||
func (c *Client) GetCalendarObject(ctx context.Context, path string) (*CalendarObject, error) {
|
||||
req, err := c.ic.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", ical.MIMEType)
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -331,13 +338,13 @@ func (c *Client) GetCalendarObject(path string) (*CalendarObject, error) {
|
||||
Path: resp.Request.URL.Path,
|
||||
Data: cal,
|
||||
}
|
||||
if err := populateCalendarObject(co, resp); err != nil {
|
||||
if err := populateCalendarObject(co, resp.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return co, nil
|
||||
}
|
||||
|
||||
func (c *Client) PutCalendarObject(path string, cal *ical.Calendar) (*CalendarObject, error) {
|
||||
func (c *Client) PutCalendarObject(ctx context.Context, path string, cal *ical.Calendar) (*CalendarObject, error) {
|
||||
// TODO: add support for If-None-Match and If-Match
|
||||
|
||||
// TODO: some servers want a Content-Length header, so we can't stream the
|
||||
@ -355,14 +362,14 @@ func (c *Client) PutCalendarObject(path string, cal *ical.Calendar) (*CalendarOb
|
||||
}
|
||||
req.Header.Set("Content-Type", ical.MIMEType)
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
co := &CalendarObject{Path: path}
|
||||
if err := populateCalendarObject(co, resp); err != nil {
|
||||
if err := populateCalendarObject(co, resp.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return co, nil
|
||||
|
@ -228,3 +228,10 @@ func (r *reportReq) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
|
||||
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?
|
||||
}
|
||||
|
@ -138,7 +138,6 @@ func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error
|
||||
return len(rset.Between(start, end, true)) > 0, nil
|
||||
}
|
||||
|
||||
// TODO handle "infinity" values in query
|
||||
// TODO handle more than just events
|
||||
if comp.Name != ical.CompEvent {
|
||||
return false, nil
|
||||
@ -155,15 +154,15 @@ func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error
|
||||
}
|
||||
|
||||
// Event starts in time range
|
||||
if eventStart.After(start) && eventStart.Before(end) {
|
||||
if eventStart.After(start) && (end.IsZero() || eventStart.Before(end)) {
|
||||
return true, nil
|
||||
}
|
||||
// Event ends in time range
|
||||
if eventEnd.After(start) && eventEnd.Before(end) {
|
||||
if eventEnd.After(start) && (end.IsZero() || eventEnd.Before(end)) {
|
||||
return true, nil
|
||||
}
|
||||
// Event covers entire time range plus some
|
||||
if eventStart.Before(start) && eventEnd.After(end) {
|
||||
if eventStart.Before(start) && (!end.IsZero() && eventEnd.After(end)) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
@ -172,13 +171,11 @@ func matchCompTimeRange(start, end time.Time, comp *ical.Component) (bool, error
|
||||
func matchPropTimeRange(start, end time.Time, field *ical.Prop) (bool, error) {
|
||||
// See https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
|
||||
|
||||
// TODO handle "infinity" values in query
|
||||
|
||||
ptime, err := field.DateTime(start.Location())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ptime.After(start) && ptime.Before(end) {
|
||||
if ptime.After(start) && (end.IsZero() || ptime.Before(end)) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
|
@ -209,6 +209,23 @@ END:VCALENDAR`)
|
||||
addrs: []CalendarObject{event1, event2, event3, todo1},
|
||||
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
|
||||
name: "events by UID",
|
||||
|
238
caldav/server.go
238
caldav/server.go
@ -17,8 +17,6 @@ import (
|
||||
"github.com/emersion/go-webdav/internal"
|
||||
)
|
||||
|
||||
// TODO: add support for multiple calendars
|
||||
|
||||
// TODO if nothing more Caldav-specific needs to be added this should be merged with carddav.PutAddressObjectOptions
|
||||
type PutCalendarObjectOptions struct {
|
||||
// IfNoneMatch indicates that the client does not want to overwrite
|
||||
@ -32,11 +30,15 @@ type PutCalendarObjectOptions struct {
|
||||
// Backend is a CalDAV server backend.
|
||||
type Backend interface {
|
||||
CalendarHomeSetPath(ctx context.Context) (string, error)
|
||||
Calendar(ctx context.Context) (*Calendar, error)
|
||||
|
||||
CreateCalendar(ctx context.Context, calendar *Calendar) error
|
||||
ListCalendars(ctx context.Context) ([]Calendar, error)
|
||||
GetCalendar(ctx context.Context, path string) (*Calendar, error)
|
||||
|
||||
GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error)
|
||||
ListCalendarObjects(ctx context.Context, req *CalendarCompRequest) ([]CalendarObject, error)
|
||||
QueryCalendarObjects(ctx context.Context, query *CalendarQuery) ([]CalendarObject, error)
|
||||
PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *PutCalendarObjectOptions) (loc string, err 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) (*CalendarObject, error)
|
||||
DeleteCalendarObject(ctx context.Context, path string) error
|
||||
|
||||
webdav.UserPrincipalBackend
|
||||
@ -76,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)
|
||||
}
|
||||
|
||||
@ -214,7 +216,7 @@ func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *cal
|
||||
}
|
||||
q.CompFilter = *cf
|
||||
|
||||
cos, err := h.Backend.QueryCalendarObjects(r.Context(), &q)
|
||||
cos, err := h.Backend.QueryCalendarObjects(r.Context(), r.URL.Path, &q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -371,6 +373,12 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
||||
var resps []internal.Response
|
||||
|
||||
switch resType {
|
||||
case resourceTypeRoot:
|
||||
resp, err := b.propFindRoot(r.Context(), propfind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, *resp)
|
||||
case resourceTypeUserPrincipal:
|
||||
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||
if err != nil {
|
||||
@ -418,24 +426,21 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
||||
}
|
||||
}
|
||||
case resourceTypeCalendar:
|
||||
// TODO for multiple calendars, look through all of them
|
||||
ab, err := b.Backend.Calendar(r.Context())
|
||||
ab, err := b.Backend.GetCalendar(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.URL.Path == ab.Path {
|
||||
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
|
||||
resp, err := b.propFindCalendar(r.Context(), propfind, ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, *resp)
|
||||
if depth != internal.DepthZero {
|
||||
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, *resp)
|
||||
if depth != internal.DepthZero {
|
||||
resps_, err := b.propFindAllCalendarObjects(r.Context(), propfind, ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, resps_...)
|
||||
}
|
||||
resps = append(resps, resps_...)
|
||||
}
|
||||
case resourceTypeCalendarObject:
|
||||
ao, err := b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
|
||||
@ -453,6 +458,21 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
||||
return internal.NewMultiStatus(resps...), nil
|
||||
}
|
||||
|
||||
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
props := map[xml.Name]internal.PropFindFunc{
|
||||
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
|
||||
Href: internal.Href{Path: principalPath},
|
||||
}),
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
|
||||
}
|
||||
return internal.NewPropFindResponse(principalPath, propfind, props)
|
||||
}
|
||||
|
||||
func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
@ -464,15 +484,13 @@ func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.
|
||||
}
|
||||
|
||||
props := map[xml.Name]internal.PropFindFunc{
|
||||
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||
},
|
||||
calendarHomeSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &calendarHomeSet{Href: internal.Href{Path: homeSetPath}}, nil
|
||||
},
|
||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return internal.NewResourceType(internal.CollectionName), nil
|
||||
},
|
||||
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
|
||||
Href: internal.Href{Path: principalPath},
|
||||
}),
|
||||
calendarHomeSetName: internal.PropFindValue(&calendarHomeSet{
|
||||
Href: internal.Href{Path: homeSetPath},
|
||||
}),
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
|
||||
}
|
||||
return internal.NewPropFindResponse(principalPath, propfind, props)
|
||||
}
|
||||
@ -489,12 +507,10 @@ func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFi
|
||||
|
||||
// TODO anything else to return here?
|
||||
props := map[xml.Name]internal.PropFindFunc{
|
||||
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||
},
|
||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return internal.NewResourceType(internal.CollectionName), nil
|
||||
},
|
||||
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
|
||||
Href: internal.Href{Path: principalPath},
|
||||
}),
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
|
||||
}
|
||||
return internal.NewPropFindResponse(homeSetPath, propfind, props)
|
||||
}
|
||||
@ -508,41 +524,47 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF
|
||||
}
|
||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
||||
},
|
||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return internal.NewResourceType(internal.CollectionName, calendarName), nil
|
||||
},
|
||||
internal.DisplayNameName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.DisplayName{Name: cal.Name}, nil
|
||||
},
|
||||
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
|
||||
},
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, calendarName)),
|
||||
calendarDescriptionName: internal.PropFindValue(&calendarDescription{
|
||||
Description: cal.Description,
|
||||
}),
|
||||
supportedCalendarDataName: internal.PropFindValue(&supportedCalendarData{
|
||||
Types: []calendarDataType{
|
||||
{ContentType: ical.MIMEType, Version: "2.0"},
|
||||
},
|
||||
}),
|
||||
supportedCalendarComponentSetName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
components := []comp{}
|
||||
if cal.SupportedComponentSet != nil {
|
||||
for _, name := range cal.SupportedComponentSet {
|
||||
components = append(components, comp{Name: name})
|
||||
}
|
||||
} else {
|
||||
components = append(components, comp{Name: ical.CompEvent})
|
||||
}
|
||||
return &supportedCalendarComponentSet{
|
||||
Comp: []comp{
|
||||
{Name: ical.CompEvent},
|
||||
},
|
||||
Comp: components,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
if cal.Description != "" {
|
||||
props[calendarDescriptionName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &calendarDescription{Description: cal.Description}, nil
|
||||
}
|
||||
if cal.Name != "" {
|
||||
props[internal.DisplayNameName] = internal.PropFindValue(&internal.DisplayName{
|
||||
Name: cal.Name,
|
||||
})
|
||||
}
|
||||
if cal.Description != "" {
|
||||
props[calendarDescriptionName] = internal.PropFindValue(&calendarDescription{
|
||||
Description: cal.Description,
|
||||
})
|
||||
}
|
||||
|
||||
if cal.MaxResourceSize > 0 {
|
||||
props[maxResourceSizeName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &maxResourceSize{Size: cal.MaxResourceSize}, nil
|
||||
}
|
||||
props[maxResourceSizeName] = internal.PropFindValue(&maxResourceSize{
|
||||
Size: cal.MaxResourceSize,
|
||||
})
|
||||
}
|
||||
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
|
||||
@ -551,22 +573,20 @@ func (b *backend) propFindCalendar(ctx context.Context, propfind *internal.PropF
|
||||
}
|
||||
|
||||
func (b *backend) propFindAllCalendars(ctx context.Context, propfind *internal.PropFind, recurse bool) ([]internal.Response, error) {
|
||||
// TODO iterate over all calendars once having multiple is supported
|
||||
ab, err := b.Backend.Calendar(ctx)
|
||||
abs, err := b.Backend.ListCalendars(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
abs := []*Calendar{ab}
|
||||
|
||||
var resps []internal.Response
|
||||
for _, ab := range abs {
|
||||
resp, err := b.propFindCalendar(ctx, propfind, ab)
|
||||
resp, err := b.propFindCalendar(ctx, propfind, &ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, *resp)
|
||||
if recurse {
|
||||
resps_, err := b.propFindAllCalendarObjects(ctx, propfind, ab)
|
||||
resps_, err := b.propFindAllCalendarObjects(ctx, propfind, &ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -585,9 +605,9 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal
|
||||
}
|
||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
||||
},
|
||||
internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetContentType{Type: ical.MIMEType}, nil
|
||||
},
|
||||
internal.GetContentTypeName: internal.PropFindValue(&internal.GetContentType{
|
||||
Type: ical.MIMEType,
|
||||
}),
|
||||
// TODO: calendar-data can only be used in REPORT requests
|
||||
calendarDataName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
var buf bytes.Buffer
|
||||
@ -600,20 +620,20 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal
|
||||
}
|
||||
|
||||
if co.ContentLength > 0 {
|
||||
props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetContentLength{Length: co.ContentLength}, nil
|
||||
}
|
||||
props[internal.GetContentLengthName] = internal.PropFindValue(&internal.GetContentLength{
|
||||
Length: co.ContentLength,
|
||||
})
|
||||
}
|
||||
if !co.ModTime.IsZero() {
|
||||
props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetLastModified{LastModified: internal.Time(co.ModTime)}, nil
|
||||
}
|
||||
props[internal.GetLastModifiedName] = internal.PropFindValue(&internal.GetLastModified{
|
||||
LastModified: internal.Time(co.ModTime),
|
||||
})
|
||||
}
|
||||
|
||||
if co.ETag != "" {
|
||||
props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetETag{ETag: internal.ETag(co.ETag)}, nil
|
||||
}
|
||||
props[internal.GetETagName] = internal.PropFindValue(&internal.GetETag{
|
||||
ETag: internal.ETag(co.ETag),
|
||||
})
|
||||
}
|
||||
|
||||
return internal.NewPropFindResponse(co.Path, propfind, props)
|
||||
@ -621,7 +641,7 @@ func (b *backend) propFindCalendarObject(ctx context.Context, propfind *internal
|
||||
|
||||
func (b *backend) propFindAllCalendarObjects(ctx context.Context, propfind *internal.PropFind, cal *Calendar) ([]internal.Response, error) {
|
||||
var dataReq CalendarCompRequest
|
||||
aos, err := b.Backend.ListCalendarObjects(ctx, &dataReq)
|
||||
aos, err := b.Backend.ListCalendarObjects(ctx, cal.Path, &dataReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -638,10 +658,10 @@ func (b *backend) propFindAllCalendarObjects(ctx context.Context, propfind *inte
|
||||
}
|
||||
|
||||
func (b *backend) PropPatch(r *http.Request, update *internal.PropertyUpdate) (*internal.Response, error) {
|
||||
panic("TODO")
|
||||
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"))
|
||||
|
||||
@ -652,26 +672,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 {
|
||||
@ -679,15 +712,36 @@ func (b *backend) Delete(r *http.Request) error {
|
||||
}
|
||||
|
||||
func (b *backend) Mkcol(r *http.Request) error {
|
||||
panic("TODO")
|
||||
if b.resourceTypeAtPath(r.URL.Path) != resourceTypeCalendar {
|
||||
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) {
|
||||
panic("TODO")
|
||||
return false, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: Copy not implemented")
|
||||
}
|
||||
|
||||
func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) {
|
||||
panic("TODO")
|
||||
return false, internal.HTTPErrorf(http.StatusNotImplemented, "caldav: Move not implemented")
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.2.1
|
||||
@ -708,7 +762,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,
|
||||
|
235
caldav/server_test.go
Normal file
235
caldav/server_test.go
Normal file
@ -0,0 +1,235 @@
|
||||
package caldav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-ical"
|
||||
)
|
||||
|
||||
var propFindSupportedCalendarComponentRequest = `
|
||||
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<c:supported-calendar-component-set />
|
||||
</d:prop>
|
||||
</d:propfind>
|
||||
`
|
||||
|
||||
var testPropFindSupportedCalendarComponentCases = map[*Calendar][]string{
|
||||
&Calendar{Path: "/user/calendars/cal"}: []string{"VEVENT"},
|
||||
&Calendar{Path: "/user/calendars/cal", SupportedComponentSet: []string{"VTODO"}}: []string{"VTODO"},
|
||||
&Calendar{Path: "/user/calendars/cal", SupportedComponentSet: []string{"VEVENT", "VTODO"}}: []string{"VEVENT", "VTODO"},
|
||||
}
|
||||
|
||||
func TestPropFindSupportedCalendarComponent(t *testing.T) {
|
||||
for calendar, expected := range testPropFindSupportedCalendarComponentCases {
|
||||
req := httptest.NewRequest("PROPFIND", calendar.Path, nil)
|
||||
req.Body = io.NopCloser(strings.NewReader(propFindSupportedCalendarComponentRequest))
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
w := httptest.NewRecorder()
|
||||
handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}}
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
resp := string(data)
|
||||
for _, comp := range expected {
|
||||
// Would be nicer to do a proper XML-decoding here, but this is probably good enough for now.
|
||||
if !strings.Contains(resp, comp) {
|
||||
t.Errorf("Expected component: %v not found in response:\n%v", comp, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var propFindUserPrincipal = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<A:propfind xmlns:A="DAV:">
|
||||
<A:prop>
|
||||
<A:current-user-principal/>
|
||||
<A:principal-URL/>
|
||||
<A:resourcetype/>
|
||||
</A:prop>
|
||||
</A:propfind>
|
||||
`
|
||||
|
||||
func TestPropFindRoot(t *testing.T) {
|
||||
req := httptest.NewRequest("PROPFIND", "/", strings.NewReader(propFindUserPrincipal))
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
w := httptest.NewRecorder()
|
||||
calendar := &Calendar{}
|
||||
handler := Handler{Backend: testBackend{calendars: []Calendar{*calendar}}}
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
resp := string(data)
|
||||
if !strings.Contains(resp, `<current-user-principal xmlns="DAV:"><href>/user/</href></current-user-principal>`) {
|
||||
t.Errorf("No user-principal returned when doing a PROPFIND against root, response:\n%s", resp)
|
||||
}
|
||||
}
|
||||
|
||||
var reportCalendarData = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<B:calendar-multiget xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
|
||||
<A:prop>
|
||||
<B:calendar-data/>
|
||||
</A:prop>
|
||||
<A:href>%s</A:href>
|
||||
</B:calendar-multiget>
|
||||
`
|
||||
|
||||
func TestMultiCalendarBackend(t *testing.T) {
|
||||
calendarB := Calendar{Path: "/user/calendars/b", SupportedComponentSet: []string{"VTODO"}}
|
||||
calendars := []Calendar{
|
||||
Calendar{Path: "/user/calendars/a"},
|
||||
calendarB,
|
||||
}
|
||||
eventSummary := "This is a todo"
|
||||
event := ical.NewEvent()
|
||||
event.Name = ical.CompToDo
|
||||
event.Props.SetText(ical.PropUID, "46bbf47a-1861-41a3-ae06-8d8268c6d41e")
|
||||
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now())
|
||||
event.Props.SetText(ical.PropSummary, eventSummary)
|
||||
cal := ical.NewCalendar()
|
||||
cal.Props.SetText(ical.PropVersion, "2.0")
|
||||
cal.Props.SetText(ical.PropProductID, "-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN")
|
||||
cal.Children = []*ical.Component{
|
||||
event.Component,
|
||||
}
|
||||
object := CalendarObject{
|
||||
Path: "/user/calendars/b/test.ics",
|
||||
Data: cal,
|
||||
}
|
||||
req := httptest.NewRequest("PROPFIND", "/user/calendars/", strings.NewReader(propFindUserPrincipal))
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
w := httptest.NewRecorder()
|
||||
handler := Handler{Backend: testBackend{
|
||||
calendars: calendars,
|
||||
objectMap: map[string][]CalendarObject{
|
||||
calendarB.Path: []CalendarObject{object},
|
||||
},
|
||||
}}
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
resp := string(data)
|
||||
for _, calendar := range calendars {
|
||||
if !strings.Contains(resp, fmt.Sprintf(`<response xmlns="DAV:"><href>%s</href>`, calendar.Path)) {
|
||||
t.Errorf("Calendar: %v not returned in PROPFIND, response:\n%s", calendar, resp)
|
||||
}
|
||||
}
|
||||
|
||||
// Now do a PROPFIND for the last calendar
|
||||
req = httptest.NewRequest("PROPFIND", calendarB.Path, strings.NewReader(propFindSupportedCalendarComponentRequest))
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
w = httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
res = w.Result()
|
||||
defer res.Body.Close()
|
||||
data, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
resp = string(data)
|
||||
if !strings.Contains(resp, "VTODO") {
|
||||
t.Errorf("Expected component: VTODO not found in response:\n%v", resp)
|
||||
}
|
||||
if !strings.Contains(resp, object.Path) {
|
||||
t.Errorf("Expected calendar object: %v not found in response:\n%v", object, resp)
|
||||
}
|
||||
|
||||
// Now do a REPORT to get the actual data for the event
|
||||
req = httptest.NewRequest("REPORT", calendarB.Path, strings.NewReader(fmt.Sprintf(reportCalendarData, object.Path)))
|
||||
req.Header.Set("Content-Type", "application/xml")
|
||||
w = httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
res = w.Result()
|
||||
defer res.Body.Close()
|
||||
data, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
resp = string(data)
|
||||
if !strings.Contains(resp, fmt.Sprintf("SUMMARY:%s", eventSummary)) {
|
||||
t.Errorf("ICAL content not properly returned in response:\n%v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
type testBackend struct {
|
||||
calendars []Calendar
|
||||
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) {
|
||||
return t.calendars, nil
|
||||
}
|
||||
|
||||
func (t testBackend) GetCalendar(ctx context.Context, path string) (*Calendar, error) {
|
||||
for _, cal := range t.calendars {
|
||||
if cal.Path == path {
|
||||
return &cal, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Calendar for path: %s not found", path)
|
||||
}
|
||||
|
||||
func (t testBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
|
||||
return "/user/calendars/", nil
|
||||
}
|
||||
|
||||
func (t testBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
|
||||
return "/user/", nil
|
||||
}
|
||||
|
||||
func (t testBackend) DeleteCalendarObject(ctx context.Context, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t testBackend) GetCalendarObject(ctx context.Context, path string, req *CalendarCompRequest) (*CalendarObject, error) {
|
||||
for _, objs := range t.objectMap {
|
||||
for _, obj := range objs {
|
||||
if obj.Path == path {
|
||||
return &obj, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t testBackend) ListCalendarObjects(ctx context.Context, path string, req *CalendarCompRequest) ([]CalendarObject, error) {
|
||||
return t.objectMap[path], nil
|
||||
}
|
||||
|
||||
func (t testBackend) QueryCalendarObjects(ctx context.Context, path string, query *CalendarQuery) ([]CalendarObject, error) {
|
||||
return nil, nil
|
||||
}
|
@ -12,7 +12,9 @@ import (
|
||||
"github.com/emersion/go-webdav"
|
||||
)
|
||||
|
||||
type testBackend struct{}
|
||||
type testBackend struct {
|
||||
addressBooks []AddressBook
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
@ -37,22 +39,46 @@ func (*testBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (*testBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
|
||||
func (*testBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
|
||||
r := ctx.Value(homeSetPathKey).(string)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (*testBackend) AddressBook(ctx context.Context) (*AddressBook, error) {
|
||||
func (*testBackend) ListAddressBooks(ctx context.Context) ([]AddressBook, error) {
|
||||
p := ctx.Value(addressBookPathKey).(string)
|
||||
return &AddressBook{
|
||||
Path: p,
|
||||
Name: "My contacts",
|
||||
Description: "Default address book",
|
||||
MaxResourceSize: 1024,
|
||||
SupportedAddressData: nil,
|
||||
return []AddressBook{
|
||||
AddressBook{
|
||||
Path: p,
|
||||
Name: "My contacts",
|
||||
Description: "Default address book",
|
||||
MaxResourceSize: 1024,
|
||||
SupportedAddressData: 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) {
|
||||
if path == alicePath {
|
||||
card, err := vcard.NewDecoder(strings.NewReader(aliceData)).Decode()
|
||||
@ -68,7 +94,11 @@ func (*testBackend) GetAddressObject(ctx context.Context, path string, req *Addr
|
||||
}
|
||||
}
|
||||
|
||||
func (b *testBackend) ListAddressObjects(ctx context.Context, req *AddressDataRequest) ([]AddressObject, error) {
|
||||
func (b *testBackend) ListAddressObjects(ctx context.Context, path string, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -77,11 +107,11 @@ func (b *testBackend) ListAddressObjects(ctx context.Context, req *AddressDataRe
|
||||
return []AddressObject{*alice}, nil
|
||||
}
|
||||
|
||||
func (*testBackend) QueryAddressObjects(ctx context.Context, query *AddressBookQuery) ([]AddressObject, error) {
|
||||
func (*testBackend) QueryAddressObjects(ctx context.Context, path string, query *AddressBookQuery) ([]AddressObject, error) {
|
||||
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")
|
||||
}
|
||||
|
||||
@ -113,6 +143,7 @@ func TestAddressBookDiscovery(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
h := Handler{&testBackend{}, tc.prefix}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -135,21 +166,21 @@ func TestAddressBookDiscovery(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("error creating client: %s", err)
|
||||
}
|
||||
cup, err := client.FindCurrentUserPrincipal()
|
||||
cup, err := client.FindCurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("error finding user principal url: %s", err)
|
||||
}
|
||||
if cup != tc.currentUserPrincipal {
|
||||
t.Fatalf("Found current user principal URL '%s', expected '%s'", cup, tc.currentUserPrincipal)
|
||||
}
|
||||
hsp, err := client.FindAddressBookHomeSet(cup)
|
||||
hsp, err := client.FindAddressBookHomeSet(ctx, cup)
|
||||
if err != nil {
|
||||
t.Fatalf("error finding home set path: %s", err)
|
||||
}
|
||||
if hsp != tc.homeSetPath {
|
||||
t.Fatalf("Found home set path '%s', expected '%s'", hsp, tc.homeSetPath)
|
||||
}
|
||||
abs, err := client.FindAddressBooks(hsp)
|
||||
abs, err := client.FindAddressBooks(ctx, hsp)
|
||||
if err != nil {
|
||||
t.Fatalf("error finding address books: %s", err)
|
||||
}
|
||||
@ -162,3 +193,50 @@ 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)
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ package carddav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@ -16,38 +16,10 @@ import (
|
||||
"github.com/emersion/go-webdav/internal"
|
||||
)
|
||||
|
||||
// Discover performs a DNS-based CardDAV service discovery as described in
|
||||
// RFC 6352 section 11. It returns the URL to the CardDAV server.
|
||||
func Discover(domain string) (string, error) {
|
||||
// Only lookup carddavs (not carddav), plaintext connections are insecure
|
||||
_, addrs, err := net.LookupSRV("carddavs", "tcp", domain)
|
||||
if dnsErr, ok := err.(*net.DNSError); ok {
|
||||
if dnsErr.IsTemporary {
|
||||
return "", err
|
||||
}
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(addrs) == 0 {
|
||||
return "", fmt.Errorf("carddav: domain doesn't have an SRV record")
|
||||
}
|
||||
addr := addrs[0]
|
||||
|
||||
target := strings.TrimSuffix(addr.Target, ".")
|
||||
if target == "" {
|
||||
return "", fmt.Errorf("carddav: empty target in SRV record")
|
||||
}
|
||||
|
||||
// TODO: perform a TXT lookup, check for a "path" key in the response
|
||||
u := url.URL{Scheme: "https"}
|
||||
if addr.Port == 443 {
|
||||
u.Host = target
|
||||
} else {
|
||||
u.Host = fmt.Sprintf("%v:%v", target, addr.Port)
|
||||
}
|
||||
u.Path = "/.well-known/carddav"
|
||||
return u.String(), nil
|
||||
// DiscoverContextURL performs a DNS-based CardDAV service discovery as
|
||||
// described in RFC 6352 section 11. It returns the URL to the CardDAV server.
|
||||
func DiscoverContextURL(ctx context.Context, domain string) (string, error) {
|
||||
return internal.DiscoverContextURL(ctx, "carddavs", domain)
|
||||
}
|
||||
|
||||
// Client provides access to a remote CardDAV server.
|
||||
@ -69,8 +41,8 @@ func NewClient(c webdav.HTTPClient, endpoint string) (*Client, error) {
|
||||
return &Client{wc, ic}, nil
|
||||
}
|
||||
|
||||
func (c *Client) HasSupport() error {
|
||||
classes, _, err := c.ic.Options("")
|
||||
func (c *Client) HasSupport(ctx context.Context) error {
|
||||
classes, _, err := c.ic.Options(ctx, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -81,9 +53,9 @@ func (c *Client) HasSupport() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) FindAddressBookHomeSet(principal string) (string, error) {
|
||||
func (c *Client) FindAddressBookHomeSet(ctx context.Context, principal string) (string, error) {
|
||||
propfind := internal.NewPropNamePropFind(addressBookHomeSetName)
|
||||
resp, err := c.ic.PropFindFlat(principal, propfind)
|
||||
resp, err := c.ic.PropFindFlat(ctx, principal, propfind)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -104,7 +76,7 @@ func decodeSupportedAddressData(supported *supportedAddressData) []AddressDataTy
|
||||
return l
|
||||
}
|
||||
|
||||
func (c *Client) FindAddressBooks(addressBookHomeSet string) ([]AddressBook, error) {
|
||||
func (c *Client) FindAddressBooks(ctx context.Context, addressBookHomeSet string) ([]AddressBook, error) {
|
||||
propfind := internal.NewPropNamePropFind(
|
||||
internal.ResourceTypeName,
|
||||
internal.DisplayNameName,
|
||||
@ -112,7 +84,7 @@ func (c *Client) FindAddressBooks(addressBookHomeSet string) ([]AddressBook, err
|
||||
maxResourceSizeName,
|
||||
supportedAddressDataName,
|
||||
)
|
||||
ms, err := c.ic.PropFind(addressBookHomeSet, internal.DepthOne, propfind)
|
||||
ms, err := c.ic.PropFind(ctx, addressBookHomeSet, internal.DepthOne, propfind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -271,7 +243,7 @@ func decodeAddressList(ms *internal.MultiStatus) ([]AddressObject, error) {
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryAddressBook(addressBook string, query *AddressBookQuery) ([]AddressObject, error) {
|
||||
func (c *Client) QueryAddressBook(ctx context.Context, addressBook string, query *AddressBookQuery) ([]AddressObject, error) {
|
||||
propReq, err := encodeAddressPropReq(&query.DataRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -297,7 +269,7 @@ func (c *Client) QueryAddressBook(addressBook string, query *AddressBookQuery) (
|
||||
|
||||
req.Header.Add("Depth", "1")
|
||||
|
||||
ms, err := c.ic.DoMultiStatus(req)
|
||||
ms, err := c.ic.DoMultiStatus(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -305,7 +277,7 @@ func (c *Client) QueryAddressBook(addressBook string, query *AddressBookQuery) (
|
||||
return decodeAddressList(ms)
|
||||
}
|
||||
|
||||
func (c *Client) MultiGetAddressBook(path string, multiGet *AddressBookMultiGet) ([]AddressObject, error) {
|
||||
func (c *Client) MultiGetAddressBook(ctx context.Context, path string, multiGet *AddressBookMultiGet) ([]AddressObject, error) {
|
||||
propReq, err := encodeAddressPropReq(&multiGet.DataRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -313,7 +285,7 @@ func (c *Client) MultiGetAddressBook(path string, multiGet *AddressBookMultiGet)
|
||||
|
||||
addressbookMultiget := addressbookMultiget{Prop: propReq}
|
||||
|
||||
if multiGet == nil || len(multiGet.Paths) == 0 {
|
||||
if len(multiGet.Paths) == 0 {
|
||||
href := internal.Href{Path: path}
|
||||
addressbookMultiget.Hrefs = []internal.Href{href}
|
||||
} else {
|
||||
@ -330,7 +302,7 @@ func (c *Client) MultiGetAddressBook(path string, multiGet *AddressBookMultiGet)
|
||||
|
||||
req.Header.Add("Depth", "1")
|
||||
|
||||
ms, err := c.ic.DoMultiStatus(req)
|
||||
ms, err := c.ic.DoMultiStatus(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -338,29 +310,29 @@ func (c *Client) MultiGetAddressBook(path string, multiGet *AddressBookMultiGet)
|
||||
return decodeAddressList(ms)
|
||||
}
|
||||
|
||||
func populateAddressObject(ao *AddressObject, resp *http.Response) error {
|
||||
if loc := resp.Header.Get("Location"); loc != "" {
|
||||
func populateAddressObject(ao *AddressObject, h http.Header) error {
|
||||
if loc := h.Get("Location"); loc != "" {
|
||||
u, err := url.Parse(loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ao.Path = u.Path
|
||||
}
|
||||
if etag := resp.Header.Get("ETag"); etag != "" {
|
||||
if etag := h.Get("ETag"); etag != "" {
|
||||
etag, err := strconv.Unquote(etag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ao.ETag = etag
|
||||
}
|
||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||
if contentLength := h.Get("Content-Length"); contentLength != "" {
|
||||
n, err := strconv.ParseInt(contentLength, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ao.ContentLength = n
|
||||
}
|
||||
if lastModified := resp.Header.Get("Last-Modified"); lastModified != "" {
|
||||
if lastModified := h.Get("Last-Modified"); lastModified != "" {
|
||||
t, err := http.ParseTime(lastModified)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -371,14 +343,14 @@ func populateAddressObject(ao *AddressObject, resp *http.Response) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetAddressObject(path string) (*AddressObject, error) {
|
||||
func (c *Client) GetAddressObject(ctx context.Context, path string) (*AddressObject, error) {
|
||||
req, err := c.ic.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", vcard.MIMEType)
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -401,13 +373,13 @@ func (c *Client) GetAddressObject(path string) (*AddressObject, error) {
|
||||
Path: resp.Request.URL.Path,
|
||||
Card: card,
|
||||
}
|
||||
if err := populateAddressObject(ao, resp); err != nil {
|
||||
if err := populateAddressObject(ao, resp.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ao, nil
|
||||
}
|
||||
|
||||
func (c *Client) PutAddressObject(path string, card vcard.Card) (*AddressObject, error) {
|
||||
func (c *Client) PutAddressObject(ctx context.Context, path string, card vcard.Card) (*AddressObject, error) {
|
||||
// TODO: add support for If-None-Match and If-Match
|
||||
|
||||
// TODO: some servers want a Content-Length header, so we can't stream the
|
||||
@ -432,14 +404,14 @@ func (c *Client) PutAddressObject(path string, card vcard.Card) (*AddressObject,
|
||||
}
|
||||
req.Header.Set("Content-Type", vcard.MIMEType)
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
ao := &AddressObject{Path: path}
|
||||
if err := populateAddressObject(ao, resp); err != nil {
|
||||
if err := populateAddressObject(ao, resp.Header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ao, nil
|
||||
@ -447,7 +419,7 @@ func (c *Client) PutAddressObject(path string, card vcard.Card) (*AddressObject,
|
||||
|
||||
// SyncCollection performs a collection synchronization operation on the
|
||||
// specified resource, as defined in RFC 6578.
|
||||
func (c *Client) SyncCollection(path string, query *SyncQuery) (*SyncResponse, error) {
|
||||
func (c *Client) SyncCollection(ctx context.Context, path string, query *SyncQuery) (*SyncResponse, error) {
|
||||
var limit *internal.Limit
|
||||
if query.Limit > 0 {
|
||||
limit = &internal.Limit{NResults: uint(query.Limit)}
|
||||
@ -458,7 +430,7 @@ func (c *Client) SyncCollection(path string, query *SyncQuery) (*SyncResponse, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ms, err := c.ic.SyncCollection(path, query.SyncToken, internal.DepthOne, limit, propReq)
|
||||
ms, err := c.ic.SyncCollection(ctx, path, query.SyncToken, internal.DepthOne, limit, propReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -211,3 +211,11 @@ func (r *reportReq) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
|
||||
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?
|
||||
}
|
||||
|
@ -16,8 +16,6 @@ import (
|
||||
"github.com/emersion/go-webdav/internal"
|
||||
)
|
||||
|
||||
// TODO: add support for multiple address books
|
||||
|
||||
type PutAddressObjectOptions struct {
|
||||
// IfNoneMatch indicates that the client does not want to overwrite
|
||||
// an existing resource.
|
||||
@ -29,12 +27,15 @@ type PutAddressObjectOptions struct {
|
||||
|
||||
// Backend is a CardDAV server backend.
|
||||
type Backend interface {
|
||||
AddressbookHomeSetPath(ctx context.Context) (string, error)
|
||||
AddressBook(ctx context.Context) (*AddressBook, error)
|
||||
AddressBookHomeSetPath(ctx context.Context) (string, error)
|
||||
ListAddressBooks(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)
|
||||
ListAddressObjects(ctx context.Context, req *AddressDataRequest) ([]AddressObject, error)
|
||||
QueryAddressObjects(ctx context.Context, query *AddressBookQuery) ([]AddressObject, error)
|
||||
PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *PutAddressObjectOptions) (loc string, err 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) (*AddressObject, error)
|
||||
DeleteAddressObject(ctx context.Context, path string) error
|
||||
|
||||
webdav.UserPrincipalBackend
|
||||
@ -74,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)
|
||||
}
|
||||
|
||||
@ -90,7 +91,7 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
|
||||
if report.Query != nil {
|
||||
return h.handleQuery(r.Context(), w, report.Query)
|
||||
return h.handleQuery(r, w, report.Query)
|
||||
} else if report.Multiget != nil {
|
||||
return h.handleMultiget(r.Context(), w, report.Multiget)
|
||||
}
|
||||
@ -152,7 +153,7 @@ func decodeAddressDataReq(addressData *addressDataReq) (*AddressDataRequest, err
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (h *Handler) handleQuery(ctx context.Context, w http.ResponseWriter, query *addressbookQuery) error {
|
||||
func (h *Handler) handleQuery(r *http.Request, w http.ResponseWriter, query *addressbookQuery) error {
|
||||
var q AddressBookQuery
|
||||
if query.Prop != nil {
|
||||
var addressData addressDataReq
|
||||
@ -169,7 +170,7 @@ func (h *Handler) handleQuery(ctx context.Context, w http.ResponseWriter, query
|
||||
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)
|
||||
}
|
||||
@ -180,7 +181,7 @@ func (h *Handler) handleQuery(ctx context.Context, w http.ResponseWriter, query
|
||||
}
|
||||
}
|
||||
|
||||
aos, err := h.Backend.QueryAddressObjects(ctx, &q)
|
||||
aos, err := h.Backend.QueryAddressObjects(r.Context(), r.URL.Path, &q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -196,7 +197,7 @@ func (h *Handler) handleQuery(ctx context.Context, w http.ResponseWriter, query
|
||||
AllProp: query.AllProp,
|
||||
PropName: query.PropName,
|
||||
}
|
||||
resp, err := b.propFindAddressObject(ctx, &propfind, &ao)
|
||||
resp, err := b.propFindAddressObject(r.Context(), &propfind, &ao)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -338,6 +339,12 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
||||
var resps []internal.Response
|
||||
|
||||
switch resType {
|
||||
case resourceTypeRoot:
|
||||
resp, err := b.propFindRoot(r.Context(), propfind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, *resp)
|
||||
case resourceTypeUserPrincipal:
|
||||
principalPath, err := b.Backend.CurrentUserPrincipal(r.Context())
|
||||
if err != nil {
|
||||
@ -365,7 +372,7 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
||||
}
|
||||
}
|
||||
case resourceTypeAddressBookHomeSet:
|
||||
homeSetPath, err := b.Backend.AddressbookHomeSetPath(r.Context())
|
||||
homeSetPath, err := b.Backend.AddressBookHomeSetPath(r.Context())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -385,24 +392,21 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
||||
}
|
||||
}
|
||||
case resourceTypeAddressBook:
|
||||
// TODO for multiple address books, look through all of them
|
||||
ab, err := b.Backend.AddressBook(r.Context())
|
||||
ab, err := b.Backend.GetAddressBook(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.URL.Path == ab.Path {
|
||||
resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
|
||||
resp, err := b.propFindAddressBook(r.Context(), propfind, ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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, *resp)
|
||||
if depth != internal.DepthZero {
|
||||
resps_, err := b.propFindAllAddressObjects(r.Context(), propfind, ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, resps_...)
|
||||
}
|
||||
resps = append(resps, resps_...)
|
||||
}
|
||||
case resourceTypeAddressObject:
|
||||
ao, err := b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
|
||||
@ -420,36 +424,45 @@ func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth i
|
||||
return internal.NewMultiStatus(resps...), nil
|
||||
}
|
||||
|
||||
func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||
func (b *backend) propFindRoot(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
homeSetPath, err := b.Backend.AddressbookHomeSetPath(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
props := map[xml.Name]internal.PropFindFunc{
|
||||
internal.CurrentUserPrincipalName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: principalPath}}, nil
|
||||
},
|
||||
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
|
||||
Href: internal.Href{Path: principalPath},
|
||||
}),
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
|
||||
}
|
||||
return internal.NewPropFindResponse(principalPath, propfind, props)
|
||||
}
|
||||
|
||||
func (b *backend) propFindUserPrincipal(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
props := map[xml.Name]internal.PropFindFunc{
|
||||
internal.CurrentUserPrincipalName: internal.PropFindValue(&internal.CurrentUserPrincipal{
|
||||
Href: internal.Href{Path: principalPath},
|
||||
}),
|
||||
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
|
||||
},
|
||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return internal.NewResourceType(internal.CollectionName), nil
|
||||
},
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
|
||||
}
|
||||
return internal.NewPropFindResponse(principalPath, propfind, props)
|
||||
}
|
||||
|
||||
func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFind) (*internal.Response, error) {
|
||||
principalPath, err := b.Backend.CurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
homeSetPath, err := b.Backend.AddressbookHomeSetPath(ctx)
|
||||
homeSetPath, err := b.Backend.AddressBookHomeSetPath(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -457,11 +470,13 @@ func (b *backend) propFindHomeSet(ctx context.Context, propfind *internal.PropFi
|
||||
// TODO anything else to return here?
|
||||
props := map[xml.Name]internal.PropFindFunc{
|
||||
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
|
||||
},
|
||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return internal.NewResourceType(internal.CollectionName), nil
|
||||
},
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName)),
|
||||
}
|
||||
return internal.NewPropFindResponse(homeSetPath, propfind, props)
|
||||
}
|
||||
@ -475,51 +490,52 @@ func (b *backend) propFindAddressBook(ctx context.Context, propfind *internal.Pr
|
||||
}
|
||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
||||
},
|
||||
internal.ResourceTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return internal.NewResourceType(internal.CollectionName, addressBookName), nil
|
||||
},
|
||||
internal.DisplayNameName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
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
|
||||
},
|
||||
internal.ResourceTypeName: internal.PropFindValue(internal.NewResourceType(internal.CollectionName, addressBookName)),
|
||||
supportedAddressDataName: internal.PropFindValue(&supportedAddressData{
|
||||
Types: []addressDataType{
|
||||
{ContentType: vcard.MIMEType, Version: "3.0"},
|
||||
{ContentType: vcard.MIMEType, Version: "4.0"},
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
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 {
|
||||
props[maxResourceSizeName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &maxResourceSize{Size: ab.MaxResourceSize}, nil
|
||||
}
|
||||
props[maxResourceSizeName] = internal.PropFindValue(&maxResourceSize{
|
||||
Size: ab.MaxResourceSize,
|
||||
})
|
||||
}
|
||||
props[internal.CurrentUserPrivilegeSetName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.CurrentUserPrivilegeSet{Privilege: internal.NewAllPrivileges()}, nil
|
||||
}
|
||||
|
||||
return internal.NewPropFindResponse(ab.Path, propfind, props)
|
||||
}
|
||||
|
||||
func (b *backend) propFindAllAddressBooks(ctx context.Context, propfind *internal.PropFind, recurse bool) ([]internal.Response, error) {
|
||||
// TODO iterate over all address books once having multiple is supported
|
||||
ab, err := b.Backend.AddressBook(ctx)
|
||||
abs, err := b.Backend.ListAddressBooks(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
abs := []*AddressBook{ab}
|
||||
|
||||
var resps []internal.Response
|
||||
for _, ab := range abs {
|
||||
resp, err := b.propFindAddressBook(ctx, propfind, ab)
|
||||
resp, err := b.propFindAddressBook(ctx, propfind, &ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resps = append(resps, *resp)
|
||||
if recurse {
|
||||
resps_, err := b.propFindAllAddressObjects(ctx, propfind, ab)
|
||||
resps_, err := b.propFindAllAddressObjects(ctx, propfind, &ab)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -538,9 +554,9 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal.
|
||||
}
|
||||
return &internal.CurrentUserPrincipal{Href: internal.Href{Path: path}}, nil
|
||||
},
|
||||
internal.GetContentTypeName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetContentType{Type: vcard.MIMEType}, nil
|
||||
},
|
||||
internal.GetContentTypeName: internal.PropFindValue(&internal.GetContentType{
|
||||
Type: vcard.MIMEType,
|
||||
}),
|
||||
// TODO: address-data can only be used in REPORT requests
|
||||
addressDataName: func(*internal.RawXMLValue) (interface{}, error) {
|
||||
var buf bytes.Buffer
|
||||
@ -553,20 +569,20 @@ func (b *backend) propFindAddressObject(ctx context.Context, propfind *internal.
|
||||
}
|
||||
|
||||
if ao.ContentLength > 0 {
|
||||
props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetContentLength{Length: ao.ContentLength}, nil
|
||||
}
|
||||
props[internal.GetContentLengthName] = internal.PropFindValue(&internal.GetContentLength{
|
||||
Length: ao.ContentLength,
|
||||
})
|
||||
}
|
||||
if !ao.ModTime.IsZero() {
|
||||
props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetLastModified{LastModified: internal.Time(ao.ModTime)}, nil
|
||||
}
|
||||
props[internal.GetLastModifiedName] = internal.PropFindValue(&internal.GetLastModified{
|
||||
LastModified: internal.Time(ao.ModTime),
|
||||
})
|
||||
}
|
||||
|
||||
if ao.ETag != "" {
|
||||
props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetETag{ETag: internal.ETag(ao.ETag)}, nil
|
||||
}
|
||||
props[internal.GetETagName] = internal.PropFindValue(&internal.GetETag{
|
||||
ETag: internal.ETag(ao.ETag),
|
||||
})
|
||||
}
|
||||
|
||||
return internal.NewPropFindResponse(ao.Path, propfind, props)
|
||||
@ -574,7 +590,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) {
|
||||
var dataReq AddressDataRequest
|
||||
aos, err := b.Backend.ListAddressObjects(ctx, &dataReq)
|
||||
aos, err := b.Backend.ListAddressObjects(ctx, ab.Path, &dataReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -591,7 +607,7 @@ func (b *backend) propFindAllAddressObjects(ctx context.Context, propfind *inter
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -630,7 +646,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"))
|
||||
|
||||
@ -641,46 +657,86 @@ 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 {
|
||||
return b.Backend.DeleteAddressObject(r.Context(), r.URL.Path)
|
||||
switch b.resourceTypeAtPath(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 {
|
||||
return internal.HTTPErrorf(http.StatusForbidden, "carddav: address book creation unsupported")
|
||||
if b.resourceTypeAtPath(r.URL.Path) != resourceTypeAddressBook {
|
||||
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) {
|
||||
panic("TODO")
|
||||
return false, internal.HTTPErrorf(http.StatusNotImplemented, "carddav: Copy not implemented")
|
||||
}
|
||||
|
||||
func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) {
|
||||
panic("TODO")
|
||||
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 (
|
||||
@ -691,7 +747,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,
|
||||
|
75
client.go
75
client.go
@ -1,6 +1,7 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -39,6 +40,11 @@ type Client struct {
|
||||
ic *internal.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new WebDAV client.
|
||||
//
|
||||
// If the HTTPClient is nil, http.DefaultClient is used.
|
||||
//
|
||||
// To use HTTP basic authentication, HTTPClientWithBasicAuth can be used.
|
||||
func NewClient(c HTTPClient, endpoint string) (*Client, error) {
|
||||
ic, err := internal.NewClient(c, endpoint)
|
||||
if err != nil {
|
||||
@ -47,12 +53,13 @@ func NewClient(c HTTPClient, endpoint string) (*Client, error) {
|
||||
return &Client{ic}, nil
|
||||
}
|
||||
|
||||
func (c *Client) FindCurrentUserPrincipal() (string, error) {
|
||||
// FindCurrentUserPrincipal finds the current user's principal path.
|
||||
func (c *Client) FindCurrentUserPrincipal(ctx context.Context) (string, error) {
|
||||
propfind := internal.NewPropNamePropFind(internal.CurrentUserPrincipalName)
|
||||
|
||||
// TODO: consider retrying on the root URI "/" if this fails, as suggested
|
||||
// by the RFC?
|
||||
resp, err := c.ic.PropFindFlat("", propfind)
|
||||
resp, err := c.ic.PropFindFlat(ctx, "", propfind)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -121,21 +128,23 @@ func fileInfoFromResponse(resp *internal.Response) (*FileInfo, error) {
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
func (c *Client) Stat(name string) (*FileInfo, error) {
|
||||
resp, err := c.ic.PropFindFlat(name, fileInfoPropFind)
|
||||
// Stat fetches a FileInfo for a single file.
|
||||
func (c *Client) Stat(ctx context.Context, name string) (*FileInfo, error) {
|
||||
resp, err := c.ic.PropFindFlat(ctx, name, fileInfoPropFind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileInfoFromResponse(resp)
|
||||
}
|
||||
|
||||
func (c *Client) Open(name string) (io.ReadCloser, error) {
|
||||
// Open fetches a file's contents.
|
||||
func (c *Client) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||
req, err := c.ic.NewRequest(http.MethodGet, name, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -143,13 +152,14 @@ func (c *Client) Open(name string) (io.ReadCloser, error) {
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func (c *Client) Readdir(name string, recursive bool) ([]FileInfo, error) {
|
||||
// ReadDir lists files in a directory.
|
||||
func (c *Client) ReadDir(ctx context.Context, name string, recursive bool) ([]FileInfo, error) {
|
||||
depth := internal.DepthOne
|
||||
if recursive {
|
||||
depth = internal.DepthInfinity
|
||||
}
|
||||
|
||||
ms, err := c.ic.PropFind(name, depth, fileInfoPropFind)
|
||||
ms, err := c.ic.PropFind(ctx, name, depth, fileInfoPropFind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -182,7 +192,8 @@ func (fw *fileWriter) Close() error {
|
||||
return <-fw.done
|
||||
}
|
||||
|
||||
func (c *Client) Create(name string) (io.WriteCloser, error) {
|
||||
// Create writes a file's contents.
|
||||
func (c *Client) Create(ctx context.Context, name string) (io.WriteCloser, error) {
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
req, err := c.ic.NewRequest(http.MethodPut, name, pr)
|
||||
@ -193,7 +204,7 @@ func (c *Client) Create(name string) (io.WriteCloser, error) {
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
@ -205,13 +216,15 @@ func (c *Client) Create(name string) (io.WriteCloser, error) {
|
||||
return &fileWriter{pw, done}, nil
|
||||
}
|
||||
|
||||
func (c *Client) RemoveAll(name string) error {
|
||||
// RemoveAll deletes a file. If the file is a directory, all of its descendants
|
||||
// are recursively deleted as well.
|
||||
func (c *Client) RemoveAll(ctx context.Context, name string) error {
|
||||
req, err := c.ic.NewRequest(http.MethodDelete, name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -219,13 +232,14 @@ func (c *Client) RemoveAll(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Mkdir(name string) error {
|
||||
// Mkdir creates a new directory.
|
||||
func (c *Client) Mkdir(ctx context.Context, name string) error {
|
||||
req, err := c.ic.NewRequest("MKCOL", name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -233,16 +247,30 @@ func (c *Client) Mkdir(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) CopyAll(name, dest string, overwrite bool) error {
|
||||
// Copy copies a file.
|
||||
//
|
||||
// By default, if the file is a directory, all descendants are recursively
|
||||
// copied as well.
|
||||
func (c *Client) Copy(ctx context.Context, name, dest string, options *CopyOptions) error {
|
||||
if options == nil {
|
||||
options = new(CopyOptions)
|
||||
}
|
||||
|
||||
req, err := c.ic.NewRequest("COPY", name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Destination", c.ic.ResolveHref(dest).String())
|
||||
req.Header.Set("Overwrite", internal.FormatOverwrite(overwrite))
|
||||
depth := internal.DepthInfinity
|
||||
if options.NoRecursive {
|
||||
depth = internal.DepthZero
|
||||
}
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
req.Header.Set("Destination", c.ic.ResolveHref(dest).String())
|
||||
req.Header.Set("Overwrite", internal.FormatOverwrite(!options.NoOverwrite))
|
||||
req.Header.Set("Depth", depth.String())
|
||||
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -250,16 +278,21 @@ func (c *Client) CopyAll(name, dest string, overwrite bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) MoveAll(name, dest string, overwrite bool) error {
|
||||
// Move moves a file.
|
||||
func (c *Client) Move(ctx context.Context, name, dest string, options *MoveOptions) error {
|
||||
if options == nil {
|
||||
options = new(MoveOptions)
|
||||
}
|
||||
|
||||
req, err := c.ic.NewRequest("MOVE", name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Destination", c.ic.ResolveHref(dest).String())
|
||||
req.Header.Set("Overwrite", internal.FormatOverwrite(overwrite))
|
||||
req.Header.Set("Overwrite", internal.FormatOverwrite(!options.NoOverwrite))
|
||||
|
||||
resp, err := c.ic.Do(req)
|
||||
resp, err := c.ic.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
21
elements.go
21
elements.go
@ -30,24 +30,3 @@ type groupMembership struct {
|
||||
XMLName xml.Name `xml:"DAV: group-membership"`
|
||||
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
|
||||
}
|
||||
|
125
fs_local.go
125
fs_local.go
@ -1,6 +1,7 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
@ -13,8 +14,11 @@ import (
|
||||
"github.com/emersion/go-webdav/internal"
|
||||
)
|
||||
|
||||
// LocalFileSystem implements FileSystem for a local directory.
|
||||
type LocalFileSystem string
|
||||
|
||||
var _ FileSystem = LocalFileSystem("")
|
||||
|
||||
func (fs LocalFileSystem) localPath(name string) (string, error) {
|
||||
if (filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0) || strings.Contains(name, "\x00") {
|
||||
return "", internal.HTTPErrorf(http.StatusBadRequest, "webdav: invalid character in path")
|
||||
@ -34,7 +38,7 @@ func (fs LocalFileSystem) externalPath(name string) (string, error) {
|
||||
return "/" + filepath.ToSlash(rel), nil
|
||||
}
|
||||
|
||||
func (fs LocalFileSystem) Open(name string) (io.ReadCloser, error) {
|
||||
func (fs LocalFileSystem) Open(ctx context.Context, name string) (io.ReadCloser, error) {
|
||||
p, err := fs.localPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -59,19 +63,31 @@ func fileInfoFromOS(p string, fi os.FileInfo) *FileInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func (fs LocalFileSystem) Stat(name string) (*FileInfo, error) {
|
||||
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) {
|
||||
p, err := fs.localPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errFromOS(err)
|
||||
}
|
||||
return fileInfoFromOS(name, fi), nil
|
||||
}
|
||||
|
||||
func (fs LocalFileSystem) Readdir(name string, recursive bool) ([]FileInfo, error) {
|
||||
func (fs LocalFileSystem) ReadDir(ctx context.Context, name string, recursive bool) ([]FileInfo, error) {
|
||||
path, err := fs.localPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -95,18 +111,60 @@ func (fs LocalFileSystem) Readdir(name string, recursive bool) ([]FileInfo, erro
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return l, err
|
||||
return l, errFromOS(err)
|
||||
}
|
||||
|
||||
func (fs LocalFileSystem) Create(name string) (io.WriteCloser, error) {
|
||||
func (fs LocalFileSystem) Create(ctx context.Context, name string, body io.ReadCloser, opts *CreateOptions) (fi *FileInfo, created bool, err error) {
|
||||
p, err := fs.localPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, false, err
|
||||
}
|
||||
return os.Create(p)
|
||||
fi, _ = fs.Stat(ctx, name)
|
||||
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(name string) error {
|
||||
func (fs LocalFileSystem) RemoveAll(ctx context.Context, name string) error {
|
||||
p, err := fs.localPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -115,31 +173,32 @@ func (fs LocalFileSystem) RemoveAll(name string) error {
|
||||
// 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.
|
||||
if _, err = os.Stat(p); err != nil {
|
||||
return err
|
||||
return errFromOS(err)
|
||||
}
|
||||
|
||||
return os.RemoveAll(p)
|
||||
return errFromOS(os.RemoveAll(p))
|
||||
}
|
||||
|
||||
func (fs LocalFileSystem) Mkdir(name string) error {
|
||||
func (fs LocalFileSystem) Mkdir(ctx context.Context, name string) error {
|
||||
p, err := fs.localPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Mkdir(p, 0755)
|
||||
return errFromOS(os.Mkdir(p, 0755))
|
||||
}
|
||||
|
||||
func copyRegularFile(src, dst string, perm os.FileMode) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
return errFromOS(err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm)
|
||||
if err != nil {
|
||||
// TODO: send http.StatusConflict on os.IsNotExist
|
||||
return err
|
||||
if os.IsNotExist(err) {
|
||||
return NewHTTPError(http.StatusConflict, err)
|
||||
} else if err != nil {
|
||||
return errFromOS(err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
@ -150,7 +209,7 @@ func copyRegularFile(src, dst string, perm os.FileMode) error {
|
||||
return dstFile.Close()
|
||||
}
|
||||
|
||||
func (fs LocalFileSystem) Copy(src, dst string, recursive, overwrite bool) (created bool, err error) {
|
||||
func (fs LocalFileSystem) Copy(ctx context.Context, src, dst string, options *CopyOptions) (created bool, err error) {
|
||||
srcPath, err := fs.localPath(src)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -165,21 +224,21 @@ func (fs LocalFileSystem) Copy(src, dst string, recursive, overwrite bool) (crea
|
||||
|
||||
srcInfo, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, errFromOS(err)
|
||||
}
|
||||
srcPerm := srcInfo.Mode() & os.ModePerm
|
||||
|
||||
if _, err := os.Stat(dstPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, err
|
||||
return false, errFromOS(err)
|
||||
}
|
||||
created = true
|
||||
} else {
|
||||
if !overwrite {
|
||||
return false, os.ErrExist
|
||||
if options.NoOverwrite {
|
||||
return false, NewHTTPError(http.StatusPreconditionFailed, os.ErrExist)
|
||||
}
|
||||
if err := os.RemoveAll(dstPath); err != nil {
|
||||
return false, err
|
||||
return false, errFromOS(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -190,7 +249,7 @@ func (fs LocalFileSystem) Copy(src, dst string, recursive, overwrite bool) (crea
|
||||
|
||||
if fi.IsDir() {
|
||||
if err := os.Mkdir(dstPath, srcPerm); err != nil {
|
||||
return err
|
||||
return errFromOS(err)
|
||||
}
|
||||
} else {
|
||||
if err := copyRegularFile(srcPath, dstPath, srcPerm); err != nil {
|
||||
@ -198,19 +257,19 @@ func (fs LocalFileSystem) Copy(src, dst string, recursive, overwrite bool) (crea
|
||||
}
|
||||
}
|
||||
|
||||
if fi.IsDir() && !recursive {
|
||||
if fi.IsDir() && options.NoRecursive {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, errFromOS(err)
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (fs LocalFileSystem) MoveAll(src, dst string, overwrite bool) (created bool, err error) {
|
||||
func (fs LocalFileSystem) Move(ctx context.Context, src, dst string, options *MoveOptions) (created bool, err error) {
|
||||
srcPath, err := fs.localPath(src)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -222,23 +281,21 @@ func (fs LocalFileSystem) MoveAll(src, dst string, overwrite bool) (created bool
|
||||
|
||||
if _, err := os.Stat(dstPath); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, err
|
||||
return false, errFromOS(err)
|
||||
}
|
||||
created = true
|
||||
} else {
|
||||
if !overwrite {
|
||||
return false, os.ErrExist
|
||||
if options.NoOverwrite {
|
||||
return false, NewHTTPError(http.StatusPreconditionFailed, os.ErrExist)
|
||||
}
|
||||
if err := os.RemoveAll(dstPath); err != nil {
|
||||
return false, err
|
||||
return false, errFromOS(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(srcPath, dstPath); err != nil {
|
||||
return false, err
|
||||
return false, errFromOS(err)
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
var _ FileSystem = LocalFileSystem("")
|
||||
|
4
go.mod
4
go.mod
@ -3,6 +3,6 @@ module github.com/emersion/go-webdav
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f
|
||||
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7
|
||||
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6
|
||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
|
||||
)
|
||||
|
12
go.sum
12
go.sum
@ -1,6 +1,6 @@
|
||||
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f h1:feGUUxxvOtWVOhTko8Cbmp33a+tU0IMZxMEmnkoAISQ=
|
||||
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
|
||||
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7 h1:SE+tcd+0kn0cT4MqTo66gmkjqWHF1Z+Yha5/rhLs/H8=
|
||||
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||
github.com/teambition/rrule-go v1.7.2 h1:goEajFWYydfCgavn2m/3w5U+1b3PGqPUHx/fFSVfTy0=
|
||||
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM=
|
||||
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
|
||||
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/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
|
||||
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
||||
|
@ -2,10 +2,12 @@ package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@ -13,6 +15,42 @@ import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// DiscoverContextURL performs a DNS-based CardDAV/CalDAV service discovery as
|
||||
// described in RFC 6352 section 11. It returns the URL to the CardDAV server.
|
||||
func DiscoverContextURL(ctx context.Context, service, domain string) (string, error) {
|
||||
var resolver net.Resolver
|
||||
|
||||
// Only lookup TLS records, plaintext connections are insecure
|
||||
_, addrs, err := resolver.LookupSRV(ctx, service+"s", "tcp", domain)
|
||||
if dnsErr, ok := err.(*net.DNSError); ok {
|
||||
if dnsErr.IsTemporary {
|
||||
return "", err
|
||||
}
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(addrs) == 0 {
|
||||
return "", fmt.Errorf("webdav: domain doesn't have an SRV record")
|
||||
}
|
||||
addr := addrs[0]
|
||||
|
||||
target := strings.TrimSuffix(addr.Target, ".")
|
||||
if target == "" {
|
||||
return "", fmt.Errorf("webdav: empty target in SRV record")
|
||||
}
|
||||
|
||||
// TODO: perform a TXT lookup, check for a "path" key in the response
|
||||
u := url.URL{Scheme: "https"}
|
||||
if addr.Port == 443 {
|
||||
u.Host = target
|
||||
} else {
|
||||
u.Host = fmt.Sprintf("%v:%v", target, addr.Port)
|
||||
}
|
||||
u.Path = "/.well-known/" + service
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// HTTPClient performs HTTP requests. It's implemented by *http.Client.
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
@ -131,7 +169,7 @@ func (c *Client) DoMultiStatus(req *http.Request) (*MultiStatus, error) {
|
||||
return &ms, nil
|
||||
}
|
||||
|
||||
func (c *Client) PropFind(path string, depth Depth, propfind *PropFind) (*MultiStatus, error) {
|
||||
func (c *Client) PropFind(ctx context.Context, path string, depth Depth, propfind *PropFind) (*MultiStatus, error) {
|
||||
req, err := c.NewXMLRequest("PROPFIND", path, propfind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -139,12 +177,12 @@ func (c *Client) PropFind(path string, depth Depth, propfind *PropFind) (*MultiS
|
||||
|
||||
req.Header.Add("Depth", depth.String())
|
||||
|
||||
return c.DoMultiStatus(req)
|
||||
return c.DoMultiStatus(req.WithContext(ctx))
|
||||
}
|
||||
|
||||
// PropfindFlat performs a PROPFIND request with a zero depth.
|
||||
func (c *Client) PropFindFlat(path string, propfind *PropFind) (*Response, error) {
|
||||
ms, err := c.PropFind(path, DepthZero, propfind)
|
||||
func (c *Client) PropFindFlat(ctx context.Context, path string, propfind *PropFind) (*Response, error) {
|
||||
ms, err := c.PropFind(ctx, path, DepthZero, propfind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -174,13 +212,13 @@ func parseCommaSeparatedSet(values []string, upper bool) map[string]bool {
|
||||
return m
|
||||
}
|
||||
|
||||
func (c *Client) Options(path string) (classes map[string]bool, methods map[string]bool, err error) {
|
||||
func (c *Client) Options(ctx context.Context, path string) (classes map[string]bool, methods map[string]bool, err error) {
|
||||
req, err := c.NewRequest(http.MethodOptions, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := c.Do(req)
|
||||
resp, err := c.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@ -196,7 +234,7 @@ func (c *Client) Options(path string) (classes map[string]bool, methods map[stri
|
||||
}
|
||||
|
||||
// SyncCollection perform a `sync-collection` REPORT operation on a resource
|
||||
func (c *Client) SyncCollection(path, syncToken string, level Depth, limit *Limit, prop *Prop) (*MultiStatus, error) {
|
||||
func (c *Client) SyncCollection(ctx context.Context, path, syncToken string, level Depth, limit *Limit, prop *Prop) (*MultiStatus, error) {
|
||||
q := SyncCollectionQuery{
|
||||
SyncToken: syncToken,
|
||||
SyncLevel: level.String(),
|
||||
@ -209,7 +247,7 @@ func (c *Client) SyncCollection(path, syncToken string, level Depth, limit *Limi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ms, err := c.DoMultiStatus(req)
|
||||
ms, err := c.DoMultiStatus(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
@ -22,6 +23,8 @@ var (
|
||||
GetETagName = xml.Name{Namespace, "getetag"}
|
||||
|
||||
CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"}
|
||||
|
||||
CurrentUserPrivilegeSetName = xml.Name{Namespace, "current-user-privilege-set"}
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
@ -143,7 +146,10 @@ func (resp *Response) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error = resp.Error
|
||||
var err error
|
||||
if resp.Error != nil {
|
||||
err = resp.Error
|
||||
}
|
||||
if resp.ResponseDescription != "" {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%v (%w)", resp.ResponseDescription, err)
|
||||
@ -348,7 +354,7 @@ type Time time.Time
|
||||
func (t *Time) UnmarshalText(b []byte) error {
|
||||
tt, err := http.ParseTime(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.New(err.Error() + " : time_data : " + base64.StdEncoding.EncodeToString(b))
|
||||
}
|
||||
*t = Time(tt)
|
||||
return nil
|
||||
@ -414,6 +420,30 @@ type CurrentUserPrincipal struct {
|
||||
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
|
||||
type PropertyUpdate struct {
|
||||
XMLName xml.Name `xml:"DAV: propertyupdate"`
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -27,9 +28,13 @@ func ServeError(w http.ResponseWriter, err error) {
|
||||
http.Error(w, err.Error(), code)
|
||||
}
|
||||
|
||||
func isContentXML(h http.Header) bool {
|
||||
t, _, _ := mime.ParseMediaType(h.Get("Content-Type"))
|
||||
return t == "application/xml" || t == "text/xml"
|
||||
}
|
||||
|
||||
func DecodeXMLRequest(r *http.Request, v interface{}) error {
|
||||
t, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if t != "application/xml" && t != "text/xml" {
|
||||
if !isContentXML(r.Header) {
|
||||
return HTTPErrorf(http.StatusBadRequest, "webdav: expected application/xml request")
|
||||
}
|
||||
|
||||
@ -39,8 +44,13 @@ func DecodeXMLRequest(r *http.Request, v interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsRequestBodyEmpty(r *http.Request) bool {
|
||||
_, err := r.Body.Read(nil)
|
||||
return err == io.EOF
|
||||
}
|
||||
|
||||
func ServeXML(w http.ResponseWriter) *xml.Encoder {
|
||||
w.Header().Add("Content-Type", "text/xml; charset=\"utf-8\"")
|
||||
w.Header().Add("Content-Type", "application/xml; charset=\"utf-8\"")
|
||||
w.Write([]byte(xml.Header))
|
||||
return xml.NewEncoder(w)
|
||||
}
|
||||
@ -56,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)
|
||||
@ -78,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)
|
||||
@ -125,14 +125,22 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
w.Header().Add("DAV", strings.Join(caps, ", "))
|
||||
w.Header().Add("Allow", strings.Join(allow, ", "))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) error {
|
||||
var propfind PropFind
|
||||
if err := DecodeXMLRequest(r, &propfind); err != nil {
|
||||
return err
|
||||
if isContentXML(r.Header) {
|
||||
if err := DecodeXMLRequest(r, &propfind); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var b [1]byte
|
||||
if _, err := r.Body.Read(b[:]); err != io.EOF {
|
||||
return HTTPErrorf(http.StatusBadRequest, "webdav: unsupported request body")
|
||||
}
|
||||
propfind.AllProp = &struct{}{}
|
||||
}
|
||||
|
||||
depth := DepthInfinity
|
||||
@ -154,13 +162,17 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) 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) {
|
||||
resp := NewOKResponse(path)
|
||||
resp := &Response{Hrefs: []Href{Href{Path: path}}}
|
||||
|
||||
if _, ok := props[ResourceTypeName]; !ok {
|
||||
props[ResourceTypeName] = func(*RawXMLValue) (interface{}, error) {
|
||||
return NewResourceType(), nil
|
||||
}
|
||||
props[ResourceTypeName] = PropFindValue(NewResourceType())
|
||||
}
|
||||
|
||||
if propfind.PropName != nil {
|
||||
@ -179,9 +191,8 @@ func NewPropFindResponse(path string, propfind *PropFind, props map[xml.Name]Pro
|
||||
|
||||
code := http.StatusOK
|
||||
if err != nil {
|
||||
// TODO: don't throw away error message here
|
||||
code = HTTPErrorFromError(err).Code
|
||||
val = emptyVal
|
||||
val = NewRawXMLElement(xmlName, []xml.Attr{{Name: xml.Name{Space: "ERR", Local: "Error"}, Value: err.Error()}}, nil)
|
||||
}
|
||||
|
||||
if err := resp.EncodeProp(code, val); err != nil {
|
||||
@ -202,8 +213,8 @@ func NewPropFindResponse(path string, propfind *PropFind, props map[xml.Name]Pro
|
||||
f, ok := props[xmlName]
|
||||
if ok {
|
||||
if v, err := f(&raw); err != nil {
|
||||
// TODO: don't throw away error message here
|
||||
code = HTTPErrorFromError(err).Code
|
||||
val = NewRawXMLElement(xmlName, []xml.Attr{{Name: xml.Name{Space: "ERR", Local: "Error"}, Value: err.Error()}}, nil)
|
||||
} else {
|
||||
code = http.StatusOK
|
||||
val = v
|
||||
|
127
server.go
127
server.go
@ -14,14 +14,14 @@ import (
|
||||
|
||||
// FileSystem is a WebDAV server backend.
|
||||
type FileSystem interface {
|
||||
Open(name string) (io.ReadCloser, error)
|
||||
Stat(name string) (*FileInfo, error)
|
||||
Readdir(name string, recursive bool) ([]FileInfo, error)
|
||||
Create(name string) (io.WriteCloser, error)
|
||||
RemoveAll(name string) error
|
||||
Mkdir(name string) error
|
||||
Copy(name, dest string, recursive, overwrite bool) (created bool, err error)
|
||||
MoveAll(name, dest string, overwrite bool) (created bool, err error)
|
||||
Open(ctx context.Context, name string) (io.ReadCloser, error)
|
||||
Stat(ctx context.Context, name string) (*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)
|
||||
RemoveAll(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)
|
||||
Move(ctx context.Context, name, dest string, options *MoveOptions) (created bool, err error)
|
||||
}
|
||||
|
||||
// Handler handles WebDAV HTTP requests. It can be used to create a WebDAV
|
||||
@ -38,14 +38,14 @@ 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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).
|
||||
func NewHTTPError(statusCode int, cause error) error {
|
||||
return &internal.HTTPError{Code: statusCode, Err: cause}
|
||||
@ -56,8 +56,8 @@ type backend struct {
|
||||
}
|
||||
|
||||
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
|
||||
fi, err := b.FileSystem.Stat(r.URL.Path)
|
||||
if os.IsNotExist(err) {
|
||||
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
||||
if internal.IsNotFound(err) {
|
||||
return nil, []string{http.MethodOptions, http.MethodPut, "MKCOL"}, nil
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
@ -79,17 +79,15 @@ func (b *backend) Options(r *http.Request) (caps []string, allow []string, err e
|
||||
}
|
||||
|
||||
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
||||
fi, err := b.FileSystem.Stat(r.URL.Path)
|
||||
if os.IsNotExist(err) {
|
||||
return &internal.HTTPError{Code: http.StatusNotFound, Err: err}
|
||||
} else if err != nil {
|
||||
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir {
|
||||
return &internal.HTTPError{Code: http.StatusMethodNotAllowed}
|
||||
}
|
||||
|
||||
f, err := b.FileSystem.Open(r.URL.Path)
|
||||
f, err := b.FileSystem.Open(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -120,16 +118,14 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
|
||||
func (b *backend) PropFind(r *http.Request, propfind *internal.PropFind, depth internal.Depth) (*internal.MultiStatus, error) {
|
||||
// TODO: use partial error Response on error
|
||||
|
||||
fi, err := b.FileSystem.Stat(r.URL.Path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, &internal.HTTPError{Code: http.StatusNotFound, Err: err}
|
||||
} else if err != nil {
|
||||
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resps []internal.Response
|
||||
if depth != internal.DepthZero && fi.IsDir {
|
||||
children, err := b.FileSystem.Readdir(r.URL.Path, depth == internal.DepthInfinity)
|
||||
children, err := b.FileSystem.ReadDir(r.Context(), r.URL.Path, depth == internal.DepthInfinity)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -166,26 +162,26 @@ func (b *backend) propFindFile(propfind *internal.PropFind, fi *FileInfo) (*inte
|
||||
}
|
||||
|
||||
if !fi.IsDir {
|
||||
props[internal.GetContentLengthName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetContentLength{Length: fi.Size}, nil
|
||||
}
|
||||
props[internal.GetContentLengthName] = internal.PropFindValue(&internal.GetContentLength{
|
||||
Length: fi.Size,
|
||||
})
|
||||
|
||||
if !fi.ModTime.IsZero() {
|
||||
props[internal.GetLastModifiedName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetLastModified{LastModified: internal.Time(fi.ModTime)}, nil
|
||||
}
|
||||
props[internal.GetLastModifiedName] = internal.PropFindValue(&internal.GetLastModified{
|
||||
LastModified: internal.Time(fi.ModTime),
|
||||
})
|
||||
}
|
||||
|
||||
if fi.MIMEType != "" {
|
||||
props[internal.GetContentTypeName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetContentType{Type: fi.MIMEType}, nil
|
||||
}
|
||||
props[internal.GetContentTypeName] = internal.PropFindValue(&internal.GetContentType{
|
||||
Type: fi.MIMEType,
|
||||
})
|
||||
}
|
||||
|
||||
if fi.ETag != "" {
|
||||
props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) {
|
||||
return &internal.GetETag{ETag: internal.ETag(fi.ETag)}, nil
|
||||
}
|
||||
props[internal.GetETagName] = internal.PropFindValue(&internal.GetETag{
|
||||
ETag: internal.ETag(fi.ETag),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,41 +193,59 @@ 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) {
|
||||
wc, err := b.FileSystem.Create(r.URL.Path)
|
||||
func (b *backend) Put(w http.ResponseWriter, r *http.Request) error {
|
||||
ifNoneMatch := ConditionalMatch(r.Header.Get("If-None-Match"))
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
defer wc.Close()
|
||||
|
||||
if _, err := io.Copy(wc, r.Body); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
return nil, wc.Close()
|
||||
if fi.MIMEType != "" {
|
||||
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 {
|
||||
err := b.FileSystem.RemoveAll(r.URL.Path)
|
||||
if os.IsNotExist(err) {
|
||||
return &internal.HTTPError{Code: http.StatusNotFound, Err: err}
|
||||
}
|
||||
return err
|
||||
return b.FileSystem.RemoveAll(r.Context(), r.URL.Path)
|
||||
}
|
||||
|
||||
func (b *backend) Mkcol(r *http.Request) error {
|
||||
if r.Header.Get("Content-Type") != "" {
|
||||
return internal.HTTPErrorf(http.StatusUnsupportedMediaType, "webdav: request body not supported in MKCOL request")
|
||||
}
|
||||
err := b.FileSystem.Mkdir(r.URL.Path)
|
||||
if os.IsNotExist(err) {
|
||||
err := b.FileSystem.Mkdir(r.Context(), r.URL.Path)
|
||||
if internal.IsNotFound(err) {
|
||||
return &internal.HTTPError{Code: http.StatusConflict, Err: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrite bool) (created bool, err error) {
|
||||
created, err = b.FileSystem.Copy(r.URL.Path, dest.Path, recursive, overwrite)
|
||||
options := CopyOptions{
|
||||
NoRecursive: !recursive,
|
||||
NoOverwrite: !overwrite,
|
||||
}
|
||||
created, err = b.FileSystem.Copy(r.Context(), r.URL.Path, dest.Path, &options)
|
||||
if os.IsExist(err) {
|
||||
return false, &internal.HTTPError{http.StatusPreconditionFailed, err}
|
||||
}
|
||||
@ -239,7 +253,10 @@ func (b *backend) Copy(r *http.Request, dest *internal.Href, recursive, overwrit
|
||||
}
|
||||
|
||||
func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (created bool, err error) {
|
||||
created, err = b.FileSystem.MoveAll(r.URL.Path, dest.Path, overwrite)
|
||||
options := MoveOptions{
|
||||
NoOverwrite: !overwrite,
|
||||
}
|
||||
created, err = b.FileSystem.Move(r.Context(), r.URL.Path, dest.Path, &options)
|
||||
if os.IsExist(err) {
|
||||
return false, &internal.HTTPError{http.StatusPreconditionFailed, err}
|
||||
}
|
||||
@ -248,7 +265,7 @@ func (b *backend) Move(r *http.Request, dest *internal.Href, overwrite bool) (cr
|
||||
|
||||
// BackendSuppliedHomeSet represents either a CalDAV calendar-home-set or a
|
||||
// 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
|
||||
// (external) backend.
|
||||
type BackendSuppliedHomeSet interface {
|
||||
@ -261,8 +278,10 @@ type UserPrincipalBackend interface {
|
||||
CurrentUserPrincipal(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// Capability indicates the features that a server supports.
|
||||
type Capability string
|
||||
|
||||
// ServePrincipalOptions holds options for ServePrincipal.
|
||||
type ServePrincipalOptions struct {
|
||||
CurrentUserPrincipalPath string
|
||||
HomeSets []BackendSuppliedHomeSet
|
||||
@ -280,7 +299,7 @@ func ServePrincipal(w http.ResponseWriter, r *http.Request, options *ServePrinci
|
||||
allow := []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}
|
||||
w.Header().Add("DAV", strings.Join(caps, ", "))
|
||||
w.Header().Add("Allow", strings.Join(allow, ", "))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case "PROPFIND":
|
||||
if err := servePrincipalPropfind(w, r, options); err != nil {
|
||||
internal.ServeError(w, err)
|
||||
|
46
webdav.go
46
webdav.go
@ -5,8 +5,11 @@ package webdav
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-webdav/internal"
|
||||
)
|
||||
|
||||
// FileInfo holds information about a WebDAV file.
|
||||
type FileInfo struct {
|
||||
Path string
|
||||
Size int64
|
||||
@ -15,3 +18,46 @@ type FileInfo struct {
|
||||
MIMEType string
|
||||
ETag string
|
||||
}
|
||||
|
||||
type CreateOptions struct {
|
||||
IfMatch ConditionalMatch
|
||||
IfNoneMatch ConditionalMatch
|
||||
}
|
||||
|
||||
type CopyOptions struct {
|
||||
NoRecursive bool
|
||||
NoOverwrite bool
|
||||
}
|
||||
|
||||
type MoveOptions struct {
|
||||
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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user