Properly added replacement modules
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
f9a40f5d02
commit
4d30e12f56
5
go.mod
5
go.mod
|
@ -75,4 +75,7 @@ require (
|
|||
src.techknowlogick.com/xormigrate v0.0.0-20190321151057-24497c23c09c
|
||||
)
|
||||
|
||||
replace github.com/labstack/echo/v4 => /home/konrad/go/src/github.com/labstack/echo
|
||||
replace (
|
||||
github.com/labstack/echo/v4 => ../../github.com/labstack/echo // Branch: feature/report-method, PR https://github.com/labstack/echo/pull/1332
|
||||
github.com/samedi/caldav-go => ../../github.com/samedi/caldav-go // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -16,6 +16,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
|||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beevik/etree v0.0.0-20171015221209-af219c0c7ea1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
|
@ -120,6 +121,7 @@ github.com/labstack/echo/v4 v4.1.5 h1:RztCXCvfMljychg0G/IzW5T7hL6ADqqwREwcX279Q1
|
|||
github.com/labstack/echo/v4 v4.1.5/go.mod h1:3LbYC6VkwmUnmLPZ8WFdHdQHG77e9GQbjyhWdb1QvC4=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/laurent22/ical-go v0.0.0-20170824131750-e4fec3492969/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/laurent22/ical-go v0.1.0 h1:4vZHBD3/+ne+IN+c3B+v6d9Ff8+70pzTjCWsjfDRvL0=
|
||||
github.com/laurent22/ical-go v0.1.0/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
|
|
|
@ -6,13 +6,13 @@ v3.0.0
|
|||
|
||||
Main change:
|
||||
|
||||
Add two ways to get resource from the storage: shallow or not.
|
||||
Add two ways to get resources from the storage: shallow or not.
|
||||
|
||||
`data.GetShallowResource`: means that, if it's collection resource, it will not include its child VEVENTs in the ICS data.
|
||||
This is used throughout the palces where the children dont matter.
|
||||
`data.GetShallowResource`: means that, if it's a collection resource, it will not include its child VEVENTs in the ICS data.
|
||||
This is used throughout the palces where children don't matter.
|
||||
|
||||
`data.GetResource`: means that the child VEVENTs will be included in the returned ICS content data for collection resources.
|
||||
This is used then sending a GET request to fetch a specific resource and expecting its full ICS data in response.
|
||||
This is used when sending a GET request to fetch a specific resource and expecting its full ICS data in response.
|
||||
|
||||
Other changes:
|
||||
|
||||
|
@ -33,7 +33,7 @@ v1.0.1
|
|||
2017-01-25 Daniel Ferraz <d.ferrazm@gmail.com>
|
||||
|
||||
Escape the contents in `<calendar-data>` and `<displayname>` in the `multistatus` XML responses. Fixing possible bugs
|
||||
related to having special characters (e.g. &) in the XML multistatus responses that would possible break the encoding.
|
||||
related to having special characters (e.g. &) in the XML multistatus responses that would possibly break the encoding.
|
||||
|
||||
v1.0.0
|
||||
-----------
|
||||
|
|
|
@ -56,56 +56,113 @@ func runServer() {
|
|||
}
|
||||
|
||||
func myHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
response := caldav.HandleRequest(writer, request)
|
||||
// ... do something with the `response` ...
|
||||
// the response is written with the current `http.ResponseWriter` and ready to be sent back
|
||||
response := caldav.HandleRequest(request)
|
||||
// ... do something with the response object before writing it back to the client ...
|
||||
response.Write(writer)
|
||||
}
|
||||
```
|
||||
|
||||
### Storage & Resources
|
||||
|
||||
The storage is where the caldav resources are stored. To interact with that, the caldav lib needs only a type that conforms with the `data.Storage` interface to operate on top of the storage. Basically, this interface defines all the CRUD functions to work on top of the resources. With that, resources can be stored anywhere: in the filesystem, in the cloud, database, etc. As long as the used storage implements all the required storage interface functions, the caldav lib will work fine.
|
||||
The storage is where the CalDAV resources are stored. To interact with that, the caldav lib needs only a type that conforms with the `data.Storage` interface to operate on top of the storage. Basically, this interface defines all the CRUD functions to work on top of the resources. With that, resources can be stored anywhere: in the filesystem, in the cloud, database, etc. As long as the used storage implements all the required storage interface functions, the caldav lib will work fine.
|
||||
|
||||
For example, we could use the following dummy storage implementation:
|
||||
For example, we could use the following dummy read-only storage implementation, which returns dummy hard-coded resources:
|
||||
|
||||
```go
|
||||
type DummyStorage struct{
|
||||
resources map[string]string{
|
||||
"/foo": `BEGING:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20160914T170000\nEND:VEVENT\nEND:VCALENDAR`,
|
||||
"/bar": `BEGING:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20160915T180000\nEND:VEVENT\nEND:VCALENDAR`,
|
||||
"/baz": `BEGING:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20160916T190000\nEND:VEVENT\nEND:VCALENDAR`,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResources(rpath string, withChildren bool) ([]Resource, error) {
|
||||
return []Resource{}, nil
|
||||
return d.GetResourcesByList([]string{rpath})
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResourcesByFilters(rpath string, filters *ResourceFilter) ([]Resource, error) {
|
||||
return []Resource{}, nil
|
||||
return nil, errors.New("filters are not supported")
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResourcesByList(rpaths []string) ([]Resource, error) {
|
||||
return []Resource{}, nil
|
||||
result := []Resource{}
|
||||
|
||||
for _, rpath := range rpaths {
|
||||
resource, found, _ := d.GetResource(rpath)
|
||||
if found {
|
||||
result = append(result, resource)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResource(rpath string) (*Resource, bool, error) {
|
||||
return nil, false, nil
|
||||
return d.GetShallowResource(rpath)
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetShallowResource(rpath string) (*Resource, bool, error) {
|
||||
result := []Resource{}
|
||||
resContent := d.resources[rpath]
|
||||
|
||||
if resContent != "" {
|
||||
resource = NewResource(rpath, DummyResourceAdapter{rpath, resContent})
|
||||
return &resource, true, nil
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (d *DummyStorage) CreateResource(rpath, content string) (*Resource, error) {
|
||||
return nil, nil
|
||||
return nil, errors.New("creating resources are not supported")
|
||||
}
|
||||
|
||||
func (d *DummyStorage) UpdateResource(rpath, content string) (*Resource, error) {
|
||||
return nil, nil
|
||||
return nil, errors.New("updating resources are not supported")
|
||||
}
|
||||
|
||||
func (d *DummyStorage) DeleteResource(rpath string) error {
|
||||
return nil
|
||||
return nil, errors.New("deleting resources are not supported")
|
||||
}
|
||||
```
|
||||
|
||||
Normally, when you provide your own storage implementation, you will need to provide also a custom `data.ResourceAdapter` interface implementation.
|
||||
The resource adapter deals with the specificities of how resources are stored, which formats and how to deal with them. For example,
|
||||
for file resources, the resources contents are the content read from the file itself, for resources in the cloud, it could be in JSON needing
|
||||
some additional processing to parse the content, etc.
|
||||
|
||||
In our example here, we could say that the adapter for this case would be:
|
||||
|
||||
```go
|
||||
type DummyResourceAdapter struct {
|
||||
resourcePath string
|
||||
resourceData string
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) GetContent() string {
|
||||
return a.resourceData
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) GetContentSize() int64 {
|
||||
return len(a.GetContent())
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) CalculateEtag() string {
|
||||
return hashify(a.GetContent())
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) GetModTime() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
```
|
||||
|
||||
Note that this adapter implementation is passed over whenever we initialize a new `Resource` instance in the storage implementation.
|
||||
|
||||
Then we just need to tell the caldav lib to use our dummy storage:
|
||||
|
||||
```go
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
package caldav
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
)
|
||||
|
||||
// SetupStorage sets the storage to be used by the server. The storage is where the resources data will be fetched from.
|
||||
// You can provide a custom storage for your own purposes (which might be looking for data in the cloud, DB, etc).
|
||||
// Just make sure it implements the `data.Storage` interface.
|
||||
func SetupStorage(stg data.Storage) {
|
||||
global.Storage = stg
|
||||
global.Storage = stg
|
||||
}
|
||||
|
||||
// SetupUser sets the current user which is currently interacting with the calendar.
|
||||
// It is used, for example, in some of the CALDAV responses, when rendering the path where to find the user's resources.
|
||||
func SetupUser(username string) {
|
||||
global.User = &data.CalUser{username}
|
||||
global.User = &data.CalUser{Name: username}
|
||||
}
|
||||
|
|
|
@ -1,362 +1,363 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
"strings"
|
||||
"errors"
|
||||
"github.com/beevik/etree"
|
||||
"errors"
|
||||
"github.com/beevik/etree"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
// ================ FILTERS ==================
|
||||
// Filters are a set of rules used to retrieve a range of resources. It is used primarily
|
||||
// on REPORT requests and is described in details here (RFC4791#7.8).
|
||||
|
||||
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"
|
||||
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"
|
||||
// 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.
|
||||
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.
|
||||
}
|
||||
|
||||
// This function creates a new filter object from a piece of XML string.
|
||||
// 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
|
||||
}
|
||||
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")
|
||||
}
|
||||
// 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
|
||||
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),
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// set attributes
|
||||
for _, attr := range elem.Attr {
|
||||
filter.attrs[attr.Key] = attr.Value
|
||||
}
|
||||
|
||||
return filter
|
||||
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]
|
||||
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
|
||||
}
|
||||
|
||||
t, err := time.Parse(FILTER_TIME_FORMAT, f.attrs[attrName])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &t
|
||||
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)
|
||||
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)
|
||||
}
|
||||
if f.name == TAG_FILTER {
|
||||
return f.rootFilterMatch(target)
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) rootFilterMatch(target ResourceInterface) bool {
|
||||
if f.isEmpty() {
|
||||
return false
|
||||
}
|
||||
if f.isEmpty() {
|
||||
return false
|
||||
}
|
||||
|
||||
return f.rootChildrenMatch(target)
|
||||
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{}
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.1.
|
||||
func (f *ResourceFilter) compMatch(target ResourceInterface, scope []string) bool {
|
||||
targetComp := target.ComponentName()
|
||||
compName := f.attrs["name"]
|
||||
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)
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.9
|
||||
func (f *ResourceFilter) timeRangeMatch(target ResourceInterface) bool {
|
||||
startAttr := f.attrs["start"]
|
||||
endAttr := f.attrs["end"]
|
||||
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"
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.3
|
||||
func (f *ResourceFilter) paramMatch(target ResourceInterface, parentPropPath []string) bool {
|
||||
paramName := f.attrs["name"]
|
||||
paramPath := append(parentPropPath, paramName)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
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.
|
||||
// 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)
|
||||
targetText = strings.ToLower(targetText)
|
||||
expectedSubstr := strings.ToLower(f.text)
|
||||
|
||||
match := strings.Contains(targetText, expectedSubstr)
|
||||
match := strings.Contains(targetText, expectedSubstr)
|
||||
|
||||
if f.attrs["negate-condition"] == "yes" {
|
||||
return !match
|
||||
}
|
||||
if f.attrs["negate-condition"] == "yes" {
|
||||
return !match
|
||||
}
|
||||
|
||||
return match
|
||||
return match
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) isEmpty() bool {
|
||||
return len(f.getChildren()) == 0 && f.text == ""
|
||||
return len(f.getChildren()) == 0 && f.text == ""
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) contains(filterName string) bool {
|
||||
if f.findChild(filterName, false) != nil {
|
||||
return true
|
||||
}
|
||||
if f.findChild(filterName, false) != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) findChild(filterName string, dig bool) *ResourceFilter {
|
||||
for _, child := range f.getChildren() {
|
||||
if child.name == filterName {
|
||||
return &child
|
||||
}
|
||||
for _, child := range f.getChildren() {
|
||||
if child.name == filterName {
|
||||
return &child
|
||||
}
|
||||
|
||||
if !dig {
|
||||
continue
|
||||
}
|
||||
if !dig {
|
||||
continue
|
||||
}
|
||||
|
||||
dugChild := child.findChild(filterName, true)
|
||||
dugChild := child.findChild(filterName, true)
|
||||
|
||||
if dugChild != nil {
|
||||
return dugChild
|
||||
}
|
||||
}
|
||||
if dugChild != nil {
|
||||
return dugChild
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// lazy evaluation of the child filters
|
||||
func (f *ResourceFilter) getChildren() []ResourceFilter {
|
||||
if f.children == nil {
|
||||
f.children = []ResourceFilter{}
|
||||
if f.children == nil {
|
||||
f.children = []ResourceFilter{}
|
||||
|
||||
for _, childElem := range f.etreeElem.ChildElements() {
|
||||
childFilter := newFilterFromEtreeElem(childElem)
|
||||
f.children = append(f.children, childFilter)
|
||||
}
|
||||
}
|
||||
for _, childElem := range f.etreeElem.ChildElements() {
|
||||
childFilter := newFilterFromEtreeElem(childElem)
|
||||
f.children = append(f.children, childFilter)
|
||||
}
|
||||
}
|
||||
|
||||
return f.children
|
||||
return f.children
|
||||
}
|
||||
|
|
|
@ -1,277 +1,384 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"os"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"strings"
|
||||
"strconv"
|
||||
"io/ioutil"
|
||||
"fmt"
|
||||
"github.com/laurent22/ical-go/ical"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/laurent22/ical-go/ical"
|
||||
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"github.com/samedi/caldav-go/files"
|
||||
"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
|
||||
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
|
||||
IsCollection() bool
|
||||
CalculateEtag() string
|
||||
GetContent() string
|
||||
GetContentSize() int64
|
||||
GetModTime() time.Time
|
||||
}
|
||||
|
||||
// ResourceComponenter returns all supported components as strings, such as vtodo vevent etc
|
||||
type ResourceComponenter interface {
|
||||
GetSupportedComponents() []string
|
||||
}
|
||||
|
||||
// ResourceRecurrence represents a recurrence for a resource.
|
||||
// NOTE: recurrences are not supported yet.
|
||||
type ResourceRecurrence struct {
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
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
|
||||
Name string
|
||||
Path string
|
||||
|
||||
pathSplit []string
|
||||
adapter ResourceAdapter
|
||||
pathSplit []string
|
||||
adapter ResourceAdapter
|
||||
|
||||
emptyTime time.Time
|
||||
emptyTime time.Time
|
||||
}
|
||||
|
||||
func NewResource(resPath string, adp ResourceAdapter) Resource {
|
||||
pClean := lib.ToSlashPath(resPath)
|
||||
pSplit := strings.Split(strings.Trim(pClean, "/"), "/")
|
||||
// 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,
|
||||
}
|
||||
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()
|
||||
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
|
||||
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
|
||||
} else {
|
||||
return lib.VEVENT
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
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()
|
||||
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)
|
||||
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)
|
||||
}
|
||||
// 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()
|
||||
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{}
|
||||
// TODO: Implement. This server does not support iCal recurrences yet. We just return an empty array.
|
||||
return []ResourceRecurrence{}
|
||||
}
|
||||
|
||||
func (r *Resource) HasProperty(propPath... string) bool {
|
||||
return r.GetPropertyValue(propPath...) != ""
|
||||
// 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...) != ""
|
||||
}
|
||||
|
||||
func (r *Resource) GetPropertyValue(propPath... string) string {
|
||||
if propPath[0] == ical.VCALENDAR {
|
||||
propPath = propPath[1:]
|
||||
}
|
||||
// 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
|
||||
prop, _ := r.icalendar().DigProperty(propPath...)
|
||||
return prop
|
||||
}
|
||||
|
||||
func (r *Resource) HasPropertyParam(paramPath... string) bool {
|
||||
return r.GetPropertyParamValue(paramPath...) != ""
|
||||
// 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...) != ""
|
||||
}
|
||||
|
||||
func (r *Resource) GetPropertyParamValue(paramPath... string) string {
|
||||
if paramPath[0] == ical.VCALENDAR {
|
||||
paramPath = paramPath[1:]
|
||||
}
|
||||
// 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
|
||||
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
|
||||
}
|
||||
if r.IsCollection() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return r.adapter.CalculateEtag(), true
|
||||
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
|
||||
} else {
|
||||
return "text/calendar; component=vcalendar", true
|
||||
}
|
||||
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
|
||||
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 != ""
|
||||
data := r.adapter.GetContent()
|
||||
found := data != ""
|
||||
|
||||
return data, found
|
||||
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
|
||||
}
|
||||
// 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
|
||||
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
|
||||
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 = ""
|
||||
}
|
||||
var owner string
|
||||
if len(r.pathSplit) > 1 {
|
||||
owner = r.pathSplit[0]
|
||||
} else {
|
||||
owner = ""
|
||||
}
|
||||
|
||||
return owner, true
|
||||
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()
|
||||
owner, _ := r.GetOwner()
|
||||
|
||||
if owner != "" {
|
||||
return fmt.Sprintf("/%s/", owner), true
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
if owner != "" {
|
||||
return fmt.Sprintf("/%s/", owner), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetSupportedComponents returns the components supported by a store
|
||||
func (r *Resource) GetSupportedComponents() []string {
|
||||
// We're checking if the storage adapter implements the interface to not break any implementations
|
||||
if sc, ok := interface{}(r.adapter).(ResourceComponenter); ok {
|
||||
return sc.GetSupportedComponents()
|
||||
}
|
||||
return []string{lib.VCALENDAR, lib.VEVENT}
|
||||
}
|
||||
|
||||
// TODO: memoize
|
||||
func (r *Resource) icalVEVENT() *ical.Node {
|
||||
vevent := r.icalendar().ChildByName(ical.VEVENT)
|
||||
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)
|
||||
// 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 &ical.Node{
|
||||
Name: ical.VEVENT,
|
||||
}
|
||||
}
|
||||
|
||||
return vevent
|
||||
return vevent
|
||||
}
|
||||
|
||||
// TODO: memoize
|
||||
func (r *Resource) icalendar() *ical.Node {
|
||||
data, found := r.GetContentData()
|
||||
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,
|
||||
}
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
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
|
||||
return icalNode
|
||||
}
|
||||
|
||||
// FileResourceAdapter implements the `ResourceAdapter` for resources stored as files in the file system.
|
||||
type FileResourceAdapter struct {
|
||||
finfo os.FileInfo
|
||||
resourcePath string
|
||||
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()
|
||||
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 ""
|
||||
}
|
||||
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 ""
|
||||
}
|
||||
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)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// GetContentSize returns the content length.
|
||||
func (adp *FileResourceAdapter) GetContentSize() int64 {
|
||||
return adp.finfo.Size()
|
||||
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
|
||||
// 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 ""
|
||||
}
|
||||
if adp.IsCollection() {
|
||||
return ""
|
||||
}
|
||||
|
||||
fi := adp.finfo
|
||||
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
||||
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()
|
||||
return adp.finfo.ModTime()
|
||||
}
|
||||
|
|
|
@ -1,217 +1,229 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"os"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/files"
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/files"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// The Storage is the responsible for the CRUD operations on the caldav resources.
|
||||
// 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
|
||||
// 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. On the other hand, non-collection
|
||||
// resources are just plain files.
|
||||
// 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{}
|
||||
result := []Resource{}
|
||||
|
||||
// tries to open the file by the given path
|
||||
f, e := fs.openResourceFile(rpath, os.O_RDONLY)
|
||||
if e != nil {
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
// 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
|
||||
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{}
|
||||
result := []Resource{}
|
||||
|
||||
childPaths := fs.getDirectoryChildPaths(rpath)
|
||||
for _, path := range childPaths {
|
||||
resource, _, err := fs.GetShallowResource(path)
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
// only add it if the resource matches the filters
|
||||
if filters == nil || filters.Match(resource) {
|
||||
result = append(result, *resource)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
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{}
|
||||
results := []Resource{}
|
||||
|
||||
for _, rpath := range rpaths {
|
||||
resource, found, err := fs.GetShallowResource(rpath)
|
||||
for _, rpath := range rpaths {
|
||||
resource, found, err := fs.GetShallowResource(rpath)
|
||||
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found {
|
||||
results = append(results, *resource)
|
||||
}
|
||||
}
|
||||
if found {
|
||||
results = append(results, *resource)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
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)
|
||||
// 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)
|
||||
resources, err := fs.GetResources(rpath, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if resources == nil || len(resources) == 0 {
|
||||
return nil, false, errs.ResourceNotFoundError
|
||||
}
|
||||
if resources == nil || len(resources) == 0 {
|
||||
return nil, false, errs.ResourceNotFoundError
|
||||
}
|
||||
|
||||
res := resources[0]
|
||||
return &res, true, nil
|
||||
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)
|
||||
rAbsPath := files.AbsPath(rpath)
|
||||
|
||||
if fs.isResourcePresent(rAbsPath) {
|
||||
return nil, errs.ResourceAlreadyExistsError
|
||||
}
|
||||
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 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)
|
||||
// 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
|
||||
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 {
|
||||
f, e := fs.openResourceFile(rpath, os.O_RDWR)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
|
||||
// update content
|
||||
f.Truncate(0)
|
||||
f.WriteString(content)
|
||||
// update content
|
||||
f.Truncate(0)
|
||||
f.WriteString(content)
|
||||
|
||||
finfo, _ := f.Stat()
|
||||
res := NewResource(rpath, &FileResourceAdapter{finfo, rpath})
|
||||
return &res, nil
|
||||
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))
|
||||
err := os.Remove(files.AbsPath(rpath))
|
||||
|
||||
return err
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *FileStorage) isResourcePresent(rpath string) bool {
|
||||
_, found, _ := fs.GetShallowResource(rpath)
|
||||
_, found, _ := fs.GetShallowResource(rpath)
|
||||
|
||||
return found
|
||||
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) {
|
||||
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
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (fs *FileStorage) getDirectoryChildPaths(dirpath string) []string {
|
||||
content, err := ioutil.ReadDir(files.AbsPath(dirpath))
|
||||
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
|
||||
log.Printf("ERROR: Could not read resource as file directory.\nError: %s.\nResource path: %s.", err, dirpath)
|
||||
return nil
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
result := []string{}
|
||||
for _, file := range content {
|
||||
fpath := files.JoinPaths(dirpath, file.Name())
|
||||
result = append(result, fpath)
|
||||
fpath := files.JoinPaths(dirpath, file.Name())
|
||||
result = append(result, fpath)
|
||||
}
|
||||
|
||||
return result
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,5 +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
|
||||
Name string
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ResourceNotFoundError = errors.New("caldav: resource not found")
|
||||
ResourceAlreadyExistsError = errors.New("caldav: resource already exists")
|
||||
UnauthorizedError = errors.New("caldav: unauthorized. credentials needed.")
|
||||
ForbiddenError = errors.New("caldav: forbidden operation.")
|
||||
ResourceNotFoundError = errors.New("caldav: resource not found")
|
||||
ResourceAlreadyExistsError = errors.New("caldav: resource already exists")
|
||||
UnauthorizedError = errors.New("caldav: unauthorized. credentials needed.")
|
||||
ForbiddenError = errors.New("caldav: forbidden operation.")
|
||||
)
|
||||
|
|
|
@ -1,30 +1,34 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"path/filepath"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Separator = string(filepath.Separator)
|
||||
Separator = string(filepath.Separator)
|
||||
)
|
||||
|
||||
// AbsPath converts the path into absolute path based on the current working directory.
|
||||
func AbsPath(path string) string {
|
||||
path = strings.Trim(path, "/")
|
||||
absPath, _ := filepath.Abs(path)
|
||||
path = strings.Trim(path, "/")
|
||||
absPath, _ := filepath.Abs(path)
|
||||
|
||||
return absPath
|
||||
return absPath
|
||||
}
|
||||
|
||||
// DirPath returns all but the last element of path, typically the path's directory.
|
||||
func DirPath(path string) string {
|
||||
return filepath.Dir(path)
|
||||
return filepath.Dir(path)
|
||||
}
|
||||
|
||||
// JoinPaths joins two or more paths into a single path.
|
||||
func JoinPaths(paths ...string) string {
|
||||
return filepath.Join(paths...)
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
|
||||
// ToSlashPath slashify the path, using '/' as separator.
|
||||
func ToSlashPath(path string) string {
|
||||
return lib.ToSlashPath(path)
|
||||
return lib.ToSlashPath(path)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
hash: 2796726e69757f4af1a13f6ebd056ebc626d712051aa213875bb03f5bdc1ebfd
|
||||
updated: 2017-01-18T11:43:23.127761353+01:00
|
||||
hash: ff037054c7b689c20a4fd4368897b8998d6ffa31b0f3f4a79e523a132e6a9e94
|
||||
updated: 2018-04-04T08:38:45.523594-04:00
|
||||
imports:
|
||||
- name: github.com/beevik/etree
|
||||
version: 4cd0dd976db869f817248477718071a28e978df0
|
||||
version: af219c0c7ea1b67ec263c0b1b1b96d284a9181ce
|
||||
- name: github.com/laurent22/ical-go
|
||||
version: 4811ac5553eae5fed7cd5d7a9024727f1311b2a2
|
||||
subpackages:
|
||||
- ical
|
||||
version: e4fec34929693e2a4ba299d16380c55bac3fb42c
|
||||
testImports: []
|
||||
|
|
|
@ -2,6 +2,3 @@ package: github.com/samedi/caldav-go
|
|||
import:
|
||||
- package: github.com/beevik/etree
|
||||
- package: github.com/laurent22/ical-go
|
||||
version: ~0.1.0
|
||||
subpackages:
|
||||
- ical
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
// Package global defines the globally accessible variables in the caldav server
|
||||
// and the interface to setup them.
|
||||
package global
|
||||
|
||||
// This file defines accessible variables used to setup the caldav server.
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
)
|
||||
|
||||
// The global storage used in the CRUD operations of resources. Default storage is the `FileStorage`.
|
||||
// Storage represents the global storage used in the CRUD operations of resources. Default storage is the `data.FileStorage`.
|
||||
var Storage data.Storage = new(data.FileStorage)
|
||||
// Current caldav user. It is used to keep the info of the current user that is interacting with the calendar.
|
||||
|
||||
// User defines the current caldav user, which is the user currently interacting with the calendar.
|
||||
var User *data.CalUser
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
package caldav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/handlers"
|
||||
"net/http"
|
||||
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/handlers"
|
||||
)
|
||||
|
||||
// RequestHandler handles the given CALDAV request and writes the reponse righ away. This function is to be
|
||||
// used by passing it directly as the handle func to the `http` lib. Example: http.HandleFunc("/", caldav.RequestHandler).
|
||||
func RequestHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
response := HandleRequest(request)
|
||||
response.Write(writer)
|
||||
response := HandleRequest(request)
|
||||
response.Write(writer)
|
||||
}
|
||||
|
||||
// HandleRequest handles the given CALDAV request and returns the response. Useful when the caller
|
||||
// wants to do something else with the response before writing it to the response stream.
|
||||
func HandleRequest(request *http.Request) *handlers.Response {
|
||||
handler := handlers.NewHandler(request)
|
||||
return handler.Handle()
|
||||
handler := handlers.NewHandler(request)
|
||||
return handler.Handle()
|
||||
}
|
||||
|
||||
// HandleRequestWithStorage handles the request the same way as `HandleRequest` does, but before,
|
||||
// it sets the given storage that will be used throughout the request handling flow.
|
||||
func HandleRequestWithStorage(request *http.Request, stg data.Storage) *handlers.Response {
|
||||
SetupStorage(stg)
|
||||
return HandleRequest(request)
|
||||
SetupStorage(stg)
|
||||
return HandleRequest(request)
|
||||
}
|
||||
|
|
|
@ -1,24 +1,36 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type handlerInterface interface {
|
||||
Handle() *Response
|
||||
// HandlerInterface represents a CalDAV request handler. It has only one function `Handle`,
|
||||
// which is used to handle the CalDAV request and returns the response.
|
||||
type HandlerInterface interface {
|
||||
Handle() *Response
|
||||
}
|
||||
|
||||
func NewHandler(request *http.Request) handlerInterface {
|
||||
response := NewResponse()
|
||||
// NewHandler returns a new CalDAV request handler object based on the provided request.
|
||||
// With the returned request handler, you can call `Handle()` to handle the request.
|
||||
func NewHandler(request *http.Request) HandlerInterface {
|
||||
response := NewResponse()
|
||||
|
||||
switch request.Method {
|
||||
case "GET": return getHandler{request, response, false}
|
||||
case "HEAD": return getHandler{request, response, true}
|
||||
case "PUT": return putHandler{request, response}
|
||||
case "DELETE": return deleteHandler{request, response}
|
||||
case "PROPFIND": return propfindHandler{request, response}
|
||||
case "OPTIONS": return optionsHandler{response}
|
||||
case "REPORT": return reportHandler{request, response}
|
||||
default: return notImplementedHandler{response}
|
||||
}
|
||||
switch request.Method {
|
||||
case "GET":
|
||||
return getHandler{request, response, false}
|
||||
case "HEAD":
|
||||
return getHandler{request, response, true}
|
||||
case "PUT":
|
||||
return putHandler{request, response}
|
||||
case "DELETE":
|
||||
return deleteHandler{request, response}
|
||||
case "PROPFIND":
|
||||
return propfindHandler{request, response}
|
||||
case "OPTIONS":
|
||||
return optionsHandler{response}
|
||||
case "REPORT":
|
||||
return reportHandler{request, response}
|
||||
default:
|
||||
return notImplementedHandler{response}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,40 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type deleteHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (dh deleteHandler) Handle() *Response {
|
||||
precond := requestPreconditions{dh.request}
|
||||
precond := requestPreconditions{dh.request}
|
||||
|
||||
// get the event from the storage
|
||||
resource, _, err := global.Storage.GetShallowResource(dh.request.URL.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
// get the event from the storage
|
||||
resource, _, err := global.Storage.GetShallowResource(dh.request.URL.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
|
||||
// TODO: Handle delete on collections
|
||||
if resource.IsCollection() {
|
||||
return dh.response.Set(http.StatusMethodNotAllowed, "")
|
||||
}
|
||||
// TODO: Handle delete on collections
|
||||
if resource.IsCollection() {
|
||||
return dh.response.Set(http.StatusMethodNotAllowed, "")
|
||||
}
|
||||
|
||||
// check ETag pre-condition
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if !precond.IfMatch(resourceEtag) {
|
||||
return dh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
// check ETag pre-condition
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if !precond.IfMatch(resourceEtag) {
|
||||
return dh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
// delete event after pre-condition passed
|
||||
err = global.Storage.DeleteResource(resource.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
// delete event after pre-condition passed
|
||||
err = global.Storage.DeleteResource(resource.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
|
||||
return dh.response.Set(http.StatusNoContent, "")
|
||||
return dh.response.Set(http.StatusNoContent, "")
|
||||
}
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type getHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
onlyHeaders bool
|
||||
request *http.Request
|
||||
response *Response
|
||||
onlyHeaders bool
|
||||
}
|
||||
|
||||
func (gh getHandler) Handle() *Response {
|
||||
resource, _, err := global.Storage.GetResource(gh.request.URL.Path)
|
||||
if err != nil {
|
||||
return gh.response.SetError(err)
|
||||
}
|
||||
resource, _, err := global.Storage.GetResource(gh.request.URL.Path)
|
||||
if err != nil {
|
||||
return gh.response.SetError(err)
|
||||
}
|
||||
|
||||
var response string
|
||||
if gh.onlyHeaders {
|
||||
response = ""
|
||||
} else {
|
||||
response, _ = resource.GetContentData()
|
||||
}
|
||||
var response string
|
||||
if gh.onlyHeaders {
|
||||
response = ""
|
||||
} else {
|
||||
response, _ = resource.GetContentData()
|
||||
}
|
||||
|
||||
etag, _ := resource.GetEtag()
|
||||
lastm, _ := resource.GetLastModified(http.TimeFormat)
|
||||
ctype, _ := resource.GetContentType()
|
||||
etag, _ := resource.GetEtag()
|
||||
lastm, _ := resource.GetLastModified(http.TimeFormat)
|
||||
ctype, _ := resource.GetContentType()
|
||||
|
||||
gh.response.SetHeader("ETag", etag).
|
||||
SetHeader("Last-Modified", lastm).
|
||||
SetHeader("Content-Type", ctype).
|
||||
Set(http.StatusOK, response)
|
||||
gh.response.SetHeader("ETag", etag).
|
||||
SetHeader("Last-Modified", lastm).
|
||||
SetHeader("Content-Type", ctype).
|
||||
Set(http.StatusOK, response)
|
||||
|
||||
return gh.response
|
||||
return gh.response
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
HD_DEPTH = "Depth"
|
||||
HD_DEPTH_DEEP = "1"
|
||||
HD_PREFER = "Prefer"
|
||||
HD_PREFER_MINIMAL = "return=minimal"
|
||||
HD_PREFERENCE_APPLIED = "Preference-Applied"
|
||||
HD_DEPTH = "Depth"
|
||||
HD_DEPTH_DEEP = "1"
|
||||
HD_PREFER = "Prefer"
|
||||
HD_PREFER_MINIMAL = "return=minimal"
|
||||
HD_PREFERENCE_APPLIED = "Preference-Applied"
|
||||
)
|
||||
|
||||
type headers struct {
|
||||
http.Header
|
||||
http.Header
|
||||
}
|
||||
|
||||
func (this headers) IsDeep() bool {
|
||||
depth := this.Get(HD_DEPTH)
|
||||
return (depth == HD_DEPTH_DEEP)
|
||||
func (h headers) IsDeep() bool {
|
||||
depth := h.Get(HD_DEPTH)
|
||||
return (depth == HD_DEPTH_DEEP)
|
||||
}
|
||||
|
||||
func (this headers) IsMinimal() bool {
|
||||
prefer := this.Get(HD_PREFER)
|
||||
return (prefer == HD_PREFER_MINIMAL)
|
||||
func (h headers) IsMinimal() bool {
|
||||
prefer := h.Get(HD_PREFER)
|
||||
return (prefer == HD_PREFER_MINIMAL)
|
||||
}
|
||||
|
|
|
@ -1,56 +1,56 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"encoding/xml"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Wraps a multistatus response. It contains the set of `Responses`
|
||||
// that will serve to build the final XML. Multistatus responses are
|
||||
// used by the REPORT and PROPFIND methods.
|
||||
type multistatusResp struct {
|
||||
// The set of multistatus responses used to build each of the <DAV:response> nodes.
|
||||
Responses []msResponse
|
||||
// Flag that XML should be minimal or not
|
||||
// [defined in the draft https://tools.ietf.org/html/draft-murchison-webdav-prefer-05]
|
||||
Minimal bool
|
||||
// The set of multistatus responses used to build each of the <DAV:response> nodes.
|
||||
Responses []msResponse
|
||||
// Flag that XML should be minimal or not
|
||||
// [defined in the draft https://tools.ietf.org/html/draft-murchison-webdav-prefer-05]
|
||||
Minimal bool
|
||||
}
|
||||
|
||||
type msResponse struct {
|
||||
Href string
|
||||
Found bool
|
||||
Propstats msPropstats
|
||||
Href string
|
||||
Found bool
|
||||
Propstats msPropstats
|
||||
}
|
||||
|
||||
type msPropstats map[int]msProps
|
||||
|
||||
// Adds a msProp to the map with the key being the prop status.
|
||||
func (stats msPropstats) Add(prop msProp) {
|
||||
stats[prop.Status] = append(stats[prop.Status], prop)
|
||||
stats[prop.Status] = append(stats[prop.Status], prop)
|
||||
}
|
||||
|
||||
func (stats msPropstats) Clone() msPropstats {
|
||||
clone := make(msPropstats)
|
||||
clone := make(msPropstats)
|
||||
|
||||
for k, v := range stats {
|
||||
clone[k] = v
|
||||
}
|
||||
for k, v := range stats {
|
||||
clone[k] = v
|
||||
}
|
||||
|
||||
return clone
|
||||
return clone
|
||||
}
|
||||
|
||||
type msProps []msProp
|
||||
|
||||
type msProp struct {
|
||||
Tag xml.Name
|
||||
Content string
|
||||
Contents []string
|
||||
Status int
|
||||
Tag xml.Name
|
||||
Content string
|
||||
Contents []string
|
||||
Status int
|
||||
}
|
||||
|
||||
// Function that processes all the required props for a given resource.
|
||||
|
@ -62,146 +62,146 @@ type msProp struct {
|
|||
// So if a prop is found and processed ok, it'll be mapped to 200. If it's not found,
|
||||
// it'll be mapped to 404, and so on.
|
||||
func (ms *multistatusResp) Propstats(resource *data.Resource, reqprops []xml.Name) msPropstats {
|
||||
if resource == nil {
|
||||
return nil
|
||||
}
|
||||
if resource == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(msPropstats)
|
||||
result := make(msPropstats)
|
||||
|
||||
for _, ptag := range reqprops {
|
||||
pvalue := msProp{
|
||||
Tag: ptag,
|
||||
Status: http.StatusOK,
|
||||
}
|
||||
for _, ptag := range reqprops {
|
||||
pvalue := msProp{
|
||||
Tag: ptag,
|
||||
Status: http.StatusOK,
|
||||
}
|
||||
|
||||
pfound := false
|
||||
switch ptag {
|
||||
case ixml.CALENDAR_DATA_TG:
|
||||
pvalue.Content, pfound = resource.GetContentData()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_ETAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.GET_CONTENT_TYPE_TG:
|
||||
pvalue.Content, pfound = resource.GetContentType()
|
||||
case ixml.GET_CONTENT_LENGTH_TG:
|
||||
pvalue.Content, pfound = resource.GetContentLength()
|
||||
case ixml.DISPLAY_NAME_TG:
|
||||
pvalue.Content, pfound = resource.GetDisplayName()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_LAST_MODIFIED_TG:
|
||||
pvalue.Content, pfound = resource.GetLastModified(http.TimeFormat)
|
||||
case ixml.OWNER_TG:
|
||||
pvalue.Content, pfound = resource.GetOwnerPath()
|
||||
case ixml.GET_CTAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.PRINCIPAL_URL_TG,
|
||||
ixml.PRINCIPAL_COLLECTION_SET_TG,
|
||||
ixml.CALENDAR_USER_ADDRESS_SET_TG,
|
||||
ixml.CALENDAR_HOME_SET_TG:
|
||||
pvalue.Content, pfound = ixml.HrefTag(resource.Path), true
|
||||
case ixml.RESOURCE_TYPE_TG:
|
||||
if resource.IsCollection() {
|
||||
pvalue.Content, pfound = ixml.Tag(ixml.COLLECTION_TG, "") + ixml.Tag(ixml.CALENDAR_TG, ""), true
|
||||
pfound := false
|
||||
switch ptag {
|
||||
case ixml.CALENDAR_DATA_TG:
|
||||
pvalue.Content, pfound = resource.GetContentData()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_ETAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.GET_CONTENT_TYPE_TG:
|
||||
pvalue.Content, pfound = resource.GetContentType()
|
||||
case ixml.GET_CONTENT_LENGTH_TG:
|
||||
pvalue.Content, pfound = resource.GetContentLength()
|
||||
case ixml.DISPLAY_NAME_TG:
|
||||
pvalue.Content, pfound = resource.GetDisplayName()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_LAST_MODIFIED_TG:
|
||||
pvalue.Content, pfound = resource.GetLastModified(http.TimeFormat)
|
||||
case ixml.OWNER_TG:
|
||||
pvalue.Content, pfound = resource.GetOwnerPath()
|
||||
case ixml.GET_CTAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.PRINCIPAL_URL_TG,
|
||||
ixml.PRINCIPAL_COLLECTION_SET_TG,
|
||||
ixml.CALENDAR_USER_ADDRESS_SET_TG,
|
||||
ixml.CALENDAR_HOME_SET_TG:
|
||||
pvalue.Content, pfound = ixml.HrefTag(resource.Path), true
|
||||
case ixml.RESOURCE_TYPE_TG:
|
||||
if resource.IsCollection() {
|
||||
pvalue.Content, pfound = ixml.Tag(ixml.COLLECTION_TG, "")+ixml.Tag(ixml.CALENDAR_TG, ""), true
|
||||
|
||||
if resource.IsPrincipal() {
|
||||
pvalue.Content += ixml.Tag(ixml.PRINCIPAL_TG, "")
|
||||
}
|
||||
} else {
|
||||
// resourcetype must be returned empty for non-collection elements
|
||||
pvalue.Content, pfound = "", true
|
||||
}
|
||||
case ixml.CURRENT_USER_PRINCIPAL_TG:
|
||||
if global.User != nil {
|
||||
path := fmt.Sprintf("/%s/", global.User.Name)
|
||||
pvalue.Content, pfound = ixml.HrefTag(path), true
|
||||
}
|
||||
case ixml.SUPPORTED_CALENDAR_COMPONENT_SET_TG:
|
||||
if resource.IsCollection() {
|
||||
for _, component := range supportedComponents {
|
||||
// TODO: use ixml somehow to build the below tag
|
||||
compTag := fmt.Sprintf(`<C:comp name="%s"/>`, component)
|
||||
pvalue.Contents = append(pvalue.Contents, compTag)
|
||||
}
|
||||
pfound = true
|
||||
}
|
||||
}
|
||||
if resource.IsPrincipal() {
|
||||
pvalue.Content += ixml.Tag(ixml.PRINCIPAL_TG, "")
|
||||
}
|
||||
} else {
|
||||
// resourcetype must be returned empty for non-collection elements
|
||||
pvalue.Content, pfound = "", true
|
||||
}
|
||||
case ixml.CURRENT_USER_PRINCIPAL_TG:
|
||||
if global.User != nil {
|
||||
path := fmt.Sprintf("/%s/", global.User.Name)
|
||||
pvalue.Content, pfound = ixml.HrefTag(path), true
|
||||
}
|
||||
case ixml.SUPPORTED_CALENDAR_COMPONENT_SET_TG:
|
||||
if resource.IsCollection() {
|
||||
for _, component := range resource.GetSupportedComponents() {
|
||||
// TODO: use ixml somehow to build the below tag
|
||||
compTag := fmt.Sprintf(`<C:comp name="%s"/>`, component)
|
||||
pvalue.Contents = append(pvalue.Contents, compTag)
|
||||
}
|
||||
pfound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !pfound {
|
||||
pvalue.Status = http.StatusNotFound
|
||||
}
|
||||
if !pfound {
|
||||
pvalue.Status = http.StatusNotFound
|
||||
}
|
||||
|
||||
result.Add(pvalue)
|
||||
}
|
||||
result.Add(pvalue)
|
||||
}
|
||||
|
||||
return result
|
||||
return result
|
||||
}
|
||||
|
||||
// Adds a new `msResponse` to the `Responses` array.
|
||||
func (ms *multistatusResp) AddResponse(href string, found bool, propstats msPropstats) {
|
||||
ms.Responses = append(ms.Responses, msResponse{
|
||||
Href: href,
|
||||
Found: found,
|
||||
Propstats: propstats,
|
||||
})
|
||||
ms.Responses = append(ms.Responses, msResponse{
|
||||
Href: href,
|
||||
Found: found,
|
||||
Propstats: propstats,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *multistatusResp) ToXML() string {
|
||||
// init multistatus
|
||||
var bf lib.StringBuffer
|
||||
bf.Write(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
bf.Write(`<D:multistatus %s>`, ixml.Namespaces())
|
||||
// init multistatus
|
||||
var bf lib.StringBuffer
|
||||
bf.Write(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
bf.Write(`<D:multistatus %s>`, ixml.Namespaces())
|
||||
|
||||
// iterate over event hrefs and build multistatus XML on the fly
|
||||
for _, response := range ms.Responses {
|
||||
bf.Write("<D:response>")
|
||||
bf.Write(ixml.HrefTag(response.Href))
|
||||
// iterate over event hrefs and build multistatus XML on the fly
|
||||
for _, response := range ms.Responses {
|
||||
bf.Write("<D:response>")
|
||||
bf.Write(ixml.HrefTag(response.Href))
|
||||
|
||||
if response.Found {
|
||||
propstats := response.Propstats.Clone()
|
||||
if response.Found {
|
||||
propstats := response.Propstats.Clone()
|
||||
|
||||
if ms.Minimal {
|
||||
delete(propstats, http.StatusNotFound)
|
||||
if ms.Minimal {
|
||||
delete(propstats, http.StatusNotFound)
|
||||
|
||||
if len(propstats) == 0 {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop/>")
|
||||
bf.Write(ixml.StatusTag(http.StatusOK))
|
||||
bf.Write("</D:propstat>")
|
||||
bf.Write("</D:response>")
|
||||
if len(propstats) == 0 {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop/>")
|
||||
bf.Write(ixml.StatusTag(http.StatusOK))
|
||||
bf.Write("</D:propstat>")
|
||||
bf.Write("</D:response>")
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for status, props := range propstats {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop>")
|
||||
for _, prop := range props {
|
||||
bf.Write(ms.propToXML(prop))
|
||||
}
|
||||
bf.Write("</D:prop>")
|
||||
bf.Write(ixml.StatusTag(status))
|
||||
bf.Write("</D:propstat>")
|
||||
}
|
||||
} else {
|
||||
// if does not find the resource set 404
|
||||
bf.Write(ixml.StatusTag(http.StatusNotFound))
|
||||
}
|
||||
bf.Write("</D:response>")
|
||||
}
|
||||
bf.Write("</D:multistatus>")
|
||||
for status, props := range propstats {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop>")
|
||||
for _, prop := range props {
|
||||
bf.Write(ms.propToXML(prop))
|
||||
}
|
||||
bf.Write("</D:prop>")
|
||||
bf.Write(ixml.StatusTag(status))
|
||||
bf.Write("</D:propstat>")
|
||||
}
|
||||
} else {
|
||||
// if does not find the resource set 404
|
||||
bf.Write(ixml.StatusTag(http.StatusNotFound))
|
||||
}
|
||||
bf.Write("</D:response>")
|
||||
}
|
||||
bf.Write("</D:multistatus>")
|
||||
|
||||
return bf.String()
|
||||
return bf.String()
|
||||
}
|
||||
|
||||
func (ms *multistatusResp) propToXML(prop msProp) string {
|
||||
for _, content := range prop.Contents {
|
||||
prop.Content += content
|
||||
}
|
||||
xmlString := ixml.Tag(prop.Tag, prop.Content)
|
||||
return xmlString
|
||||
for _, content := range prop.Contents {
|
||||
prop.Content += content
|
||||
}
|
||||
xmlString := ixml.Tag(prop.Tag, prop.Content)
|
||||
return xmlString
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type notImplementedHandler struct {
|
||||
response *Response
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (h notImplementedHandler) Handle() *Response {
|
||||
return h.response.Set(http.StatusNotImplemented, "")
|
||||
return h.response.Set(http.StatusNotImplemented, "")
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type optionsHandler struct {
|
||||
response *Response
|
||||
response *Response
|
||||
}
|
||||
|
||||
// Returns the allowed methods and the DAV features implemented by the current server.
|
||||
// For more information about the values and format read RFC4918 Sections 10.1 and 18.
|
||||
func (oh optionsHandler) Handle() *Response {
|
||||
// Set the DAV compliance header:
|
||||
// 1: Server supports all the requirements specified in RFC2518
|
||||
// 3: Server supports all the revisions specified in RFC4918
|
||||
// calendar-access: Server supports all the extensions specified in RFC4791
|
||||
oh.response.SetHeader("DAV", "1, 3, calendar-access").
|
||||
SetHeader("Allow", "GET, HEAD, PUT, DELETE, OPTIONS, PROPFIND, REPORT").
|
||||
Set(http.StatusOK, "")
|
||||
// Set the DAV compliance header:
|
||||
// 1: Server supports all the requirements specified in RFC2518
|
||||
// 3: Server supports all the revisions specified in RFC4918
|
||||
// calendar-access: Server supports all the extensions specified in RFC4791
|
||||
oh.response.SetHeader("DAV", "1, 3, calendar-access").
|
||||
SetHeader("Allow", "GET, HEAD, PUT, DELETE, OPTIONS, PROPFIND, REPORT").
|
||||
Set(http.StatusOK, "")
|
||||
|
||||
return oh.response
|
||||
return oh.response
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type requestPreconditions struct {
|
||||
request *http.Request
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfMatch(etag string) bool {
|
||||
etagMatch := p.request.Header["If-Match"]
|
||||
return len(etagMatch) == 0 || etagMatch[0] == "*" || etagMatch[0] == etag
|
||||
etagMatch := p.request.Header["If-Match"]
|
||||
return len(etagMatch) == 0 || etagMatch[0] == "*" || etagMatch[0] == etag
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfMatchPresent() bool {
|
||||
return len(p.request.Header["If-Match"]) != 0
|
||||
return len(p.request.Header["If-Match"]) != 0
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfNoneMatch(value string) bool {
|
||||
valueMatch := p.request.Header["If-None-Match"]
|
||||
return len(valueMatch) == 1 && valueMatch[0] == value
|
||||
valueMatch := p.request.Header["If-None-Match"]
|
||||
return len(valueMatch) == 1 && valueMatch[0] == value
|
||||
}
|
||||
|
|
|
@ -1,49 +1,49 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/xml"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"encoding/xml"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type propfindHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (ph propfindHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(ph.request)
|
||||
header := headers{ph.request.Header}
|
||||
requestBody := readRequestBody(ph.request)
|
||||
header := headers{ph.request.Header}
|
||||
|
||||
// get the target resources based on the request URL
|
||||
resources, err := global.Storage.GetResources(ph.request.URL.Path, header.IsDeep())
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
// get the target resources based on the request URL
|
||||
resources, err := global.Storage.GetResources(ph.request.URL.Path, header.IsDeep())
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
// read body string to xml struct
|
||||
type XMLProp2 struct {
|
||||
Tags []xml.Name `xml:",any"`
|
||||
}
|
||||
type XMLRoot2 struct {
|
||||
XMLName xml.Name
|
||||
Prop XMLProp2 `xml:"DAV: prop"`
|
||||
}
|
||||
var requestXML XMLRoot2
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
// read body string to xml struct
|
||||
type XMLProp2 struct {
|
||||
Tags []xml.Name `xml:",any"`
|
||||
}
|
||||
type XMLRoot2 struct {
|
||||
XMLName xml.Name
|
||||
Prop XMLProp2 `xml:"DAV: prop"`
|
||||
}
|
||||
var requestXML XMLRoot2
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, resource := range resources {
|
||||
propstats := multistatus.Propstats(&resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(resource.Path, true, propstats)
|
||||
}
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, resource := range resources {
|
||||
propstats := multistatus.Propstats(&resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(resource.Path, true, propstats)
|
||||
}
|
||||
|
||||
if multistatus.Minimal {
|
||||
ph.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
if multistatus.Minimal {
|
||||
ph.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
|
||||
return ph.response.Set(207, multistatus.ToXML())
|
||||
return ph.response.Set(207, multistatus.ToXML())
|
||||
}
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type putHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (ph putHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(ph.request)
|
||||
precond := requestPreconditions{ph.request}
|
||||
success := false
|
||||
requestBody := readRequestBody(ph.request)
|
||||
precond := requestPreconditions{ph.request}
|
||||
success := false
|
||||
|
||||
// check if resource exists
|
||||
resourcePath := ph.request.URL.Path
|
||||
resource, found, err := global.Storage.GetShallowResource(resourcePath)
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
// check if resource exists
|
||||
resourcePath := ph.request.URL.Path
|
||||
resource, found, err := global.Storage.GetShallowResource(resourcePath)
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
// PUT is allowed in 2 cases:
|
||||
//
|
||||
// 1. Item NOT FOUND and there is NO ETAG match header: CREATE a new item
|
||||
if !found && !precond.IfMatchPresent() {
|
||||
// create new event resource
|
||||
resource, err = global.Storage.CreateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
// PUT is allowed in 2 cases:
|
||||
//
|
||||
// 1. Item NOT FOUND and there is NO ETAG match header: CREATE a new item
|
||||
if !found && !precond.IfMatchPresent() {
|
||||
// create new event resource
|
||||
resource, err = global.Storage.CreateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
success = true
|
||||
}
|
||||
success = true
|
||||
}
|
||||
|
||||
if found {
|
||||
// TODO: Handle PUT on collections
|
||||
if resource.IsCollection() {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
if found {
|
||||
// TODO: Handle PUT on collections
|
||||
if resource.IsCollection() {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
// 2. Item exists, the resource etag is verified and there's no IF-NONE-MATCH=* header: UPDATE the item
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if found && precond.IfMatch(resourceEtag) && !precond.IfNoneMatch("*") {
|
||||
// update resource
|
||||
resource, err = global.Storage.UpdateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
// 2. Item exists, the resource etag is verified and there's no IF-NONE-MATCH=* header: UPDATE the item
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if found && precond.IfMatch(resourceEtag) && !precond.IfNoneMatch("*") {
|
||||
// update resource
|
||||
resource, err = global.Storage.UpdateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
success = true
|
||||
}
|
||||
}
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
if !success {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
return ph.response.SetHeader("ETag", resourceEtag).
|
||||
Set(http.StatusCreated, "")
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
return ph.response.SetHeader("ETag", resourceEtag).
|
||||
Set(http.StatusCreated, "")
|
||||
}
|
||||
|
|
|
@ -1,97 +1,97 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"net/http"
|
||||
"encoding/xml"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
)
|
||||
|
||||
type reportHandler struct{
|
||||
request *http.Request
|
||||
response *Response
|
||||
type reportHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
// See more at RFC4791#section-7.1
|
||||
func (rh reportHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(rh.request)
|
||||
header := headers{rh.request.Header}
|
||||
requestBody := readRequestBody(rh.request)
|
||||
header := headers{rh.request.Header}
|
||||
|
||||
urlResource, found, err := global.Storage.GetShallowResource(rh.request.URL.Path)
|
||||
if !found {
|
||||
return rh.response.Set(http.StatusNotFound, "")
|
||||
} else if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
urlResource, found, err := global.Storage.GetShallowResource(rh.request.URL.Path)
|
||||
if !found {
|
||||
return rh.response.Set(http.StatusNotFound, "")
|
||||
} else if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
|
||||
// read body string to xml struct
|
||||
var requestXML reportRootXML
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
// read body string to xml struct
|
||||
var requestXML reportRootXML
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
|
||||
// The resources to be reported are fetched by the type of the request. If it is
|
||||
// a `calendar-multiget`, the resources come based on a set of `hrefs` in the request body.
|
||||
// If it is a `calendar-query`, the resources are calculated based on set of filters in the request.
|
||||
var resourcesToReport []reportRes
|
||||
switch requestXML.XMLName {
|
||||
case ixml.CALENDAR_MULTIGET_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByList(urlResource, requestXML.Hrefs)
|
||||
case ixml.CALENDAR_QUERY_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByFilters(urlResource, requestXML.Filters)
|
||||
default:
|
||||
return rh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
// The resources to be reported are fetched by the type of the request. If it is
|
||||
// a `calendar-multiget`, the resources come based on a set of `hrefs` in the request body.
|
||||
// If it is a `calendar-query`, the resources are calculated based on set of filters in the request.
|
||||
var resourcesToReport []reportRes
|
||||
switch requestXML.XMLName {
|
||||
case ixml.CALENDAR_MULTIGET_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByList(urlResource, requestXML.Hrefs)
|
||||
case ixml.CALENDAR_QUERY_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByFilters(urlResource, requestXML.Filters)
|
||||
default:
|
||||
return rh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, r := range resourcesToReport {
|
||||
propstats := multistatus.Propstats(r.resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(r.href, r.found, propstats)
|
||||
}
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, r := range resourcesToReport {
|
||||
propstats := multistatus.Propstats(r.resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(r.href, r.found, propstats)
|
||||
}
|
||||
|
||||
if multistatus.Minimal {
|
||||
rh.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
if multistatus.Minimal {
|
||||
rh.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
|
||||
return rh.response.Set(207, multistatus.ToXML())
|
||||
return rh.response.Set(207, multistatus.ToXML())
|
||||
}
|
||||
|
||||
type reportPropXML struct {
|
||||
Tags []xml.Name `xml:",any"`
|
||||
Tags []xml.Name `xml:",any"`
|
||||
}
|
||||
|
||||
type reportRootXML struct {
|
||||
XMLName xml.Name
|
||||
Prop reportPropXML `xml:"DAV: prop"`
|
||||
Hrefs []string `xml:"DAV: href"`
|
||||
Filters reportFilterXML `xml:"urn:ietf:params:xml:ns:caldav filter"`
|
||||
XMLName xml.Name
|
||||
Prop reportPropXML `xml:"DAV: prop"`
|
||||
Hrefs []string `xml:"DAV: href"`
|
||||
Filters reportFilterXML `xml:"urn:ietf:params:xml:ns:caldav filter"`
|
||||
}
|
||||
|
||||
type reportFilterXML struct {
|
||||
XMLName xml.Name
|
||||
InnerContent string `xml:",innerxml"`
|
||||
XMLName xml.Name
|
||||
InnerContent string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (this reportFilterXML) toString() string {
|
||||
return fmt.Sprintf("<%s>%s</%s>", this.XMLName.Local, this.InnerContent, this.XMLName.Local)
|
||||
func (rfXml reportFilterXML) toString() string {
|
||||
return fmt.Sprintf("<%s>%s</%s>", rfXml.XMLName.Local, rfXml.InnerContent, rfXml.XMLName.Local)
|
||||
}
|
||||
|
||||
// Wraps a resource that has to be reported, either fetched by filters or by a list.
|
||||
// Basically it contains the original requested `href`, the actual `resource` (can be nil)
|
||||
// and if the `resource` was `found` or not
|
||||
type reportRes struct {
|
||||
href string
|
||||
resource *data.Resource
|
||||
found bool
|
||||
href string
|
||||
resource *data.Resource
|
||||
found bool
|
||||
}
|
||||
|
||||
// The resources are fetched based on the origin resource and a set of filters.
|
||||
|
@ -102,26 +102,26 @@ type reportRes struct {
|
|||
// If the origin resource is not a collection, the function just returns it and ignore any filter processing.
|
||||
// [See RFC4791#section-7.8]
|
||||
func (rh reportHandler) fetchResourcesByFilters(origin *data.Resource, filtersXML reportFilterXML) ([]reportRes, error) {
|
||||
// The list of resources that has to be reported back in the response.
|
||||
reps := []reportRes{}
|
||||
// The list of resources that has to be reported back in the response.
|
||||
reps := []reportRes{}
|
||||
|
||||
if origin.IsCollection() {
|
||||
filters, _ := data.ParseResourceFilters(filtersXML.toString())
|
||||
resources, err := global.Storage.GetResourcesByFilters(origin.Path, filters)
|
||||
if origin.IsCollection() {
|
||||
filters, _ := data.ParseResourceFilters(filtersXML.toString())
|
||||
resources, err := global.Storage.GetResourcesByFilters(origin.Path, filters)
|
||||
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
|
||||
for _, resource := range resources {
|
||||
reps = append(reps, reportRes{resource.Path, &resource, true})
|
||||
}
|
||||
} else {
|
||||
// the origin resource is not a collection, so returns just that as the result
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
for _, resource := range resources {
|
||||
reps = append(reps, reportRes{resource.Path, &resource, true})
|
||||
}
|
||||
} else {
|
||||
// the origin resource is not a collection, so returns just that as the result
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
|
||||
return reps, nil
|
||||
return reps, nil
|
||||
}
|
||||
|
||||
// The hrefs can come from (1) the request URL or (2) from the request body itself.
|
||||
|
@ -132,37 +132,37 @@ func (rh reportHandler) fetchResourcesByFilters(origin *data.Resource, filtersXM
|
|||
// and ignore any othre requested hrefs that might be present in the request body.
|
||||
// [See RFC4791#section-7.9]
|
||||
func (rh reportHandler) fetchResourcesByList(origin *data.Resource, requestedPaths []string) ([]reportRes, error) {
|
||||
reps := []reportRes{}
|
||||
reps := []reportRes{}
|
||||
|
||||
if origin.IsCollection() {
|
||||
resources, err := global.Storage.GetResourcesByList(requestedPaths)
|
||||
if origin.IsCollection() {
|
||||
resources, err := global.Storage.GetResourcesByList(requestedPaths)
|
||||
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
|
||||
// we put all the resources found in a map path -> resource.
|
||||
// this will be used later to query which requested resource was found
|
||||
// or not and mount the response
|
||||
resourcesMap := make(map[string]*data.Resource)
|
||||
for _, resource := range resources {
|
||||
r := resource
|
||||
resourcesMap[resource.Path] = &r
|
||||
}
|
||||
// we put all the resources found in a map path -> resource.
|
||||
// this will be used later to query which requested resource was found
|
||||
// or not and mount the response
|
||||
resourcesMap := make(map[string]*data.Resource)
|
||||
for _, resource := range resources {
|
||||
r := resource
|
||||
resourcesMap[resource.Path] = &r
|
||||
}
|
||||
|
||||
for _, requestedPath := range requestedPaths {
|
||||
// if the requested path does not belong to the origin collection, skip
|
||||
// ('belonging' means that the path's prefix is the same as the collection path)
|
||||
if !strings.HasPrefix(requestedPath, origin.Path) {
|
||||
continue
|
||||
}
|
||||
for _, requestedPath := range requestedPaths {
|
||||
// if the requested path does not belong to the origin collection, skip
|
||||
// ('belonging' means that the path's prefix is the same as the collection path)
|
||||
if !strings.HasPrefix(requestedPath, origin.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
resource, found := resourcesMap[requestedPath]
|
||||
reps = append(reps, reportRes{requestedPath, resource, found})
|
||||
}
|
||||
} else {
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
resource, found := resourcesMap[requestedPath]
|
||||
reps = append(reps, reportRes{requestedPath, resource, found})
|
||||
}
|
||||
} else {
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
|
||||
return reps, nil
|
||||
return reps, nil
|
||||
}
|
||||
|
|
|
@ -1,65 +1,72 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Response represents the handled CalDAV response. Used this when one needs to proxy the generated
|
||||
// response before being sent back to the client.
|
||||
type Response struct {
|
||||
Status int
|
||||
Header http.Header
|
||||
Body string
|
||||
Error error
|
||||
Status int
|
||||
Header http.Header
|
||||
Body string
|
||||
Error error
|
||||
}
|
||||
|
||||
// NewResponse initializes a new response object.
|
||||
func NewResponse() *Response {
|
||||
return &Response{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
return &Response{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
func (this *Response) Set(status int, body string) *Response {
|
||||
this.Status = status
|
||||
this.Body = body
|
||||
// Set sets the the status and body of the response.
|
||||
func (r *Response) Set(status int, body string) *Response {
|
||||
r.Status = status
|
||||
r.Body = body
|
||||
|
||||
return this
|
||||
return r
|
||||
}
|
||||
|
||||
func (this *Response) SetHeader(key, value string) *Response {
|
||||
this.Header.Set(key, value)
|
||||
// SetHeader adds a header to the response.
|
||||
func (r *Response) SetHeader(key, value string) *Response {
|
||||
r.Header.Set(key, value)
|
||||
|
||||
return this
|
||||
return r
|
||||
}
|
||||
|
||||
func (this *Response) SetError(err error) *Response {
|
||||
this.Error = err
|
||||
// SetError sets the response as an error. It inflects the response status based on the provided error.
|
||||
func (r *Response) SetError(err error) *Response {
|
||||
r.Error = err
|
||||
|
||||
switch err {
|
||||
case errs.ResourceNotFoundError:
|
||||
this.Status = http.StatusNotFound
|
||||
case errs.UnauthorizedError:
|
||||
this.Status = http.StatusUnauthorized
|
||||
case errs.ForbiddenError:
|
||||
this.Status = http.StatusForbidden
|
||||
default:
|
||||
this.Status = http.StatusInternalServerError
|
||||
}
|
||||
switch err {
|
||||
case errs.ResourceNotFoundError:
|
||||
r.Status = http.StatusNotFound
|
||||
case errs.UnauthorizedError:
|
||||
r.Status = http.StatusUnauthorized
|
||||
case errs.ForbiddenError:
|
||||
r.Status = http.StatusForbidden
|
||||
default:
|
||||
r.Status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return this
|
||||
return r
|
||||
}
|
||||
|
||||
func (this *Response) Write(writer http.ResponseWriter) {
|
||||
if this.Error == errs.UnauthorizedError {
|
||||
this.SetHeader("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
}
|
||||
// Write writes the response back to the client using the provided `ResponseWriter`.
|
||||
func (r *Response) Write(writer http.ResponseWriter) {
|
||||
if r.Error == errs.UnauthorizedError {
|
||||
r.SetHeader("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
}
|
||||
|
||||
for key, values := range this.Header {
|
||||
for _, value := range values {
|
||||
writer.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
writer.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteHeader(this.Status)
|
||||
io.WriteString(writer, this.Body)
|
||||
writer.WriteHeader(r.Status)
|
||||
io.WriteString(writer, r.Body)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"io/ioutil"
|
||||
"bytes"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Supported ICal components on this server.
|
||||
var supportedComponents = []string{lib.VCALENDAR, lib.VEVENT}
|
||||
|
||||
// This function reads the request body and restore its content, so that
|
||||
// the request body can be read a second time.
|
||||
func readRequestBody(request *http.Request) string {
|
||||
// Read the content
|
||||
body, _ := ioutil.ReadAll(request.Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
// Use the content
|
||||
return string(body)
|
||||
// Read the content
|
||||
body, _ := ioutil.ReadAll(request.Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
// Use the content
|
||||
return string(body)
|
||||
}
|
||||
|
|
|
@ -1,93 +1,94 @@
|
|||
package ixml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"net/http"
|
||||
"encoding/xml"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
const (
|
||||
DAV_NS = "DAV:"
|
||||
CALDAV_NS = "urn:ietf:params:xml:ns:caldav"
|
||||
CALSERV_NS = "http://calendarserver.org/ns/"
|
||||
DAV_NS = "DAV:"
|
||||
CALDAV_NS = "urn:ietf:params:xml:ns:caldav"
|
||||
CALSERV_NS = "http://calendarserver.org/ns/"
|
||||
)
|
||||
|
||||
var NS_PREFIXES = map[string]string{
|
||||
DAV_NS: "D",
|
||||
CALDAV_NS: "C",
|
||||
CALSERV_NS: "CS",
|
||||
DAV_NS: "D",
|
||||
CALDAV_NS: "C",
|
||||
CALSERV_NS: "CS",
|
||||
}
|
||||
|
||||
var (
|
||||
CALENDAR_TG = xml.Name{CALDAV_NS, "calendar"}
|
||||
CALENDAR_DATA_TG = xml.Name{CALDAV_NS, "calendar-data"}
|
||||
CALENDAR_HOME_SET_TG = xml.Name{CALDAV_NS, "calendar-home-set"}
|
||||
CALENDAR_QUERY_TG = xml.Name{CALDAV_NS, "calendar-query"}
|
||||
CALENDAR_MULTIGET_TG = xml.Name{CALDAV_NS, "calendar-multiget"}
|
||||
CALENDAR_USER_ADDRESS_SET_TG = xml.Name{CALDAV_NS, "calendar-user-address-set"}
|
||||
COLLECTION_TG = xml.Name{DAV_NS, "collection"}
|
||||
CURRENT_USER_PRINCIPAL_TG = xml.Name{DAV_NS, "current-user-principal"}
|
||||
DISPLAY_NAME_TG = xml.Name{DAV_NS, "displayname"}
|
||||
GET_CONTENT_LENGTH_TG = xml.Name{DAV_NS, "getcontentlength"}
|
||||
GET_CONTENT_TYPE_TG = xml.Name{DAV_NS, "getcontenttype"}
|
||||
GET_CTAG_TG = xml.Name{CALSERV_NS, "getctag"}
|
||||
GET_ETAG_TG = xml.Name{DAV_NS, "getetag"}
|
||||
GET_LAST_MODIFIED_TG = xml.Name{DAV_NS, "getlastmodified"}
|
||||
HREF_TG = xml.Name{DAV_NS, "href"}
|
||||
OWNER_TG = xml.Name{DAV_NS, "owner"}
|
||||
PRINCIPAL_TG = xml.Name{DAV_NS, "principal"}
|
||||
PRINCIPAL_COLLECTION_SET_TG = xml.Name{DAV_NS, "principal-collection-set"}
|
||||
PRINCIPAL_URL_TG = xml.Name{DAV_NS, "principal-URL"}
|
||||
RESOURCE_TYPE_TG = xml.Name{DAV_NS, "resourcetype"}
|
||||
STATUS_TG = xml.Name{DAV_NS, "status"}
|
||||
SUPPORTED_CALENDAR_COMPONENT_SET_TG = xml.Name{CALDAV_NS, "supported-calendar-component-set"}
|
||||
CALENDAR_TG = xml.Name{CALDAV_NS, "calendar"}
|
||||
CALENDAR_DATA_TG = xml.Name{CALDAV_NS, "calendar-data"}
|
||||
CALENDAR_HOME_SET_TG = xml.Name{CALDAV_NS, "calendar-home-set"}
|
||||
CALENDAR_QUERY_TG = xml.Name{CALDAV_NS, "calendar-query"}
|
||||
CALENDAR_MULTIGET_TG = xml.Name{CALDAV_NS, "calendar-multiget"}
|
||||
CALENDAR_USER_ADDRESS_SET_TG = xml.Name{CALDAV_NS, "calendar-user-address-set"}
|
||||
COLLECTION_TG = xml.Name{DAV_NS, "collection"}
|
||||
CURRENT_USER_PRINCIPAL_TG = xml.Name{DAV_NS, "current-user-principal"}
|
||||
DISPLAY_NAME_TG = xml.Name{DAV_NS, "displayname"}
|
||||
GET_CONTENT_LENGTH_TG = xml.Name{DAV_NS, "getcontentlength"}
|
||||
GET_CONTENT_TYPE_TG = xml.Name{DAV_NS, "getcontenttype"}
|
||||
GET_CTAG_TG = xml.Name{CALSERV_NS, "getctag"}
|
||||
GET_ETAG_TG = xml.Name{DAV_NS, "getetag"}
|
||||
GET_LAST_MODIFIED_TG = xml.Name{DAV_NS, "getlastmodified"}
|
||||
HREF_TG = xml.Name{DAV_NS, "href"}
|
||||
OWNER_TG = xml.Name{DAV_NS, "owner"}
|
||||
PRINCIPAL_TG = xml.Name{DAV_NS, "principal"}
|
||||
PRINCIPAL_COLLECTION_SET_TG = xml.Name{DAV_NS, "principal-collection-set"}
|
||||
PRINCIPAL_URL_TG = xml.Name{DAV_NS, "principal-URL"}
|
||||
RESOURCE_TYPE_TG = xml.Name{DAV_NS, "resourcetype"}
|
||||
STATUS_TG = xml.Name{DAV_NS, "status"}
|
||||
SUPPORTED_CALENDAR_COMPONENT_SET_TG = xml.Name{CALDAV_NS, "supported-calendar-component-set"}
|
||||
)
|
||||
|
||||
// Namespaces returns the default XML namespaces in for CalDAV contents.
|
||||
func Namespaces() string {
|
||||
bf := new(lib.StringBuffer)
|
||||
bf.Write(`xmlns:%s="%s" `, NS_PREFIXES[DAV_NS], DAV_NS)
|
||||
bf.Write(`xmlns:%s="%s" `, NS_PREFIXES[CALDAV_NS], CALDAV_NS)
|
||||
bf.Write(`xmlns:%s="%s"`, NS_PREFIXES[CALSERV_NS], CALSERV_NS)
|
||||
bf := new(lib.StringBuffer)
|
||||
bf.Write(`xmlns:%s="%s" `, NS_PREFIXES[DAV_NS], DAV_NS)
|
||||
bf.Write(`xmlns:%s="%s" `, NS_PREFIXES[CALDAV_NS], CALDAV_NS)
|
||||
bf.Write(`xmlns:%s="%s"`, NS_PREFIXES[CALSERV_NS], CALSERV_NS)
|
||||
|
||||
return bf.String()
|
||||
return bf.String()
|
||||
}
|
||||
|
||||
// Tag returns a XML tag as string based on the given tag name and content. It
|
||||
// takes in consideration the namespace and also if it is an empty content or not.
|
||||
func Tag(xmlName xml.Name, content string) string {
|
||||
name := xmlName.Local
|
||||
ns := NS_PREFIXES[xmlName.Space]
|
||||
name := xmlName.Local
|
||||
ns := NS_PREFIXES[xmlName.Space]
|
||||
|
||||
if ns != "" {
|
||||
ns = ns + ":"
|
||||
}
|
||||
if ns != "" {
|
||||
ns = ns + ":"
|
||||
}
|
||||
|
||||
if content != "" {
|
||||
return fmt.Sprintf("<%s%s>%s</%s%s>", ns, name, content, ns, name)
|
||||
} else {
|
||||
return fmt.Sprintf("<%s%s/>", ns, name)
|
||||
}
|
||||
if content != "" {
|
||||
return fmt.Sprintf("<%s%s>%s</%s%s>", ns, name, content, ns, name)
|
||||
} else {
|
||||
return fmt.Sprintf("<%s%s/>", ns, name)
|
||||
}
|
||||
}
|
||||
|
||||
// HrefTag returns a DAV <D:href> tag with the given href path.
|
||||
func HrefTag(href string) (tag string) {
|
||||
return Tag(HREF_TG, href)
|
||||
return Tag(HREF_TG, href)
|
||||
}
|
||||
|
||||
// StatusTag returns a DAV <D:status> tag with the given HTTP status. The
|
||||
// status is translated into a label, e.g.: HTTP/1.1 404 NotFound.
|
||||
func StatusTag(status int) string {
|
||||
statusText := fmt.Sprintf("HTTP/1.1 %d %s", status, http.StatusText(status))
|
||||
return Tag(STATUS_TG, statusText)
|
||||
statusText := fmt.Sprintf("HTTP/1.1 %d %s", status, http.StatusText(status))
|
||||
return Tag(STATUS_TG, statusText)
|
||||
}
|
||||
|
||||
// EscapeText escapes any special character in the given text and returns the result.
|
||||
func EscapeText(text string) string {
|
||||
buffer := bytes.NewBufferString("")
|
||||
xml.EscapeText(buffer, []byte(text))
|
||||
buffer := bytes.NewBufferString("")
|
||||
xml.EscapeText(buffer, []byte(text))
|
||||
|
||||
return buffer.String()
|
||||
return buffer.String()
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package lib
|
||||
|
||||
const (
|
||||
VCALENDAR = "VCALENDAR"
|
||||
VEVENT = "VEVENT"
|
||||
VJOURNAL = "VJOURNAL"
|
||||
VTODO = "VTODO"
|
||||
VCALENDAR = "VCALENDAR"
|
||||
VEVENT = "VEVENT"
|
||||
VJOURNAL = "VJOURNAL"
|
||||
VTODO = "VTODO"
|
||||
)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func ToSlashPath(path string) string {
|
||||
cleanPath := filepath.Clean(path)
|
||||
return filepath.ToSlash(cleanPath)
|
||||
cleanPath := filepath.Clean(path)
|
||||
return filepath.ToSlash(cleanPath)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type StringBuffer struct {
|
||||
buffer bytes.Buffer
|
||||
buffer bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *StringBuffer) Write(format string, elem ...interface{}) {
|
||||
b.buffer.WriteString(fmt.Sprintf(format, elem...))
|
||||
b.buffer.WriteString(fmt.Sprintf(format, elem...))
|
||||
}
|
||||
|
||||
func (b *StringBuffer) String() string {
|
||||
return b.buffer.String()
|
||||
return b.buffer.String()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
package caldav
|
||||
|
||||
const (
|
||||
VERSION = "3.0.0"
|
||||
VERSION = "3.0.0"
|
||||
)
|
||||
|
|
|
@ -80,7 +80,7 @@ github.com/inconshreveable/mousetrap
|
|||
# github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb
|
||||
github.com/jgautheron/goconst/cmd/goconst
|
||||
github.com/jgautheron/goconst
|
||||
# github.com/labstack/echo/v4 v4.1.5 => /home/konrad/go/src/github.com/labstack/echo
|
||||
# github.com/labstack/echo/v4 v4.1.5 => ../../github.com/labstack/echo
|
||||
github.com/labstack/echo/v4
|
||||
github.com/labstack/echo/v4/middleware
|
||||
# github.com/labstack/gommon v0.2.8
|
||||
|
@ -134,7 +134,7 @@ github.com/prometheus/procfs
|
|||
github.com/prometheus/procfs/nfs
|
||||
github.com/prometheus/procfs/xfs
|
||||
github.com/prometheus/procfs/internal/util
|
||||
# github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
# github.com/samedi/caldav-go v3.0.0+incompatible => ../../github.com/samedi/caldav-go
|
||||
github.com/samedi/caldav-go
|
||||
github.com/samedi/caldav-go/data
|
||||
github.com/samedi/caldav-go/lib
|
||||
|
|
Loading…
Reference in New Issue