mirror of
https://git.sr.ht/~sircmpwn/tokidoki
synced 2025-12-12 06:07:22 +01:00
Initial multi-calendar/address book support
Thanks to the latest version of go-webdav, this is now a thing. A lot of operations (like creating a calendar) are not yet supported. But the basics work fine. Note that multi-calendar means that different users can each have their own calenders. Resource sharing is not yet implemented either. Includes the adding of a lot of debug logs, as issues are otherwise pretty hard to figure out. The logging still needs to be made more consistent, and probably cleaned up a bit in some places.
This commit is contained in:
parent
1d871b000a
commit
a74c76857d
7 changed files with 347 additions and 155 deletions
|
|
@ -6,7 +6,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
|
@ -19,7 +18,9 @@ import (
|
|||
"github.com/emersion/go-webdav/carddav"
|
||||
)
|
||||
|
||||
func (b *filesystemBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
|
||||
const addressBookFileName = "addressbook.json"
|
||||
|
||||
func (b *filesystemBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
|
||||
upPath, err := b.CurrentUserPrincipal(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -28,8 +29,17 @@ func (b *filesystemBackend) AddressbookHomeSetPath(ctx context.Context) (string,
|
|||
return path.Join(upPath, b.carddavPrefix) + "/", nil
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) localCardDAVPath(ctx context.Context, urlPath string) (string, error) {
|
||||
homeSetPath, err := b.AddressbookHomeSetPath(ctx)
|
||||
func (b *filesystemBackend) localCardDAVDir(ctx context.Context, components ...string) (string, error) {
|
||||
homeSetPath, err := b.AddressBookHomeSetPath(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return b.localDir(homeSetPath, components...)
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) safeLocalCardDAVPath(ctx context.Context, urlPath string) (string, error) {
|
||||
homeSetPath, err := b.AddressBookHomeSetPath(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -74,10 +84,69 @@ func vcardFromFile(path string, propFilter []string) (vcard.Card, error) {
|
|||
return vcardPropFilter(card, propFilter), nil
|
||||
}
|
||||
|
||||
func createDefaultAddressBook(path, localPath string) error {
|
||||
func (b *filesystemBackend) loadAllAddressObjects(ctx context.Context, urlPath string, propFilter []string) ([]carddav.AddressObject, error) {
|
||||
var result []carddav.AddressObject
|
||||
|
||||
localPath, err := b.safeLocalCardDAVPath(ctx, urlPath)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
log.Debug().Str("path", localPath).Msg("loading address objects")
|
||||
|
||||
err = filepath.Walk(localPath, func(filename string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error accessing %s: %s", filename, err)
|
||||
}
|
||||
|
||||
if !info.Mode().IsRegular() || filepath.Ext(filename) != ".vcf" {
|
||||
return nil
|
||||
}
|
||||
|
||||
card, err := vcardFromFile(filename, propFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
etag, err := etagForFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO can this potentially be called on an address object resource?
|
||||
// would work (as Walk() includes root), except for the path construction below
|
||||
obj := carddav.AddressObject{
|
||||
Path: path.Join(urlPath, filepath.Base(filename)),
|
||||
ModTime: info.ModTime(),
|
||||
ContentLength: info.Size(),
|
||||
ETag: etag,
|
||||
Card: card,
|
||||
}
|
||||
result = append(result, obj)
|
||||
return nil
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) createDefaultAddressBook(ctx context.Context) (*carddav.AddressBook, error) {
|
||||
// TODO what should the default address book look like?
|
||||
localPath, err_ := b.localCardDAVDir(ctx, defaultResourceName)
|
||||
if err_ != nil {
|
||||
return nil, fmt.Errorf("error creating default address book: %s", err_.Error())
|
||||
}
|
||||
|
||||
homeSetPath, err_ := b.AddressBookHomeSetPath(ctx)
|
||||
if err_ != nil {
|
||||
return nil, fmt.Errorf("error creating default address book: %s", err_.Error())
|
||||
}
|
||||
|
||||
urlPath := path.Join(homeSetPath, defaultResourceName) + "/"
|
||||
|
||||
log.Debug().Str("local", localPath).Str("url", urlPath).Msg("filesystem.createDefaultAddressBook()")
|
||||
|
||||
defaultAB := carddav.AddressBook{
|
||||
Path: path,
|
||||
Path: urlPath,
|
||||
Name: "My contacts",
|
||||
Description: "Default address book",
|
||||
MaxResourceSize: 1024,
|
||||
|
|
@ -85,40 +154,92 @@ func createDefaultAddressBook(path, localPath string) error {
|
|||
}
|
||||
blob, err := json.MarshalIndent(defaultAB, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating default address book: %s", err.Error())
|
||||
return nil, fmt.Errorf("error creating default address book: %s", err.Error())
|
||||
}
|
||||
err = os.WriteFile(localPath, blob, 0644)
|
||||
err = os.WriteFile(path.Join(localPath, addressBookFileName), blob, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing default address book: %s", err.Error())
|
||||
return nil, fmt.Errorf("error writing default address book: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
return &defaultAB, nil
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) {
|
||||
log.Debug().Msg("filesystem.AddressBook()")
|
||||
localPath, err := b.localCardDAVPath(ctx, "")
|
||||
func (b *filesystemBackend) ListAddressBooks(ctx context.Context) ([]carddav.AddressBook, error) {
|
||||
log.Debug().Msg("filesystem.ListAddressBooks()")
|
||||
|
||||
localPath, err := b.localCardDAVDir(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localPath = filepath.Join(localPath, "addressbook.json")
|
||||
|
||||
log.Debug().Str("path", localPath).Msg("looking for address books")
|
||||
|
||||
var result []carddav.AddressBook
|
||||
|
||||
err = filepath.Walk(localPath, func(filename string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error accessing %s: %s", filename, err.Error())
|
||||
}
|
||||
|
||||
if !info.IsDir() || filename == localPath {
|
||||
return nil
|
||||
}
|
||||
|
||||
abPath := path.Join(filename, addressBookFileName)
|
||||
data, err := os.ReadFile(abPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // not an address book dir
|
||||
} else {
|
||||
return fmt.Errorf("error accessing %s: %s", abPath, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
var addressBook carddav.AddressBook
|
||||
err = json.Unmarshal(data, &addressBook)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading address book %s: %s", abPath, err.Error())
|
||||
}
|
||||
|
||||
result = append(result, addressBook)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil && len(result) == 0 {
|
||||
// Nothing here yet? Create the default address book
|
||||
log.Debug().Msg("no address books found, creating default address book")
|
||||
ab, err_ := b.createDefaultAddressBook(ctx)
|
||||
if err_ != nil {
|
||||
log.Debug().Int("results", len(result)).Bool("success", false).Str("error", err_.Error()).Msg("filesystem.ListAddressBooks() done")
|
||||
return nil, fmt.Errorf("error creating default address book: %s", err_.Error())
|
||||
}
|
||||
result = append(result, *ab)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListAddressBooks() done")
|
||||
} else {
|
||||
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListAddressBooks() done")
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) GetAddressBook(ctx context.Context, urlPath string) (*carddav.AddressBook, error) {
|
||||
log.Debug().Str("path", urlPath).Msg("filesystem.AddressBook()")
|
||||
|
||||
localPath, err := b.safeLocalCardDAVPath(ctx, urlPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localPath = filepath.Join(localPath, addressBookFileName)
|
||||
|
||||
log.Debug().Str("local_path", localPath).Msg("loading addressbook")
|
||||
|
||||
data, readErr := ioutil.ReadFile(localPath)
|
||||
if os.IsNotExist(readErr) {
|
||||
urlPath, err := b.AddressbookHomeSetPath(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urlPath = path.Join(urlPath, defaultResourceName) + "/"
|
||||
log.Debug().Str("local_path", localPath).Str("url_path", urlPath).Msg("creating addressbook")
|
||||
err = createDefaultAddressBook(urlPath, localPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, readErr = ioutil.ReadFile(localPath)
|
||||
}
|
||||
data, readErr := os.ReadFile(localPath)
|
||||
|
||||
if readErr != nil {
|
||||
if os.IsNotExist(readErr) {
|
||||
return nil, webdav.NewHTTPError(404, err)
|
||||
}
|
||||
return nil, fmt.Errorf("error opening address book: %s", readErr.Error())
|
||||
}
|
||||
var addressBook carddav.AddressBook
|
||||
|
|
@ -132,7 +253,8 @@ func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBo
|
|||
|
||||
func (b *filesystemBackend) GetAddressObject(ctx context.Context, objPath string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
|
||||
log.Debug().Str("url_path", objPath).Msg("filesystem.GetAddressObject()")
|
||||
localPath, err := b.localCardDAVPath(ctx, objPath)
|
||||
|
||||
localPath, err := b.safeLocalCardDAVPath(ctx, objPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -170,76 +292,46 @@ func (b *filesystemBackend) GetAddressObject(ctx context.Context, objPath string
|
|||
return &obj, nil
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) loadAllContacts(ctx context.Context, propFilter []string) ([]carddav.AddressObject, error) {
|
||||
var result []carddav.AddressObject
|
||||
func (b *filesystemBackend) ListAddressObjects(ctx context.Context, urlPath string, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) {
|
||||
log.Debug().Str("path", urlPath).Msg("filesystem.ListAddressObjects()")
|
||||
|
||||
localPath, err := b.localCardDAVPath(ctx, "")
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
homeSetPath, err := b.AddressbookHomeSetPath(ctx)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
err = filepath.Walk(localPath, func(filename string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error accessing %s: %s", filename, err)
|
||||
}
|
||||
|
||||
if !info.Mode().IsRegular() || filepath.Ext(filename) != ".vcf" {
|
||||
return nil
|
||||
}
|
||||
|
||||
card, err := vcardFromFile(filename, propFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
etag, err := etagForFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj := carddav.AddressObject{
|
||||
Path: path.Join(homeSetPath, defaultResourceName, filepath.Base(filename)),
|
||||
ModTime: info.ModTime(),
|
||||
ContentLength: info.Size(),
|
||||
ETag: etag,
|
||||
Card: card,
|
||||
}
|
||||
result = append(result, obj)
|
||||
return nil
|
||||
})
|
||||
|
||||
log.Debug().Int("results", len(result)).Str("path", localPath).Msg("filesystem.loadAllContacts() successful")
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) {
|
||||
log.Debug().Msg("filesystem.ListAddressObjects()")
|
||||
var propFilter []string
|
||||
if req != nil && !req.AllProp {
|
||||
propFilter = req.Props
|
||||
}
|
||||
|
||||
return b.loadAllContacts(ctx, propFilter)
|
||||
result, err := b.loadAllAddressObjects(ctx, urlPath, propFilter)
|
||||
if err != nil {
|
||||
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListAddressObjects() done")
|
||||
} else {
|
||||
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListAddressObjects() done")
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) {
|
||||
log.Debug().Msg("filesystem.QueryAddressObjects()")
|
||||
func (b *filesystemBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) {
|
||||
log.Debug().Str("path", urlPath).Msg("filesystem.QueryAddressObjects()")
|
||||
|
||||
var propFilter []string
|
||||
if query != nil && !query.DataRequest.AllProp {
|
||||
propFilter = query.DataRequest.Props
|
||||
}
|
||||
|
||||
result, err := b.loadAllContacts(ctx, propFilter)
|
||||
result, err := b.loadAllAddressObjects(ctx, urlPath, propFilter)
|
||||
if err != nil {
|
||||
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryAddressObjects() error loading")
|
||||
return result, err
|
||||
}
|
||||
|
||||
return carddav.Filter(query, result)
|
||||
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.QueryAddressObjects() load done")
|
||||
|
||||
filtered, err := carddav.Filter(query, result)
|
||||
if err != nil {
|
||||
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryAddressObjects() error filtering")
|
||||
return result, err
|
||||
}
|
||||
log.Debug().Int("results", len(filtered)).Bool("success", true).Msg("filesystem.QueryAddressObjects() done")
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) {
|
||||
|
|
@ -249,7 +341,7 @@ func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string
|
|||
dirname, _ := path.Split(objPath)
|
||||
objPath = path.Join(dirname, card.Value(vcard.FieldUID)+".vcf")
|
||||
|
||||
localPath, err := b.localCardDAVPath(ctx, objPath)
|
||||
localPath, err := b.safeLocalCardDAVPath(ctx, objPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -298,7 +390,7 @@ func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string
|
|||
func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string) error {
|
||||
log.Debug().Str("url_path", path).Msg("filesystem.DeleteAddressObject()")
|
||||
|
||||
localPath, err := b.localCardDAVPath(ctx, path)
|
||||
localPath, err := b.safeLocalCardDAVPath(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue