Better caldav support (#73)
This commit is contained in:
363
vendor/github.com/samedi/caldav-go/data/filters.go
generated
vendored
Normal file
363
vendor/github.com/samedi/caldav-go/data/filters.go
generated
vendored
Normal file
@ -0,0 +1,363 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/beevik/etree"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
const (
|
||||
TAG_FILTER = "filter"
|
||||
TAG_COMP_FILTER = "comp-filter"
|
||||
TAG_PROP_FILTER = "prop-filter"
|
||||
TAG_PARAM_FILTER = "param-filter"
|
||||
TAG_TIME_RANGE = "time-range"
|
||||
TAG_TEXT_MATCH = "text-match"
|
||||
TAG_IS_NOT_DEFINED = "is-not-defined"
|
||||
|
||||
// From the RFC, the time range `start` and `end` attributes MUST be in UTC and in this specific format
|
||||
FILTER_TIME_FORMAT = "20060102T150405Z"
|
||||
)
|
||||
|
||||
// ResourceFilter represents filters to filter out resources.
|
||||
// Filters are basically a set of rules used to retrieve a range of resources.
|
||||
// It is used primarily on REPORT requests and is described in details in RFC4791#7.8.
|
||||
type ResourceFilter struct {
|
||||
name string
|
||||
text string
|
||||
attrs map[string]string
|
||||
children []ResourceFilter // collection of child filters.
|
||||
etreeElem *etree.Element // holds the parsed XML node/tag as an `etree` element.
|
||||
}
|
||||
|
||||
// ParseResourceFilters initializes a new `ResourceFilter` object from a snippet of XML string.
|
||||
func ParseResourceFilters(xml string) (*ResourceFilter, error) {
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromString(xml); err != nil {
|
||||
log.Printf("ERROR: Could not parse filter from XML string. XML:\n%s", xml)
|
||||
return new(ResourceFilter), err
|
||||
}
|
||||
|
||||
// Right now we're searching for a <filter> tag to initialize the filter struct from it.
|
||||
// It SHOULD be a valid XML CALDAV:filter tag (RFC4791#9.7). We're not checking namespaces yet.
|
||||
// TODO: check for XML namespaces and restrict it to accept only CALDAV:filter tag.
|
||||
elem := doc.FindElement("//" + TAG_FILTER)
|
||||
if elem == nil {
|
||||
log.Printf("WARNING: The filter XML should contain a <%s> element. XML:\n%s", TAG_FILTER, xml)
|
||||
return new(ResourceFilter), errors.New("invalid XML filter")
|
||||
}
|
||||
|
||||
filter := newFilterFromEtreeElem(elem)
|
||||
return &filter, nil
|
||||
}
|
||||
|
||||
func newFilterFromEtreeElem(elem *etree.Element) ResourceFilter {
|
||||
// init filter from etree element
|
||||
filter := ResourceFilter{
|
||||
name: elem.Tag,
|
||||
text: strings.TrimSpace(elem.Text()),
|
||||
etreeElem: elem,
|
||||
attrs: make(map[string]string),
|
||||
}
|
||||
|
||||
// set attributes
|
||||
for _, attr := range elem.Attr {
|
||||
filter.attrs[attr.Key] = attr.Value
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
// Attr searches an attribute by its name in the list of filter attributes and returns it.
|
||||
func (f *ResourceFilter) Attr(attrName string) string {
|
||||
return f.attrs[attrName]
|
||||
}
|
||||
|
||||
// TimeAttr searches and returns a filter attribute as a `time.Time` object.
|
||||
func (f *ResourceFilter) TimeAttr(attrName string) *time.Time {
|
||||
|
||||
t, err := time.Parse(FILTER_TIME_FORMAT, f.attrs[attrName])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &t
|
||||
}
|
||||
|
||||
// GetTimeRangeFilter checks if the current filter has a child "time-range" filter and
|
||||
// returns it (wrapped in a `ResourceFilter` type). It returns nil if the current filter does
|
||||
// not contain any "time-range" filter.
|
||||
func (f *ResourceFilter) GetTimeRangeFilter() *ResourceFilter {
|
||||
return f.findChild(TAG_TIME_RANGE, true)
|
||||
}
|
||||
|
||||
// Match returns whether a provided resource matches the filters.
|
||||
func (f *ResourceFilter) Match(target ResourceInterface) bool {
|
||||
if f.name == TAG_FILTER {
|
||||
return f.rootFilterMatch(target)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) rootFilterMatch(target ResourceInterface) bool {
|
||||
if f.isEmpty() {
|
||||
return false
|
||||
}
|
||||
|
||||
return f.rootChildrenMatch(target)
|
||||
}
|
||||
|
||||
// checks if all the root's child filters match the target resource
|
||||
func (f *ResourceFilter) rootChildrenMatch(target ResourceInterface) bool {
|
||||
scope := []string{}
|
||||
|
||||
for _, child := range f.getChildren() {
|
||||
// root filters only accept comp filters as children
|
||||
if child.name != TAG_COMP_FILTER || !child.compMatch(target, scope) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.1.
|
||||
func (f *ResourceFilter) compMatch(target ResourceInterface, scope []string) bool {
|
||||
targetComp := target.ComponentName()
|
||||
compName := f.attrs["name"]
|
||||
|
||||
if f.isEmpty() {
|
||||
// Point #1 of RFC4791#9.7.1
|
||||
return compName == targetComp
|
||||
} else if f.contains(TAG_IS_NOT_DEFINED) {
|
||||
// Point #2 of RFC4791#9.7.1
|
||||
return compName != targetComp
|
||||
} else {
|
||||
// check each child of the current filter if they all match.
|
||||
childrenScope := append(scope, compName)
|
||||
return f.compChildrenMatch(target, childrenScope)
|
||||
}
|
||||
}
|
||||
|
||||
// checks if all the comp's child filters match the target resource
|
||||
func (f *ResourceFilter) compChildrenMatch(target ResourceInterface, scope []string) bool {
|
||||
for _, child := range f.getChildren() {
|
||||
var match bool
|
||||
|
||||
switch child.name {
|
||||
case TAG_TIME_RANGE:
|
||||
// Point #3 of RFC4791#9.7.1
|
||||
match = child.timeRangeMatch(target)
|
||||
case TAG_PROP_FILTER:
|
||||
// Point #4 of RFC4791#9.7.1
|
||||
match = child.propMatch(target, scope)
|
||||
case TAG_COMP_FILTER:
|
||||
// Point #4 of RFC4791#9.7.1
|
||||
match = child.compMatch(target, scope)
|
||||
}
|
||||
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.9
|
||||
func (f *ResourceFilter) timeRangeMatch(target ResourceInterface) bool {
|
||||
startAttr := f.attrs["start"]
|
||||
endAttr := f.attrs["end"]
|
||||
|
||||
// at least one of the two MUST be present
|
||||
if startAttr == "" && endAttr == "" {
|
||||
// if both of them are missing, return false
|
||||
return false
|
||||
} else if startAttr == "" {
|
||||
// if missing only the `start`, set it open ended to the left
|
||||
startAttr = "00010101T000000Z"
|
||||
} else if endAttr == "" {
|
||||
// if missing only the `end`, set it open ended to the right
|
||||
endAttr = "99991231T235959Z"
|
||||
}
|
||||
|
||||
// The logic below is only applicable for VEVENT components. So
|
||||
// we return false if the resource is not a VEVENT component.
|
||||
if target.ComponentName() != lib.VEVENT {
|
||||
return false
|
||||
}
|
||||
|
||||
rangeStart, err := time.Parse(FILTER_TIME_FORMAT, startAttr)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not parse start time in time-range filter.\nError: %s.\nStart attr: %s", err, startAttr)
|
||||
return false
|
||||
}
|
||||
|
||||
rangeEnd, err := time.Parse(FILTER_TIME_FORMAT, endAttr)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not parse end time in time-range filter.\nError: %s.\nEnd attr: %s", err, endAttr)
|
||||
return false
|
||||
}
|
||||
|
||||
// the following logic is inferred from the rules table for VEVENT components,
|
||||
// described in RFC4791-9.9.
|
||||
overlapRange := func(dtStart, dtEnd, rangeStart, rangeEnd time.Time) bool {
|
||||
if dtStart.Equal(dtEnd) {
|
||||
// Lines 3 and 4 of the table deal when the DTSTART and DTEND dates are equals.
|
||||
// In this case we use the rule: (start <= DTSTART && end > DTSTART)
|
||||
return (rangeStart.Before(dtStart) || rangeStart.Equal(dtStart)) && rangeEnd.After(dtStart)
|
||||
} else {
|
||||
// Lines 1, 2 and 6 of the table deal when the DTSTART and DTEND dates are different.
|
||||
// In this case we use the rule: (start < DTEND && end > DTSTART)
|
||||
return rangeStart.Before(dtEnd) && rangeEnd.After(dtStart)
|
||||
}
|
||||
}
|
||||
|
||||
// first we check each of the target recurrences (if any).
|
||||
for _, recurrence := range target.Recurrences() {
|
||||
// if any of them overlap the filter range, we return true right away
|
||||
if overlapRange(recurrence.StartTime, recurrence.EndTime, rangeStart, rangeEnd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// if none of the recurrences match, we just return if the actual
|
||||
// resource's `start` and `end` times match the filter range
|
||||
return overlapRange(target.StartTimeUTC(), target.EndTimeUTC(), rangeStart, rangeEnd)
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.2.
|
||||
func (f *ResourceFilter) propMatch(target ResourceInterface, scope []string) bool {
|
||||
propName := f.attrs["name"]
|
||||
propPath := append(scope, propName)
|
||||
|
||||
if f.isEmpty() {
|
||||
// Point #1 of RFC4791#9.7.2
|
||||
return target.HasProperty(propPath...)
|
||||
} else if f.contains(TAG_IS_NOT_DEFINED) {
|
||||
// Point #2 of RFC4791#9.7.2
|
||||
return !target.HasProperty(propPath...)
|
||||
} else {
|
||||
// check each child of the current filter if they all match.
|
||||
return f.propChildrenMatch(target, propPath)
|
||||
}
|
||||
}
|
||||
|
||||
// checks if all the prop's child filters match the target resource
|
||||
func (f *ResourceFilter) propChildrenMatch(target ResourceInterface, propPath []string) bool {
|
||||
for _, child := range f.getChildren() {
|
||||
var match bool
|
||||
|
||||
switch child.name {
|
||||
case TAG_TIME_RANGE:
|
||||
// Point #3 of RFC4791#9.7.2
|
||||
// TODO: this point is not very clear on how to match time range against properties.
|
||||
// So we're returning `false` in the meantime.
|
||||
match = false
|
||||
case TAG_TEXT_MATCH:
|
||||
// Point #4 of RFC4791#9.7.2
|
||||
propText := target.GetPropertyValue(propPath...)
|
||||
match = child.textMatch(propText)
|
||||
case TAG_PARAM_FILTER:
|
||||
// Point #4 of RFC4791#9.7.2
|
||||
match = child.paramMatch(target, propPath)
|
||||
}
|
||||
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.3
|
||||
func (f *ResourceFilter) paramMatch(target ResourceInterface, parentPropPath []string) bool {
|
||||
paramName := f.attrs["name"]
|
||||
paramPath := append(parentPropPath, paramName)
|
||||
|
||||
if f.isEmpty() {
|
||||
// Point #1 of RFC4791#9.7.3
|
||||
return target.HasPropertyParam(paramPath...)
|
||||
} else if f.contains(TAG_IS_NOT_DEFINED) {
|
||||
// Point #2 of RFC4791#9.7.3
|
||||
return !target.HasPropertyParam(paramPath...)
|
||||
} else {
|
||||
child := f.getChildren()[0]
|
||||
// param filters can also have (only-one) nested text-match filter
|
||||
if child.name == TAG_TEXT_MATCH {
|
||||
paramValue := target.GetPropertyParamValue(paramPath...)
|
||||
return child.textMatch(paramValue)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.5
|
||||
func (f *ResourceFilter) textMatch(targetText string) bool {
|
||||
// TODO: collations are not being considered/supported yet.
|
||||
// Texts are lowered to be case-insensitive, almost as the "i;ascii-casemap" value.
|
||||
|
||||
targetText = strings.ToLower(targetText)
|
||||
expectedSubstr := strings.ToLower(f.text)
|
||||
|
||||
match := strings.Contains(targetText, expectedSubstr)
|
||||
|
||||
if f.attrs["negate-condition"] == "yes" {
|
||||
return !match
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) isEmpty() bool {
|
||||
return len(f.getChildren()) == 0 && f.text == ""
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) contains(filterName string) bool {
|
||||
if f.findChild(filterName, false) != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) findChild(filterName string, dig bool) *ResourceFilter {
|
||||
for _, child := range f.getChildren() {
|
||||
if child.name == filterName {
|
||||
return &child
|
||||
}
|
||||
|
||||
if !dig {
|
||||
continue
|
||||
}
|
||||
|
||||
dugChild := child.findChild(filterName, true)
|
||||
|
||||
if dugChild != nil {
|
||||
return dugChild
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// lazy evaluation of the child filters
|
||||
func (f *ResourceFilter) getChildren() []ResourceFilter {
|
||||
if f.children == nil {
|
||||
f.children = []ResourceFilter{}
|
||||
|
||||
for _, childElem := range f.etreeElem.ChildElements() {
|
||||
childFilter := newFilterFromEtreeElem(childElem)
|
||||
f.children = append(f.children, childFilter)
|
||||
}
|
||||
}
|
||||
|
||||
return f.children
|
||||
}
|
370
vendor/github.com/samedi/caldav-go/data/resource.go
generated
vendored
Normal file
370
vendor/github.com/samedi/caldav-go/data/resource.go
generated
vendored
Normal file
@ -0,0 +1,370 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/laurent22/ical-go/ical"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samedi/caldav-go/files"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
// ResourceInterface defines the main interface of a CalDAV resource object. This
|
||||
// interface exists only to define the common resource operation and should not be custom-implemented.
|
||||
// The default and canonical implementation is provided by `data.Resource`, convering all the commonalities.
|
||||
// Any specifics in implementations should be handled by the `data.ResourceAdapter`.
|
||||
type ResourceInterface interface {
|
||||
ComponentName() string
|
||||
StartTimeUTC() time.Time
|
||||
EndTimeUTC() time.Time
|
||||
Recurrences() []ResourceRecurrence
|
||||
HasProperty(propPath ...string) bool
|
||||
GetPropertyValue(propPath ...string) string
|
||||
HasPropertyParam(paramName ...string) bool
|
||||
GetPropertyParamValue(paramName ...string) string
|
||||
}
|
||||
|
||||
// ResourceAdapter serves as the object to abstract all the specicities in different resources implementations.
|
||||
// For example, the way to tell whether a resource is a collection or how to read its content differentiates
|
||||
// on resources stored in the file system, coming from a relational DB or from the cloud as JSON. These differentiations
|
||||
// should be covered by providing a specific implementation of the `ResourceAdapter` interface. So, depending on the current
|
||||
// resource storage strategy, a matching resource adapter implementation should be provided whenever a new resource is initialized.
|
||||
type ResourceAdapter interface {
|
||||
IsCollection() bool
|
||||
CalculateEtag() string
|
||||
GetContent() string
|
||||
GetContentSize() int64
|
||||
GetModTime() time.Time
|
||||
}
|
||||
|
||||
// ResourceRecurrence represents a recurrence for a resource.
|
||||
// NOTE: recurrences are not supported yet.
|
||||
type ResourceRecurrence struct {
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
}
|
||||
|
||||
// Resource represents the CalDAV resource. Basically, it has a name it's accessible based on path.
|
||||
// A resource can be a collection, meaning it doesn't have any data content, but it has child resources.
|
||||
// A non-collection is the actual resource which has the data in iCal format and which will feed the calendar.
|
||||
// When visualizing the whole resources set in a tree representation, the collection resource would be the inner nodes and
|
||||
// the non-collection would be the leaves.
|
||||
type Resource struct {
|
||||
Name string
|
||||
Path string
|
||||
|
||||
pathSplit []string
|
||||
adapter ResourceAdapter
|
||||
|
||||
emptyTime time.Time
|
||||
}
|
||||
|
||||
// NewResource initializes a new `Resource` instance based on its path and the `ResourceAdapter` implementation to be used.
|
||||
func NewResource(rawPath string, adp ResourceAdapter) Resource {
|
||||
pClean := lib.ToSlashPath(rawPath)
|
||||
pSplit := strings.Split(strings.Trim(pClean, "/"), "/")
|
||||
|
||||
return Resource{
|
||||
Name: pSplit[len(pSplit)-1],
|
||||
Path: pClean,
|
||||
pathSplit: pSplit,
|
||||
adapter: adp,
|
||||
}
|
||||
}
|
||||
|
||||
// IsCollection tells whether a resource is a collection or not.
|
||||
func (r *Resource) IsCollection() bool {
|
||||
return r.adapter.IsCollection()
|
||||
}
|
||||
|
||||
// IsPrincipal tells whether a resource is the principal resource or not.
|
||||
// A principal resource means it's a root resource.
|
||||
func (r *Resource) IsPrincipal() bool {
|
||||
return len(r.pathSplit) <= 1
|
||||
}
|
||||
|
||||
// ComponentName returns the type of the resource. VCALENDAR for collection resources, VEVENT otherwise.
|
||||
func (r *Resource) ComponentName() string {
|
||||
if r.IsCollection() {
|
||||
return lib.VCALENDAR
|
||||
}
|
||||
|
||||
return lib.VEVENT
|
||||
}
|
||||
|
||||
// StartTimeUTC returns the start time in UTC of a VEVENT resource.
|
||||
func (r *Resource) StartTimeUTC() time.Time {
|
||||
vevent := r.icalVEVENT()
|
||||
dtstart := vevent.PropDate(ical.DTSTART, r.emptyTime)
|
||||
|
||||
if dtstart == r.emptyTime {
|
||||
log.Printf("WARNING: The property DTSTART was not found in the resource's ical data.\nResource path: %s", r.Path)
|
||||
return r.emptyTime
|
||||
}
|
||||
|
||||
return dtstart.UTC()
|
||||
}
|
||||
|
||||
// EndTimeUTC returns the end time in UTC of a VEVENT resource.
|
||||
func (r *Resource) EndTimeUTC() time.Time {
|
||||
vevent := r.icalVEVENT()
|
||||
dtend := vevent.PropDate(ical.DTEND, r.emptyTime)
|
||||
|
||||
// when the DTEND property is not present, we just add the DURATION (if any) to the DTSTART
|
||||
if dtend == r.emptyTime {
|
||||
duration := vevent.PropDuration(ical.DURATION)
|
||||
dtend = r.StartTimeUTC().Add(duration)
|
||||
}
|
||||
|
||||
return dtend.UTC()
|
||||
}
|
||||
|
||||
// Recurrences returns an array of resource recurrences.
|
||||
// NOTE: Recurrences are not supported yet. An empty array will always be returned.
|
||||
func (r *Resource) Recurrences() []ResourceRecurrence {
|
||||
// TODO: Implement. This server does not support iCal recurrences yet. We just return an empty array.
|
||||
return []ResourceRecurrence{}
|
||||
}
|
||||
|
||||
// HasProperty tells whether the resource has the provided property in its iCal content.
|
||||
// The path to the property should be provided in case of nested properties.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// DTSTART:20160914T170000
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// HasProperty("VEVENT", "DTSTART") => returns true
|
||||
// HasProperty("VEVENT", "DTEND") => returns false
|
||||
func (r *Resource) HasProperty(propPath ...string) bool {
|
||||
return r.GetPropertyValue(propPath...) != ""
|
||||
}
|
||||
|
||||
// GetPropertyValue gets a property value from the resource's iCal content.
|
||||
// The path to the property should be provided in case of nested properties.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// DTSTART:20160914T170000
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// GetPropertyValue("VEVENT", "DTSTART") => returns "20160914T170000"
|
||||
// GetPropertyValue("VEVENT", "DTEND") => returns ""
|
||||
func (r *Resource) GetPropertyValue(propPath ...string) string {
|
||||
if propPath[0] == ical.VCALENDAR {
|
||||
propPath = propPath[1:]
|
||||
}
|
||||
|
||||
prop, _ := r.icalendar().DigProperty(propPath...)
|
||||
return prop
|
||||
}
|
||||
|
||||
// HasPropertyParam tells whether the resource has the provided property param in its iCal content.
|
||||
// The path to the param should be provided in case of nested params.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// ATTENDEE;PARTSTAT=NEEDS-ACTION:FOO
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// HasPropertyParam("VEVENT", "ATTENDEE", "PARTSTAT") => returns true
|
||||
// HasPropertyParam("VEVENT", "ATTENDEE", "OTHER") => returns false
|
||||
func (r *Resource) HasPropertyParam(paramPath ...string) bool {
|
||||
return r.GetPropertyParamValue(paramPath...) != ""
|
||||
}
|
||||
|
||||
// GetPropertyParamValue gets a property param value from the resource's iCal content.
|
||||
// The path to the param should be provided in case of nested params.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// ATTENDEE;PARTSTAT=NEEDS-ACTION:FOO
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// GetPropertyParamValue("VEVENT", "ATTENDEE", "PARTSTAT") => returns "NEEDS-ACTION"
|
||||
// GetPropertyParamValue("VEVENT", "ATTENDEE", "OTHER") => returns ""
|
||||
func (r *Resource) GetPropertyParamValue(paramPath ...string) string {
|
||||
if paramPath[0] == ical.VCALENDAR {
|
||||
paramPath = paramPath[1:]
|
||||
}
|
||||
|
||||
param, _ := r.icalendar().DigParameter(paramPath...)
|
||||
return param
|
||||
}
|
||||
|
||||
// GetEtag returns the ETag of the resource and a flag saying if the ETag is present.
|
||||
// For collection resource, it returns an empty string and false.
|
||||
func (r *Resource) GetEtag() (string, bool) {
|
||||
if r.IsCollection() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return r.adapter.CalculateEtag(), true
|
||||
}
|
||||
|
||||
// GetContentType returns the type of the content of the resource.
|
||||
// Collection resources are "text/calendar". Non-collection resources are "text/calendar; component=vcalendar".
|
||||
func (r *Resource) GetContentType() (string, bool) {
|
||||
if r.IsCollection() {
|
||||
return "text/calendar", true
|
||||
}
|
||||
|
||||
return "text/calendar; component=vcalendar", true
|
||||
}
|
||||
|
||||
// GetDisplayName returns the name/identifier of the resource.
|
||||
func (r *Resource) GetDisplayName() (string, bool) {
|
||||
return r.Name, true
|
||||
}
|
||||
|
||||
// GetContentData reads and returns the raw content of the resource as string and flag saying if the content was found.
|
||||
// If the resource does not have content (like collection resource), it returns an empty string and false.
|
||||
func (r *Resource) GetContentData() (string, bool) {
|
||||
data := r.adapter.GetContent()
|
||||
found := data != ""
|
||||
|
||||
return data, found
|
||||
}
|
||||
|
||||
// GetContentLength returns the length of the resource's content and flag saying if the length is present.
|
||||
// If the resource does not have content (like collection resource), it returns an empty string and false.
|
||||
func (r *Resource) GetContentLength() (string, bool) {
|
||||
// If its collection, it does not have any content, so mark it as not found
|
||||
if r.IsCollection() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
contentSize := r.adapter.GetContentSize()
|
||||
return strconv.FormatInt(contentSize, 10), true
|
||||
}
|
||||
|
||||
// GetLastModified returns the last time the resource was modified. The returned time
|
||||
// is returned formatted in the provided `format`.
|
||||
func (r *Resource) GetLastModified(format string) (string, bool) {
|
||||
return r.adapter.GetModTime().Format(format), true
|
||||
}
|
||||
|
||||
// GetOwner returns the owner of the resource. This is usually the principal resource associated (the root resource).
|
||||
// If the resource does not have a owner (for example it's a principal resource alread), it returns an empty string.
|
||||
func (r *Resource) GetOwner() (string, bool) {
|
||||
var owner string
|
||||
if len(r.pathSplit) > 1 {
|
||||
owner = r.pathSplit[0]
|
||||
} else {
|
||||
owner = ""
|
||||
}
|
||||
|
||||
return owner, true
|
||||
}
|
||||
|
||||
// GetOwnerPath returns the path to this resource's owner, or an empty string when the resource does not have any owner.
|
||||
func (r *Resource) GetOwnerPath() (string, bool) {
|
||||
owner, _ := r.GetOwner()
|
||||
|
||||
if owner != "" {
|
||||
return fmt.Sprintf("/%s/", owner), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// TODO: memoize
|
||||
func (r *Resource) icalVEVENT() *ical.Node {
|
||||
vevent := r.icalendar().ChildByName(ical.VEVENT)
|
||||
|
||||
// if nil, log it and return an empty vevent
|
||||
if vevent == nil {
|
||||
log.Printf("WARNING: The resource's ical data is missing the VEVENT property.\nResource path: %s", r.Path)
|
||||
|
||||
return &ical.Node{
|
||||
Name: ical.VEVENT,
|
||||
}
|
||||
}
|
||||
|
||||
return vevent
|
||||
}
|
||||
|
||||
// TODO: memoize
|
||||
func (r *Resource) icalendar() *ical.Node {
|
||||
data, found := r.GetContentData()
|
||||
|
||||
if !found {
|
||||
log.Printf("WARNING: The resource's ical data does not have any data.\nResource path: %s", r.Path)
|
||||
return &ical.Node{
|
||||
Name: ical.VCALENDAR,
|
||||
}
|
||||
}
|
||||
|
||||
icalNode, err := ical.ParseCalendar(data)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not parse the resource's ical data.\nError: %s.\nResource path: %s", err, r.Path)
|
||||
return &ical.Node{
|
||||
Name: ical.VCALENDAR,
|
||||
}
|
||||
}
|
||||
|
||||
return icalNode
|
||||
}
|
||||
|
||||
// FileResourceAdapter implements the `ResourceAdapter` for resources stored as files in the file system.
|
||||
type FileResourceAdapter struct {
|
||||
finfo os.FileInfo
|
||||
resourcePath string
|
||||
}
|
||||
|
||||
// IsCollection tells whether the file resource is a directory or not.
|
||||
func (adp *FileResourceAdapter) IsCollection() bool {
|
||||
return adp.finfo.IsDir()
|
||||
}
|
||||
|
||||
// GetContent reads the file content and returns it as string. For collection resources (directories), it
|
||||
// returns an empty string.
|
||||
func (adp *FileResourceAdapter) GetContent() string {
|
||||
if adp.IsCollection() {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(files.AbsPath(adp.resourcePath))
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not read file content for the resource.\nError: %s.\nResource path: %s.", err, adp.resourcePath)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// GetContentSize returns the content length.
|
||||
func (adp *FileResourceAdapter) GetContentSize() int64 {
|
||||
return adp.finfo.Size()
|
||||
}
|
||||
|
||||
// CalculateEtag calculates an ETag based on the file current modification status and returns it.
|
||||
func (adp *FileResourceAdapter) CalculateEtag() string {
|
||||
// returns ETag as the concatenated hex values of a file's
|
||||
// modification time and size. This is not a reliable synchronization
|
||||
// mechanism for directories, so for collections we return empty.
|
||||
if adp.IsCollection() {
|
||||
return ""
|
||||
}
|
||||
|
||||
fi := adp.finfo
|
||||
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
||||
}
|
||||
|
||||
// GetModTime returns the time when the file was last modified.
|
||||
func (adp *FileResourceAdapter) GetModTime() time.Time {
|
||||
return adp.finfo.ModTime()
|
||||
}
|
229
vendor/github.com/samedi/caldav-go/data/storage.go
generated
vendored
Normal file
229
vendor/github.com/samedi/caldav-go/data/storage.go
generated
vendored
Normal file
@ -0,0 +1,229 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/files"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Storage is the inteface responsible for the CRUD operations on the CalDAV resources. It represents
|
||||
// where the resources should be fetched from and the various operations which can be performed on it.
|
||||
// This is the interface one should implement in case it needs a custom storage strategy, like fetching
|
||||
// data from the cloud, local DB, etc. After that, the custom storage implementation can be setup to be used
|
||||
// in the server by passing the object instance to `caldav.SetupStorage`.
|
||||
type Storage interface {
|
||||
// GetResources gets a list of resources based on a given `rpath`. The
|
||||
// `rpath` is the path to the original resource that's being requested. The resultant list
|
||||
// will/must contain that original resource in it, apart from any additional resources. It also receives
|
||||
// `withChildren` flag to say if the result must also include all the original resource`s
|
||||
// children (if original is a collection resource). If `true`, the result will have the requested resource + children.
|
||||
// If `false`, it will have only the requested original resource (from the `rpath` path).
|
||||
// It returns errors if anything went wrong or if it could not find any resource on `rpath` path.
|
||||
GetResources(rpath string, withChildren bool) ([]Resource, error)
|
||||
// GetResourcesByList fetches a list of resources by path from the storage.
|
||||
// This method fetches all the `rpaths` and return an array of the reosurces found.
|
||||
// No error 404 will be returned if one of the resources cannot be found.
|
||||
// Errors are returned if any errors other than "not found" happens.
|
||||
GetResourcesByList(rpaths []string) ([]Resource, error)
|
||||
// GetResourcesByFilters returns the filtered children of a target collection resource.
|
||||
// The target collection resource is the one pointed by the `rpath` parameter. All of its children
|
||||
// will be checked against a set of `filters` and the matching ones are returned. The results
|
||||
// contains only the filtered children and does NOT include the target resource. If the target resource
|
||||
// is not a collection, an empty array is returned as the result.
|
||||
GetResourcesByFilters(rpath string, filters *ResourceFilter) ([]Resource, error)
|
||||
// GetResource gets the requested resource based on a given `rpath` path. It returns the resource (if found) or
|
||||
// nil (if not found). Also returns a flag specifying if the resource was found or not.
|
||||
GetResource(rpath string) (*Resource, bool, error)
|
||||
// GetShallowResource has the same behaviour of `storage.GetResource`. The only difference is that, for collection resources,
|
||||
// it does not return its children in the collection `storage.Resource` struct (hence the name shallow). The motive is
|
||||
// for optimizations reasons, as this function is used on places where the collection's children are not important.
|
||||
GetShallowResource(rpath string) (*Resource, bool, error)
|
||||
// CreateResource creates a new resource on the `rpath` path with a given `content`.
|
||||
CreateResource(rpath, content string) (*Resource, error)
|
||||
// UpdateResource udpates a resource on the `rpath` path with a given `content`.
|
||||
UpdateResource(rpath, content string) (*Resource, error)
|
||||
// DeleteResource deletes a resource on the `rpath` path.
|
||||
DeleteResource(rpath string) error
|
||||
}
|
||||
|
||||
// FileStorage is the storage that deals with resources as files in the file system. So, a collection resource
|
||||
// is treated as a folder/directory and its children resources are the files it contains. Non-collection resources are just plain files.
|
||||
// Each file represents then a CalAV resource and the data expects to contain the iCal data to feed the calendar events.
|
||||
type FileStorage struct {
|
||||
}
|
||||
|
||||
// GetResources get the file resources based on the `rpath`. See `Storage.GetResources` doc.
|
||||
func (fs *FileStorage) GetResources(rpath string, withChildren bool) ([]Resource, error) {
|
||||
result := []Resource{}
|
||||
|
||||
// tries to open the file by the given path
|
||||
f, e := fs.openResourceFile(rpath, os.O_RDONLY)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// add it as a resource to the result list
|
||||
finfo, _ := f.Stat()
|
||||
resource := NewResource(rpath, &FileResourceAdapter{finfo, rpath})
|
||||
result = append(result, resource)
|
||||
|
||||
// if the file is a dir, add its children to the result list
|
||||
if withChildren && finfo.IsDir() {
|
||||
dirFiles, _ := f.Readdir(0)
|
||||
for _, finfo := range dirFiles {
|
||||
childPath := files.JoinPaths(rpath, finfo.Name())
|
||||
resource = NewResource(childPath, &FileResourceAdapter{finfo, childPath})
|
||||
result = append(result, resource)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetResourcesByFilters get the file resources based on the `rpath` and a set of filters. See `Storage.GetResourcesByFilters` doc.
|
||||
func (fs *FileStorage) GetResourcesByFilters(rpath string, filters *ResourceFilter) ([]Resource, error) {
|
||||
result := []Resource{}
|
||||
|
||||
childPaths := fs.getDirectoryChildPaths(rpath)
|
||||
for _, path := range childPaths {
|
||||
resource, _, err := fs.GetShallowResource(path)
|
||||
|
||||
if err != nil {
|
||||
// if we can't find this resource, something weird went wrong, but not that serious, so we log it and continue
|
||||
log.Printf("WARNING: returned error when trying to get resource with path %s from collection with path %s. Error: %s", path, rpath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// only add it if the resource matches the filters
|
||||
if filters == nil || filters.Match(resource) {
|
||||
result = append(result, *resource)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetResourcesByList get a list of file resources based on a list of `rpaths`. See `Storage.GetResourcesByList` doc.
|
||||
func (fs *FileStorage) GetResourcesByList(rpaths []string) ([]Resource, error) {
|
||||
results := []Resource{}
|
||||
|
||||
for _, rpath := range rpaths {
|
||||
resource, found, err := fs.GetShallowResource(rpath)
|
||||
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found {
|
||||
results = append(results, *resource)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetResource fetches and returns a single resource for a `rpath`. See `Storage.GetResoure` doc.
|
||||
func (fs *FileStorage) GetResource(rpath string) (*Resource, bool, error) {
|
||||
// For simplicity we just return the shallow resource.
|
||||
return fs.GetShallowResource(rpath)
|
||||
}
|
||||
|
||||
// GetShallowResource fetches and returns a single resource file/directory without any related children. See `Storage.GetShallowResource` doc.
|
||||
func (fs *FileStorage) GetShallowResource(rpath string) (*Resource, bool, error) {
|
||||
resources, err := fs.GetResources(rpath, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if resources == nil || len(resources) == 0 {
|
||||
return nil, false, errs.ResourceNotFoundError
|
||||
}
|
||||
|
||||
res := resources[0]
|
||||
return &res, true, nil
|
||||
}
|
||||
|
||||
// CreateResource creates a file resource with the provided `content`. See `Storage.CreateResource` doc.
|
||||
func (fs *FileStorage) CreateResource(rpath, content string) (*Resource, error) {
|
||||
rAbsPath := files.AbsPath(rpath)
|
||||
|
||||
if fs.isResourcePresent(rAbsPath) {
|
||||
return nil, errs.ResourceAlreadyExistsError
|
||||
}
|
||||
|
||||
// create parent directories (if needed)
|
||||
if err := os.MkdirAll(files.DirPath(rAbsPath), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create file/resource and write content
|
||||
f, err := os.Create(rAbsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.WriteString(content)
|
||||
|
||||
finfo, _ := f.Stat()
|
||||
res := NewResource(rpath, &FileResourceAdapter{finfo, rpath})
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// UpdateResource updates a file resource with the provided `content`. See `Storage.UpdateResource` doc.
|
||||
func (fs *FileStorage) UpdateResource(rpath, content string) (*Resource, error) {
|
||||
f, e := fs.openResourceFile(rpath, os.O_RDWR)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// update content
|
||||
f.Truncate(0)
|
||||
f.WriteString(content)
|
||||
|
||||
finfo, _ := f.Stat()
|
||||
res := NewResource(rpath, &FileResourceAdapter{finfo, rpath})
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// DeleteResource deletes a file resource (and possibly all its children in case of a collection). See `Storage.DeleteResource` doc.
|
||||
func (fs *FileStorage) DeleteResource(rpath string) error {
|
||||
err := os.Remove(files.AbsPath(rpath))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *FileStorage) isResourcePresent(rpath string) bool {
|
||||
_, found, _ := fs.GetShallowResource(rpath)
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func (fs *FileStorage) openResourceFile(filepath string, mode int) (*os.File, error) {
|
||||
f, e := os.OpenFile(files.AbsPath(filepath), mode, 0666)
|
||||
if e != nil {
|
||||
if os.IsNotExist(e) {
|
||||
return nil, errs.ResourceNotFoundError
|
||||
}
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (fs *FileStorage) getDirectoryChildPaths(dirpath string) []string {
|
||||
content, err := ioutil.ReadDir(files.AbsPath(dirpath))
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not read resource as file directory.\nError: %s.\nResource path: %s.", err, dirpath)
|
||||
return nil
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
for _, file := range content {
|
||||
fpath := files.JoinPaths(dirpath, file.Name())
|
||||
result = append(result, fpath)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
8
vendor/github.com/samedi/caldav-go/data/user.go
generated
vendored
Normal file
8
vendor/github.com/samedi/caldav-go/data/user.go
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
package data
|
||||
|
||||
// CalUser represents the calendar user. It is used, for example, to
|
||||
// keep track globally what is the current user interacting with the calendar.
|
||||
// This user data can be used in various places, including in some of the CALDAV responses.
|
||||
type CalUser struct {
|
||||
Name string
|
||||
}
|
Reference in New Issue
Block a user