370 lines
12 KiB
Go
370 lines
12 KiB
Go
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()
|
|
}
|