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

@ -58,7 +58,7 @@ func (u *tokidokiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
if u.carddavBackend != nil { if u.carddavBackend != nil {
path, err := u.carddavBackend.AddressbookHomeSetPath(r.Context()) path, err := u.carddavBackend.AddressBookHomeSetPath(r.Context())
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
} else { } else {

4
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9
github.com/emersion/go-webdav v0.4.0 github.com/emersion/go-webdav v0.5.1-0.20240202164822-eaac65215b3a
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.10
github.com/msteinert/pam v1.2.0 github.com/msteinert/pam v1.2.0
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
@ -17,6 +17,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/teambition/rrule-go v1.8.2 // indirect github.com/teambition/rrule-go v1.8.2 // indirect
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
) )

9
go.sum
View File

@ -8,11 +8,10 @@ github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTe
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20191221110513-5f81fa0d3cc7/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9 h1:ATgqloALX6cHCranzkLb8/zjivwQ9DWWDCQRnxTPfaA= 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-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.4.0 h1:iIkgitJBUNu2c1vL0KqqRb5jDjs38bzM/H7WxewrIh4= github.com/emersion/go-webdav v0.5.1-0.20240202164822-eaac65215b3a h1:IHOPSOw+XXKRSkuXTOeEtDejkuXqf0ohjZq9hew6ArA=
github.com/emersion/go-webdav v0.4.0/go.mod h1:lkPYZO/vsDNV9GPyVMBBsAUZzzxINL97bEVFykApo58= github.com/emersion/go-webdav v0.5.1-0.20240202164822-eaac65215b3a/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -34,8 +33,8 @@ github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@ -57,9 +57,20 @@ func ensureLocalDir(path string) error {
return nil return nil
} }
func (b *filesystemBackend) localDir(homeSetPath string, components ...string) (string, error) {
c := append([]string{b.path}, homeSetPath)
c = append(c, components...)
localPath := filepath.Join(c...)
if err := ensureLocalDir(localPath); err != nil {
return "", err
}
return localPath, nil
}
// don't use this directly, use localCalDAVPath or localCardDAVPath instead. // don't use this directly, use localCalDAVPath or localCardDAVPath instead.
// note that homesetpath is expected to end in /
func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (string, error) { func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (string, error) {
localPath := filepath.Join(b.path, homeSetPath, defaultResourceName) localPath := filepath.Join(b.path, homeSetPath)
if err := ensureLocalDir(localPath); err != nil { if err := ensureLocalDir(localPath); err != nil {
return "", err return "", err
} }
@ -69,27 +80,26 @@ func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (s
} }
// We are mapping to local filesystem path, so be conservative about what to accept // We are mapping to local filesystem path, so be conservative about what to accept
// TODO this changes once multiple addess books are supported if strings.HasSuffix(urlPath, "/") {
dir, file := path.Split(urlPath) urlPath = path.Clean(urlPath) + "/"
// only accept resources in default calendar/adress book for now
if path.Clean(dir) != path.Join(homeSetPath, defaultResourceName) {
if strings.HasPrefix(dir, homeSetPath) {
err := fmt.Errorf("invalid request path: %s (%s is not %s)", urlPath, dir, path.Join(homeSetPath, defaultResourceName))
return "", webdav.NewHTTPError(400, err)
} else { } else {
urlPath = path.Clean(urlPath)
}
if !strings.HasPrefix(urlPath, homeSetPath) {
err := fmt.Errorf("access to resource outside of home set: %s", urlPath) err := fmt.Errorf("access to resource outside of home set: %s", urlPath)
return "", webdav.NewHTTPError(403, err) return "", webdav.NewHTTPError(403, err)
} }
} urlPath = strings.TrimPrefix(urlPath, homeSetPath)
// only accept simple file names for now // only accept simple file names for now
if !validFilenameRegex.MatchString(file) { dir, file := path.Split(urlPath)
if file != "" && !validFilenameRegex.MatchString(file) {
log.Debug().Str("file", file).Msg("file name does not match regex") log.Debug().Str("file", file).Msg("file name does not match regex")
err := fmt.Errorf("invalid file name: %s", file) err := fmt.Errorf("invalid file name: %s", file)
return "", webdav.NewHTTPError(400, err) return "", webdav.NewHTTPError(400, err)
} }
// dir (= homeSetPath) is already included in path, so only file here return filepath.Join(localPath, dir, file), nil
return filepath.Join(localPath, file), nil
} }
func etagForFile(path string) (string, error) { func etagForFile(path string) (string, error) {

View File

@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -19,6 +18,8 @@ import (
"github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/caldav"
) )
const calendarFileName = "calendar.json"
func (b *filesystemBackend) CalendarHomeSetPath(ctx context.Context) (string, error) { func (b *filesystemBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
upPath, err := b.CurrentUserPrincipal(ctx) upPath, err := b.CurrentUserPrincipal(ctx)
if err != nil { if err != nil {
@ -28,7 +29,16 @@ func (b *filesystemBackend) CalendarHomeSetPath(ctx context.Context) (string, er
return path.Join(upPath, b.caldavPrefix) + "/", nil 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) homeSetPath, err := b.CalendarHomeSetPath(ctx)
if err != nil { if err != nil {
return "", err return "", err
@ -55,18 +65,15 @@ func calendarFromFile(path string, propFilter []string) (*ical.Calendar, error)
//return icalPropFilter(cal, propFilter), nil //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 var result []caldav.CalendarObject
localPath, err := b.localCalDAVPath(ctx, "") localPath, err := b.safeLocalCalDAVPath(ctx, urlPath)
if err != nil { if err != nil {
return result, err return result, err
} }
homeSetPath, err := b.CalendarHomeSetPath(ctx) log.Debug().Str("path", localPath).Msg("loading calendar objects")
if err != nil {
return result, err
}
err = filepath.Walk(localPath, func(filename string, info os.FileInfo, err error) error { err = filepath.Walk(localPath, func(filename string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -89,8 +96,10 @@ func (b *filesystemBackend) loadAllCalendars(ctx context.Context, propFilter []s
return err 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{ obj := caldav.CalendarObject{
Path: path.Join(homeSetPath, defaultResourceName, filepath.Base(filename)), Path: path.Join(urlPath, filepath.Base(filename)),
ModTime: info.ModTime(), ModTime: info.ModTime(),
ContentLength: info.Size(), ContentLength: info.Size(),
ETag: etag, ETag: etag,
@ -100,55 +109,118 @@ func (b *filesystemBackend) loadAllCalendars(ctx context.Context, propFilter []s
return nil return nil
}) })
log.Debug().Int("results", len(result)).Str("path", localPath).Msg("filesystem.loadAllCalendars() successful")
return result, err 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? // 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{ defaultC := caldav.Calendar{
Path: path, Path: urlPath,
Name: "My calendar", Name: "My calendar",
Description: "Default calendar", Description: "Default calendar",
MaxResourceSize: 4096, MaxResourceSize: 4096,
} }
blob, err := json.MarshalIndent(defaultC, "", " ") blob, err := json.MarshalIndent(defaultC, "", " ")
if err != nil { 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 { 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 &defaultC, nil
}
func (b *filesystemBackend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error) {
log.Debug().Msg("filesystem.ListCalendars()")
localPath, err := b.localCalDAVDir(ctx)
if err != nil {
return nil, err
}
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 return nil
} }
func (b *filesystemBackend) Calendar(ctx context.Context) (*caldav.Calendar, error) { calPath := path.Join(filename, calendarFileName)
log.Debug().Msg("filesystem.Calendar()") 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())
}
}
localPath, err := b.localCalDAVPath(ctx, "") 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 { if err != nil {
return nil, err return nil, err
} }
localPath = filepath.Join(localPath, "calendar.json") localPath = filepath.Join(localPath, calendarFileName)
log.Debug().Str("local_path", localPath).Msg("loading calendar") log.Debug().Str("local_path", localPath).Msg("loading calendar")
data, readErr := ioutil.ReadFile(localPath) data, readErr := os.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)
}
if readErr != nil { if readErr != nil {
if os.IsNotExist(readErr) {
return nil, webdav.NewHTTPError(404, err)
}
return nil, fmt.Errorf("error opening calendar: %s", readErr.Error()) return nil, fmt.Errorf("error opening calendar: %s", readErr.Error())
} }
var calendar caldav.Calendar 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) { func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) {
log.Debug().Str("url_path", objPath).Msg("filesystem.GetCalendarObject()") log.Debug().Str("url_path", objPath).Msg("filesystem.GetCalendarObject()")
localPath, err := b.localCalDAVPath(ctx, objPath) localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -203,31 +275,46 @@ func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath strin
return &obj, nil return &obj, nil
} }
func (b *filesystemBackend) ListCalendarObjects(ctx context.Context, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) { func (b *filesystemBackend) ListCalendarObjects(ctx context.Context, urlPath string, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) {
log.Debug().Msg("filesystem.ListCalendarObjects()") log.Debug().Str("path", urlPath).Msg("filesystem.ListCalendarObjects()")
var propFilter []string var propFilter []string
if req != nil && !req.AllProps { if req != nil && !req.AllProps {
propFilter = req.Props 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) { func (b *filesystemBackend) QueryCalendarObjects(ctx context.Context, urlPath string, query *caldav.CalendarQuery) ([]caldav.CalendarObject, error) {
log.Debug().Msg("filesystem.QueryCalendarObjects()") log.Debug().Str("path", urlPath).Msg("filesystem.QueryCalendarObjects()")
var propFilter []string var propFilter []string
if query != nil && !query.CompRequest.AllProps { if query != nil && !query.CompRequest.AllProps {
propFilter = query.CompRequest.Props propFilter = query.CompRequest.Props
} }
result, err := b.loadAllCalendars(ctx, propFilter) result, err := b.loadAllCalendarObjects(ctx, urlPath, propFilter)
if err != nil { if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryCalendarObjects() error loading")
return result, err 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) { 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) dirname, _ := path.Split(objPath)
objPath = path.Join(dirname, uid+".ics") objPath = path.Join(dirname, uid+".ics")
localPath, err := b.localCalDAVPath(ctx, objPath) localPath, err := b.safeLocalCalDAVPath(ctx, objPath)
if err != nil { if err != nil {
return "", err 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 { func (b *filesystemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
log.Debug().Str("url_path", path).Msg("filesystem.DeleteCalendarObject()") log.Debug().Str("url_path", path).Msg("filesystem.DeleteCalendarObject()")
localPath, err := b.localCalDAVPath(ctx, path) localPath, err := b.safeLocalCalDAVPath(ctx, path)
if err != nil { if err != nil {
return err return err
} }

View File

@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -19,7 +18,9 @@ import (
"github.com/emersion/go-webdav/carddav" "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) upPath, err := b.CurrentUserPrincipal(ctx)
if err != nil { if err != nil {
return "", err return "", err
@ -28,8 +29,17 @@ func (b *filesystemBackend) AddressbookHomeSetPath(ctx context.Context) (string,
return path.Join(upPath, b.carddavPrefix) + "/", nil return path.Join(upPath, b.carddavPrefix) + "/", nil
} }
func (b *filesystemBackend) localCardDAVPath(ctx context.Context, urlPath string) (string, error) { func (b *filesystemBackend) localCardDAVDir(ctx context.Context, components ...string) (string, error) {
homeSetPath, err := b.AddressbookHomeSetPath(ctx) 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 { if err != nil {
return "", err return "", err
} }
@ -74,10 +84,69 @@ func vcardFromFile(path string, propFilter []string) (vcard.Card, error) {
return vcardPropFilter(card, propFilter), nil 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? // 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{ defaultAB := carddav.AddressBook{
Path: path, Path: urlPath,
Name: "My contacts", Name: "My contacts",
Description: "Default address book", Description: "Default address book",
MaxResourceSize: 1024, MaxResourceSize: 1024,
@ -85,40 +154,92 @@ func createDefaultAddressBook(path, localPath string) error {
} }
blob, err := json.MarshalIndent(defaultAB, "", " ") blob, err := json.MarshalIndent(defaultAB, "", " ")
if err != nil { 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 { 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 &defaultAB, nil
}
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
}
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 return nil
} }
func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) { abPath := path.Join(filename, addressBookFileName)
log.Debug().Msg("filesystem.AddressBook()") data, err := os.ReadFile(abPath)
localPath, err := b.localCardDAVPath(ctx, "") 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 { if err != nil {
return nil, err return nil, err
} }
localPath = filepath.Join(localPath, "addressbook.json") localPath = filepath.Join(localPath, addressBookFileName)
log.Debug().Str("local_path", localPath).Msg("loading addressbook") log.Debug().Str("local_path", localPath).Msg("loading addressbook")
data, readErr := ioutil.ReadFile(localPath) data, readErr := os.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)
}
if readErr != nil { if readErr != nil {
if os.IsNotExist(readErr) {
return nil, webdav.NewHTTPError(404, err)
}
return nil, fmt.Errorf("error opening address book: %s", readErr.Error()) return nil, fmt.Errorf("error opening address book: %s", readErr.Error())
} }
var addressBook carddav.AddressBook 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) { func (b *filesystemBackend) GetAddressObject(ctx context.Context, objPath string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) {
log.Debug().Str("url_path", objPath).Msg("filesystem.GetAddressObject()") log.Debug().Str("url_path", objPath).Msg("filesystem.GetAddressObject()")
localPath, err := b.localCardDAVPath(ctx, objPath)
localPath, err := b.safeLocalCardDAVPath(ctx, objPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -170,76 +292,46 @@ func (b *filesystemBackend) GetAddressObject(ctx context.Context, objPath string
return &obj, nil return &obj, nil
} }
func (b *filesystemBackend) loadAllContacts(ctx context.Context, propFilter []string) ([]carddav.AddressObject, error) { func (b *filesystemBackend) ListAddressObjects(ctx context.Context, urlPath string, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) {
var result []carddav.AddressObject 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 var propFilter []string
if req != nil && !req.AllProp { if req != nil && !req.AllProp {
propFilter = req.Props 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) { func (b *filesystemBackend) QueryAddressObjects(ctx context.Context, urlPath string, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) {
log.Debug().Msg("filesystem.QueryAddressObjects()") log.Debug().Str("path", urlPath).Msg("filesystem.QueryAddressObjects()")
var propFilter []string var propFilter []string
if query != nil && !query.DataRequest.AllProp { if query != nil && !query.DataRequest.AllProp {
propFilter = query.DataRequest.Props propFilter = query.DataRequest.Props
} }
result, err := b.loadAllContacts(ctx, propFilter) result, err := b.loadAllAddressObjects(ctx, urlPath, propFilter)
if err != nil { if err != nil {
log.Warn().Int("results", len(result)).Str("error", err.Error()).Msg("filesystem.QueryAddressObjects() error loading")
return result, err 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) { 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) dirname, _ := path.Split(objPath)
objPath = path.Join(dirname, card.Value(vcard.FieldUID)+".vcf") objPath = path.Join(dirname, card.Value(vcard.FieldUID)+".vcf")
localPath, err := b.localCardDAVPath(ctx, objPath) localPath, err := b.safeLocalCardDAVPath(ctx, objPath)
if err != nil { if err != nil {
return "", err 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 { func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string) error {
log.Debug().Str("url_path", path).Msg("filesystem.DeleteAddressObject()") log.Debug().Str("url_path", path).Msg("filesystem.DeleteAddressObject()")
localPath, err := b.localCardDAVPath(ctx, path) localPath, err := b.safeLocalCardDAVPath(ctx, path)
if err != nil { if err != nil {
return err return err
} }

View File

@ -20,11 +20,15 @@ func (*psqlBackend) CurrentUserPrincipal(ctx context.Context) (string, error) {
panic("TODO") panic("TODO")
} }
func (*psqlBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) { func (*psqlBackend) AddressBookHomeSetPath(ctx context.Context) (string, error) {
panic("TODO") panic("TODO")
} }
func (*psqlBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) { func (*psqlBackend) ListAddressBooks(ctx context.Context) ([]carddav.AddressBook, error) {
panic("TODO")
}
func (*psqlBackend) GetAddressBook(ctx context.Context, path string) (*carddav.AddressBook, error) {
panic("TODO") panic("TODO")
} }
@ -32,11 +36,11 @@ func (*psqlBackend) GetAddressObject(ctx context.Context, path string, req *card
panic("TODO") panic("TODO")
} }
func (*psqlBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) { func (*psqlBackend) ListAddressObjects(ctx context.Context, path string, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) {
panic("TODO") panic("TODO")
} }
func (*psqlBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) { func (*psqlBackend) QueryAddressObjects(ctx context.Context, path string, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) {
panic("TODO") panic("TODO")
} }