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:
Conrad Hoffmann 2024-02-02 22:19:36 +01:00
parent 1d871b000a
commit a74c76857d
7 changed files with 347 additions and 155 deletions

View file

@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"io/fs"
"io/ioutil"
"net/http"
"os"
"path"
@ -19,6 +18,8 @@ import (
"github.com/emersion/go-webdav/caldav"
)
const calendarFileName = "calendar.json"
func (b *filesystemBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
upPath, err := b.CurrentUserPrincipal(ctx)
if err != nil {
@ -28,7 +29,16 @@ func (b *filesystemBackend) CalendarHomeSetPath(ctx context.Context) (string, er
return path.Join(upPath, b.caldavPrefix) + "/", nil
}
func (b *filesystemBackend) localCalDAVPath(ctx context.Context, urlPath string) (string, error) {
func (b *filesystemBackend) localCalDAVDir(ctx context.Context, components ...string) (string, error) {
homeSetPath, err := b.CalendarHomeSetPath(ctx)
if err != nil {
return "", err
}
return b.localDir(homeSetPath, components...)
}
func (b *filesystemBackend) safeLocalCalDAVPath(ctx context.Context, urlPath string) (string, error) {
homeSetPath, err := b.CalendarHomeSetPath(ctx)
if err != nil {
return "", err
@ -55,18 +65,15 @@ func calendarFromFile(path string, propFilter []string) (*ical.Calendar, error)
//return icalPropFilter(cal, propFilter), nil
}
func (b *filesystemBackend) loadAllCalendars(ctx context.Context, propFilter []string) ([]caldav.CalendarObject, error) {
func (b *filesystemBackend) loadAllCalendarObjects(ctx context.Context, urlPath string, propFilter []string) ([]caldav.CalendarObject, error) {
var result []caldav.CalendarObject
localPath, err := b.localCalDAVPath(ctx, "")
localPath, err := b.safeLocalCalDAVPath(ctx, urlPath)
if err != nil {
return result, err
}
homeSetPath, err := b.CalendarHomeSetPath(ctx)
if err != nil {
return result, err
}
log.Debug().Str("path", localPath).Msg("loading calendar objects")
err = filepath.Walk(localPath, func(filename string, info os.FileInfo, err error) error {
if err != nil {
@ -89,8 +96,10 @@ func (b *filesystemBackend) loadAllCalendars(ctx context.Context, propFilter []s
return err
}
// TODO can this potentially be called on a calendar object resource?
// Would work (as Walk() includes root), except for the path construction below
obj := caldav.CalendarObject{
Path: path.Join(homeSetPath, defaultResourceName, filepath.Base(filename)),
Path: path.Join(urlPath, filepath.Base(filename)),
ModTime: info.ModTime(),
ContentLength: info.Size(),
ETag: etag,
@ -100,55 +109,118 @@ func (b *filesystemBackend) loadAllCalendars(ctx context.Context, propFilter []s
return nil
})
log.Debug().Int("results", len(result)).Str("path", localPath).Msg("filesystem.loadAllCalendars() successful")
return result, err
}
func createDefaultCalendar(path, localPath string) error {
func (b *filesystemBackend) createDefaultCalendar(ctx context.Context) (*caldav.Calendar, error) {
// TODO what should the default calendar look like?
localPath, err_ := b.localCalDAVDir(ctx, defaultResourceName)
if err_ != nil {
return nil, fmt.Errorf("error creating default calendar: %s", err_.Error())
}
homeSetPath, err_ := b.CalendarHomeSetPath(ctx)
if err_ != nil {
return nil, fmt.Errorf("error creating default calendar: %s", err_.Error())
}
urlPath := path.Join(homeSetPath, defaultResourceName) + "/"
log.Debug().Str("local", localPath).Str("url", urlPath).Msg("filesystem.createDefaultCalendar()")
defaultC := caldav.Calendar{
Path: path,
Path: urlPath,
Name: "My calendar",
Description: "Default calendar",
MaxResourceSize: 4096,
}
blob, err := json.MarshalIndent(defaultC, "", " ")
if err != nil {
return fmt.Errorf("error creating default calendar: %s", err.Error())
return nil, fmt.Errorf("error creating default calendar: %s", err.Error())
}
err = os.WriteFile(localPath, blob, 0644)
err = os.WriteFile(path.Join(localPath, calendarFileName), blob, 0644)
if err != nil {
return fmt.Errorf("error writing default calendar: %s", err.Error())
return nil, fmt.Errorf("error writing default calendar: %s", err.Error())
}
return nil
return &defaultC, nil
}
func (b *filesystemBackend) Calendar(ctx context.Context) (*caldav.Calendar, error) {
log.Debug().Msg("filesystem.Calendar()")
func (b *filesystemBackend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error) {
log.Debug().Msg("filesystem.ListCalendars()")
localPath, err := b.localCalDAVPath(ctx, "")
localPath, err := b.localCalDAVDir(ctx)
if err != nil {
return nil, err
}
localPath = filepath.Join(localPath, "calendar.json")
log.Debug().Str("path", localPath).Msg("looking for calendars")
var result []caldav.Calendar
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
}
calPath := path.Join(filename, calendarFileName)
data, err := os.ReadFile(calPath)
if err != nil {
if os.IsNotExist(err) {
return nil // not a calendar dir
} else {
return fmt.Errorf("error accessing %s: %s", calPath, err.Error())
}
}
var calendar caldav.Calendar
err = json.Unmarshal(data, &calendar)
if err != nil {
return fmt.Errorf("error reading calendar %s: %s", calPath, err.Error())
}
result = append(result, calendar)
return nil
})
if err == nil && len(result) == 0 {
// Nothing here yet? Create the default calendar.
log.Debug().Msg("no calendars found, creating default calendar")
cal, err_ := b.createDefaultCalendar(ctx)
if err_ != nil {
log.Debug().Int("results", len(result)).Bool("success", false).Str("error", err_.Error()).Msg("filesystem.ListCalendars() done")
return nil, fmt.Errorf("error creating default calendar: %s", err_.Error())
}
result = append(result, *cal)
}
if err != nil {
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListCalendars() done")
} else {
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListCalendars() done")
}
return result, err
}
func (b *filesystemBackend) GetCalendar(ctx context.Context, urlPath string) (*caldav.Calendar, error) {
log.Debug().Str("path", urlPath).Msg("filesystem.GetCalendar()")
localPath, err := b.safeLocalCalDAVPath(ctx, urlPath)
if err != nil {
return nil, err
}
localPath = filepath.Join(localPath, calendarFileName)
log.Debug().Str("local_path", localPath).Msg("loading calendar")
data, readErr := ioutil.ReadFile(localPath)
if os.IsNotExist(readErr) {
urlPath, err := b.CalendarHomeSetPath(ctx)
if err != nil {
return nil, err
}
urlPath = path.Join(urlPath, defaultResourceName) + "/"
log.Debug().Str("local_path", localPath).Str("url_path", urlPath).Msg("creating calendar")
err = createDefaultCalendar(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 calendar: %s", readErr.Error())
}
var calendar caldav.Calendar
@ -163,7 +235,7 @@ func (b *filesystemBackend) Calendar(ctx context.Context) (*caldav.Calendar, err
func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) {
log.Debug().Str("url_path", objPath).Msg("filesystem.GetCalendarObject()")
localPath, err := b.localCalDAVPath(ctx, objPath)
localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil {
return nil, err
}
@ -203,31 +275,46 @@ func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath strin
return &obj, nil
}
func (b *filesystemBackend) ListCalendarObjects(ctx context.Context, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) {
log.Debug().Msg("filesystem.ListCalendarObjects()")
func (b *filesystemBackend) ListCalendarObjects(ctx context.Context, urlPath string, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) {
log.Debug().Str("path", urlPath).Msg("filesystem.ListCalendarObjects()")
var propFilter []string
if req != nil && !req.AllProps {
propFilter = req.Props
}
return b.loadAllCalendars(ctx, propFilter)
result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter)
if err != nil {
log.Warn().Int("results", len(result)).Bool("success", false).Str("error", err.Error()).Msg("filesystem.ListCalendarObjects() done")
} else {
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.ListCalendarObjects() done")
}
return result, err
}
func (b *filesystemBackend) QueryCalendarObjects(ctx context.Context, query *caldav.CalendarQuery) ([]caldav.CalendarObject, error) {
log.Debug().Msg("filesystem.QueryCalendarObjects()")
func (b *filesystemBackend) QueryCalendarObjects(ctx context.Context, urlPath string, query *caldav.CalendarQuery) ([]caldav.CalendarObject, error) {
log.Debug().Str("path", urlPath).Msg("filesystem.QueryCalendarObjects()")
var propFilter []string
if query != nil && !query.CompRequest.AllProps {
propFilter = query.CompRequest.Props
}
result, err := b.loadAllCalendars(ctx, propFilter)
result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter)
if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryCalendarObjects() error loading")
return result, err
}
return caldav.Filter(query, result)
log.Debug().Int("results", len(result)).Bool("success", true).Msg("filesystem.QueryCalendarObjects() load done")
filtered, err := caldav.Filter(query, result)
if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryCalendarObjects() error filtering")
return result, err
}
log.Debug().Int("results", len(filtered)).Bool("success", true).Msg("filesystem.QueryCalendarObjects() done")
return filtered, nil
}
func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (loc string, err error) {
@ -242,7 +329,7 @@ func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath strin
dirname, _ := path.Split(objPath)
objPath = path.Join(dirname, uid+".ics")
localPath, err := b.localCalDAVPath(ctx, objPath)
localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil {
return "", err
}
@ -291,7 +378,7 @@ func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath strin
func (b *filesystemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
log.Debug().Str("url_path", path).Msg("filesystem.DeleteCalendarObject()")
localPath, err := b.localCalDAVPath(ctx, path)
localPath, err := b.safeLocalCalDAVPath(ctx, path)
if err != nil {
return err
}