From e3897edc148db59b70ed05743ec5210452e47e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Fri, 25 Oct 2024 01:03:51 +0800 Subject: [PATCH 1/9] :art: add CardDAV server --- kernel/go.mod | 2 ++ kernel/go.sum | 7 ++++ kernel/model/carddav.go | 74 +++++++++++++++++++++++++++++++++++++++++ kernel/server/serve.go | 28 +++++++++++++++- 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 kernel/model/carddav.go diff --git a/kernel/go.mod b/kernel/go.mod index f4ef57ab75d..1856a389034 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -98,6 +98,8 @@ require ( github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dsnet/compress v0.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 // indirect + github.com/emersion/go-webdav v0.5.0 // indirect github.com/fatih/set v0.2.1 // indirect github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect github.com/gin-contrib/sse v0.1.0 // indirect diff --git a/kernel/go.sum b/kernel/go.sum index a852cf4911a..949add86596 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -89,6 +89,11 @@ 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-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ= +github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA= +github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/emersion/go-webdav v0.5.0 h1:Ak/BQLgAihJt/UxJbCsEXDPxS5Uw4nZzgIMOq3rkKjc= +github.com/emersion/go-webdav v0.5.0/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= @@ -367,6 +372,8 @@ 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.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU= +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= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go new file mode 100644 index 00000000000..249d8f32ada --- /dev/null +++ b/kernel/model/carddav.go @@ -0,0 +1,74 @@ +// 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-vcard" + "github.com/emersion/go-webdav/carddav" +) + +const ( + // REF: https://developers.google.com/people/carddav#resources + CardDavPrincipalPath = "/carddav" + CardDavHomeSetPath = CardDavPrincipalPath + "/contacts" + CardDavAddressBookPath = CardDavHomeSetPath + "/default" +) + +type CardDavBackend struct{} + +func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { + return CardDavPrincipalPath, nil +} + +func (b *CardDavBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) { + return CardDavHomeSetPath, nil +} + +func (b *CardDavBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) { + return &carddav.AddressBook{ + Path: CardDavAddressBookPath, + Name: "SiYuan", + Description: "SiYuan default contacts", + }, nil +} + +func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { + // TODO + return +} + +func (b *CardDavBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { + // TODO + return +} + +func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { + // TODO + return +} + +func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) { + // TODO + return +} + +func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) { + // TODO + return +} diff --git a/kernel/server/serve.go b/kernel/server/serve.go index 4a42d169132..d4b3cfb377c 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/carddav" "github.com/gin-contrib/gzip" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" @@ -88,6 +89,7 @@ func Serve(fastMode bool) { serveAppearance(ginServer) serveWebSocket(ginServer) serveWebDAV(ginServer) + serveCardDAV(ginServer) serveExport(ginServer) serveWidgets(ginServer) servePlugins(ginServer) @@ -616,7 +618,31 @@ func serveWebDAV(ginServer *gin.Engine) { } ginGroup := ginServer.Group("/webdav", model.CheckAuth, model.CheckAdminRole) - ginGroup.Match(WebDavMethod, "/*path", func(c *gin.Context) { + ginGroup.Any("/*path", func(c *gin.Context) { + if util.ReadOnly { + switch c.Request.Method { + case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": + c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34))) + return + } + } + handler.ServeHTTP(c.Writer, c.Request) + }) +} + +func serveCardDAV(ginServer *gin.Engine) { + // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go + handler := carddav.Handler{ + Backend: &model.CardDavBackend{}, + Prefix: "", + } + + ginServer.Any("/.well-known/caldav", func(c *gin.Context) { + handler.ServeHTTP(c.Writer, c.Request) + }) + + ginGroup := ginServer.Group("/carddav", model.CheckAuth, model.CheckAdminRole) + ginGroup.Any("/*path", func(c *gin.Context) { if util.ReadOnly { switch c.Request.Method { case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": From d8d76cd32c503404a6efc40ac37aa535c33110a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:36:39 +0800 Subject: [PATCH 2/9] :art: change CardDAV principals path --- kernel/model/carddav.go | 10 ++++++---- kernel/server/serve.go | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index 249d8f32ada..765b45972c9 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -25,15 +25,17 @@ import ( const ( // REF: https://developers.google.com/people/carddav#resources - CardDavPrincipalPath = "/carddav" - CardDavHomeSetPath = CardDavPrincipalPath + "/contacts" - CardDavAddressBookPath = CardDavHomeSetPath + "/default" + CardDavPrefixPath = "/carddav" + CardDavPrincipalsPath = CardDavPrefixPath + "/principals" + CardDavCurrentUserPrincipalPath = CardDavPrincipalsPath + "/main" + CardDavHomeSetPath = CardDavCurrentUserPrincipalPath + "/contacts" + CardDavAddressBookPath = CardDavHomeSetPath + "/default" ) type CardDavBackend struct{} func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { - return CardDavPrincipalPath, nil + return CardDavCurrentUserPrincipalPath, nil } func (b *CardDavBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) { diff --git a/kernel/server/serve.go b/kernel/server/serve.go index d4b3cfb377c..dae5bb096a0 100644 --- a/kernel/server/serve.go +++ b/kernel/server/serve.go @@ -634,14 +634,14 @@ func serveCardDAV(ginServer *gin.Engine) { // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go handler := carddav.Handler{ Backend: &model.CardDavBackend{}, - Prefix: "", + Prefix: model.CardDavPrefixPath, } ginServer.Any("/.well-known/caldav", func(c *gin.Context) { handler.ServeHTTP(c.Writer, c.Request) }) - ginGroup := ginServer.Group("/carddav", model.CheckAuth, model.CheckAdminRole) + ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole) ginGroup.Any("/*path", func(c *gin.Context) { if util.ReadOnly { switch c.Request.Method { From 734c799aa59e5acb958e50e371d50a206d681e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:17:22 +0800 Subject: [PATCH 3/9] :art: implement load contacts feature --- kernel/go.mod | 4 +- kernel/go.sum | 7 +- kernel/model/carddav.go | 198 ++++++++++++++++++++++++++++++++++++---- kernel/model/session.go | 2 +- kernel/server/serve.go | 56 +++++++++--- 5 files changed, 230 insertions(+), 37 deletions(-) diff --git a/kernel/go.mod b/kernel/go.mod index 1856a389034..99d3402ec15 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -23,6 +23,8 @@ 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-vcard v0.0.0-20230815062825-8fda7d206ec9 + github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 github.com/emirpasic/gods v1.18.1 github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb github.com/flopp/go-findfont v0.1.0 @@ -98,8 +100,6 @@ require ( github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dsnet/compress v0.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 // indirect - github.com/emersion/go-webdav v0.5.0 // indirect github.com/fatih/set v0.2.1 // indirect github.com/gigawattio/window v0.0.0-20180317192513-0f5467e35573 // indirect github.com/gin-contrib/sse v0.1.0 // indirect diff --git a/kernel/go.sum b/kernel/go.sum index 949add86596..3c0ab3a949d 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -89,11 +89,11 @@ 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-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ= +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 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA= github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= -github.com/emersion/go-webdav v0.5.0 h1:Ak/BQLgAihJt/UxJbCsEXDPxS5Uw4nZzgIMOq3rkKjc= -github.com/emersion/go-webdav v0.5.0/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno= +github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 h1:Ssk00uh7jhctJ23eclGxhhGqplSQB+wCt6fmbjhnOS8= +github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= @@ -372,7 +372,6 @@ 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.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU= 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/carddav.go b/kernel/model/carddav.go index 765b45972c9..03501463001 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -17,60 +17,226 @@ package model import ( + "bytes" "context" + "fmt" + "os" + "path" + "path/filepath" + "strings" + "sync" + "github.com/88250/gulu" "github.com/emersion/go-vcard" "github.com/emersion/go-webdav/carddav" + "github.com/siyuan-note/logging" + "github.com/siyuan-note/siyuan/kernel/util" ) const ( // REF: https://developers.google.com/people/carddav#resources - CardDavPrefixPath = "/carddav" - CardDavPrincipalsPath = CardDavPrefixPath + "/principals" - CardDavCurrentUserPrincipalPath = CardDavPrincipalsPath + "/main" - CardDavHomeSetPath = CardDavCurrentUserPrincipalPath + "/contacts" - CardDavAddressBookPath = CardDavHomeSetPath + "/default" + CardDavPrefixPath = "/carddav" + CardDavRootPath = CardDavPrefixPath + "/principals" // 0 resourceTypeRoot + CardDavUserPrincipalPath = CardDavRootPath + "/main" // 1 resourceTypeUserPrincipal + CardDavHomeSetPath = CardDavUserPrincipalPath + "/contacts" // 2 resourceTypeAddressBookHomeSet + + CardDavDefaultAddressBookPath = CardDavHomeSetPath + "/default" // 3 resourceTypeAddressBook + CardDavDefaultAddressBookName = "default" + + CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json" ) +var ( + defaultAddressBook = carddav.AddressBook{ + Path: CardDavDefaultAddressBookPath, + Name: CardDavDefaultAddressBookName, + Description: "Default address book", + // MaxResourceSize: math.MaxInt32, + } + contacts = Contacts{ + M: sync.Map{}, + } +) + +type Contacts struct { + M sync.Map // Path -> *AddressBook +} + +func (c *Contacts) reload() error { + c.M.Clear() + addressBooksMetaDataFilePath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/")) + metaData, err := os.ReadFile(addressBooksMetaDataFilePath) + if os.IsNotExist(err) { + // create meta data file + if err = os.MkdirAll(CardDavDefaultAddressBookPath, 0755); err != nil { + logging.LogErrorf("make dir [%s] failed: %s", CardDavDefaultAddressBookPath, err) + return err + } + + addressBooksMetaData := []*carddav.AddressBook{&defaultAddressBook} + data, err := gulu.JSON.MarshalIndentJSON(addressBooksMetaData, "", " ") + if err != nil { + logging.LogErrorf("marshal address books meta data failed: %s", err) + return err + } + + if err := os.WriteFile(addressBooksMetaDataFilePath, data, 0755); err != nil { + logging.LogErrorf("write file [%s] failed: %s", addressBooksMetaDataFilePath, err) + return err + } + } else { + // load meta data file + addressBooksMetaData := []*carddav.AddressBook{} + if err = gulu.JSON.UnmarshalJSON(metaData, &addressBooksMetaData); err != nil { + logging.LogErrorf("unmarshal address books meta data failed: %s", err) + return err + } + + wg := &sync.WaitGroup{} + wg.Add(len(addressBooksMetaData)) + for _, addressBookMetaData := range addressBooksMetaData { + addressBook := &AddressBook{ + MetaData: addressBookMetaData, + Addresses: sync.Map{}, + } + c.M.Store(addressBookMetaData.Path, addressBook) + go addressBook.load(wg) + } + wg.Wait() + } + return nil +} + +type AddressBook struct { + MetaData *carddav.AddressBook + Addresses sync.Map // id -> *carddav.AddressObject +} + +func (b *AddressBook) load(wg *sync.WaitGroup) { + defer wg.Done() + addressBookPath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(b.MetaData.Path, "/")) + entries, err := os.ReadDir(addressBookPath) + if err != nil { + logging.LogErrorf("read dir [%s] failed: %s", addressBookPath, err) + } else { + for _, entry := range entries { + if !entry.IsDir() { + filename := entry.Name() + ext := path.Ext(filename) + if ext == ".vcf" { + wg.Add(1) + go func() { + defer wg.Done() + + addressFilePath := path.Join(addressBookPath, filename) + + addressFileInfo, err := entry.Info() + if err != nil { + logging.LogErrorf("get file [%s] info failed: %s", addressFilePath, err) + return + } + + // read file + addressData, err := os.ReadFile(addressFilePath) + if err != nil { + logging.LogErrorf("read file [%s] failed: %s", addressFilePath, err) + return + } + + // decode file + reader := bytes.NewReader(addressData) + decoder := vcard.NewDecoder(reader) + card, err := decoder.Decode() + if err != nil { + logging.LogErrorf("decode file [%s] failed: %s", addressFilePath, err) + return + } + + // load data + address := &carddav.AddressObject{ + Path: b.MetaData.Path + "/" + filename, + ModTime: addressFileInfo.ModTime(), + ContentLength: addressFileInfo.Size(), + ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()), + Card: card, + } + + id := path.Base(filename) + b.Addresses.Store(id, address) + }() + } + } + } + } +} + +func (b *AddressBook) save(wg *sync.WaitGroup) { + defer wg.Done() + // TODO: save addresses data to files +} + type CardDavBackend struct{} func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { - return CardDavCurrentUserPrincipalPath, nil + logging.LogInfof("CardDAV CurrentUserPrincipal") + return CardDavUserPrincipalPath, nil } -func (b *CardDavBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) { +func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) { + logging.LogInfof("CardDAV AddressBookHomeSetPath") return CardDavHomeSetPath, nil } -func (b *CardDavBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) { - return &carddav.AddressBook{ - Path: CardDavAddressBookPath, - Name: "SiYuan", - Description: "SiYuan default contacts", - }, nil +func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) { + logging.LogInfof("CardDAV ListAddressBooks") + // TODO + return +} + +func (b *CardDavBackend) GetAddressBook(ctx context.Context, path string) (addressBook *carddav.AddressBook, err error) { + logging.LogInfof("CardDAV GetAddressBook") + // TODO + return +} + +func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) { + logging.LogInfof("CardDAV CreateAddressBook") + // TODO + return +} + +func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, path string) (err error) { + logging.LogInfof("CardDAV DeleteAddressBook") + // TODO + return } func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { + logging.LogInfof("CardDAV GetAddressObject: %s", path) // TODO return } -func (b *CardDavBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { +func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { + logging.LogInfof("CardDAV ListAddressObjects") // TODO return } -func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { +func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { + logging.LogInfof("CardDAV QueryAddressObjects: %v", query) // TODO return } -func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) { +func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { + logging.LogInfof("CardDAV PutAddressObject: %s", path) // TODO return } func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) { + logging.LogInfof("CardDAV DeleteAddressObject: %s", path) // TODO return } diff --git a/kernel/model/session.go b/kernel/model/session.go index ef0d4244026..3dc3bca8efa 100644 --- a/kernel/model/session.go +++ b/kernel/model/session.go @@ -300,7 +300,7 @@ func CheckAuth(c *gin.Context) { } // WebDAV BasicAuth Authenticate - if strings.HasPrefix(c.Request.RequestURI, "/webdav") { + if strings.HasPrefix(c.Request.RequestURI, "/webdav") || strings.HasPrefix(c.Request.RequestURI, "/carddav") { c.Header("WWW-Authenticate", "Basic realm=Authorization Required") c.AbortWithStatus(http.StatusUnauthorized) return diff --git a/kernel/server/serve.go b/kernel/server/serve.go index dae5bb096a0..8be8a63f437 100644 --- a/kernel/server/serve.go +++ b/kernel/server/serve.go @@ -48,17 +48,42 @@ import ( "golang.org/x/net/webdav" ) +const ( + MethodMkcol = "MKCOL" + MethodCopy = "COPY" + MethodMove = "MOVE" + MethodLock = "LOCK" + MethodUnlock = "UNLOCK" + MethodPropfind = "PROPFIND" + MethodProppatch = "PROPPATCH" +) + var ( - cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf")) - WebDavMethod = []string{ - "OPTIONS", - "GET", "HEAD", - "POST", "PUT", - "DELETE", - "MKCOL", - "COPY", "MOVE", - "LOCK", "UNLOCK", - "PROPFIND", "PROPPATCH", + cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf")) + WebDavMethods = []string{ + http.MethodOptions, + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + + MethodMkcol, + MethodCopy, + MethodMove, + MethodLock, + MethodUnlock, + MethodPropfind, + MethodProppatch, + } + CardDavMethods = []string{ + http.MethodOptions, + http.MethodHead, + http.MethodGet, + http.MethodPut, + http.MethodDelete, + + MethodPropfind, } ) @@ -618,7 +643,8 @@ func serveWebDAV(ginServer *gin.Engine) { } ginGroup := ginServer.Group("/webdav", model.CheckAuth, model.CheckAdminRole) - ginGroup.Any("/*path", func(c *gin.Context) { + // ginGroup.Any NOT support extension methods (PROPFIND etc.) + ginGroup.Match(WebDavMethods, "/*path", func(c *gin.Context) { if util.ReadOnly { switch c.Request.Method { case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": @@ -634,15 +660,17 @@ func serveCardDAV(ginServer *gin.Engine) { // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go handler := carddav.Handler{ Backend: &model.CardDavBackend{}, - Prefix: model.CardDavPrefixPath, + Prefix: model.CardDavRootPath, } - ginServer.Any("/.well-known/caldav", func(c *gin.Context) { + ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) { + logging.LogInfof("CardDAV %s: /.well-known/carddav", c.Request.Method) handler.ServeHTTP(c.Writer, c.Request) }) ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole) - ginGroup.Any("/*path", func(c *gin.Context) { + ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) { + logging.LogInfof("CardDAV %s: %s", c.Request.Method, c.Request.URL.Path) if util.ReadOnly { switch c.Request.Method { case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": From 173c1b09dba4dc0b77ce30513fefb8eaba950fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:56:08 +0800 Subject: [PATCH 4/9] :art: implement save contacts feature --- kernel/go.mod | 2 +- kernel/go.sum | 2 + kernel/model/carddav.go | 161 ++++++++++++++++++++++++++++++---------- 3 files changed, 124 insertions(+), 41 deletions(-) diff --git a/kernel/go.mod b/kernel/go.mod index 99d3402ec15..f898835a4e0 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -23,7 +23,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-vcard v0.0.0-20230815062825-8fda7d206ec9 + 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 github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb diff --git a/kernel/go.sum b/kernel/go.sum index 3c0ab3a949d..18285d54769 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -92,6 +92,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m 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 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA= 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/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135 h1:Ssk00uh7jhctJ23eclGxhhGqplSQB+wCt6fmbjhnOS8= github.com/emersion/go-webdav v0.5.1-0.20240713135526-7f8c17ad7135/go.mod h1:mI8iBx3RAODwX7PJJ7qzsKAKs/vY429YfS2/9wKnDbQ= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index 03501463001..3d6b3c0fff0 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -54,16 +54,28 @@ var ( // MaxResourceSize: math.MaxInt32, } contacts = Contacts{ - M: sync.Map{}, + Loaded: false, + Changed: false, + Lock: sync.Mutex{}, + Books: sync.Map{}, + BooksMetaData: []*carddav.AddressBook{}, } ) type Contacts struct { - M sync.Map // Path -> *AddressBook + Loaded bool + Changed bool + Lock sync.Mutex // load & save + Books sync.Map // Path -> *AddressBook + BooksMetaData []*carddav.AddressBook } +// reload all contacts func (c *Contacts) reload() error { - c.M.Clear() + c.Lock.Lock() + defer c.Lock.Unlock() + + c.Books.Clear() addressBooksMetaDataFilePath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/")) metaData, err := os.ReadFile(addressBooksMetaDataFilePath) if os.IsNotExist(err) { @@ -73,8 +85,8 @@ func (c *Contacts) reload() error { return err } - addressBooksMetaData := []*carddav.AddressBook{&defaultAddressBook} - data, err := gulu.JSON.MarshalIndentJSON(addressBooksMetaData, "", " ") + c.BooksMetaData = []*carddav.AddressBook{&defaultAddressBook} + data, err := gulu.JSON.MarshalIndentJSON(c.BooksMetaData, "", " ") if err != nil { logging.LogErrorf("marshal address books meta data failed: %s", err) return err @@ -86,32 +98,37 @@ func (c *Contacts) reload() error { } } else { // load meta data file - addressBooksMetaData := []*carddav.AddressBook{} - if err = gulu.JSON.UnmarshalJSON(metaData, &addressBooksMetaData); err != nil { + c.BooksMetaData = []*carddav.AddressBook{} + if err = gulu.JSON.UnmarshalJSON(metaData, &c.BooksMetaData); err != nil { logging.LogErrorf("unmarshal address books meta data failed: %s", err) return err } wg := &sync.WaitGroup{} - wg.Add(len(addressBooksMetaData)) - for _, addressBookMetaData := range addressBooksMetaData { + wg.Add(len(c.BooksMetaData)) + for _, addressBookMetaData := range c.BooksMetaData { addressBook := &AddressBook{ + Changed: false, MetaData: addressBookMetaData, Addresses: sync.Map{}, } - c.M.Store(addressBookMetaData.Path, addressBook) + c.Books.Store(addressBookMetaData.Path, addressBook) go addressBook.load(wg) } wg.Wait() } + c.Loaded = true + c.Changed = false return nil } type AddressBook struct { + Changed bool MetaData *carddav.AddressBook - Addresses sync.Map // id -> *carddav.AddressObject + Addresses sync.Map // id -> *AddressObject } +// load an address book from multiple *.vcf files func (b *AddressBook) load(wg *sync.WaitGroup) { defer wg.Done() addressBookPath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(b.MetaData.Path, "/")) @@ -130,37 +147,15 @@ func (b *AddressBook) load(wg *sync.WaitGroup) { addressFilePath := path.Join(addressBookPath, filename) - addressFileInfo, err := entry.Info() - if err != nil { - logging.LogErrorf("get file [%s] info failed: %s", addressFilePath, err) - return + // load data + address := &AddressObject{ + FilePath: addressFilePath, + BookPath: b.MetaData.Path, } - - // read file - addressData, err := os.ReadFile(addressFilePath) - if err != nil { - logging.LogErrorf("read file [%s] failed: %s", addressFilePath, err) + if err := address.load(); err != nil { return } - // decode file - reader := bytes.NewReader(addressData) - decoder := vcard.NewDecoder(reader) - card, err := decoder.Decode() - if err != nil { - logging.LogErrorf("decode file [%s] failed: %s", addressFilePath, err) - return - } - - // load data - address := &carddav.AddressObject{ - Path: b.MetaData.Path + "/" + filename, - ModTime: addressFileInfo.ModTime(), - ContentLength: addressFileInfo.Size(), - ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()), - Card: card, - } - id := path.Base(filename) b.Addresses.Store(id, address) }() @@ -170,9 +165,95 @@ func (b *AddressBook) load(wg *sync.WaitGroup) { } } -func (b *AddressBook) save(wg *sync.WaitGroup) { +// save an address book to multiple *.vcf files +func (b *AddressBook) save(force bool, wg *sync.WaitGroup) { defer wg.Done() - // TODO: save addresses data to files + + b.Addresses.Range(func(id any, address any) bool { + // id_ := id.(string) + address_ := address.(*AddressObject) + address_.save(force) + return true + }) +} + +type AddressObject struct { + Changed bool + FilePath string + BookPath string + Data *carddav.AddressObject +} + +// load an address from *.vcf file +func (o *AddressObject) load() error { + // get file info + addressFileInfo, err := os.Stat(o.FilePath) + if err != nil { + logging.LogErrorf("get file [%s] info failed: %s", o.FilePath, err) + return err + } + + // read file + addressData, err := os.ReadFile(o.FilePath) + if err != nil { + logging.LogErrorf("read file [%s] failed: %s", o.FilePath, err) + return err + } + + // decode file + 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{ + Path: o.BookPath + "/" + addressFileInfo.Name(), + ModTime: addressFileInfo.ModTime(), + ContentLength: addressFileInfo.Size(), + ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()), + Card: card, + } + return nil +} + +// save an address to *.vcf file +func (o *AddressObject) save(force bool) error { + if force || o.Changed { + var addressData bytes.Buffer + + // encode data + encoder := vcard.NewEncoder(&addressData) + if err := encoder.Encode(o.Data.Card); err != nil { + logging.LogErrorf("encode card [%s] failed: %s", o.Data.Path, err) + return err + } + + // write file + if err := os.WriteFile(o.FilePath, addressData.Bytes(), 0755); err != nil { + logging.LogErrorf("write file [%s] failed: %s", o.FilePath, err) + return err + } + + // 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 = 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.Changed = false + } + return nil } type CardDavBackend struct{} From c24a8779a62ffb06d634c5bffba8fc862b466a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:17:52 +0800 Subject: [PATCH 5/9] :art: implement address books CURD --- kernel/model/carddav.go | 383 +++++++++++++++++++++++++++++++--------- 1 file changed, 296 insertions(+), 87 deletions(-) diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index 3d6b3c0fff0..aefd3e48d32 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -54,29 +54,34 @@ var ( // MaxResourceSize: math.MaxInt32, } contacts = Contacts{ - Loaded: false, - Changed: false, - Lock: sync.Mutex{}, - Books: sync.Map{}, - BooksMetaData: []*carddav.AddressBook{}, + loaded: false, + changed: false, + lock: sync.Mutex{}, + books: sync.Map{}, + booksMetaData: []*carddav.AddressBook{}, } ) -type Contacts struct { - Loaded bool - Changed bool - Lock sync.Mutex // load & save - Books sync.Map // Path -> *AddressBook - BooksMetaData []*carddav.AddressBook +func AddressBookPath2DirectoryPath(addressBookPath string) string { + return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(addressBookPath, "/")) +} + +func AddressBooksMetaDataFilePath() string { + return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/")) } -// reload all contacts -func (c *Contacts) reload() error { - c.Lock.Lock() - defer c.Lock.Unlock() +type Contacts struct { + loaded bool + changed bool + lock sync.Mutex // load & save + books sync.Map // Path -> *AddressBook + booksMetaData []*carddav.AddressBook +} - c.Books.Clear() - addressBooksMetaDataFilePath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/")) +// load all contacts +func (c *Contacts) load() error { + c.books.Clear() + addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath() metaData, err := os.ReadFile(addressBooksMetaDataFilePath) if os.IsNotExist(err) { // create meta data file @@ -85,96 +90,270 @@ func (c *Contacts) reload() error { return err } - c.BooksMetaData = []*carddav.AddressBook{&defaultAddressBook} - data, err := gulu.JSON.MarshalIndentJSON(c.BooksMetaData, "", " ") - if err != nil { - logging.LogErrorf("marshal address books meta data failed: %s", err) - return err - } - - if err := os.WriteFile(addressBooksMetaDataFilePath, data, 0755); err != nil { - logging.LogErrorf("write file [%s] failed: %s", addressBooksMetaDataFilePath, err) + c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook} + if err := c.saveAddressBooksMetaData(); err != nil { return err } } else { // load meta data file - c.BooksMetaData = []*carddav.AddressBook{} - if err = gulu.JSON.UnmarshalJSON(metaData, &c.BooksMetaData); err != nil { + c.booksMetaData = []*carddav.AddressBook{} + if err = gulu.JSON.UnmarshalJSON(metaData, &c.booksMetaData); err != nil { logging.LogErrorf("unmarshal address books meta data failed: %s", err) return err } wg := &sync.WaitGroup{} - wg.Add(len(c.BooksMetaData)) - for _, addressBookMetaData := range c.BooksMetaData { + wg.Add(len(c.booksMetaData)) + for _, addressBookMetaData := range c.booksMetaData { addressBook := &AddressBook{ - Changed: false, - MetaData: addressBookMetaData, - Addresses: sync.Map{}, + Changed: false, + DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path), + MetaData: addressBookMetaData, + Addresses: sync.Map{}, } - c.Books.Store(addressBookMetaData.Path, addressBook) - go addressBook.load(wg) + c.books.Store(addressBookMetaData.Path, addressBook) + go func() { + defer wg.Done() + addressBook.load() + }() } wg.Wait() } - c.Loaded = true - c.Changed = false + + c.loaded = true + c.changed = false + return nil +} + +// save all contacts +func (c *Contacts) save(force bool) error { + if force || c.changed { + // save address books meta data + if err := c.saveAddressBooksMetaData(); err != nil { + return err + } + + // save all address to *.vbf files + wg := &sync.WaitGroup{} + c.books.Range(func(path any, book any) bool { + wg.Add(1) + go func() { + defer wg.Done() + // path_ := path.(string) + book_ := book.(*AddressBook) + book_.save(force) + }() + return true + }) + wg.Wait() + c.changed = false + } + return nil +} + +// 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 + } + + 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 { + c.lock.Lock() + defer c.lock.Unlock() + + if !c.loaded { + return c.load() + } + return nil +} + +func (c *Contacts) ListAddressBooks() (addressBooks []carddav.AddressBook, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + for _, addressBook := range contacts.booksMetaData { + addressBooks = append(addressBooks, *addressBook) + } + return +} + +func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + if book, ok := contacts.books.Load(path); ok { + addressBook = book.(*AddressBook).MetaData + } + return +} + +func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) (err error) { + c.lock.Lock() + defer c.lock.Unlock() + + var addressBook *AddressBook + + // update map + if value, ok := c.books.Load(addressBookMetaData.Path); ok { + // update map item + addressBook = value.(*AddressBook) + addressBook.MetaData = addressBookMetaData + } else { + // insert map item + addressBook = &AddressBook{ + Changed: false, + DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path), + MetaData: addressBookMetaData, + Addresses: sync.Map{}, + } + c.books.Store(addressBookMetaData.Path, addressBook) + } + + var index = -1 + for i, item := range c.booksMetaData { + if item.Path == addressBookMetaData.Path { + index = i + break + } + } + + if index >= 0 { + // update list + c.booksMetaData[index] = addressBookMetaData + } else { + // insert list + c.booksMetaData = append(c.booksMetaData, addressBookMetaData) + } + + // create address book directory + if err = os.MkdirAll(addressBook.DirectoryPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", addressBook, err) + return + } + + // save meta data + if err = c.saveAddressBooksMetaData(); err != nil { + return + } + + return +} + +func (c *Contacts) DeleteAddressBook(path string) (err error) { + c.lock.Lock() + defer c.lock.Unlock() + + var addressBook *AddressBook + + // delete map item + if value, loaded := c.books.LoadAndDelete(path); loaded { + addressBook = value.(*AddressBook) + } + + // delete list item + for i, item := range c.booksMetaData { + if item.Path == path { + c.booksMetaData = append(c.booksMetaData[:i], c.booksMetaData[i+1:]...) + break + } + } + + // remove address book directory + if err = os.RemoveAll(addressBook.DirectoryPath); err != nil { + logging.LogErrorf("remove directory [%s] failed: %s", addressBook, err) + return + } + + // save meta data + if err = c.saveAddressBooksMetaData(); err != nil { + return + } + return nil } type AddressBook struct { - Changed bool - MetaData *carddav.AddressBook - Addresses sync.Map // id -> *AddressObject + Changed bool + DirectoryPath string + MetaData *carddav.AddressBook + Addresses sync.Map // id -> *AddressObject } // load an address book from multiple *.vcf files -func (b *AddressBook) load(wg *sync.WaitGroup) { - defer wg.Done() - addressBookPath := filepath.Join(util.DataDir, "storage", strings.TrimPrefix(b.MetaData.Path, "/")) - entries, err := os.ReadDir(addressBookPath) +func (b *AddressBook) load() error { + entries, err := os.ReadDir(b.DirectoryPath) if err != nil { - logging.LogErrorf("read dir [%s] failed: %s", addressBookPath, err) - } else { - for _, entry := range entries { - if !entry.IsDir() { - filename := entry.Name() - ext := path.Ext(filename) - if ext == ".vcf" { - wg.Add(1) - go func() { - defer wg.Done() - - addressFilePath := path.Join(addressBookPath, filename) - - // load data - address := &AddressObject{ - FilePath: addressFilePath, - BookPath: b.MetaData.Path, - } - if err := address.load(); err != nil { - return - } - - id := path.Base(filename) - b.Addresses.Store(id, address) - }() - } + logging.LogErrorf("read dir [%s] failed: %s", b.DirectoryPath, err) + return err + } + + wg := &sync.WaitGroup{} + for _, entry := range entries { + if !entry.IsDir() { + filename := entry.Name() + ext := path.Ext(filename) + if ext == ".vcf" { + wg.Add(1) + go func() { + defer wg.Done() + + addressFilePath := path.Join(b.DirectoryPath, filename) + + // load data + address := &AddressObject{ + FilePath: addressFilePath, + BookPath: b.MetaData.Path, + } + if err := address.load(); err != nil { + return + } + + id := path.Base(filename) + b.Addresses.Store(id, address) + }() } } } + wg.Wait() + return nil } // save an address book to multiple *.vcf files -func (b *AddressBook) save(force bool, wg *sync.WaitGroup) { - defer wg.Done() - - b.Addresses.Range(func(id any, address any) bool { - // id_ := id.(string) - address_ := address.(*AddressObject) - address_.save(force) - return true - }) +func (b *AddressBook) save(force bool) error { + if force || b.Changed { + // create directory + if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err) + return err + } + + wg := &sync.WaitGroup{} + b.Addresses.Range(func(id any, address any) bool { + wg.Add(1) + go func() { + defer wg.Done() + // id_ := id.(string) + address_ := address.(*AddressObject) + address_.save(force) + }() + return true + }) + wg.Wait() + b.Changed = false + } + + return nil } type AddressObject struct { @@ -270,54 +449,84 @@ func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, er func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) { logging.LogInfof("CardDAV ListAddressBooks") - // TODO - return + if err = contacts.Load(); err != nil { + return + } + + return contacts.ListAddressBooks() } func (b *CardDavBackend) GetAddressBook(ctx context.Context, path string) (addressBook *carddav.AddressBook, err error) { logging.LogInfof("CardDAV GetAddressBook") - // TODO - return + if err = contacts.Load(); err != nil { + return + } + + return contacts.GetAddressBook(path) } func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) { logging.LogInfof("CardDAV CreateAddressBook") - // TODO - return + if err = contacts.Load(); err != nil { + return + } + return contacts.CreateAddressBook(addressBook) } func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, path string) (err error) { logging.LogInfof("CardDAV DeleteAddressBook") - // TODO - return + if err = contacts.Load(); err != nil { + return + } + return contacts.DeleteAddressBook(path) } func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { logging.LogInfof("CardDAV GetAddressObject: %s", path) + if err = contacts.Load(); err != nil { + return + } + // TODO return } func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { logging.LogInfof("CardDAV ListAddressObjects") + if err = contacts.Load(); err != nil { + return + } + // TODO return } func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { logging.LogInfof("CardDAV QueryAddressObjects: %v", query) + if err = contacts.Load(); err != nil { + return + } + // TODO return } func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { logging.LogInfof("CardDAV PutAddressObject: %s", path) + if err = contacts.Load(); err != nil { + return + } + // TODO return } func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) { logging.LogInfof("CardDAV DeleteAddressObject: %s", path) + if err = contacts.Load(); err != nil { + return + } + // TODO return } From 743e24b954a2d7e1d72a9569132b5dd7eaa85b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:35:00 +0800 Subject: [PATCH 6/9] :bug: fix CardDAV method `OPTIONS` --- kernel/go.sum | 1 - kernel/model/carddav.go | 115 +++++++++++++++++++++++++--------------- kernel/server/serve.go | 53 ++++++++++++++---- 3 files changed, 115 insertions(+), 54 deletions(-) diff --git a/kernel/go.sum b/kernel/go.sum index 18285d54769..7dd3a058f6c 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -90,7 +90,6 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf 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/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw= -github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA= 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/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index aefd3e48d32..25b08a8be98 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -19,6 +19,7 @@ package model import ( "bytes" "context" + "errors" "fmt" "os" "path" @@ -48,10 +49,10 @@ const ( var ( defaultAddressBook = carddav.AddressBook{ - Path: CardDavDefaultAddressBookPath, - Name: CardDavDefaultAddressBookName, - Description: "Default address book", - // MaxResourceSize: math.MaxInt32, + Path: CardDavDefaultAddressBookPath, + Name: CardDavDefaultAddressBookName, + Description: "Default address book", + MaxResourceSize: 0, } contacts = Contacts{ loaded: false, @@ -60,14 +61,19 @@ var ( books: sync.Map{}, booksMetaData: []*carddav.AddressBook{}, } + ErrorNotFound = errors.New("carddav: not found") ) func AddressBookPath2DirectoryPath(addressBookPath string) string { return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(addressBookPath, "/")) } +func HomeSetPathPath() string { + return filepath.Join(util.DataDir, "storage", CardDavHomeSetPath) +} + func AddressBooksMetaDataFilePath() string { - return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(CardDavAddressBooksMetaDataFilePath, "/")) + return filepath.Join(util.DataDir, "storage", CardDavAddressBooksMetaDataFilePath) } type Contacts struct { @@ -84,12 +90,6 @@ func (c *Contacts) load() error { addressBooksMetaDataFilePath := AddressBooksMetaDataFilePath() metaData, err := os.ReadFile(addressBooksMetaDataFilePath) if os.IsNotExist(err) { - // create meta data file - if err = os.MkdirAll(CardDavDefaultAddressBookPath, 0755); err != nil { - logging.LogErrorf("make dir [%s] failed: %s", CardDavDefaultAddressBookPath, err) - return err - } - c.booksMetaData = []*carddav.AddressBook{&defaultAddressBook} if err := c.saveAddressBooksMetaData(); err != nil { return err @@ -101,24 +101,24 @@ func (c *Contacts) load() error { logging.LogErrorf("unmarshal address books meta data failed: %s", err) return err } + } - wg := &sync.WaitGroup{} - wg.Add(len(c.booksMetaData)) - for _, addressBookMetaData := range c.booksMetaData { - addressBook := &AddressBook{ - Changed: false, - DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path), - MetaData: addressBookMetaData, - Addresses: sync.Map{}, - } - c.books.Store(addressBookMetaData.Path, addressBook) - go func() { - defer wg.Done() - addressBook.load() - }() + wg := &sync.WaitGroup{} + wg.Add(len(c.booksMetaData)) + for _, addressBookMetaData := range c.booksMetaData { + addressBook := &AddressBook{ + Changed: false, + DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path), + MetaData: addressBookMetaData, + Addresses: sync.Map{}, } - wg.Wait() + c.books.Store(addressBookMetaData.Path, addressBook) + go func() { + defer wg.Done() + addressBook.load() + }() } + wg.Wait() c.loaded = true c.changed = false @@ -159,6 +159,12 @@ func (c *Contacts) saveAddressBooksMetaData() error { 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) @@ -194,7 +200,10 @@ func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook if book, ok := contacts.books.Load(path); ok { addressBook = book.(*AddressBook).MetaData + return } + + err = ErrorNotFound return } @@ -292,6 +301,11 @@ type AddressBook struct { // load an address book from multiple *.vcf files func (b *AddressBook) load() error { + if err := os.MkdirAll(b.DirectoryPath, 0755); err != nil { + logging.LogErrorf("create directory [%s] failed: %s", b.DirectoryPath, err) + return err + } + entries, err := os.ReadDir(b.DirectoryPath) if err != nil { logging.LogErrorf("read dir [%s] failed: %s", b.DirectoryPath, err) @@ -412,6 +426,13 @@ func (o *AddressObject) save(force bool) error { 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, addressData.Bytes(), 0755); err != nil { logging.LogErrorf("write file [%s] failed: %s", o.FilePath, err) @@ -453,16 +474,18 @@ func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []c return } - return contacts.ListAddressBooks() + addressBooks, err = contacts.ListAddressBooks() + return } -func (b *CardDavBackend) GetAddressBook(ctx context.Context, path string) (addressBook *carddav.AddressBook, err error) { - logging.LogInfof("CardDAV GetAddressBook") +func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) { + logging.LogInfof("CardDAV GetAddressBook: %s", bookPath) if err = contacts.Load(); err != nil { return } - return contacts.GetAddressBook(path) + addressBook, err = contacts.GetAddressBook(bookPath) + return } func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) { @@ -470,19 +493,23 @@ func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *car if err = contacts.Load(); err != nil { return } - return contacts.CreateAddressBook(addressBook) + + err = contacts.CreateAddressBook(addressBook) + return } -func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, path string) (err error) { - logging.LogInfof("CardDAV DeleteAddressBook") +func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) { + logging.LogInfof("CardDAV DeleteAddressBook: %s", bookPath) if err = contacts.Load(); err != nil { return } - return contacts.DeleteAddressBook(path) + + err = contacts.DeleteAddressBook(bookPath) + return } -func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { - logging.LogInfof("CardDAV GetAddressObject: %s", path) +func (b *CardDavBackend) GetAddressObject(ctx context.Context, urlPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { + logging.LogInfof("CardDAV GetAddressObject: %s", urlPath) if err = contacts.Load(); err != nil { return } @@ -491,8 +518,8 @@ func (b *CardDavBackend) GetAddressObject(ctx context.Context, path string, req return } -func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { - logging.LogInfof("CardDAV ListAddressObjects") +func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { + logging.LogInfof("CardDAV ListAddressObjects: %s", bookPath) if err = contacts.Load(); err != nil { return } @@ -501,8 +528,8 @@ func (b *CardDavBackend) ListAddressObjects(ctx context.Context, path string, re return } -func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { - logging.LogInfof("CardDAV QueryAddressObjects: %v", query) +func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { + logging.LogInfof("CardDAV QueryAddressObjects: %s %v", urlPath, query) if err = contacts.Load(); err != nil { return } @@ -511,8 +538,8 @@ func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, path string, q return } -func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { - logging.LogInfof("CardDAV PutAddressObject: %s", path) +func (b *CardDavBackend) PutAddressObject(ctx context.Context, urlPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { + logging.LogInfof("CardDAV PutAddressObject: %s %v", urlPath, card) if err = contacts.Load(); err != nil { return } @@ -521,8 +548,8 @@ func (b *CardDavBackend) PutAddressObject(ctx context.Context, path string, card return } -func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, path string) (err error) { - logging.LogInfof("CardDAV DeleteAddressObject: %s", path) +func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, urlPath string) (err error) { + logging.LogInfof("CardDAV DeleteAddressObject: %s", urlPath) if err = contacts.Load(); err != nil { return } diff --git a/kernel/server/serve.go b/kernel/server/serve.go index 8be8a63f437..3f782e61e82 100644 --- a/kernel/server/serve.go +++ b/kernel/server/serve.go @@ -54,12 +54,23 @@ const ( MethodMove = "MOVE" MethodLock = "LOCK" MethodUnlock = "UNLOCK" - MethodPropfind = "PROPFIND" - MethodProppatch = "PROPPATCH" + MethodPropFind = "PROPFIND" + MethodPropPatch = "PROPPATCH" ) var ( - cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf")) + cookieStore = cookie.NewStore([]byte("ATN51UlxVq1Gcvdf")) + HttpMethods = []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodOptions, + http.MethodTrace, + } WebDavMethods = []string{ http.MethodOptions, http.MethodHead, @@ -73,17 +84,24 @@ var ( MethodMove, MethodLock, MethodUnlock, - MethodPropfind, - MethodProppatch, + MethodPropFind, + MethodPropPatch, } CardDavMethods = []string{ http.MethodOptions, http.MethodHead, http.MethodGet, + http.MethodPost, http.MethodPut, http.MethodDelete, - MethodPropfind, + MethodMkcol, + // MethodCopy, + // MethodMove, + // MethodLock, + // MethodUnlock, + MethodPropFind, + MethodPropPatch, } ) @@ -698,15 +716,32 @@ func shortReqMsg(msg []byte) []byte { } func corsMiddleware() gin.HandlerFunc { - return func(c *gin.Context) { + allowMethods := strings.Join(HttpMethods, ", ") + allowWebDavMethods := strings.Join(WebDavMethods, ", ") + allowCardDavMethods := strings.Join(CardDavMethods, ", ") + return func(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Credentials", "true") c.Header("Access-Control-Allow-Headers", "origin, Content-Length, Content-Type, Authorization") - c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS") c.Header("Access-Control-Allow-Private-Network", "true") - if c.Request.Method == "OPTIONS" { + if strings.HasPrefix(c.Request.RequestURI, "/webdav/") { + c.Header("Access-Control-Allow-Methods", allowWebDavMethods) + c.Next() + return + } + + if strings.HasPrefix(c.Request.RequestURI, "/carddav/") { + c.Header("Access-Control-Allow-Methods", allowCardDavMethods) + c.Next() + return + } + + c.Header("Access-Control-Allow-Methods", allowMethods) + + switch c.Request.Method { + case http.MethodOptions: c.Header("Access-Control-Max-Age", "600") c.AbortWithStatus(204) return From 61cbba14517fe4211ac39ebddf109fb6d7a7c167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Wed, 6 Nov 2024 03:39:19 +0800 Subject: [PATCH 7/9] :art: implement addresses CURD --- kernel/model/carddav.go | 268 ++++++++++++++++++++++++++++++++++------ kernel/server/serve.go | 2 - 2 files changed, 228 insertions(+), 42 deletions(-) diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index 25b08a8be98..3891152a4d8 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -61,19 +61,72 @@ var ( books: sync.Map{}, booksMetaData: []*carddav.AddressBook{}, } - ErrorNotFound = errors.New("carddav: not found") + + ErrorNotFound = errors.New("CardDAV: not found") + + ErrorBookNotFound = errors.New("CardDAV: address book not found") + ErrorBookPathInvalid = 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") ) -func AddressBookPath2DirectoryPath(addressBookPath string) string { - return filepath.Join(util.DataDir, "storage", strings.TrimPrefix(addressBookPath, "/")) +// 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, "/")) } +// HomeSetPathPath returns the absolute path of the address book home set directory func HomeSetPathPath() string { - return filepath.Join(util.DataDir, "storage", CardDavHomeSetPath) + return CardDavPath2DirectoryPath(CardDavHomeSetPath) } +// AddressBooksMetaDataFilePath returns the absolute path of the address books meta data file func AddressBooksMetaDataFilePath() string { - return filepath.Join(util.DataDir, "storage", CardDavAddressBooksMetaDataFilePath) + return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath) +} + +// ParseAddressPath parses address path to address book path and address ID +func ParseAddressPath(addressParh string) (addressBookPath string, addressID string, err error) { + addressBookPath, addressFileName := path.Split(addressParh) + addressID = path.Base(addressFileName) + addressFileExt := path.Ext(addressFileName) + + if len(strings.Split(addressBookPath, "/")) != 6 { + err = ErrorBookPathInvalid + return + } + + if addressFileExt != ".vcf" { + err = ErrorAddressFileExtensionNameInvalid + return + } + + return +} + +func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject { + var card *vcard.Card + if req.AllProp { + card = &address.Card + } else { + card = &vcard.Card{} + for _, prop := range req.Props { + fields := address.Card[prop] + if fields != nil { + for _, field := range fields { + card.Add(prop, field) + } + } + } + } + return &carddav.AddressObject{ + Path: address.Path, + ModTime: address.ModTime, + ContentLength: address.ContentLength, + ETag: address.ETag, + Card: *card, + } } type Contacts struct { @@ -108,7 +161,7 @@ func (c *Contacts) load() error { for _, addressBookMetaData := range c.booksMetaData { addressBook := &AddressBook{ Changed: false, - DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path), + DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path), MetaData: addressBookMetaData, Addresses: sync.Map{}, } @@ -203,7 +256,7 @@ func (c *Contacts) GetAddressBook(path string) (addressBook *carddav.AddressBook return } - err = ErrorNotFound + err = ErrorBookNotFound return } @@ -222,7 +275,7 @@ func (c *Contacts) CreateAddressBook(addressBookMetaData *carddav.AddressBook) ( // insert map item addressBook = &AddressBook{ Changed: false, - DirectoryPath: AddressBookPath2DirectoryPath(addressBookMetaData.Path), + DirectoryPath: CardDavPath2DirectoryPath(addressBookMetaData.Path), MetaData: addressBookMetaData, Addresses: sync.Map{}, } @@ -292,6 +345,146 @@ func (c *Contacts) DeleteAddressBook(path string) (err error) { return nil } +func (c *Contacts) GetAddressObject(addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + bookPath, addressID, err := ParseAddressPath(addressPath) + if err != nil { + logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err) + return + } + + var addressBook *AddressBook + if value, ok := c.books.Load(bookPath); ok { + addressBook = value.(*AddressBook) + } else { + err = ErrorBookNotFound + return + } + + if value, ok := addressBook.Addresses.Load(addressID); ok { + addressObject = AddressPropsFilter(value.(*AddressObject).Data, req) + } else { + err = ErrorAddressNotFound + return + } + + return +} + +func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + var addressBook *AddressBook + if value, ok := c.books.Load(bookPath); ok { + addressBook = value.(*AddressBook) + } else { + err = ErrorBookNotFound + return + } + + addressBook.Addresses.Range(func(id any, address any) bool { + addressObjects = append(addressObjects, *AddressPropsFilter(address.(*AddressObject).Data, req)) + return true + }) + + return +} + +func (c *Contacts) QueryAddressObjects(addressPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + // TODO + return +} + +func (c *Contacts) PutAddressObject(addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + bookPath, addressID, err := ParseAddressPath(addressPath) + if err != nil { + logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err) + return + } + + var addressBook *AddressBook + if value, ok := c.books.Load(bookPath); ok { + addressBook = value.(*AddressBook) + } else { + err = ErrorBookNotFound + return + } + + 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), + BookPath: bookPath, + Data: &carddav.AddressObject{ + Card: card, + }, + } + } + + err = address.save(true) + if err != nil { + return + } + + err = address.update() + if err != nil { + return + } + + addressBook.Addresses.Store(addressID, address) + addressObject = address.Data + return +} + +func (c *Contacts) DeleteAddressObject(addressPath string) (err error) { + c.lock.Lock() + defer c.lock.Unlock() + + bookPath, addressID, err := ParseAddressPath(addressPath) + if err != nil { + logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err) + return + } + + var addressBook *AddressBook + if value, ok := c.books.Load(bookPath); ok { + addressBook = value.(*AddressBook) + } else { + err = ErrorBookNotFound + return + } + + if value, loaded := addressBook.Addresses.LoadAndDelete(addressID); loaded { + address := value.(*AddressObject) + + if err = os.Remove(address.FilePath); err != nil { + logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err) + return + } + } + + return +} + type AddressBook struct { Changed bool DirectoryPath string @@ -360,6 +553,7 @@ func (b *AddressBook) save(force bool) error { // id_ := id.(string) address_ := address.(*AddressObject) address_.save(force) + address_.update() }() return true }) @@ -439,37 +633,39 @@ func (o *AddressObject) save(force bool) error { return err } - // 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 = 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.Changed = false } return nil } +// 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 = 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()) + + return nil +} + type CardDavBackend struct{} func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { - logging.LogInfof("CardDAV CurrentUserPrincipal") return CardDavUserPrincipalPath, nil } func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) { - logging.LogInfof("CardDAV AddressBookHomeSetPath") return CardDavHomeSetPath, nil } func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) { - logging.LogInfof("CardDAV ListAddressBooks") if err = contacts.Load(); err != nil { return } @@ -479,7 +675,6 @@ func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []c } func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) { - logging.LogInfof("CardDAV GetAddressBook: %s", bookPath) if err = contacts.Load(); err != nil { return } @@ -489,7 +684,6 @@ func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (a } func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) { - logging.LogInfof("CardDAV CreateAddressBook") if err = contacts.Load(); err != nil { return } @@ -499,7 +693,6 @@ func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *car } func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) { - logging.LogInfof("CardDAV DeleteAddressBook: %s", bookPath) if err = contacts.Load(); err != nil { return } @@ -508,52 +701,47 @@ func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) return } -func (b *CardDavBackend) GetAddressObject(ctx context.Context, urlPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { - logging.LogInfof("CardDAV GetAddressObject: %s", urlPath) +func (b *CardDavBackend) GetAddressObject(ctx context.Context, addressPath string, req *carddav.AddressDataRequest) (addressObject *carddav.AddressObject, err error) { if err = contacts.Load(); err != nil { return } - // TODO + addressObject, err = contacts.GetAddressObject(addressPath, req) return } func (b *CardDavBackend) ListAddressObjects(ctx context.Context, bookPath string, req *carddav.AddressDataRequest) (addressObjects []carddav.AddressObject, err error) { - logging.LogInfof("CardDAV ListAddressObjects: %s", bookPath) if err = contacts.Load(); err != nil { return } - // TODO + addressObjects, err = contacts.ListAddressObjects(bookPath, req) return } -func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { - logging.LogInfof("CardDAV QueryAddressObjects: %s %v", urlPath, query) +func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, addressPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { if err = contacts.Load(); err != nil { return } - // TODO + addressObjects, err = contacts.QueryAddressObjects(addressPath, query) return } -func (b *CardDavBackend) PutAddressObject(ctx context.Context, urlPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { - logging.LogInfof("CardDAV PutAddressObject: %s %v", urlPath, card) +func (b *CardDavBackend) PutAddressObject(ctx context.Context, addressPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (addressObject *carddav.AddressObject, err error) { if err = contacts.Load(); err != nil { return } - // TODO + addressObject, err = contacts.PutAddressObject(addressPath, card, opts) return } -func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, urlPath string) (err error) { - logging.LogInfof("CardDAV DeleteAddressObject: %s", urlPath) +func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) { if err = contacts.Load(); err != nil { return } - // TODO + err = contacts.DeleteAddressObject(addressPath) return } diff --git a/kernel/server/serve.go b/kernel/server/serve.go index 3f782e61e82..4f3dc3cc42b 100644 --- a/kernel/server/serve.go +++ b/kernel/server/serve.go @@ -682,13 +682,11 @@ func serveCardDAV(ginServer *gin.Engine) { } ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) { - logging.LogInfof("CardDAV %s: /.well-known/carddav", c.Request.Method) handler.ServeHTTP(c.Writer, c.Request) }) ginGroup := ginServer.Group(model.CardDavPrefixPath, model.CheckAuth, model.CheckAdminRole) ginGroup.Match(CardDavMethods, "/*path", func(c *gin.Context) { - logging.LogInfof("CardDAV %s: %s", c.Request.Method, c.Request.URL.Path) if util.ReadOnly { switch c.Request.Method { case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": From dea1b910c618bbc5935198e8a9be251035dfd914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:53:22 +0800 Subject: [PATCH 8/9] :art: implement CardDAV `REPORT` method --- kernel/model/carddav.go | 182 ++++++++++++++++++++++----------- kernel/model/session.go | 7 +- kernel/server/proxy/publish.go | 3 +- kernel/server/serve.go | 27 ++++- 4 files changed, 154 insertions(+), 65 deletions(-) diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index 3891152a4d8..fd381182918 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -37,8 +37,8 @@ import ( const ( // REF: https://developers.google.com/people/carddav#resources CardDavPrefixPath = "/carddav" - CardDavRootPath = CardDavPrefixPath + "/principals" // 0 resourceTypeRoot - CardDavUserPrincipalPath = CardDavRootPath + "/main" // 1 resourceTypeUserPrincipal + CardDavPrincipalsPath = CardDavPrefixPath + "/principals" // 0 resourceTypeRoot + CardDavUserPrincipalPath = CardDavPrincipalsPath + "/main" // 1 resourceTypeUserPrincipal CardDavHomeSetPath = CardDavUserPrincipalPath + "/contacts" // 2 resourceTypeAddressBookHomeSet CardDavDefaultAddressBookPath = CardDavHomeSetPath + "/default" // 3 resourceTypeAddressBook @@ -47,6 +47,17 @@ const ( CardDavAddressBooksMetaDataFilePath = CardDavHomeSetPath + "/address-books.json" ) +type PathDepth 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 +) + var ( defaultAddressBook = carddav.AddressBook{ Path: CardDavDefaultAddressBookPath, @@ -62,7 +73,8 @@ var ( booksMetaData: []*carddav.AddressBook{}, } - ErrorNotFound = errors.New("CardDAV: not found") + ErrorNotFound = errors.New("CardDAV: not found") + ErrorPathInvalid = errors.New("CardDAV: path is invalid") ErrorBookNotFound = errors.New("CardDAV: address book not found") ErrorBookPathInvalid = errors.New("CardDAV: address book path is invalid") @@ -86,13 +98,18 @@ func AddressBooksMetaDataFilePath() string { return CardDavPath2DirectoryPath(CardDavAddressBooksMetaDataFilePath) } +func GetPathDepth(urlPath string) PathDepth { + urlPath = path.Clean(urlPath) + return PathDepth(len(strings.Split(urlPath, "/")) - 1) +} + // ParseAddressPath parses address path to address book path and address ID -func ParseAddressPath(addressParh string) (addressBookPath string, addressID string, err error) { - addressBookPath, addressFileName := path.Split(addressParh) +func ParseAddressPath(addressPath string) (addressBookPath string, addressID string, err error) { + addressBookPath, addressFileName := path.Split(addressPath) addressID = path.Base(addressFileName) addressFileExt := path.Ext(addressFileName) - if len(strings.Split(addressBookPath, "/")) != 6 { + if GetPathDepth(addressBookPath) != pathDepth_AddressBook { err = ErrorBookPathInvalid return } @@ -107,19 +124,22 @@ func ParseAddressPath(addressParh string) (addressBookPath string, addressID str func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject { var card *vcard.Card - if req.AllProp { - card = &address.Card - } else { - card = &vcard.Card{} - for _, prop := range req.Props { - fields := address.Card[prop] - if fields != nil { - for _, field := range fields { - card.Add(prop, field) - } - } - } - } + card = &address.Card + + // if req.AllProp { + // card = &address.Card + // } else { + // card = &vcard.Card{} + // for _, prop := range req.Props { + // fields := address.Card[prop] + // if fields != nil { + // for _, field := range fields { + // card.Add(prop, field) + // } + // } + // } + // } + return &carddav.AddressObject{ Path: address.Path, ModTime: address.ModTime, @@ -237,6 +257,30 @@ func (c *Contacts) Load() error { return nil } +func (c *Contacts) GetAddress(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 = ErrorBookNotFound + return + } + + if value, ok := addressBook.Addresses.Load(addressID); ok { + addressObject = value.(*AddressObject) + } else { + err = ErrorAddressNotFound + return + } + + return +} + func (c *Contacts) ListAddressBooks() (addressBooks []carddav.AddressBook, err error) { c.lock.Lock() defer c.lock.Unlock() @@ -349,27 +393,12 @@ func (c *Contacts) GetAddressObject(addressPath string, req *carddav.AddressData c.lock.Lock() defer c.lock.Unlock() - bookPath, addressID, err := ParseAddressPath(addressPath) + _, address, err := c.GetAddress(addressPath) if err != nil { - logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err) - return - } - - var addressBook *AddressBook - if value, ok := c.books.Load(bookPath); ok { - addressBook = value.(*AddressBook) - } else { - err = ErrorBookNotFound - return - } - - if value, ok := addressBook.Addresses.Load(addressID); ok { - addressObject = AddressPropsFilter(value.(*AddressObject).Data, req) - } else { - err = ErrorAddressNotFound return } + addressObject = AddressPropsFilter(address.Data, req) return } @@ -393,10 +422,38 @@ func (c *Contacts) ListAddressObjects(bookPath string, req *carddav.AddressDataR return } -func (c *Contacts) QueryAddressObjects(addressPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { +func (c *Contacts) QueryAddressObjects(urlPath string, query *carddav.AddressBookQuery) (addressObjects []carddav.AddressObject, err error) { c.lock.Lock() defer c.lock.Unlock() - // TODO + + switch GetPathDepth(urlPath) { + case pathDepth_Root, pathDepth_Principals, pathDepth_UserPrincipal, pathDepth_HomeSet: + c.books.Range(func(path any, book any) bool { + addressBook := book.(*AddressBook) + addressBook.Addresses.Range(func(id any, address any) bool { + addressObjects = append(addressObjects, *address.(*AddressObject).Data) + return true + }) + return true + }) + case pathDepth_AddressBook: + if value, ok := c.books.Load(urlPath); ok { + addressBook := value.(*AddressBook) + addressBook.Addresses.Range(func(id any, address any) bool { + addressObjects = append(addressObjects, *address.(*AddressObject).Data) + return true + }) + } + case pathDepth_Address: + if _, address, _ := c.GetAddress(urlPath); address != nil { + addressObjects = append(addressObjects, *address.Data) + } + default: + err = ErrorPathInvalid + return + } + + addressObjects, err = carddav.Filter(query, addressObjects) return } @@ -459,29 +516,16 @@ func (c *Contacts) DeleteAddressObject(addressPath string) (err error) { c.lock.Lock() defer c.lock.Unlock() - bookPath, addressID, err := ParseAddressPath(addressPath) - if err != nil { - logging.LogErrorf("parse address path [%s] failed: %s", addressPath, err) + _, address, err := c.GetAddress(addressPath) + if err != nil && err != ErrorAddressNotFound { return } - var addressBook *AddressBook - if value, ok := c.books.Load(bookPath); ok { - addressBook = value.(*AddressBook) - } else { - err = ErrorBookNotFound + if err = os.Remove(address.FilePath); err != nil { + logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err) return } - if value, loaded := addressBook.Addresses.LoadAndDelete(addressID); loaded { - address := value.(*AddressObject) - - if err = os.Remove(address.FilePath); err != nil { - logging.LogErrorf("remove file [%s] failed: %s", address.FilePath, err) - return - } - } - return } @@ -599,7 +643,7 @@ func (o *AddressObject) load() error { // load data o.Changed = false o.Data = &carddav.AddressObject{ - Path: o.BookPath + "/" + addressFileInfo.Name(), + Path: path.Join(o.BookPath, addressFileInfo.Name()), ModTime: addressFileInfo.ModTime(), ContentLength: addressFileInfo.Size(), ETag: fmt.Sprintf("%x-%x", addressFileInfo.ModTime(), addressFileInfo.Size()), @@ -647,7 +691,7 @@ func (o *AddressObject) update() error { return err } - o.Data.Path = o.BookPath + "/" + addressFileInfo.Name() + o.Data.Path = path.Join(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()) @@ -658,90 +702,110 @@ func (o *AddressObject) update() error { type CardDavBackend struct{} func (b *CardDavBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { + // logging.LogDebugf("CardDAV CurrentUserPrincipal") return CardDavUserPrincipalPath, nil } func (b *CardDavBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) { + // logging.LogDebugf("CardDAV AddressBookHomeSetPath") return CardDavHomeSetPath, nil } func (b *CardDavBackend) ListAddressBooks(ctx context.Context) (addressBooks []carddav.AddressBook, err error) { + // logging.LogDebugf("CardDAV ListAddressBooks") if err = contacts.Load(); err != nil { return } addressBooks, err = contacts.ListAddressBooks() + // logging.LogDebugf("CardDAV ListAddressBooks <- addressBooks: %#v, err: %s", addressBooks, err) return } func (b *CardDavBackend) GetAddressBook(ctx context.Context, bookPath string) (addressBook *carddav.AddressBook, err error) { + // logging.LogDebugf("CardDAV GetAddressBook -> bookPath: %s", bookPath) if err = contacts.Load(); err != nil { return } addressBook, err = contacts.GetAddressBook(bookPath) + // logging.LogDebugf("CardDAV GetAddressBook <- addressBook: %#v, err: %s", addressBook, err) return } func (b *CardDavBackend) CreateAddressBook(ctx context.Context, addressBook *carddav.AddressBook) (err error) { + // logging.LogDebugf("CardDAV CreateAddressBook -> addressBook: %#v", addressBook) if err = contacts.Load(); err != nil { return } err = contacts.CreateAddressBook(addressBook) + // logging.LogDebugf("CardDAV CreateAddressBook <- err: %s", err) return } func (b *CardDavBackend) DeleteAddressBook(ctx context.Context, bookPath string) (err error) { + // logging.LogDebugf("CardDAV DeleteAddressBook -> bookPath: %s", bookPath) if err = contacts.Load(); err != nil { return } err = contacts.DeleteAddressBook(bookPath) + // logging.LogDebugf("CardDAV DeleteAddressBook <- err: %s", err) return } 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) if err = contacts.Load(); err != nil { return } addressObject, err = contacts.GetAddressObject(addressPath, req) + // logging.LogDebugf("CardDAV GetAddressObject <- addressObject: %#v, err: %s", addressObject, err) return } 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) if err = contacts.Load(); err != nil { return } addressObjects, err = contacts.ListAddressObjects(bookPath, req) + // logging.LogDebugf("CardDAV ListAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err) return } -func (b *CardDavBackend) QueryAddressObjects(ctx context.Context, addressPath 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) if err = contacts.Load(); err != nil { return } - addressObjects, err = contacts.QueryAddressObjects(addressPath, query) + addressObjects, err = contacts.QueryAddressObjects(urlPath, query) + // logging.LogDebugf("CardDAV QueryAddressObjects <- addressObjects: %#v, err: %s", addressObjects, err) return } 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) if err = contacts.Load(); err != nil { return } addressObject, err = contacts.PutAddressObject(addressPath, card, opts) + // logging.LogDebugf("CardDAV PutAddressObject <- addressObject: %#v, err: %s", addressObject, err) return } func (b *CardDavBackend) DeleteAddressObject(ctx context.Context, addressPath string) (err error) { + // logging.LogDebugf("CardDAV DeleteAddressObject -> addressPath: %s", addressPath) if err = contacts.Load(); err != nil { return } err = contacts.DeleteAddressObject(addressPath) + // logging.LogDebugf("CardDAV DeleteAddressObject <- err: %s", err) return } diff --git a/kernel/model/session.go b/kernel/model/session.go index 3dc3bca8efa..a86d3268a16 100644 --- a/kernel/model/session.go +++ b/kernel/model/session.go @@ -34,6 +34,11 @@ import ( "github.com/steambap/captcha" ) +var ( + BasicAuthHeaderKey = "WWW-Authenticate" + BasicAuthHeaderValue = "Basic realm=\"SiYuan Authorization Require\", charset=\"UTF-8\"" +) + func LogoutAuth(c *gin.Context) { ret := gulu.Ret.NewResult() defer c.JSON(http.StatusOK, ret) @@ -301,7 +306,7 @@ func CheckAuth(c *gin.Context) { // WebDAV BasicAuth Authenticate if strings.HasPrefix(c.Request.RequestURI, "/webdav") || strings.HasPrefix(c.Request.RequestURI, "/carddav") { - c.Header("WWW-Authenticate", "Basic realm=Authorization Required") + c.Header(BasicAuthHeaderKey, BasicAuthHeaderValue) c.AbortWithStatus(http.StatusUnauthorized) return } diff --git a/kernel/server/proxy/publish.go b/kernel/server/proxy/publish.go index 52506270e87..65d9a766637 100644 --- a/kernel/server/proxy/publish.go +++ b/kernel/server/proxy/publish.go @@ -21,7 +21,6 @@ import ( "net" "net/http" "net/http/httputil" - "strconv" "github.com/siyuan-note/logging" "github.com/siyuan-note/siyuan/kernel/model" @@ -143,7 +142,7 @@ func (PublishServiceTransport) RoundTrip(request *http.Request) (response *http. ProtoMinor: request.ProtoMinor, Request: request, Header: http.Header{ - "WWW-Authenticate": {"Basic realm=" + strconv.Quote("Authorization Required")}, + model.BasicAuthHeaderKey: {model.BasicAuthHeaderValue}, }, Close: false, ContentLength: -1, diff --git a/kernel/server/serve.go b/kernel/server/serve.go index 4f3dc3cc42b..3d1bb676fe0 100644 --- a/kernel/server/serve.go +++ b/kernel/server/serve.go @@ -56,6 +56,7 @@ const ( MethodUnlock = "UNLOCK" MethodPropFind = "PROPFIND" MethodPropPatch = "PROPPATCH" + MethodReport = "REPORT" ) var ( @@ -102,6 +103,8 @@ var ( // MethodUnlock, MethodPropFind, MethodPropPatch, + + MethodReport, } ) @@ -665,7 +668,15 @@ func serveWebDAV(ginServer *gin.Engine) { ginGroup.Match(WebDavMethods, "/*path", func(c *gin.Context) { if util.ReadOnly { switch c.Request.Method { - case "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": + case http.MethodPost, + http.MethodPut, + http.MethodDelete, + MethodMkcol, + MethodCopy, + MethodMove, + MethodLock, + MethodUnlock, + MethodPropPatch: c.AbortWithError(http.StatusForbidden, fmt.Errorf(model.Conf.Language(34))) return } @@ -678,7 +689,7 @@ func serveCardDAV(ginServer *gin.Engine) { // REF: https://github.com/emersion/hydroxide/blob/master/carddav/carddav.go handler := carddav.Handler{ Backend: &model.CardDavBackend{}, - Prefix: model.CardDavRootPath, + Prefix: model.CardDavPrincipalsPath, } ginServer.Match(CardDavMethods, "/.well-known/carddav", func(c *gin.Context) { @@ -687,14 +698,24 @@ func serveCardDAV(ginServer *gin.Engine) { 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 "POST", "PUT", "DELETE", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPPATCH": + 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("CardDAV <- [%s] %v", c.Request.Method, c.Writer.Status()) }) } From 3d81ab41e093b11a2e6f1be608efb276497b549a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=96=E9=80=B8?= <49649786+Zuoqiu-Yingyi@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:10:17 +0800 Subject: [PATCH 9/9] :art: parse *.vcf file with multiple vCard --- kernel/model/carddav.go | 87 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 9 deletions(-) diff --git a/kernel/model/carddav.go b/kernel/model/carddav.go index fd381182918..8b2aad21a69 100644 --- a/kernel/model/carddav.go +++ b/kernel/model/carddav.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "io" "os" "path" "path/filepath" @@ -122,6 +123,7 @@ func ParseAddressPath(addressPath string) (addressBookPath string, addressID str return } +// AddressPropsFilter filters address properties func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressDataRequest) *carddav.AddressObject { var card *vcard.Card card = &address.Card @@ -149,6 +151,29 @@ func AddressPropsFilter(address *carddav.AddressObject, req *carddav.AddressData } } +func LoadCards(filePath string) (cards []*vcard.Card, err error) { + data, err := os.ReadFile(filePath) + if err != nil { + logging.LogErrorf("read vCard file [%s] failed: %s", filePath, err) + return + } + + decoder := vcard.NewDecoder(bytes.NewReader(data)) + for { + card, err := decoder.Decode() + if err != nil { + if err == io.EOF { + break + } + logging.LogErrorf("decode vCard file [%s] failed: %s", filePath, err) + return nil, err + } + cards = append(cards, &card) + } + + return +} + type Contacts struct { loaded bool changed bool @@ -559,19 +584,63 @@ func (b *AddressBook) load() error { go func() { defer wg.Done() + // load cards addressFilePath := path.Join(b.DirectoryPath, filename) - - // load data - address := &AddressObject{ - FilePath: addressFilePath, - BookPath: b.MetaData.Path, - } - if err := address.load(); err != nil { + vCards, err := LoadCards(addressFilePath) + if err != nil { return } - id := path.Base(filename) - b.Addresses.Store(id, address) + switch len(vCards) { + case 0: // invalid file + case 1: // file contain 1 card + address := &AddressObject{ + FilePath: addressFilePath, + BookPath: b.MetaData.Path, + Data: &carddav.AddressObject{ + Card: *vCards[0], + }, + } + if err := address.update(); err != nil { + return + } + + id := path.Base(filename) + b.Addresses.Store(id, address) + default: // file contain multiple cards + // Create a file for each card + addressesWaitGroup := &sync.WaitGroup{} + for _, vCard := range vCards { + addressesWaitGroup.Add(1) + go func() { + defer addressesWaitGroup.Done() + filename_ := util.AssetName(filename) + address := &AddressObject{ + FilePath: path.Join(b.DirectoryPath, filename_), + BookPath: b.MetaData.Path, + Data: &carddav.AddressObject{ + Card: *vCard, + }, + } + if err := address.save(true); err != nil { + return + } + if err := address.update(); err != nil { + return + } + + id := path.Base(filename) + b.Addresses.Store(id, address) + }() + } + + addressesWaitGroup.Wait() + // Delete original file with multiple cards + if err := os.Remove(addressFilePath); err != nil { + logging.LogErrorf("remove file [%s] failed: %s", addressFilePath, err) + return + } + } }() } }