diff --git a/carddav/carddav.go b/carddav/carddav.go index 47c366f..dec57bc 100644 --- a/carddav/carddav.go +++ b/carddav/carddav.go @@ -99,3 +99,17 @@ type AddressObject struct { ETag string Card vcard.Card } + +//SyncQuery is the query struct represents a sync-collection request +type SyncQuery struct { + DataRequest AddressDataRequest + SyncToken string + Limit int // <= 0 means unlimited +} + +//SyncResponse contains the returned sync-token for next time +type SyncResponse struct { + SyncToken string + Updated []AddressObject + Deleted []string +} diff --git a/carddav/client.go b/carddav/client.go index 53d8d97..a98dc80 100644 --- a/carddav/client.go +++ b/carddav/client.go @@ -429,3 +429,56 @@ func (c *Client) PutAddressObject(path string, card vcard.Card) (*AddressObject, } return ao, nil } + +// SyncCollection do a sync-collection operation on resource(path), it returns a SyncResponse +func (c *Client) SyncCollection(path string, query *SyncQuery) (*SyncResponse, error) { + var limit *internal.Limit + if query.Limit > 0 { + limit = &internal.Limit{NResults: uint(query.Limit)} + } + + propReq, err := encodeAddressPropReq(&query.DataRequest) + if err != nil { + return nil, err + } + + ms, err := c.ic.SyncCollection(path, query.SyncToken, internal.DepthOne, limit, propReq) + if err != nil { + return nil, err + } + + ret := &SyncResponse{SyncToken: ms.SyncToken} + for _, resp := range ms.Responses { + p, err := resp.Path() + if err != nil { + if err, ok := err.(*internal.HTTPError); ok && err.Code == http.StatusNotFound { + ret.Deleted = append(ret.Deleted, p) + continue + } + return nil, err + } + + if p == path || path == fmt.Sprintf("%s/", p) { + continue + } + + var getLastMod internal.GetLastModified + if err := resp.DecodeProp(&getLastMod); err != nil && !internal.IsNotFound(err) { + return nil, err + } + + var getETag internal.GetETag + if err := resp.DecodeProp(&getETag); err != nil && !internal.IsNotFound(err) { + return nil, err + } + + o := AddressObject{ + Path: p, + ModTime: time.Time(getLastMod.LastModified), + ETag: string(getETag.ETag), + } + ret.Updated = append(ret.Updated, o) + } + + return ret, nil +} diff --git a/internal/client.go b/internal/client.go index 030e888..5cb806c 100644 --- a/internal/client.go +++ b/internal/client.go @@ -190,3 +190,25 @@ func (c *Client) Options(path string) (classes map[string]bool, methods map[stri methods = parseCommaSeparatedSet(resp.Header["Allow"], true) return classes, methods, nil } + +// 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) { + q := SyncCollectionQuery{ + SyncToken: syncToken, + SyncLevel: string(level), + Limit: limit, + Prop: prop, + } + + req, err := c.NewXMLRequest("REPORT", path, &q) + if err != nil { + return nil, err + } + + ms, err := c.DoMultiStatus(req) + if err != nil { + return nil, err + } + + return ms, nil +} diff --git a/internal/elements.go b/internal/elements.go index 3c1932e..11d04ad 100644 --- a/internal/elements.go +++ b/internal/elements.go @@ -93,6 +93,7 @@ type Multistatus struct { XMLName xml.Name `xml:"DAV: multistatus"` Responses []Response `xml:"response"` ResponseDescription string `xml:"responsedescription,omitempty"` + SyncToken string `xml:"sync-token,omitempty"` } func NewMultistatus(resps ...Response) *Multistatus { @@ -396,3 +397,18 @@ type Set struct { XMLName xml.Name `xml:"DAV: set"` Prop Prop `xml:"prop"` } + +// https://tools.ietf.org/html/rfc6578#section-6.1 +type SyncCollectionQuery struct { + XMLName xml.Name `xml:"DAV: sync-collection"` + SyncToken string `xml:"sync-token"` + Limit *Limit `xml:"limit,omitempty"` + SyncLevel string `xml:"sync-level"` + Prop *Prop `xml:"prop"` +} + +// https://tools.ietf.org/html/rfc5323#section-5.17 +type Limit struct { + XMLName xml.Name `xml:"DAV: limit"` + NResults uint `xml:"nresults"` +}