diff --git a/kernel/go.mod b/kernel/go.mod index 76228cefc75..20e70d0f172 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -22,6 +22,7 @@ require ( github.com/denisbrodbeck/machineid v1.0.1 github.com/dgraph-io/ristretto v1.0.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-webdav v0.5.1-0.20240713135526-7f8c17ad7135 github.com/emirpasic/gods v1.18.1 @@ -154,6 +155,7 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // 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/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect diff --git a/kernel/go.sum b/kernel/go.sum index 6c1aea71877..d83019e870d 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -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/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/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-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= @@ -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/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU= 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/tetratelabs/wazero v1.7.3 h1:PBH5KVahrt3S2AHgEjKu4u+LlDbbk+nsGE3KLucy6Rw= github.com/tetratelabs/wazero v1.7.3/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= diff --git a/kernel/model/caldav.go b/kernel/model/caldav.go new file mode 100644 index 00000000000..385ab3e105b --- /dev/null +++ b/kernel/model/caldav.go @@ -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 . + +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 +} diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index 8b2aad21a69..acb940ab82b 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -24,7 +24,6 @@ import ( "io" "os" "path" - "path/filepath" "strings" "sync" @@ -46,25 +45,40 @@ const ( CardDavDefaultAddressBookName = "default" CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json" + + VCardFileExt = "." + vcard.Extension // .vcf ) -type PathDepth int +type CardDavPathDepth int const ( - pathDepth_Root PathDepth = 1 + iota // /carddav - pathDepth_Principals // /carddav/principals - pathDepth_UserPrincipal // /carddav/principals/main - pathDepth_HomeSet // /carddav/principals/main/contacts - pathDepth_AddressBook // /carddav/principals/main/contacts/default - pathDepth_Address // /carddav/principals/main/contacts/default/id.vcf + cardDavPathDepth_Root CardDavPathDepth = 1 + iota // /carddav + cardDavPathDepth_Principals // /carddav/principals + cardDavPathDepth_UserPrincipal // /carddav/principals/main + cardDavPathDepth_HomeSet // /carddav/principals/main/contacts + cardDavPathDepth_AddressBook // /carddav/principals/main/contacts/default + cardDavPathDepth_Address // /carddav/principals/main/contacts/default/id.vcf ) var ( + addressBookMaxResourceSize int64 = 0 + addressBookSupportedAddressData = []carddav.AddressDataType{ + { + ContentType: vcard.MIMEType, + Version: "3.0", + }, + { + ContentType: vcard.MIMEType, + Version: "4.0", + }, + } + defaultAddressBook = carddav.AddressBook{ - Path: CardDavDefaultAddressBookPath, - Name: CardDavDefaultAddressBookName, - Description: "Default address book", - MaxResourceSize: 0, + Path: CardDavDefaultAddressBookPath, + Name: CardDavDefaultAddressBookName, + Description: "Default address book", + MaxResourceSize: addressBookMaxResourceSize, + SupportedAddressData: addressBookSupportedAddressData, } contacts = Contacts{ loaded: false, @@ -74,34 +88,40 @@ var ( booksMetaData: []*carddav.AddressBook{}, } - ErrorNotFound = errors.New("CardDAV: not found") - ErrorPathInvalid = errors.New("CardDAV: path is invalid") + ErrorCardDavPathInvalid = errors.New("CardDAV: path is invalid") - ErrorBookNotFound = errors.New("CardDAV: address book not found") - ErrorBookPathInvalid = errors.New("CardDAV: address book path is invalid") + ErrorCardDavBookNotFound = errors.New("CardDAV: address book not found") + ErrorCardDavBookPathInvalid = errors.New("CardDAV: address book path is invalid") - ErrorAddressNotFound = errors.New("CardDAV: address not found") - ErrorAddressFileExtensionNameInvalid = errors.New("CardDAV: address file extension name is invalid") + ErrorCardDavAddressNotFound = errors.New("CardDAV: address not found") + ErrorCardDavAddressFileExtensionNameInvalid = errors.New("CardDAV: address file extension name is invalid") ) -// CardDavPath2DirectoryPath converts CardDAV path to absolute path of the file system -func CardDavPath2DirectoryPath(cardDavPath string) string { - return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(cardDavPath, "/")) +// ImportVCardFile imports a address book from a vCard file (*.vcf) +func ImportAddressBook(addressBookPath, cardContent string) (addresses []*AddressObject, err error) { + // 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 -func HomeSetPathPath() string { - return CardDavPath2DirectoryPath(CardDavHomeSetPath) +// ExportAddressBook exports a address book to a vCard file (*.vcf) +func ExportAddressBook(addressBookPath string) (cardContent string, err error) { + // 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 func AddressBooksMetaDataFilePath() string { - return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath) + return DavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath) } -func GetPathDepth(urlPath string) PathDepth { - urlPath = path.Clean(urlPath) - return PathDepth(len(strings.Split(urlPath, "/")) - 1) +func GetCardDavPathDepth(urlPath string) CardDavPathDepth { + urlPath = PathCleanWithSlash(urlPath) + return CardDavPathDepth(len(strings.Split(urlPath, "/")) - 1) } // 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) addressFileExt := path.Ext(addressFileName) - if GetPathDepth(addressBookPath) != pathDepth_AddressBook { - err = ErrorBookPathInvalid + if GetCardDavPathDepth(addressBookPath) != cardDavPathDepth_AddressBook { + err = ErrorCardDavBookPathInvalid return } - if addressFileExt != ".vcf" { - err = ErrorAddressFileExtensionNameInvalid + if addressFileExt != VCardFileExt { + err = ErrorCardDavAddressFileExtensionNameInvalid return } @@ -185,9 +205,12 @@ type Contacts struct { // load all contacts func (c *Contacts) load() error { c.books.Clear() + + // load address books meta data addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath() metaData, err := os.ReadFile(addressBooksMetaDataFilePath) if os.IsNotExist(err) { + // create & save default address book c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook} if err := c.saveAddressBooksMetaData(); err != nil { return err @@ -201,12 +224,13 @@ func (c *Contacts) load() error { } } + // load vCard files (*.vcf) wg := &sync.WaitGroup{} wg.Add(len(c.booksMetaData)) for _, addressBookMetaData := range c.booksMetaData { addressBook := &AddressBook{ Changed: false, - DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path), + DirectoryPath: DavPath2DirectoryPath(addressBookMetaData.Path), MetaData: addressBookMetaData, Addresses: sync.Map{}, } @@ -251,25 +275,7 @@ func (c *Contacts) save(force bool) error { // save all contacts func (c *Contacts) saveAddressBooksMetaData() error { - data, err := gulu.JSON.MarshalIndentJSON(c.booksMetaData, "", " ") - 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 + return SaveMetaData(c.booksMetaData, AddressBooksMetaDataFilePath()) } 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 { addressBook = value.(*AddressBook) } else { - err = ErrorBookNotFound + err = ErrorCardDavBookNotFound return } if value, ok := addressBook.Addresses.Load(addressID); ok { addressObject = value.(*AddressObject) } 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 } @@ -325,7 +360,7 @@ func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook return } - err = ErrorBookNotFound + err = ErrorCardDavBookNotFound return } @@ -344,7 +379,7 @@ func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) ( // insert map item addressBook = &AddressBook{ Changed: false, - DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path), + DirectoryPath: DavPath2DirectoryPath(addressBookMetaData.Path), MetaData: addressBookMetaData, Addresses: sync.Map{}, } @@ -369,7 +404,7 @@ func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) ( // create address book directory 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 } @@ -402,7 +437,7 @@ func (c *Contacts) DeleteAddressBook(path string) (err error) { // remove address book directory 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 } @@ -435,7 +470,7 @@ func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataR if value, ok := c.books.Load(bookPath); ok { addressBook = value.(*AddressBook) } else { - err = ErrorBookNotFound + err = ErrorCardDavBookNotFound return } @@ -451,8 +486,8 @@ func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBoo c.lock.Lock() defer c.lock.Unlock() - switch GetPathDepth(urlPath) { - case pathDepth_Root, pathDepth_Principals, pathDepth_UserPrincipal, pathDepth_HomeSet: + switch GetCardDavPathDepth(urlPath) { + case cardDavPathDepth_Root, cardDavPathDepth_Principals, cardDavPathDepth_UserPrincipal, cardDavPathDepth_HomeSet: c.books.Range(func(path any, book any) bool { addressBook := book.(*AddressBook) addressBook.Addresses.Range(func(id any, address any) bool { @@ -461,7 +496,7 @@ func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBoo }) return true }) - case pathDepth_AddressBook: + case cardDavPathDepth_AddressBook: if value, ok := c.books.Load(urlPath); ok { addressBook := value.(*AddressBook) addressBook.Addresses.Range(func(id any, address any) bool { @@ -469,12 +504,12 @@ func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBoo return true }) } - case pathDepth_Address: + case cardDavPathDepth_Address: if _, address, _ := c.GetAddress(urlPath); address != nil { addressObjects = append(addressObjects, *address.Data) } default: - err = ErrorPathInvalid + err = ErrorCardDavPathInvalid return } @@ -496,25 +531,21 @@ func (c *Contacts) PutAddressObject(addressPath string, card vcard.Card, opts *c if value, ok := c.books.Load(bookPath); ok { addressBook = value.(*AddressBook) } else { - err = ErrorBookNotFound + err = ErrorCardDavBookNotFound return } + // TODO: 处理 opts.IfNoneMatch (If-None-Match) 与 opts.IfMatch (If-Match) + var address *AddressObject if value, ok := addressBook.Addresses.Load(addressID); ok { address = value.(*AddressObject) - - if opts.IfNoneMatch.IsSet() { - addressObject = address.Data - return - } - address.Data.Card = card address.Changed = true } else { address = &AddressObject{ Changed: true, - FilePath: CardDavPath2DirectoryPath(addressPath), + FilePath: DavPath2DirectoryPath(addressPath), BookPath: bookPath, Data: &carddav.AddressObject{ Card: card, @@ -541,13 +572,8 @@ func (c *Contacts) DeleteAddressObject(addressPath string) (err error) { c.lock.Lock() defer c.lock.Unlock() - _, address, err := c.GetAddress(addressPath) - if err != nil && err != ErrorAddressNotFound { - return - } - - if err = os.Remove(address.FilePath); err != nil { - logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err) + _, _, err = c.DeleteAddress(addressPath) + if err != nil { return } @@ -579,7 +605,7 @@ func (b *AddressBook) load() error { if !entry.IsDir() { filename := entry.Name() ext := path.Ext(filename) - if ext == ".vcf" { + if ext == VCardFileExt { wg.Add(1) go func() { defer wg.Done() @@ -686,38 +712,27 @@ type AddressObject struct { // load an address from *.vcf file func (o *AddressObject) load() error { - // get file info - addressFileInfo, err := os.Stat(o.FilePath) + // load vCard file + cards, err := LoadCards(o.FilePath) if err != nil { - logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err) return err } + if len(cards) != 1 { + return fmt.Errorf("file [%s] contains multiple cards", o.FilePath) + } - // read file - addressData, err := os.ReadFile(o.FilePath) - if err != nil { - logging.LogErrorf("read file [%s] failed: %s", o.FilePath, err) - return err + // create address object + o.Data = &carddav.AddressObject{ + Card: *cards[0], } - // decode file - reader := bytes.NewReader(addressData) - decoder := vcard.NewDecoder(reader) - card, err := decoder.Decode() + // update file info + err = o.update() if err != nil { - logging.LogErrorf("decode file [%s] failed: %s", o.FilePath, err) return err } - // load data o.Changed = false - o.Data = &carddav.AddressObject{ - Path: path.Join(o.BookPath, addressFileInfo.Name()), - ModTime: addressFileInfo.ModTime(), - ContentLength: addressFileInfo.Size(), - ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()), - Card: card, - } return nil } @@ -753,17 +768,16 @@ func (o *AddressObject) save(force bool) error { // update file info func (o *AddressObject) update() error { - // update file info 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 = path.Join(o.BookPath, addressFileInfo.Name()) + o.Data.Path = PathJoinWithSlash(o.BookPath, addressFileInfo.Name()) o.Data.ModTime = addressFileInfo.ModTime() o.Data.ContentLength = addressFileInfo.Size() - o.Data.ETag = fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()) + o.Data.ETag = FileETag(addressFileInfo) 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) { // logging.LogDebugf("CardDAV GetAddressBook -> bookPath: %s", bookPath) + bookPath = PathCleanWithSlash(bookPath) + if err = contacts.Load(); err != nil { 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) { // logging.LogDebugf("CardDAV CreateAddressBook -> addressBook: %#v", addressBook) + addressBook.Path = PathCleanWithSlash(addressBook.Path) + if err = contacts.Load(); err != nil { 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) { // logging.LogDebugf("CardDAV DeleteAddressBook -> bookPath: %s", bookPath) + bookPath = PathCleanWithSlash(bookPath) + if err = contacts.Load(); err != nil { 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) { // logging.LogDebugf("CardDAV GetAddressObject -> addressPath: %s, req: %#v", addressPath, req) + addressPath = PathCleanWithSlash(addressPath) + if err = contacts.Load(); err != nil { 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) { // logging.LogDebugf("CardDAV ListAddressObjects -> bookPath: %s, req: %#v", bookPath, req) + bookPath = PathCleanWithSlash(bookPath) + if err = contacts.Load(); err != nil { 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) { // logging.LogDebugf("CardDAV QueryAddressObjects -> urlPath: %s, query: %#v", urlPath, query) + urlPath = PathCleanWithSlash(urlPath) + if err = contacts.Load(); err != nil { 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) { // logging.LogDebugf("CardDAV PutAddressObject -> addressPath: %s, card: %#v, opts: %#v", addressPath, card, opts) + addressPath = PathCleanWithSlash(addressPath) + if err = contacts.Load(); err != nil { 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) { // logging.LogDebugf("CardDAV DeleteAddressObject -> addressPath: %s", addressPath) + addressPath = PathCleanWithSlash(addressPath) + if err = contacts.Load(); err != nil { return } diff --git a/kernel/model/dav.go b/kernel/model/dav.go new file mode 100644 index 00000000000..ed0ff1bccc5 --- /dev/null +++ b/kernel/model/dav.go @@ -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 . + +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(), + ) +} diff --git a/kernel/model/session.go b/kernel/model/session.go index a5ff1f12105..46a8c1f7ece 100644 --- a/kernel/model/session.go +++ b/kernel/model/session.go @@ -305,7 +305,9 @@ func CheckAuth(c *gin.Context) { } // 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.AbortWithStatus(http.StatusUnauthorized) return diff --git a/kernel/server/serve.go b/kernel/server/serve.go index 58464851fae..c75717a0954 100644 --- a/kernel/server/serve.go +++ b/kernel/server/serve.go @@ -32,6 +32,7 @@ import ( "time" "github.com/88250/gulu" + "github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/carddav" "github.com/gin-contrib/gzip" "github.com/gin-contrib/sessions" @@ -49,7 +50,7 @@ import ( ) const ( - MethodMkcol = "MKCOL" + MethodMkCol = "MKCOL" MethodCopy = "COPY" MethodMove = "MOVE" MethodLock = "LOCK" @@ -82,7 +83,7 @@ var ( http.MethodPut, http.MethodDelete, - MethodMkcol, + MethodMkCol, MethodCopy, MethodMove, MethodLock, @@ -90,6 +91,24 @@ var ( MethodPropFind, 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{ http.MethodOptions, http.MethodHead, @@ -98,9 +117,9 @@ var ( http.MethodPut, http.MethodDelete, - MethodMkcol, - // MethodCopy, - // MethodMove, + MethodMkCol, + MethodCopy, + MethodMove, // MethodLock, // MethodUnlock, MethodPropFind, @@ -137,6 +156,7 @@ func Serve(fastMode bool) { serveAppearance(ginServer) serveWebSocket(ginServer) serveWebDAV(ginServer) + serveCalDAV(ginServer) serveCardDAV(ginServer) serveExport(ginServer) serveWidgets(ginServer) @@ -673,7 +693,41 @@ func serveWebDAV(ginServer *gin.Engine) { case http.MethodPost, http.MethodPut, http.MethodDelete, - MethodMkcol, + MethodMkCol, + MethodCopy, + MethodMove, + MethodLock, + MethodUnlock, + MethodPropPatch: + c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34))) + return + } + } + handler.ServeHTTP(c.Writer, c.Request) + }) +} + +func serveCalDAV(ginServer *gin.Engine) { + // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go + handler := caldav.Handler{ + Backend: &model.CalDavBackend{}, + Prefix: model.CalDavPrincipalsPath, + } + + 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) + }) + + ginGroup := ginServer.Group(model.CalDavPrefixPath, model.CheckAuth, model.CheckAdminRole) + ginGroup.Match(CalDavMethods, "/*path", func(c *gin.Context) { + // logging.LogDebugf("CalDAV -> [%s] %s", c.Request.Method, c.Request.URL.String()) + if util.ReadOnly { + switch c.Request.Method { + case http.MethodPost, + http.MethodPut, + http.MethodDelete, + MethodMkCol, MethodCopy, MethodMove, MethodLock, @@ -684,6 +738,7 @@ func serveWebDAV(ginServer *gin.Engine) { } } handler.ServeHTTP(c.Writer, c.Request) + // logging.LogDebugf("CalDAV <- [%s] %v", c.Request.Method, c.Writer.Status()) }) } @@ -695,18 +750,18 @@ func serveCardDAV(ginServer *gin.Engine) { } 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) { - // logging.LogDebugf("CardDAV -> [%s] %s", c.Request.Method, c.Request.URL.String()) if util.ReadOnly { switch c.Request.Method { case http.MethodPost, http.MethodPut, http.MethodDelete, - MethodMkcol, + MethodMkCol, MethodCopy, MethodMove, MethodLock, @@ -716,6 +771,7 @@ func serveCardDAV(ginServer *gin.Engine) { return } } + // TODO: Can't handle Thunderbird's PROPFIND request with prop handler.ServeHTTP(c.Writer, c.Request) // logging.LogDebugf("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status()) }) @@ -739,6 +795,7 @@ func shortReqMsg(msg []byte) []byte { func corsMiddleware() gin.HandlerFunc { allowMethods := strings.Join(HttpMethods, ", ") allowWebDavMethods := strings.Join(WebDavMethods, ", ") + allowCalDavMethods := strings.Join(CalDavMethods, ", ") allowCardDavMethods := strings.Join(CardDavMethods, ", ") 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-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.Next() 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.Next() return