mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-05-03 14:32:34 +08:00
🧑💻 Kernel serve CalDAV service on path /caldav/
(#13321)
* 🎨 define the interface of CalDAV * 🎨 Adjust iCalendar files directory structure * 🎨 Implement CalDAV Calendar manage functions * 🎨 Implement CalDAV calendar object manage functions * 🎨 improve ETag scheme
This commit is contained in:
parent
f1984cc22d
commit
62d6c13317
@ -22,6 +22,7 @@ require (
|
|||||||
github.com/denisbrodbeck/machineid v1.0.1
|
github.com/denisbrodbeck/machineid v1.0.1
|
||||||
github.com/dgraph-io/ristretto v1.0.0
|
github.com/dgraph-io/ristretto v1.0.0
|
||||||
github.com/djherbis/times v1.6.0
|
github.com/djherbis/times v1.6.0
|
||||||
|
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6
|
||||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
|
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
|
||||||
github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135
|
github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135
|
||||||
github.com/emirpasic/gods v1.18.1
|
github.com/emirpasic/gods v1.18.1
|
||||||
@ -154,6 +155,7 @@ require (
|
|||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
|
github.com/teambition/rrule-go v1.8.2 // indirect
|
||||||
github.com/tetratelabs/wazero v1.7.3 // indirect
|
github.com/tetratelabs/wazero v1.7.3 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||||
|
@ -89,6 +89,7 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
|
|||||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM=
|
||||||
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
|
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
|
||||||
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
|
||||||
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
|
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg=
|
||||||
@ -375,6 +376,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
|||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
|
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
|
||||||
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||||
|
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
|
||||||
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
|
||||||
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
|
github.com/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw=
|
||||||
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||||
|
779
kernel/model/caldav.go
Normal file
779
kernel/model/caldav.go
Normal file
@ -0,0 +1,779 @@
|
|||||||
|
// SiYuan - Refactor your thinking
|
||||||
|
// Copyright (c) 2020-present, b3log.org
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/88250/gulu"
|
||||||
|
"github.com/emersion/go-ical"
|
||||||
|
"github.com/emersion/go-webdav/caldav"
|
||||||
|
"github.com/siyuan-note/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// REF: https://developers.google.com/calendar/caldav/v2/guide
|
||||||
|
CalDavPrefixPath = "/caldav"
|
||||||
|
CalDavPrincipalsPath = CalDavPrefixPath + "/principals" // 0 resourceTypeRoot
|
||||||
|
CalDavUserPrincipalPath = CalDavPrincipalsPath + "/main" // 1 resourceTypeUserPrincipal
|
||||||
|
CalDavHomeSetPath = CalDavUserPrincipalPath + "/calendars" // 2 resourceTypeCalendarHomeSet
|
||||||
|
CalDavDefaultCalendarPath = CalDavHomeSetPath + "/default" // 3 resourceTypeCalendar
|
||||||
|
|
||||||
|
CalDavDefaultCalendarName = "default"
|
||||||
|
|
||||||
|
CalDavCalendarsMetaDataFilePath = CalDavHomeSetPath + "/calendars.json"
|
||||||
|
|
||||||
|
ICalendarFileExt = "." + ical.Extension // .ics
|
||||||
|
)
|
||||||
|
|
||||||
|
type CalDavPathDepth int
|
||||||
|
|
||||||
|
const (
|
||||||
|
calDavPathDepth_Root CalDavPathDepth = 1 + iota // /caldav
|
||||||
|
calDavPathDepth_Principals // /caldav/principals
|
||||||
|
calDavPathDepth_UserPrincipal // /caldav/principals/main
|
||||||
|
calDavPathDepth_HomeSet // /caldav/principals/main/calendars
|
||||||
|
calDavPathDepth_Calendar // /caldav/principals/main/calendars/default
|
||||||
|
calDavPathDepth_Object // /caldav/principals/main/calendars/default/id.ics
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
calendarMaxResourceSize int64 = 0
|
||||||
|
calendarSupportedComponentSet = []string{"VEVENT", "VTODO"}
|
||||||
|
|
||||||
|
defaultCalendar = caldav.Calendar{
|
||||||
|
Path: CalDavDefaultCalendarPath,
|
||||||
|
Name: CalDavDefaultCalendarName,
|
||||||
|
Description: "Default calendar",
|
||||||
|
MaxResourceSize: calendarMaxResourceSize,
|
||||||
|
SupportedComponentSet: calendarSupportedComponentSet,
|
||||||
|
}
|
||||||
|
calendars = Calendars{
|
||||||
|
loaded: false,
|
||||||
|
changed: false,
|
||||||
|
lock: sync.Mutex{},
|
||||||
|
calendars: sync.Map{},
|
||||||
|
calendarsMetaData: []*caldav.Calendar{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCalDavPathInvalid = errors.New("CalDAV: path is invalid")
|
||||||
|
|
||||||
|
ErrorCalDavCalendarNotFound = errors.New("CalDAV: calendar not found")
|
||||||
|
ErrorCalDavCalendarPathInvalid = errors.New("CalDAV: calendar path is invalid")
|
||||||
|
|
||||||
|
ErrorCalDavCalendarObjectNotFound = errors.New("CalDAV: calendar object not found")
|
||||||
|
ErrorCalDavCalendarObjectPathInvalid = errors.New("CalDAV: calendar object path is invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
// CalendarsMetaDataFilePath returns the absolute path of the calendars' meta data file
|
||||||
|
func CalendarsMetaDataFilePath() string {
|
||||||
|
return DavPath2DirectoryPath(CalDavCalendarsMetaDataFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCalDavPathDepth(urlPath string) CalDavPathDepth {
|
||||||
|
urlPath = PathCleanWithSlash(urlPath)
|
||||||
|
return CalDavPathDepth(len(strings.Split(urlPath, "/")) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCardDavPathDepth parses
|
||||||
|
func ParseCalendarObjectPath(objectPath string) (calendarPath string, objectID string, err error) {
|
||||||
|
calendarPath, objectFileName := path.Split(objectPath)
|
||||||
|
calendarPath = PathCleanWithSlash(calendarPath)
|
||||||
|
objectID = path.Base(objectFileName)
|
||||||
|
objectFileExt := path.Ext(objectFileName)
|
||||||
|
|
||||||
|
if GetCalDavPathDepth(calendarPath) != calDavPathDepth_Calendar {
|
||||||
|
err = ErrorCalDavCalendarPathInvalid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if objectFileExt != ICalendarFileExt {
|
||||||
|
err = ErrorCalDavCalendarObjectPathInvalid
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCalendarObject loads a iCalendar file (*.ics)
|
||||||
|
func LoadCalendarObject(filePath string) (calendar *ical.Calendar, err error) {
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("read iCalendar file [%s] failed: %s", filePath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := ical.NewDecoder(bytes.NewReader(data))
|
||||||
|
calendar, err = decoder.Decode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Calendars struct {
|
||||||
|
loaded bool
|
||||||
|
changed bool
|
||||||
|
lock sync.Mutex // load & save
|
||||||
|
calendars sync.Map // Path -> *Calendar
|
||||||
|
calendarsMetaData []*caldav.Calendar
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) load() error {
|
||||||
|
c.calendars.Clear()
|
||||||
|
|
||||||
|
// load calendars meta data file
|
||||||
|
calendarsMetaDataFilePath := CalendarsMetaDataFilePath()
|
||||||
|
metaData, err := os.ReadFile(calendarsMetaDataFilePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// create & save default calendar
|
||||||
|
c.calendarsMetaData = []*caldav.Calendar{&defaultCalendar}
|
||||||
|
if err := c.saveCalendarsMetaData(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// load meta data file
|
||||||
|
c.calendarsMetaData = []*caldav.Calendar{}
|
||||||
|
if err = gulu.JSON.UnmarshalJSON(metaData, &c.calendarsMetaData); err != nil {
|
||||||
|
logging.LogErrorf("unmarshal address books meta data failed: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load iCalendar files (*.ics)
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(c.calendarsMetaData))
|
||||||
|
for _, calendarMetaData := range c.calendarsMetaData {
|
||||||
|
calendar := &Calendar{
|
||||||
|
Changed: false,
|
||||||
|
DirectoryPath: DavPath2DirectoryPath(calendarMetaData.Path),
|
||||||
|
MetaData: calendarMetaData,
|
||||||
|
Objects: sync.Map{},
|
||||||
|
}
|
||||||
|
c.calendars.Store(calendarMetaData.Path, calendar)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
calendar.load()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
c.loaded = true
|
||||||
|
c.changed = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save all calendars
|
||||||
|
func (c *Calendars) save(force bool) error {
|
||||||
|
if force || c.changed {
|
||||||
|
// save calendars meta data
|
||||||
|
if err := c.saveCalendarsMetaData(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save all calendar object to *.ics files
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
c.calendars.Range(func(path any, calendar any) bool {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// path_ := path.(string)
|
||||||
|
calendar := calendar.(*Calendar)
|
||||||
|
calendar.save(force)
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
wg.Wait()
|
||||||
|
c.changed = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save all calendars meta data
|
||||||
|
func (c *Calendars) saveCalendarsMetaData() error {
|
||||||
|
return SaveMetaData(c.calendarsMetaData, CalendarsMetaDataFilePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) Load() error {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
if !c.loaded {
|
||||||
|
return c.load()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) GetObject(objectPath string) (calendar *Calendar, calendarObject *CalendarObject, err error) {
|
||||||
|
calendarPath, objectID, err := ParseCalendarObjectPath(objectPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("parse calendar object path [%s] failed: %s", objectPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := c.calendars.Load(calendarPath); ok {
|
||||||
|
calendar = value.(*Calendar)
|
||||||
|
} else {
|
||||||
|
err = ErrorCalDavCalendarNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := calendar.Objects.Load(objectID); ok {
|
||||||
|
calendarObject = value.(*CalendarObject)
|
||||||
|
} else {
|
||||||
|
err = ErrorCalDavCalendarObjectNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) DeleteObject(objectPath string) (calendar *Calendar, calendarObject *CalendarObject, err error) {
|
||||||
|
calendarPath, objectID, err := ParseCalendarObjectPath(objectPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("parse calendar object path [%s] failed: %s", objectPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := c.calendars.Load(calendarPath); ok {
|
||||||
|
calendar = value.(*Calendar)
|
||||||
|
} else {
|
||||||
|
err = ErrorCalDavCalendarNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, loaded := calendar.Objects.LoadAndDelete(objectID); loaded {
|
||||||
|
calendarObject = value.(*CalendarObject)
|
||||||
|
} else {
|
||||||
|
err = ErrorCalDavCalendarObjectNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Remove(calendarObject.FilePath); err != nil {
|
||||||
|
logging.LogErrorf("remove file [%s] failed: %s", calendarObject.FilePath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) CreateCalendar(calendarMetaData *caldav.Calendar) (err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
var calendar *Calendar
|
||||||
|
|
||||||
|
// update map
|
||||||
|
if value, ok := c.calendars.Load(calendarMetaData.Path); ok {
|
||||||
|
// update map item
|
||||||
|
calendar = value.(*Calendar)
|
||||||
|
calendar.MetaData = calendarMetaData
|
||||||
|
} else {
|
||||||
|
// insert map item
|
||||||
|
calendar = &Calendar{
|
||||||
|
Changed: false,
|
||||||
|
DirectoryPath: DavPath2DirectoryPath(calendarMetaData.Path),
|
||||||
|
MetaData: calendarMetaData,
|
||||||
|
Objects: sync.Map{},
|
||||||
|
}
|
||||||
|
c.calendars.Store(calendarMetaData.Path, calendar)
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = -1
|
||||||
|
for i, item := range c.calendarsMetaData {
|
||||||
|
if item.Path == calendarMetaData.Path {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if index >= 0 {
|
||||||
|
// update list
|
||||||
|
c.calendarsMetaData[index] = calendarMetaData
|
||||||
|
} else {
|
||||||
|
// insert list
|
||||||
|
c.calendarsMetaData = append(c.calendarsMetaData, calendarMetaData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create calendar directory
|
||||||
|
if err = os.MkdirAll(calendar.DirectoryPath, 0755); err != nil {
|
||||||
|
logging.LogErrorf("create directory [%s] failed: %s", calendar.DirectoryPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save meta data
|
||||||
|
if err = c.saveCalendarsMetaData(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) ListCalendars() (calendars []caldav.Calendar, err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
for _, calendar := range c.calendarsMetaData {
|
||||||
|
calendars = append(calendars, *calendar)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) GetCalendar(calendarPath string) (calendar *caldav.Calendar, err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
if value, ok := calendars.calendars.Load(calendarPath); ok {
|
||||||
|
calendar = value.(*Calendar).MetaData
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ErrorCalDavCalendarNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) DeleteCalendar(calendarPath string) (err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
var calendar *Calendar
|
||||||
|
|
||||||
|
// delete map item
|
||||||
|
if value, loaded := c.calendars.LoadAndDelete(calendarPath); loaded {
|
||||||
|
calendar = value.(*Calendar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete list item
|
||||||
|
for i, item := range c.calendarsMetaData {
|
||||||
|
if item.Path == calendarPath {
|
||||||
|
c.calendarsMetaData = append(c.calendarsMetaData[:i], c.calendarsMetaData[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove address book directory
|
||||||
|
if err = os.RemoveAll(calendar.DirectoryPath); err != nil {
|
||||||
|
logging.LogErrorf("remove directory [%s] failed: %s", calendar.DirectoryPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save meta data
|
||||||
|
if err = c.saveCalendarsMetaData(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) PutCalendarObject(objectPath string, calendarData *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (calendarObject *caldav.CalendarObject, err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
calendarPath, objectID, err := ParseCalendarObjectPath(objectPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("parse calendar object path [%s] failed: %s", objectPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var calendar *Calendar
|
||||||
|
if value, ok := c.calendars.Load(calendarPath); ok {
|
||||||
|
calendar = value.(*Calendar)
|
||||||
|
} else {
|
||||||
|
err = ErrorCalDavCalendarNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 处理 opts.IfNoneMatch (If-None-Match) 与 opts.IfMatch (If-Match)
|
||||||
|
|
||||||
|
var object *CalendarObject
|
||||||
|
if value, ok := calendar.Objects.Load(objectID); ok {
|
||||||
|
object = value.(*CalendarObject)
|
||||||
|
object.Data.Data = calendarData
|
||||||
|
object.Changed = true
|
||||||
|
} else {
|
||||||
|
object = &CalendarObject{
|
||||||
|
Changed: true,
|
||||||
|
FilePath: DavPath2DirectoryPath(objectPath),
|
||||||
|
CalendarPath: calendarPath,
|
||||||
|
Data: &caldav.CalendarObject{
|
||||||
|
Data: calendarData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = object.save(true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = object.update()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar.Objects.Store(objectID, object)
|
||||||
|
calendarObject = object.Data
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) ListCalendarObjects(calendarPath string, req *caldav.CalendarCompRequest) (calendarObjects []caldav.CalendarObject, err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
var calendar *Calendar
|
||||||
|
if value, ok := c.calendars.Load(calendarPath); ok {
|
||||||
|
calendar = value.(*Calendar)
|
||||||
|
} else {
|
||||||
|
err = ErrorCalDavCalendarNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar.Objects.Range(func(id any, object any) bool {
|
||||||
|
// TODO: filter calendar objects' props and comps
|
||||||
|
calendarObjects = append(calendarObjects, *object.(*CalendarObject).Data)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) GetCalendarObject(objectPath string, req *caldav.CalendarCompRequest) (calendarObject *caldav.CalendarObject, err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
_, object, err := c.GetObject(objectPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarObject = object.Data
|
||||||
|
// TODO: filter calendar object's props and comps
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) QueryCalendarObjects(calendarPath string, query *caldav.CalendarQuery) (calendarObjects []caldav.CalendarObject, err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
calendarObjects, err = c.ListCalendarObjects(calendarPath, &query.CompRequest)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarObjects, err = caldav.Filter(query, calendarObjects)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendars) DeleteCalendarObject(objectPath string) (err error) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
_, _, err = c.DeleteObject(objectPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type Calendar struct {
|
||||||
|
Changed bool
|
||||||
|
DirectoryPath string
|
||||||
|
MetaData *caldav.Calendar
|
||||||
|
Objects sync.Map // id -> *CalendarObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Calendar) load() error {
|
||||||
|
if err := os.MkdirAll(c.DirectoryPath, 0755); err != nil {
|
||||||
|
logging.LogErrorf("create directory [%s] failed: %s", c.DirectoryPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(c.DirectoryPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("read dir [%s] failed: %s", c.DirectoryPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
filename := entry.Name()
|
||||||
|
ext := path.Ext(filename)
|
||||||
|
if ext == ICalendarFileExt {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// create & load calendar object
|
||||||
|
calendarObjectFilePath := path.Join(c.DirectoryPath, filename)
|
||||||
|
calendarObject := &CalendarObject{
|
||||||
|
Changed: false,
|
||||||
|
FilePath: calendarObjectFilePath,
|
||||||
|
CalendarPath: c.MetaData.Path,
|
||||||
|
}
|
||||||
|
err = calendarObject.load()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := path.Base(filename)
|
||||||
|
c.Objects.Store(id, calendarObject)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save an calendar to multiple *.ics files
|
||||||
|
func (c *Calendar) save(force bool) error {
|
||||||
|
if force || c.Changed {
|
||||||
|
// create directory
|
||||||
|
if err := os.MkdirAll(c.DirectoryPath, 0755); err != nil {
|
||||||
|
logging.LogErrorf("create directory [%s] failed: %s", c.DirectoryPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
c.Objects.Range(func(id any, object any) bool {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
// id_ := id.(string)
|
||||||
|
object_ := object.(*CalendarObject)
|
||||||
|
object_.save(force)
|
||||||
|
object_.update()
|
||||||
|
}()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
wg.Wait()
|
||||||
|
c.Changed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalendarObject struct {
|
||||||
|
Changed bool
|
||||||
|
FilePath string
|
||||||
|
CalendarPath string
|
||||||
|
Data *caldav.CalendarObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CalendarObject) load() error {
|
||||||
|
// load iCalendar file
|
||||||
|
calendarObjectData, err := LoadCalendarObject(o.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create address object
|
||||||
|
o.Data = &caldav.CalendarObject{
|
||||||
|
Data: calendarObjectData,
|
||||||
|
}
|
||||||
|
|
||||||
|
// update file info
|
||||||
|
err = o.update()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Changed = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save an object to *.ics file
|
||||||
|
func (o *CalendarObject) save(force bool) error {
|
||||||
|
if force || o.Changed {
|
||||||
|
var objectData bytes.Buffer
|
||||||
|
|
||||||
|
// encode data
|
||||||
|
encoder := ical.NewEncoder(&objectData)
|
||||||
|
if err := encoder.Encode(o.Data.Data); err != nil {
|
||||||
|
logging.LogErrorf("encode iCalendar [%s] failed: %s", o.Data.Path, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create directory
|
||||||
|
dirPath := path.Dir(o.FilePath)
|
||||||
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
|
logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write file
|
||||||
|
if err := os.WriteFile(o.FilePath, objectData.Bytes(), 0755); err != nil {
|
||||||
|
logging.LogErrorf("write file [%s] failed: %s", o.FilePath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Changed = false
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// update file info
|
||||||
|
func (o *CalendarObject) update() error {
|
||||||
|
addressFileInfo, err := os.Stat(o.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Data.Path = PathJoinWithSlash(o.CalendarPath, addressFileInfo.Name())
|
||||||
|
o.Data.ModTime = addressFileInfo.ModTime()
|
||||||
|
o.Data.ContentLength = addressFileInfo.Size()
|
||||||
|
o.Data.ETag = FileETag(addressFileInfo)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CalDavBackend struct{}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
|
||||||
|
// logging.LogDebugf("CalDAV CurrentUserPrincipal")
|
||||||
|
return CalDavUserPrincipalPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
|
||||||
|
// logging.LogDebugf("CalDAV CalendarHomeSetPath")
|
||||||
|
return CalDavHomeSetPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) CreateCalendar(ctx context.Context, calendar *caldav.Calendar) (err error) {
|
||||||
|
// logging.LogDebugf("CalDAV CreateCalendar -> calendar: %#v", calendar)
|
||||||
|
calendar.Path = PathCleanWithSlash(calendar.Path)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = calendars.CreateCalendar(calendar)
|
||||||
|
// logging.LogDebugf("CalDAV CreateCalendar <- err: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) ListCalendars(ctx context.Context) (calendars_ []caldav.Calendar, err error) {
|
||||||
|
// logging.LogDebugf("CalDAV ListCalendars")
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendars_, err = calendars.ListCalendars()
|
||||||
|
// logging.LogDebugf("CalDAV ListCalendars <- calendars: %#v, err: %s", calendars_, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) GetCalendar(ctx context.Context, calendarPath string) (calendar *caldav.Calendar, err error) {
|
||||||
|
// logging.LogDebugf("CalDAV GetCalendar -> calendarPath: %s", calendarPath)
|
||||||
|
calendarPath = PathCleanWithSlash(calendarPath)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendar, err = calendars.GetCalendar(calendarPath)
|
||||||
|
// logging.LogDebugf("CalDAV GetCalendar <- calendar: %#v, err: %s", calendar, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) DeleteCalendar(ctx context.Context, calendarPath string) (err error) {
|
||||||
|
// logging.LogDebugf("CalDAV DeleteCalendar -> calendarPath: %s", calendarPath)
|
||||||
|
calendarPath = PathCleanWithSlash(calendarPath)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = calendars.DeleteCalendar(calendarPath)
|
||||||
|
// logging.LogDebugf("CalDAV DeleteCalendar <- err: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) PutCalendarObject(ctx context.Context, objectPath string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (calendarObject *caldav.CalendarObject, err error) {
|
||||||
|
// logging.LogDebugf("CalDAV PutCalendarObject -> objectPath: %s, opts: %#v", objectPath, opts)
|
||||||
|
objectPath = PathCleanWithSlash(objectPath)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarObject, err = calendars.PutCalendarObject(objectPath, calendar, opts)
|
||||||
|
// logging.LogDebugf("CalDAV PutCalendarObject <- calendarObject: %#v, err: %s", calendarObject, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) ListCalendarObjects(ctx context.Context, calendarPath string, req *caldav.CalendarCompRequest) (calendarObjects []caldav.CalendarObject, err error) {
|
||||||
|
// logging.LogDebugf("CalDAV ListCalendarObjects -> calendarPath: %s, req: %#v", calendarPath, req)
|
||||||
|
calendarPath = PathCleanWithSlash(calendarPath)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarObjects, err = calendars.ListCalendarObjects(calendarPath, req)
|
||||||
|
// logging.LogDebugf("CalDAV ListCalendarObjects <- calendarObjects: %#v, err: %s", calendarObjects, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) GetCalendarObject(ctx context.Context, objectPath string, req *caldav.CalendarCompRequest) (calendarObject *caldav.CalendarObject, err error) {
|
||||||
|
// logging.LogDebugf("CalDAV GetCalendarObject -> objectPath: %s, req: %#v", objectPath, req)
|
||||||
|
objectPath = PathCleanWithSlash(objectPath)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarObject, err = calendars.GetCalendarObject(objectPath, req)
|
||||||
|
// logging.LogDebugf("CalDAV GetCalendarObject <- calendarObject: %#v, err: %s", calendarObject, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) QueryCalendarObjects(ctx context.Context, calendarPath string, query *caldav.CalendarQuery) (calendarObjects []caldav.CalendarObject, err error) {
|
||||||
|
// logging.LogDebugf("CalDAV QueryCalendarObjects -> calendarPath: %s, query: %#v", calendarPath, query)
|
||||||
|
calendarPath = PathCleanWithSlash(calendarPath)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
calendarObjects, err = calendars.QueryCalendarObjects(calendarPath, query)
|
||||||
|
// logging.LogDebugf("CalDAV QueryCalendarObjects <- calendarObjects: %#v, err: %s", calendarObjects, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *CalDavBackend) DeleteCalendarObject(ctx context.Context, objectPath string) (err error) {
|
||||||
|
// logging.LogDebugf("CalDAV DeleteCalendarObject -> objectPath: %s", objectPath)
|
||||||
|
objectPath = PathCleanWithSlash(objectPath)
|
||||||
|
|
||||||
|
if err = calendars.Load(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = calendars.DeleteCalendarObject(objectPath)
|
||||||
|
// logging.LogDebugf("CalDAV DeleteCalendarObject <- err: %s", err)
|
||||||
|
return
|
||||||
|
}
|
@ -24,7 +24,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@ -46,25 +45,40 @@ const (
|
|||||||
CardDavDefaultAddressBookName = "default"
|
CardDavDefaultAddressBookName = "default"
|
||||||
|
|
||||||
CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json"
|
CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json"
|
||||||
|
|
||||||
|
VCardFileExt = "." + vcard.Extension // .vcf
|
||||||
)
|
)
|
||||||
|
|
||||||
type PathDepth int
|
type CardDavPathDepth int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
pathDepth_Root PathDepth = 1 + iota // /carddav
|
cardDavPathDepth_Root CardDavPathDepth = 1 + iota // /carddav
|
||||||
pathDepth_Principals // /carddav/principals
|
cardDavPathDepth_Principals // /carddav/principals
|
||||||
pathDepth_UserPrincipal // /carddav/principals/main
|
cardDavPathDepth_UserPrincipal // /carddav/principals/main
|
||||||
pathDepth_HomeSet // /carddav/principals/main/contacts
|
cardDavPathDepth_HomeSet // /carddav/principals/main/contacts
|
||||||
pathDepth_AddressBook // /carddav/principals/main/contacts/default
|
cardDavPathDepth_AddressBook // /carddav/principals/main/contacts/default
|
||||||
pathDepth_Address // /carddav/principals/main/contacts/default/id.vcf
|
cardDavPathDepth_Address // /carddav/principals/main/contacts/default/id.vcf
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
addressBookMaxResourceSize int64 = 0
|
||||||
|
addressBookSupportedAddressData = []carddav.AddressDataType{
|
||||||
|
{
|
||||||
|
ContentType: vcard.MIMEType,
|
||||||
|
Version: "3.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContentType: vcard.MIMEType,
|
||||||
|
Version: "4.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
defaultAddressBook = carddav.AddressBook{
|
defaultAddressBook = carddav.AddressBook{
|
||||||
Path: CardDavDefaultAddressBookPath,
|
Path: CardDavDefaultAddressBookPath,
|
||||||
Name: CardDavDefaultAddressBookName,
|
Name: CardDavDefaultAddressBookName,
|
||||||
Description: "Default address book",
|
Description: "Default address book",
|
||||||
MaxResourceSize: 0,
|
MaxResourceSize: addressBookMaxResourceSize,
|
||||||
|
SupportedAddressData: addressBookSupportedAddressData,
|
||||||
}
|
}
|
||||||
contacts = Contacts{
|
contacts = Contacts{
|
||||||
loaded: false,
|
loaded: false,
|
||||||
@ -74,34 +88,40 @@ var (
|
|||||||
booksMetaData: []*carddav.AddressBook{},
|
booksMetaData: []*carddav.AddressBook{},
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorNotFound = errors.New("CardDAV: not found")
|
ErrorCardDavPathInvalid = errors.New("CardDAV: path is invalid")
|
||||||
ErrorPathInvalid = errors.New("CardDAV: path is invalid")
|
|
||||||
|
|
||||||
ErrorBookNotFound = errors.New("CardDAV: address book not found")
|
ErrorCardDavBookNotFound = errors.New("CardDAV: address book not found")
|
||||||
ErrorBookPathInvalid = errors.New("CardDAV: address book path is invalid")
|
ErrorCardDavBookPathInvalid = errors.New("CardDAV: address book path is invalid")
|
||||||
|
|
||||||
ErrorAddressNotFound = errors.New("CardDAV: address not found")
|
ErrorCardDavAddressNotFound = errors.New("CardDAV: address not found")
|
||||||
ErrorAddressFileExtensionNameInvalid = errors.New("CardDAV: address file extension name is invalid")
|
ErrorCardDavAddressFileExtensionNameInvalid = errors.New("CardDAV: address file extension name is invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
// CardDavPath2DirectoryPath converts CardDAV path to absolute path of the file system
|
// ImportVCardFile imports a address book from a vCard file (*.vcf)
|
||||||
func CardDavPath2DirectoryPath(cardDavPath string) string {
|
func ImportAddressBook(addressBookPath, cardContent string) (addresses []*AddressObject, err error) {
|
||||||
return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(cardDavPath, "/"))
|
// TODO: Check whether the path is valid (PathDepth: Address)
|
||||||
|
// TODO: Check whether the address book exists
|
||||||
|
// TODO: Decode the card content
|
||||||
|
// TODO: Save the cards to the file system
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// HomeSetPathPath returns the absolute path of the address book home set directory
|
// ExportAddressBook exports a address book to a vCard file (*.vcf)
|
||||||
func HomeSetPathPath() string {
|
func ExportAddressBook(addressBookPath string) (cardContent string, err error) {
|
||||||
return CardDavPath2DirectoryPath(CardDavHomeSetPath)
|
// TODO: Check whether the path is valid (PathDepth: AddressBook)
|
||||||
|
// TODO: Check whether the address book exists
|
||||||
|
// TODO: Encode the card content
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddressBooksMetaDataFilePath returns the absolute path of the address books meta data file
|
// AddressBooksMetaDataFilePath returns the absolute path of the address books meta data file
|
||||||
func AddressBooksMetaDataFilePath() string {
|
func AddressBooksMetaDataFilePath() string {
|
||||||
return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath)
|
return DavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPathDepth(urlPath string) PathDepth {
|
func GetCardDavPathDepth(urlPath string) CardDavPathDepth {
|
||||||
urlPath = path.Clean(urlPath)
|
urlPath = PathCleanWithSlash(urlPath)
|
||||||
return PathDepth(len(strings.Split(urlPath, "/")) - 1)
|
return CardDavPathDepth(len(strings.Split(urlPath, "/")) - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseAddressPath parses address path to address book path and address ID
|
// ParseAddressPath parses address path to address book path and address ID
|
||||||
@ -110,13 +130,13 @@ func ParseAddressPath(addressPath string) (addressBookPath string, addressID str
|
|||||||
addressID = path.Base(addressFileName)
|
addressID = path.Base(addressFileName)
|
||||||
addressFileExt := path.Ext(addressFileName)
|
addressFileExt := path.Ext(addressFileName)
|
||||||
|
|
||||||
if GetPathDepth(addressBookPath) != pathDepth_AddressBook {
|
if GetCardDavPathDepth(addressBookPath) != cardDavPathDepth_AddressBook {
|
||||||
err = ErrorBookPathInvalid
|
err = ErrorCardDavBookPathInvalid
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if addressFileExt != ".vcf" {
|
if addressFileExt != VCardFileExt {
|
||||||
err = ErrorAddressFileExtensionNameInvalid
|
err = ErrorCardDavAddressFileExtensionNameInvalid
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -185,9 +205,12 @@ type Contacts struct {
|
|||||||
// load all contacts
|
// load all contacts
|
||||||
func (c *Contacts) load() error {
|
func (c *Contacts) load() error {
|
||||||
c.books.Clear()
|
c.books.Clear()
|
||||||
|
|
||||||
|
// load address books meta data
|
||||||
addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath()
|
addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath()
|
||||||
metaData, err := os.ReadFile(addressBooksMetaDataFilePath)
|
metaData, err := os.ReadFile(addressBooksMetaDataFilePath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
// create & save default address book
|
||||||
c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook}
|
c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook}
|
||||||
if err := c.saveAddressBooksMetaData(); err != nil {
|
if err := c.saveAddressBooksMetaData(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -201,12 +224,13 @@ func (c *Contacts) load() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load vCard files (*.vcf)
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(c.booksMetaData))
|
wg.Add(len(c.booksMetaData))
|
||||||
for _, addressBookMetaData := range c.booksMetaData {
|
for _, addressBookMetaData := range c.booksMetaData {
|
||||||
addressBook := &AddressBook{
|
addressBook := &AddressBook{
|
||||||
Changed: false,
|
Changed: false,
|
||||||
DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path),
|
DirectoryPath: DavPath2DirectoryPath(addressBookMetaData.Path),
|
||||||
MetaData: addressBookMetaData,
|
MetaData: addressBookMetaData,
|
||||||
Addresses: sync.Map{},
|
Addresses: sync.Map{},
|
||||||
}
|
}
|
||||||
@ -251,25 +275,7 @@ func (c *Contacts) save(force bool) error {
|
|||||||
|
|
||||||
// save all contacts
|
// save all contacts
|
||||||
func (c *Contacts) saveAddressBooksMetaData() error {
|
func (c *Contacts) saveAddressBooksMetaData() error {
|
||||||
data, err := gulu.JSON.MarshalIndentJSON(c.booksMetaData, "", " ")
|
return SaveMetaData(c.booksMetaData, AddressBooksMetaDataFilePath())
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("marshal address books meta data failed: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dirPath := HomeSetPathPath()
|
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := AddressBooksMetaDataFilePath()
|
|
||||||
if err := os.WriteFile(filePath, data, 0755); err != nil {
|
|
||||||
logging.LogErrorf("write file [%s] failed: %s", filePath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Contacts) Load() error {
|
func (c *Contacts) Load() error {
|
||||||
@ -292,14 +298,43 @@ func (c *Contacts) GetAddress(addressPath string) (addressBook *AddressBook, add
|
|||||||
if value, ok := c.books.Load(bookPath); ok {
|
if value, ok := c.books.Load(bookPath); ok {
|
||||||
addressBook = value.(*AddressBook)
|
addressBook = value.(*AddressBook)
|
||||||
} else {
|
} else {
|
||||||
err = ErrorBookNotFound
|
err = ErrorCardDavBookNotFound
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if value, ok := addressBook.Addresses.Load(addressID); ok {
|
if value, ok := addressBook.Addresses.Load(addressID); ok {
|
||||||
addressObject = value.(*AddressObject)
|
addressObject = value.(*AddressObject)
|
||||||
} else {
|
} else {
|
||||||
err = ErrorAddressNotFound
|
err = ErrorCardDavAddressNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Contacts) DeleteAddress(addressPath string) (addressBook *AddressBook, addressObject *AddressObject, err error) {
|
||||||
|
bookPath, addressID, err := ParseAddressPath(addressPath)
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := c.books.Load(bookPath); ok {
|
||||||
|
addressBook = value.(*AddressBook)
|
||||||
|
} else {
|
||||||
|
err = ErrorCardDavBookNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, loaded := addressBook.Addresses.LoadAndDelete(addressID); loaded {
|
||||||
|
addressObject = value.(*AddressObject)
|
||||||
|
} else {
|
||||||
|
err = ErrorCardDavAddressNotFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.Remove(addressObject.FilePath); err != nil {
|
||||||
|
logging.LogErrorf("remove file [%s] failed: %s", addressObject.FilePath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,7 +360,7 @@ func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ErrorBookNotFound
|
err = ErrorCardDavBookNotFound
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,7 +379,7 @@ func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) (
|
|||||||
// insert map item
|
// insert map item
|
||||||
addressBook = &AddressBook{
|
addressBook = &AddressBook{
|
||||||
Changed: false,
|
Changed: false,
|
||||||
DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path),
|
DirectoryPath: DavPath2DirectoryPath(addressBookMetaData.Path),
|
||||||
MetaData: addressBookMetaData,
|
MetaData: addressBookMetaData,
|
||||||
Addresses: sync.Map{},
|
Addresses: sync.Map{},
|
||||||
}
|
}
|
||||||
@ -369,7 +404,7 @@ func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) (
|
|||||||
|
|
||||||
// create address book directory
|
// create address book directory
|
||||||
if err = os.MkdirAll(addressBook.DirectoryPath, 0755); err != nil {
|
if err = os.MkdirAll(addressBook.DirectoryPath, 0755); err != nil {
|
||||||
logging.LogErrorf("create directory [%s] failed: %s", addressBook, err)
|
logging.LogErrorf("create directory [%s] failed: %s", addressBook.DirectoryPath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +437,7 @@ func (c *Contacts) DeleteAddressBook(path string) (err error) {
|
|||||||
|
|
||||||
// remove address book directory
|
// remove address book directory
|
||||||
if err = os.RemoveAll(addressBook.DirectoryPath); err != nil {
|
if err = os.RemoveAll(addressBook.DirectoryPath); err != nil {
|
||||||
logging.LogErrorf("remove directory [%s] failed: %s", addressBook, err)
|
logging.LogErrorf("remove directory [%s] failed: %s", addressBook.DirectoryPath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,7 +470,7 @@ func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataR
|
|||||||
if value, ok := c.books.Load(bookPath); ok {
|
if value, ok := c.books.Load(bookPath); ok {
|
||||||
addressBook = value.(*AddressBook)
|
addressBook = value.(*AddressBook)
|
||||||
} else {
|
} else {
|
||||||
err = ErrorBookNotFound
|
err = ErrorCardDavBookNotFound
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,8 +486,8 @@ func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBoo
|
|||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
switch GetPathDepth(urlPath) {
|
switch GetCardDavPathDepth(urlPath) {
|
||||||
case pathDepth_Root, pathDepth_Principals, pathDepth_UserPrincipal, pathDepth_HomeSet:
|
case cardDavPathDepth_Root, cardDavPathDepth_Principals, cardDavPathDepth_UserPrincipal, cardDavPathDepth_HomeSet:
|
||||||
c.books.Range(func(path any, book any) bool {
|
c.books.Range(func(path any, book any) bool {
|
||||||
addressBook := book.(*AddressBook)
|
addressBook := book.(*AddressBook)
|
||||||
addressBook.Addresses.Range(func(id any, address any) bool {
|
addressBook.Addresses.Range(func(id any, address any) bool {
|
||||||
@ -461,7 +496,7 @@ func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBoo
|
|||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
case pathDepth_AddressBook:
|
case cardDavPathDepth_AddressBook:
|
||||||
if value, ok := c.books.Load(urlPath); ok {
|
if value, ok := c.books.Load(urlPath); ok {
|
||||||
addressBook := value.(*AddressBook)
|
addressBook := value.(*AddressBook)
|
||||||
addressBook.Addresses.Range(func(id any, address any) bool {
|
addressBook.Addresses.Range(func(id any, address any) bool {
|
||||||
@ -469,12 +504,12 @@ func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBoo
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case pathDepth_Address:
|
case cardDavPathDepth_Address:
|
||||||
if _, address, _ := c.GetAddress(urlPath); address != nil {
|
if _, address, _ := c.GetAddress(urlPath); address != nil {
|
||||||
addressObjects = append(addressObjects, *address.Data)
|
addressObjects = append(addressObjects, *address.Data)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
err = ErrorPathInvalid
|
err = ErrorCardDavPathInvalid
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -496,25 +531,21 @@ func (c *Contacts) PutAddressObject(addressPath string, card vcard.Card, opts *c
|
|||||||
if value, ok := c.books.Load(bookPath); ok {
|
if value, ok := c.books.Load(bookPath); ok {
|
||||||
addressBook = value.(*AddressBook)
|
addressBook = value.(*AddressBook)
|
||||||
} else {
|
} else {
|
||||||
err = ErrorBookNotFound
|
err = ErrorCardDavBookNotFound
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 处理 opts.IfNoneMatch (If-None-Match) 与 opts.IfMatch (If-Match)
|
||||||
|
|
||||||
var address *AddressObject
|
var address *AddressObject
|
||||||
if value, ok := addressBook.Addresses.Load(addressID); ok {
|
if value, ok := addressBook.Addresses.Load(addressID); ok {
|
||||||
address = value.(*AddressObject)
|
address = value.(*AddressObject)
|
||||||
|
|
||||||
if opts.IfNoneMatch.IsSet() {
|
|
||||||
addressObject = address.Data
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
address.Data.Card = card
|
address.Data.Card = card
|
||||||
address.Changed = true
|
address.Changed = true
|
||||||
} else {
|
} else {
|
||||||
address = &AddressObject{
|
address = &AddressObject{
|
||||||
Changed: true,
|
Changed: true,
|
||||||
FilePath: CardDavPath2DirectoryPath(addressPath),
|
FilePath: DavPath2DirectoryPath(addressPath),
|
||||||
BookPath: bookPath,
|
BookPath: bookPath,
|
||||||
Data: &carddav.AddressObject{
|
Data: &carddav.AddressObject{
|
||||||
Card: card,
|
Card: card,
|
||||||
@ -541,13 +572,8 @@ func (c *Contacts) DeleteAddressObject(addressPath string) (err error) {
|
|||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
_, address, err := c.GetAddress(addressPath)
|
_, _, err = c.DeleteAddress(addressPath)
|
||||||
if err != nil && err != ErrorAddressNotFound {
|
if err != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.Remove(address.FilePath); err != nil {
|
|
||||||
logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,7 +605,7 @@ func (b *AddressBook) load() error {
|
|||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
filename := entry.Name()
|
filename := entry.Name()
|
||||||
ext := path.Ext(filename)
|
ext := path.Ext(filename)
|
||||||
if ext == ".vcf" {
|
if ext == VCardFileExt {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@ -686,38 +712,27 @@ type AddressObject struct {
|
|||||||
|
|
||||||
// load an address from *.vcf file
|
// load an address from *.vcf file
|
||||||
func (o *AddressObject) load() error {
|
func (o *AddressObject) load() error {
|
||||||
// get file info
|
// load vCard file
|
||||||
addressFileInfo, err := os.Stat(o.FilePath)
|
cards, err := LoadCards(o.FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(cards) != 1 {
|
||||||
// read file
|
return fmt.Errorf("file [%s] contains multiple cards", o.FilePath)
|
||||||
addressData, err := os.ReadFile(o.FilePath)
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("read file [%s] failed: %s", o.FilePath, err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode file
|
// create address object
|
||||||
reader := bytes.NewReader(addressData)
|
|
||||||
decoder := vcard.NewDecoder(reader)
|
|
||||||
card, err := decoder.Decode()
|
|
||||||
if err != nil {
|
|
||||||
logging.LogErrorf("decode file [%s] failed: %s", o.FilePath, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// load data
|
|
||||||
o.Changed = false
|
|
||||||
o.Data = &carddav.AddressObject{
|
o.Data = &carddav.AddressObject{
|
||||||
Path: path.Join(o.BookPath, addressFileInfo.Name()),
|
Card: *cards[0],
|
||||||
ModTime: addressFileInfo.ModTime(),
|
|
||||||
ContentLength: addressFileInfo.Size(),
|
|
||||||
ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()),
|
|
||||||
Card: card,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update file info
|
||||||
|
err = o.update()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Changed = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -753,17 +768,16 @@ func (o *AddressObject) save(force bool) error {
|
|||||||
|
|
||||||
// update file info
|
// update file info
|
||||||
func (o *AddressObject) update() error {
|
func (o *AddressObject) update() error {
|
||||||
// update file info
|
|
||||||
addressFileInfo, err := os.Stat(o.FilePath)
|
addressFileInfo, err := os.Stat(o.FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
|
logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
o.Data.Path = path.Join(o.BookPath, addressFileInfo.Name())
|
o.Data.Path = PathJoinWithSlash(o.BookPath, addressFileInfo.Name())
|
||||||
o.Data.ModTime = addressFileInfo.ModTime()
|
o.Data.ModTime = addressFileInfo.ModTime()
|
||||||
o.Data.ContentLength = addressFileInfo.Size()
|
o.Data.ContentLength = addressFileInfo.Size()
|
||||||
o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size())
|
o.Data.ETag = FileETag(addressFileInfo)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -793,6 +807,8 @@ func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []c
|
|||||||
|
|
||||||
func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) {
|
func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) {
|
||||||
// logging.LogDebugf("CardDAV GetAddressBook -> bookPath: %s", bookPath)
|
// logging.LogDebugf("CardDAV GetAddressBook -> bookPath: %s", bookPath)
|
||||||
|
bookPath = PathCleanWithSlash(bookPath)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -804,6 +820,8 @@ func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (a
|
|||||||
|
|
||||||
func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
|
func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) {
|
||||||
// logging.LogDebugf("CardDAV CreateAddressBook -> addressBook: %#v", addressBook)
|
// logging.LogDebugf("CardDAV CreateAddressBook -> addressBook: %#v", addressBook)
|
||||||
|
addressBook.Path = PathCleanWithSlash(addressBook.Path)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -815,6 +833,8 @@ func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *car
|
|||||||
|
|
||||||
func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) {
|
func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) {
|
||||||
// logging.LogDebugf("CardDAV DeleteAddressBook -> bookPath: %s", bookPath)
|
// logging.LogDebugf("CardDAV DeleteAddressBook -> bookPath: %s", bookPath)
|
||||||
|
bookPath = PathCleanWithSlash(bookPath)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -826,6 +846,8 @@ func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string)
|
|||||||
|
|
||||||
func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
|
func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) {
|
||||||
// logging.LogDebugf("CardDAV GetAddressObject -> addressPath: %s, req: %#v", addressPath, req)
|
// logging.LogDebugf("CardDAV GetAddressObject -> addressPath: %s, req: %#v", addressPath, req)
|
||||||
|
addressPath = PathCleanWithSlash(addressPath)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -837,6 +859,8 @@ func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath strin
|
|||||||
|
|
||||||
func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
|
func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) {
|
||||||
// logging.LogDebugf("CardDAV ListAddressObjects -> bookPath: %s, req: %#v", bookPath, req)
|
// logging.LogDebugf("CardDAV ListAddressObjects -> bookPath: %s, req: %#v", bookPath, req)
|
||||||
|
bookPath = PathCleanWithSlash(bookPath)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -848,6 +872,8 @@ func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string
|
|||||||
|
|
||||||
func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
|
func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) {
|
||||||
// logging.LogDebugf("CardDAV QueryAddressObjects -> urlPath: %s, query: %#v", urlPath, query)
|
// logging.LogDebugf("CardDAV QueryAddressObjects -> urlPath: %s, query: %#v", urlPath, query)
|
||||||
|
urlPath = PathCleanWithSlash(urlPath)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -859,6 +885,8 @@ func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string
|
|||||||
|
|
||||||
func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
|
func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) {
|
||||||
// logging.LogDebugf("CardDAV PutAddressObject -> addressPath: %s, card: %#v, opts: %#v", addressPath, card, opts)
|
// logging.LogDebugf("CardDAV PutAddressObject -> addressPath: %s, card: %#v, opts: %#v", addressPath, card, opts)
|
||||||
|
addressPath = PathCleanWithSlash(addressPath)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -870,6 +898,8 @@ func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath strin
|
|||||||
|
|
||||||
func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) {
|
func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) {
|
||||||
// logging.LogDebugf("CardDAV DeleteAddressObject -> addressPath: %s", addressPath)
|
// logging.LogDebugf("CardDAV DeleteAddressObject -> addressPath: %s", addressPath)
|
||||||
|
addressPath = PathCleanWithSlash(addressPath)
|
||||||
|
|
||||||
if err = contacts.Load(); err != nil {
|
if err = contacts.Load(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
77
kernel/model/dav.go
Normal file
77
kernel/model/dav.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// SiYuan - Refactor your thinking
|
||||||
|
// Copyright (c) 2020-present, b3log.org
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/88250/gulu"
|
||||||
|
"github.com/emersion/go-webdav/caldav"
|
||||||
|
"github.com/emersion/go-webdav/carddav"
|
||||||
|
"github.com/siyuan-note/logging"
|
||||||
|
"github.com/siyuan-note/siyuan/kernel/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PathJoinWithSlash joins the elements to a path with slash ('/') character
|
||||||
|
func PathJoinWithSlash(elems ...string) string {
|
||||||
|
return filepath.ToSlash(filepath.Join(elems...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PathCleanWithSlash cleans the path
|
||||||
|
func PathCleanWithSlash(p string) string {
|
||||||
|
return filepath.ToSlash(filepath.Clean(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DavPath2DirectoryPath converts CalDAV/CardDAV path to absolute path of the file system
|
||||||
|
func DavPath2DirectoryPath(davPath string) string {
|
||||||
|
return PathJoinWithSlash(util.DataDir, "storage", davPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveMetaData[T []*caldav.Calendar | []*carddav.AddressBook](metaData T, metaDataFilePath string) error {
|
||||||
|
data, err := gulu.JSON.MarshalIndentJSON(metaData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
logging.LogErrorf("marshal address books meta data failed: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dirPath := path.Dir(metaDataFilePath)
|
||||||
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
|
logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(metaDataFilePath, data, 0755); err != nil {
|
||||||
|
logging.LogErrorf("write file [%s] failed: %s", metaDataFilePath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileETag generates an ETag for a file
|
||||||
|
func FileETag(fileInfo fs.FileInfo) string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%s-%x",
|
||||||
|
fileInfo.ModTime().Format(time.RFC3339),
|
||||||
|
fileInfo.Size(),
|
||||||
|
)
|
||||||
|
}
|
@ -305,7 +305,9 @@ func CheckAuth(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WebDAV BasicAuth Authenticate
|
// WebDAV BasicAuth Authenticate
|
||||||
if strings.HasPrefix(c.Request.RequestURI, "/webdav") || strings.HasPrefix(c.Request.RequestURI, "/carddav") {
|
if strings.HasPrefix(c.Request.RequestURI, "/webdav") ||
|
||||||
|
strings.HasPrefix(c.Request.RequestURI, "/caldav") ||
|
||||||
|
strings.HasPrefix(c.Request.RequestURI, "/carddav") {
|
||||||
c.Header(BasicAuthHeaderKey, BasicAuthHeaderValue)
|
c.Header(BasicAuthHeaderKey, BasicAuthHeaderValue)
|
||||||
c.AbortWithStatus(http.StatusUnauthorized)
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/88250/gulu"
|
"github.com/88250/gulu"
|
||||||
|
"github.com/emersion/go-webdav/caldav"
|
||||||
"github.com/emersion/go-webdav/carddav"
|
"github.com/emersion/go-webdav/carddav"
|
||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
@ -49,7 +50,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MethodMkcol = "MKCOL"
|
MethodMkCol = "MKCOL"
|
||||||
MethodCopy = "COPY"
|
MethodCopy = "COPY"
|
||||||
MethodMove = "MOVE"
|
MethodMove = "MOVE"
|
||||||
MethodLock = "LOCK"
|
MethodLock = "LOCK"
|
||||||
@ -82,7 +83,7 @@ var (
|
|||||||
http.MethodPut,
|
http.MethodPut,
|
||||||
http.MethodDelete,
|
http.MethodDelete,
|
||||||
|
|
||||||
MethodMkcol,
|
MethodMkCol,
|
||||||
MethodCopy,
|
MethodCopy,
|
||||||
MethodMove,
|
MethodMove,
|
||||||
MethodLock,
|
MethodLock,
|
||||||
@ -90,6 +91,24 @@ var (
|
|||||||
MethodPropFind,
|
MethodPropFind,
|
||||||
MethodPropPatch,
|
MethodPropPatch,
|
||||||
}
|
}
|
||||||
|
CalDavMethods = []string{
|
||||||
|
http.MethodOptions,
|
||||||
|
http.MethodHead,
|
||||||
|
http.MethodGet,
|
||||||
|
http.MethodPost,
|
||||||
|
http.MethodPut,
|
||||||
|
http.MethodDelete,
|
||||||
|
|
||||||
|
MethodMkCol,
|
||||||
|
MethodCopy,
|
||||||
|
MethodMove,
|
||||||
|
// MethodLock,
|
||||||
|
// MethodUnlock,
|
||||||
|
MethodPropFind,
|
||||||
|
MethodPropPatch,
|
||||||
|
|
||||||
|
MethodReport,
|
||||||
|
}
|
||||||
CardDavMethods = []string{
|
CardDavMethods = []string{
|
||||||
http.MethodOptions,
|
http.MethodOptions,
|
||||||
http.MethodHead,
|
http.MethodHead,
|
||||||
@ -98,9 +117,9 @@ var (
|
|||||||
http.MethodPut,
|
http.MethodPut,
|
||||||
http.MethodDelete,
|
http.MethodDelete,
|
||||||
|
|
||||||
MethodMkcol,
|
MethodMkCol,
|
||||||
// MethodCopy,
|
MethodCopy,
|
||||||
// MethodMove,
|
MethodMove,
|
||||||
// MethodLock,
|
// MethodLock,
|
||||||
// MethodUnlock,
|
// MethodUnlock,
|
||||||
MethodPropFind,
|
MethodPropFind,
|
||||||
@ -137,6 +156,7 @@ func Serve(fastMode bool) {
|
|||||||
serveAppearance(ginServer)
|
serveAppearance(ginServer)
|
||||||
serveWebSocket(ginServer)
|
serveWebSocket(ginServer)
|
||||||
serveWebDAV(ginServer)
|
serveWebDAV(ginServer)
|
||||||
|
serveCalDAV(ginServer)
|
||||||
serveCardDAV(ginServer)
|
serveCardDAV(ginServer)
|
||||||
serveExport(ginServer)
|
serveExport(ginServer)
|
||||||
serveWidgets(ginServer)
|
serveWidgets(ginServer)
|
||||||
@ -673,7 +693,7 @@ func serveWebDAV(ginServer *gin.Engine) {
|
|||||||
case http.MethodPost,
|
case http.MethodPost,
|
||||||
http.MethodPut,
|
http.MethodPut,
|
||||||
http.MethodDelete,
|
http.MethodDelete,
|
||||||
MethodMkcol,
|
MethodMkCol,
|
||||||
MethodCopy,
|
MethodCopy,
|
||||||
MethodMove,
|
MethodMove,
|
||||||
MethodLock,
|
MethodLock,
|
||||||
@ -687,26 +707,27 @@ func serveWebDAV(ginServer *gin.Engine) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveCardDAV(ginServer *gin.Engine) {
|
func serveCalDAV(ginServer *gin.Engine) {
|
||||||
// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
|
// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
|
||||||
handler := carddav.Handler{
|
handler := caldav.Handler{
|
||||||
Backend: &model.CardDavBackend{},
|
Backend: &model.CalDavBackend{},
|
||||||
Prefix: model.CardDavPrincipalsPath,
|
Prefix: model.CalDavPrincipalsPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) {
|
ginServer.Match(CalDavMethods, "/.well-known/caldav", func(c *gin.Context) {
|
||||||
|
// logging.LogDebugf("CalDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
|
||||||
handler.ServeHTTP(c.Writer, c.Request)
|
handler.ServeHTTP(c.Writer, c.Request)
|
||||||
})
|
})
|
||||||
|
|
||||||
ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
|
ginGroup := ginServer.Group(model.CalDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
|
||||||
ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) {
|
ginGroup.Match(CalDavMethods, "/*path", func(c *gin.Context) {
|
||||||
// logging.LogDebugf("CardDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
|
// logging.LogDebugf("CalDAV -> [%s] %s", c.Request.Method, c.Request.URL.String())
|
||||||
if util.ReadOnly {
|
if util.ReadOnly {
|
||||||
switch c.Request.Method {
|
switch c.Request.Method {
|
||||||
case http.MethodPost,
|
case http.MethodPost,
|
||||||
http.MethodPut,
|
http.MethodPut,
|
||||||
http.MethodDelete,
|
http.MethodDelete,
|
||||||
MethodMkcol,
|
MethodMkCol,
|
||||||
MethodCopy,
|
MethodCopy,
|
||||||
MethodMove,
|
MethodMove,
|
||||||
MethodLock,
|
MethodLock,
|
||||||
@ -717,6 +738,41 @@ func serveCardDAV(ginServer *gin.Engine) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
handler.ServeHTTP(c.Writer, c.Request)
|
handler.ServeHTTP(c.Writer, c.Request)
|
||||||
|
// logging.LogDebugf("CalDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveCardDAV(ginServer *gin.Engine) {
|
||||||
|
// REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go
|
||||||
|
handler := carddav.Handler{
|
||||||
|
Backend: &model.CardDavBackend{},
|
||||||
|
Prefix: model.CardDavPrincipalsPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) {
|
||||||
|
// logging.LogDebugf("CardDAV [/.well-known/carddav]")
|
||||||
|
handler.ServeHTTP(c.Writer, c.Request)
|
||||||
|
})
|
||||||
|
|
||||||
|
ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole)
|
||||||
|
ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) {
|
||||||
|
if util.ReadOnly {
|
||||||
|
switch c.Request.Method {
|
||||||
|
case http.MethodPost,
|
||||||
|
http.MethodPut,
|
||||||
|
http.MethodDelete,
|
||||||
|
MethodMkCol,
|
||||||
|
MethodCopy,
|
||||||
|
MethodMove,
|
||||||
|
MethodLock,
|
||||||
|
MethodUnlock,
|
||||||
|
MethodPropPatch:
|
||||||
|
c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: Can't handle Thunderbird's PROPFIND request with prop <current-user-privilege-set/>
|
||||||
|
handler.ServeHTTP(c.Writer, c.Request)
|
||||||
// logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
|
// logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -739,6 +795,7 @@ func shortReqMsg(msg []byte) []byte {
|
|||||||
func corsMiddleware() gin.HandlerFunc {
|
func corsMiddleware() gin.HandlerFunc {
|
||||||
allowMethods := strings.Join(HttpMethods, ", ")
|
allowMethods := strings.Join(HttpMethods, ", ")
|
||||||
allowWebDavMethods := strings.Join(WebDavMethods, ", ")
|
allowWebDavMethods := strings.Join(WebDavMethods, ", ")
|
||||||
|
allowCalDavMethods := strings.Join(CalDavMethods, ", ")
|
||||||
allowCardDavMethods := strings.Join(CardDavMethods, ", ")
|
allowCardDavMethods := strings.Join(CardDavMethods, ", ")
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
@ -747,13 +804,19 @@ func corsMiddleware() gin.HandlerFunc {
|
|||||||
c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization")
|
c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization")
|
||||||
c.Header("Access-Control-Allow-Private-Network", "true")
|
c.Header("Access-Control-Allow-Private-Network", "true")
|
||||||
|
|
||||||
if strings.HasPrefix(c.Request.RequestURI, "/webdav/") {
|
if strings.HasPrefix(c.Request.RequestURI, "/webdav") {
|
||||||
c.Header("Access-Control-Allow-Methods", allowWebDavMethods)
|
c.Header("Access-Control-Allow-Methods", allowWebDavMethods)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(c.Request.RequestURI, "/carddav/") {
|
if strings.HasPrefix(c.Request.RequestURI, "/caldav") {
|
||||||
|
c.Header("Access-Control-Allow-Methods", allowCalDavMethods)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(c.Request.RequestURI, "/carddav") {
|
||||||
c.Header("Access-Control-Allow-Methods", allowCardDavMethods)
|
c.Header("Access-Control-Allow-Methods", allowCardDavMethods)
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user