🎨 define the interface of CalDAV

This commit is contained in:
颖逸 2024-12-01 01:08:26 +08:00
parent bbc3326426
commit 432bb826f3
No known key found for this signature in database
GPG key ID: 833DAD5428FF18A8
6 changed files with 227 additions and 36 deletions

View file

@ -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

View file

@ -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=

107
kernel/model/caldav.go Normal file
View file

@ -0,0 +1,107 @@
// 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 (
"context"
"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
ICalendarFileExt = ".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
)
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)
// TODO: create calendar
return
}
func (b *CalDavBackend) ListCalendars(ctx context.Context) (calendars []caldav.Calendar, err error) {
logging.LogDebugf("CalDAV ListCalendars")
// TODO: list calendars
return
}
func (b *CalDavBackend) GetCalendar(ctx context.Context, path string) (calendar *caldav.Calendar, err error) {
logging.LogDebugf("CalDAV GetCalendar -> path: %s", path)
// TODO: get calendar
return
}
func (b *CalDavBackend) GetCalendarObject(ctx context.Context, path string, req *caldav.CalendarCompRequest) (calendarObjects *caldav.CalendarObject, err error) {
logging.LogDebugf("CalDAV GetCalendarObject -> path: %s, req: %#v", path, req)
// TODO: get calendar object
return
}
func (b *CalDavBackend) ListCalendarObjects(ctx context.Context, path string, req *caldav.CalendarCompRequest) (calendarObjects []caldav.CalendarObject, err error) {
logging.LogDebugf("CalDAV ListCalendarObjects -> path: %s, req: %#v", path, req)
// TODO: list calendar objects
return
}
func (b *CalDavBackend) QueryCalendarObjects(ctx context.Context, path string, query *caldav.CalendarQuery) (calendarObjects []caldav.CalendarObject, err error) {
logging.LogDebugf("CalDAV QueryCalendarObjects -> path: %s, query: %#v", path, query)
// TODO: query calendar objects
return
}
func (b *CalDavBackend) PutCalendarObject(ctx context.Context, path string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (calendarObject *caldav.CalendarObject, err error) {
logging.LogDebugf("CalDAV PutCalendarObject -> path: %s, opts: %#v", path, opts)
// TODO: put calendar object
return
}
func (b *CalDavBackend) DeleteCalendarObject(ctx context.Context, path string) (err error) {
logging.LogDebugf("CalDAV DeleteCalendarObject -> path: %s", path)
// TODO: delete calendar object
return
}

View file

@ -46,25 +46,41 @@ const (
CardDavDefaultAddressBookName = "default"
CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json"
VCardFileExt = ".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
addressBookContentType = "text/vcard"
addressBookSupportedAddressData = []carddav.AddressDataType{
{
ContentType: addressBookContentType,
Version: "3.0",
},
{
ContentType: addressBookContentType,
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,
@ -89,8 +105,8 @@ func CardDavPath2DirectoryPath(cardDavPath string) string {
return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(cardDavPath, "/"))
}
// HomeSetPathPath returns the absolute path of the address book home set directory
func HomeSetPathPath() string {
// CardDavHomeSetDirectoryPath returns the absolute path of the address book home set directory
func CardDavHomeSetDirectoryPath() string {
return CardDavPath2DirectoryPath(CardDavHomeSetPath)
}
@ -99,9 +115,9 @@ func AddressBooksMetaDataFilePath() string {
return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath)
}
func GetPathDepth(urlPath string) PathDepth {
func GetCardDavPathDepth(urlPath string) CardDavPathDepth {
urlPath = path.Clean(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
@ -110,12 +126,12 @@ func ParseAddressPath(addressPath string) (addressBookPath string, addressID str
addressID = path.Base(addressFileName)
addressFileExt := path.Ext(addressFileName)
if GetPathDepth(addressBookPath) != pathDepth_AddressBook {
if GetCardDavPathDepth(addressBookPath) != cardDavPathDepth_AddressBook {
err = ErrorBookPathInvalid
return
}
if addressFileExt != ".vcf" {
if addressFileExt != VCardFileExt {
err = ErrorAddressFileExtensionNameInvalid
return
}
@ -257,7 +273,7 @@ func (c *Contacts) saveAddressBooksMetaData() error {
return err
}
dirPath := HomeSetPathPath()
dirPath := CardDavHomeSetDirectoryPath()
if err := os.MkdirAll(dirPath, 0755); err != nil {
logging.LogErrorf("create directory [%s] failed: %s", dirPath, err)
return err
@ -451,8 +467,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 +477,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,7 +485,7 @@ 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)
}
@ -579,7 +595,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()
@ -775,8 +791,8 @@ func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, erro
return CardDavUserPrincipalPath, nil
}
func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
// logging.LogDebugf("CardDAV AddressBookHomeSetPath")
func (b *CardDavBackend) AddressBookHomeSetDirectoryPath(ctx context.Context) (string, error) {
// logging.LogDebugf("CardDAV AddressBookHomeSetDirectoryPath")
return CardDavHomeSetPath, nil
}

View file

@ -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

View file

@ -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,7 @@ func serveWebDAV(ginServer *gin.Engine) {
case http.MethodPost,
http.MethodPut,
http.MethodDelete,
MethodMkcol,
MethodMkCol,
MethodCopy,
MethodMove,
MethodLock,
@ -687,6 +707,41 @@ func serveWebDAV(ginServer *gin.Engine) {
})
}
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 [/.well-known/caldav]")
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,
MethodUnlock,
MethodPropPatch:
c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34)))
return
}
}
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{
@ -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,
@ -739,6 +794,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 +803,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