diff --git a/cmd/tokidoki/main.go b/cmd/tokidoki/main.go index f190cb5..8aef588 100644 --- a/cmd/tokidoki/main.go +++ b/cmd/tokidoki/main.go @@ -1,19 +1,83 @@ package main import ( + "context" + "encoding/base64" "flag" + "fmt" "log" "net/http" "os" + "github.com/emersion/go-webdav" + "github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/carddav" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "git.sr.ht/~sircmpwn/tokidoki/auth" + "git.sr.ht/~sircmpwn/tokidoki/debug" "git.sr.ht/~sircmpwn/tokidoki/storage" ) +type userPrincipalBackend struct{} + +func (u *userPrincipalBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { + authCtx, ok := auth.FromContext(ctx) + if !ok { + panic("Invalid data in auth context!") + } + if authCtx == nil { + return "", fmt.Errorf("unauthenticated requests are not supported") + } + + userDir := base64.RawStdEncoding.EncodeToString([]byte(authCtx.UserName)) + return "/" + userDir + "/", nil +} + +type tokidokiHandler struct { + upBackend webdav.UserPrincipalBackend + authBackend auth.AuthProvider + caldavBackend caldav.Backend + carddavBackend carddav.Backend +} + +func (u *tokidokiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + userPrincipalPath, err := u.upBackend.CurrentUserPrincipal(r.Context()) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + + var homeSets []webdav.BackendSuppliedHomeSet + if u.caldavBackend != nil { + path, err := u.caldavBackend.CalendarHomeSetPath(r.Context()) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else { + homeSets = append(homeSets, caldav.NewCalendarHomeSet(path)) + } + } + if u.carddavBackend != nil { + path, err := u.carddavBackend.AddressbookHomeSetPath(r.Context()) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else { + homeSets = append(homeSets, carddav.NewAddressBookHomeSet(path)) + } + } + + opts := webdav.ServeUserPrincipalOptions{ + UserPrincipalPath: userPrincipalPath, + HomeSets: homeSets, + } + + if webdav.ServeUserPrincipal(w, r, opts) { + return + } + + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) +} + func main() { var ( addr string @@ -23,6 +87,7 @@ func main() { flag.StringVar(&addr, "addr", ":8080", "listening address") flag.StringVar(&authURL, "auth.url", "", "auth backend URL (required)") flag.StringVar(&storageURL, "storage.url", "", "storage backend URL (required)") + flag.BoolVar(&debug.Enable, "debug", false, "enable debug output") flag.Parse() if len(flag.Args()) != 0 || authURL == "" || storageURL == "" { @@ -48,17 +113,36 @@ func main() { } mux.Use(authProvider.Middleware()) - backend, err := storage.NewFromURL(storageURL) + upBackend := &userPrincipalBackend{} + + caldavBackend, carddavBackend, err := storage.NewFromURL(storageURL, "/calendar/", "/contacts/", upBackend) if err != nil { log.Fatalf("failed to load storage backend: %s", err.Error()) } - mux.Mount("/", &carddav.Handler{Backend: backend}) + + carddavHandler := carddav.Handler{Backend: carddavBackend} + caldavHandler := caldav.Handler{Backend: caldavBackend} + handler := tokidokiHandler{ + upBackend: upBackend, + authBackend: authProvider, + caldavBackend: caldavBackend, + carddavBackend: carddavBackend, + } + + mux.Mount("/", &handler) + mux.Mount("/.well-known/caldav", &caldavHandler) + mux.Mount("/.well-known/carddav", &carddavHandler) + mux.Mount("/{user}/contacts", &carddavHandler) + mux.Mount("/{user}/calendar", &caldavHandler) server := http.Server{ Addr: addr, Handler: mux, } + log.Printf("Server running on %s", addr) + debug.Printf("Debug output enabled") + err = server.ListenAndServe() if err != http.ErrServerClosed { log.Fatalf("ListenAndServe: %s", err.Error()) diff --git a/debug/debug.go b/debug/debug.go new file mode 100644 index 0000000..1d8f21f --- /dev/null +++ b/debug/debug.go @@ -0,0 +1,15 @@ +package debug + +import ( + "log" +) + +var ( + Enable = false +) + +func Printf(format string, v ...any) { + if Enable { + log.Printf("[debug] "+format, v...) + } +} diff --git a/go.mod b/go.mod index 0c0b9eb..40355ea 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module git.sr.ht/~sircmpwn/tokidoki -go 1.17 +go 1.18 require ( + github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e github.com/emersion/go-imap v1.2.0 github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac github.com/emersion/go-vcard v0.0.0-20210521075357-3445b9171995 @@ -11,3 +12,5 @@ require ( ) require golang.org/x/text v0.3.7 // indirect + +replace github.com/emersion/go-webdav v0.3.2-0.20220310154811-85d2b222bbcd => github.com/bitfehler/go-webdav v0.3.2-0.20220503133151-e5312775c02f diff --git a/go.sum b/go.sum index 976499f..c7e6754 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,6 @@ +github.com/bitfehler/go-webdav v0.3.2-0.20220503133151-e5312775c02f h1:gRchuZEVTYh+ymMqejZKbD67ZLLm59Q9lTG49e0Qu20= +github.com/bitfehler/go-webdav v0.3.2-0.20220503133151-e5312775c02f/go.mod h1:uSM1VveeKtogBVWaYccTksToczooJ0rrVGNsgnDsr4Q= +github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e h1:YGM1sI7edZOt8KAfX9Miq/X99d2QXdgjkJ7vN4HjxAA= github.com/emersion/go-ical v0.0.0-20200224201310-cd514449c39e/go.mod h1:4xVTBPcT43a1pp3vdaa+FuRdX5XhKCZPpWv7m0z9ByM= github.com/emersion/go-imap v1.2.0 h1:lyUQ3+EVM21/qbWE/4Ya5UG9r5+usDxlg4yfp3TgHFA= github.com/emersion/go-imap v1.2.0/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= diff --git a/storage/filesystem.go b/storage/filesystem.go index 481f2d1..a2d9fb3 100644 --- a/storage/filesystem.go +++ b/storage/filesystem.go @@ -5,74 +5,129 @@ import ( "crypto/sha1" "encoding/base64" "encoding/json" + "errors" "fmt" "io" + "io/fs" "io/ioutil" "os" + "path" "path/filepath" "regexp" + "github.com/emersion/go-ical" "github.com/emersion/go-vcard" + "github.com/emersion/go-webdav" + "github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/carddav" - "git.sr.ht/~sircmpwn/tokidoki/auth" + "git.sr.ht/~sircmpwn/tokidoki/debug" ) type filesystemBackend struct { - path string + path string + caldavPrefix string + carddavPrefix string + userPrincipalBackend webdav.UserPrincipalBackend } var ( - nilBackend carddav.Backend = (*filesystemBackend)(nil) - validFilenameRegex = regexp.MustCompile(`^/[A-Za-z0-9][A-Za-z0-9_-]+(.[a-zA-Z]+)?$`) + validFilenameRegex = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_-]+(.[a-zA-Z]+)?$`) ) -func NewFilesystem(path string) (carddav.Backend, error) { - info, err := os.Stat(path) +func NewFilesystem(fsPath, caldavPrefix, carddavPrefix string, userPrincipalBackend webdav.UserPrincipalBackend) (caldav.Backend, carddav.Backend, error) { + info, err := os.Stat(fsPath) if err != nil { - return nilBackend, fmt.Errorf("failed to create filesystem backend: %s", err.Error()) + return nil, nil, fmt.Errorf("failed to create filesystem backend: %s", err.Error()) } if !info.IsDir() { - return nilBackend, fmt.Errorf("base path for filesystem backend must be a directory") + return nil, nil, fmt.Errorf("base path for filesystem backend must be a directory") } - return &filesystemBackend{ - path: path, - }, nil + backend := &filesystemBackend{ + path: fsPath, + caldavPrefix: caldavPrefix, + carddavPrefix: carddavPrefix, + userPrincipalBackend: userPrincipalBackend, + } + return backend, backend, nil } -func (b *filesystemBackend) pathForContext(ctx context.Context) (string, error) { - authCtx, ok := auth.FromContext(ctx) - if !ok { - panic("Invalid data in auth context!") - } - if authCtx == nil { - return "", fmt.Errorf("unauthenticated requests are not supported") - } - - userDir := base64.RawStdEncoding.EncodeToString([]byte(authCtx.UserName)) - path := filepath.Join(b.path, userDir) - - _, err := os.Stat(path) - if os.IsNotExist(err) { - err = os.Mkdir(path, 0755) - if err != nil { - return "", fmt.Errorf("error creating '%s': %s", path, err.Error()) - } - } - return path, nil +func (b *filesystemBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { + return b.userPrincipalBackend.CurrentUserPrincipal(ctx) } -func (b *filesystemBackend) safePath(ctx context.Context, path string) (string, error) { - basePath, err := b.pathForContext(ctx) +func (b *filesystemBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) { + upPath, err := b.userPrincipalBackend.CurrentUserPrincipal(ctx) if err != nil { return "", err } + + return path.Join(upPath, b.carddavPrefix) + "/", nil +} + +func (b *filesystemBackend) CalendarHomeSetPath(ctx context.Context) (string, error) { + upPath, err := b.userPrincipalBackend.CurrentUserPrincipal(ctx) + if err != nil { + return "", err + } + + return path.Join(upPath, b.caldavPrefix) + "/", nil +} + +func ensureLocalDir(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + err = os.MkdirAll(path, 0755) + if err != nil { + return fmt.Errorf("error creating '%s': %s", path, err.Error()) + } + } + return nil +} + +// don't use this directly, use localCalDAVPath or localCardDAVPath instead. +func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (string, error) { + localPath := filepath.Join(b.path, homeSetPath) + if err := ensureLocalDir(localPath); err != nil { + return "", err + } + + if urlPath == "" { + return localPath, nil + } + // We are mapping to local filesystem path, so be conservative about what to accept // TODO this changes once multiple addess books are supported - if !validFilenameRegex.MatchString(path) { - return "", fmt.Errorf("invalid request path") + dir, file := path.Split(urlPath) + // only accept resources in prefix, no subdirs for now + if dir != homeSetPath { + return "", fmt.Errorf("invalid request path %s", urlPath) } - return filepath.Join(basePath, path), nil + // only accept simple file names for now + if !validFilenameRegex.MatchString(file) { + fmt.Printf("%s does not match regex!\n", file) + return "", fmt.Errorf("invalid file name") + } + + // dir (= homeSetPath) is already included in path, so only file here + return filepath.Join(localPath, file), nil +} + +func (b *filesystemBackend) localCalDAVPath(ctx context.Context, urlPath string) (string, error) { + homeSetPath, err := b.CalendarHomeSetPath(ctx) + if err != nil { + return "", err + } + + return b.safeLocalPath(homeSetPath, urlPath) +} + +func (b *filesystemBackend) localCardDAVPath(ctx context.Context, urlPath string) (string, error) { + homeSetPath, err := b.AddressbookHomeSetPath(ctx) + if err != nil { + return "", err + } + + return b.safeLocalPath(homeSetPath, urlPath) } func etagForFile(path string) (string, error) { @@ -128,10 +183,27 @@ func vcardFromFile(path string, propFilter []string) (vcard.Card, error) { return vcardPropFilter(card, propFilter), nil } -func createDefaultAddressBook(path string) error { +func calendarFromFile(path string, propFilter []string) (*ical.Calendar, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + dec := ical.NewDecoder(f) + cal, err := dec.Decode() + if err != nil { + return nil, err + } + + return cal, nil + // return vcardPropFilter(card, propFilter), nil +} + +func createDefaultAddressBook(path, localPath string) error { // TODO what should the default address book look like? defaultAB := carddav.AddressBook{ - Path: "/default", + Path: path, Name: "My contacts", Description: "Default address book", MaxResourceSize: 1024, @@ -141,7 +213,7 @@ func createDefaultAddressBook(path string) error { if err != nil { return fmt.Errorf("error creating default address book: %s", err.Error()) } - err = os.WriteFile(path, blob, 0644) + err = os.WriteFile(localPath, blob, 0644) if err != nil { return fmt.Errorf("error writing default address book: %s", err.Error()) } @@ -149,15 +221,23 @@ func createDefaultAddressBook(path string) error { } func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) { - path, err := b.pathForContext(ctx) + debug.Printf("filesystem.AddressBook()") + path, err := b.localCardDAVPath(ctx, "") if err != nil { return nil, err } - path = filepath.Join(path, "_default_ab.json") + path = filepath.Join(path, "addressbook.json") + + debug.Printf("loading addressbook from %s", path) data, err := ioutil.ReadFile(path) if os.IsNotExist(err) { - err = createDefaultAddressBook(path) + urlPath, err := b.AddressbookHomeSetPath(ctx) + if err != nil { + return nil, err + } + debug.Printf("creating default addressbook (URL:path): %s:%s", urlPath, path) + err = createDefaultAddressBook(urlPath, path) if err != nil { return nil, err } @@ -175,8 +255,9 @@ func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBo return &addressBook, nil } -func (b *filesystemBackend) GetAddressObject(ctx context.Context, path string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) { - localPath, err := b.safePath(ctx, path) +func (b *filesystemBackend) GetAddressObject(ctx context.Context, objPath string, req *carddav.AddressDataRequest) (*carddav.AddressObject, error) { + debug.Printf("filesystem.GetAddressObject(%s, %v)", objPath, req) + localPath, err := b.localCardDAVPath(ctx, objPath) if err != nil { return nil, err } @@ -202,7 +283,7 @@ func (b *filesystemBackend) GetAddressObject(ctx context.Context, path string, r } obj := carddav.AddressObject{ - Path: path, + Path: objPath, ModTime: info.ModTime(), ETag: etag, Card: card, @@ -210,17 +291,21 @@ func (b *filesystemBackend) GetAddressObject(ctx context.Context, path string, r return &obj, nil } -func (b *filesystemBackend) loadAll(ctx context.Context, propFilter []string) ([]carddav.AddressObject, error) { +func (b *filesystemBackend) loadAllContacts(ctx context.Context, propFilter []string) ([]carddav.AddressObject, error) { var result []carddav.AddressObject - path, err := b.pathForContext(ctx) + localPath, err := b.localCardDAVPath(ctx, "") if err != nil { return result, err } - err = filepath.Walk(path, func(filename string, info os.FileInfo, err error) error { - // Skip address book meta data files - if !info.Mode().IsRegular() || filepath.Ext(filename) == ".json" { + 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 !info.Mode().IsRegular() || filepath.Ext(filename) != ".vcf" { return nil } @@ -235,7 +320,7 @@ func (b *filesystemBackend) loadAll(ctx context.Context, propFilter []string) ([ } obj := carddav.AddressObject{ - Path: "/" + filepath.Base(filename), + Path: path.Join(homeSetPath, filepath.Base(filename)), ModTime: info.ModTime(), ETag: etag, Card: card, @@ -244,25 +329,72 @@ func (b *filesystemBackend) loadAll(ctx context.Context, propFilter []string) ([ return nil }) + debug.Printf("filesystem.loadAllContacts() returning %d results from %s", len(result), localPath) + return result, err +} + +func (b *filesystemBackend) loadAllCalendars(ctx context.Context, propFilter []string) ([]caldav.CalendarObject, error) { + var result []caldav.CalendarObject + + localPath, err := b.localCalDAVPath(ctx, "") + if err != nil { + return result, err + } + + homeSetPath, err := b.CalendarHomeSetPath(ctx) + if err != nil { + return result, err + } + + err = filepath.Walk(localPath, func(filename string, info os.FileInfo, err error) error { + // Skip address book meta data files + if !info.Mode().IsRegular() || filepath.Ext(filename) != ".ics" { + return nil + } + + cal, err := calendarFromFile(filename, propFilter) + if err != nil { + fmt.Printf("load calendar error for %s: %v\n", filename, err) + return err + } + + etag, err := etagForFile(filename) + if err != nil { + return err + } + + obj := caldav.CalendarObject{ + Path: path.Join(homeSetPath, filepath.Base(filename)), + ModTime: info.ModTime(), + ETag: etag, + Data: cal, + } + result = append(result, obj) + return nil + }) + + debug.Printf("filesystem.loadAllCalendars() returning %d results from %s", len(result), localPath) return result, err } func (b *filesystemBackend) ListAddressObjects(ctx context.Context, req *carddav.AddressDataRequest) ([]carddav.AddressObject, error) { + debug.Printf("filesystem.ListAddressObjects(%v)", req) var propFilter []string if req != nil && !req.AllProp { propFilter = req.Props } - return b.loadAll(ctx, propFilter) + return b.loadAllContacts(ctx, propFilter) } func (b *filesystemBackend) QueryAddressObjects(ctx context.Context, query *carddav.AddressBookQuery) ([]carddav.AddressObject, error) { + debug.Printf("filesystem.QueryAddressObjects(%v)", query) var propFilter []string if query != nil && !query.DataRequest.AllProp { propFilter = query.DataRequest.Props } - result, err := b.loadAll(ctx, propFilter) + result, err := b.loadAllContacts(ctx, propFilter) if err != nil { return result, err } @@ -270,31 +402,19 @@ func (b *filesystemBackend) QueryAddressObjects(ctx context.Context, query *card return carddav.Filter(query, result) } -func (b *filesystemBackend) hasUIDConflict(ctx context.Context, uid, path string) (bool, error) { - all, err := b.loadAll(ctx, nil) - if err != nil { - return false, err - } - for _, contact := range all { - if contact.Path != path && contact.Card.Value(vcard.FieldUID) == uid { - return true, nil - } - } +func (b *filesystemBackend) PutAddressObject(ctx context.Context, objPath string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) { + debug.Printf("filesystem.PutAddressObject(%v, %v, %v)", objPath, card, opts) - return false, nil -} + // Object always get saved as .vcf + dirname, _ := path.Split(objPath) + objPath = path.Join(dirname, card.Value(vcard.FieldUID)+".vcf") -func (b *filesystemBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card) (loc string, err error) { - localPath, err := b.safePath(ctx, path) + localPath, err := b.localCardDAVPath(ctx, objPath) if err != nil { return "", err } - conflict, err := b.hasUIDConflict(ctx, card.Value(vcard.FieldUID), path) - if err != nil { - return "", err - } - if conflict { + if _, err := os.Stat(localPath); !os.IsNotExist(err) { return "", carddav.NewPreconditionError(carddav.PreconditionNoUIDConflict) } @@ -310,11 +430,13 @@ func (b *filesystemBackend) PutAddressObject(ctx context.Context, path string, c return "", err } - return path, nil + return objPath, nil } func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string) error { - localPath, err := b.safePath(ctx, path) + debug.Printf("filesystem.DeleteAddressObject(%s)", path) + + localPath, err := b.localCardDAVPath(ctx, path) if err != nil { return err } @@ -324,3 +446,167 @@ func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string } return nil } + +func createDefaultCalendar(path, localPath string) error { + // TODO what should the default calendar look like? + defaultC := caldav.Calendar{ + Path: path, + 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()) + } + err = os.WriteFile(localPath, blob, 0644) + if err != nil { + return fmt.Errorf("error writing default calendar: %s", err.Error()) + } + return nil +} + +func (b *filesystemBackend) Calendar(ctx context.Context) (*caldav.Calendar, error) { + debug.Printf("filesystem.Calendar()") + + path, err := b.localCalDAVPath(ctx, "") + if err != nil { + return nil, err + } + path = filepath.Join(path, "calendar.json") + + debug.Printf("loading calendar from %s", path) + + data, err := ioutil.ReadFile(path) + if os.IsNotExist(err) { + homeSetPath, err := b.CalendarHomeSetPath(ctx) + if err != nil { + return nil, err + } + debug.Printf("creating default calendar (URL:path): %s:%s", homeSetPath, path) + err = createDefaultCalendar(homeSetPath, path) + if err != nil { + return nil, err + } + data, err = ioutil.ReadFile(path) + } + if err != nil { + return nil, fmt.Errorf("error opening calendar: %s", err.Error()) + } + var calendar caldav.Calendar + err = json.Unmarshal(data, &calendar) + if err != nil { + return nil, fmt.Errorf("error reading calendar: %s", err.Error()) + } + + return &calendar, nil +} + +func (b *filesystemBackend) GetCalendarObject(ctx context.Context, objPath string, req *caldav.CalendarCompRequest) (*caldav.CalendarObject, error) { + debug.Printf("filesystem.GetCalendarObject(%s, %v)", objPath, req) + + localPath, err := b.localCalDAVPath(ctx, objPath) + if err != nil { + return nil, err + } + + info, err := os.Stat(localPath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + debug.Printf("not found: %s", localPath) + return nil, nil + } + return nil, err + } + + var propFilter []string + if req != nil && !req.AllProps { + propFilter = req.Props + } + + calendar, err := calendarFromFile(localPath, propFilter) + if err != nil { + debug.Printf("error reading calendar: %v", err) + return nil, err + } + + etag, err := etagForFile(localPath) + if err != nil { + return nil, err + } + + obj := caldav.CalendarObject{ + Path: objPath, + ModTime: info.ModTime(), + ETag: etag, + Data: calendar, + } + return &obj, nil + return nil, fmt.Errorf("not implemented") +} + +func (b *filesystemBackend) ListCalendarObjects(ctx context.Context, req *caldav.CalendarCompRequest) ([]caldav.CalendarObject, error) { + debug.Printf("filesystem.ListCalendarObjects(%v)", req) + + var propFilter []string + if req != nil && !req.AllProps { + propFilter = req.Props + } + + return b.loadAllCalendars(ctx, propFilter) +} + +func (b *filesystemBackend) QueryCalendarObjects(ctx context.Context, query *caldav.CalendarQuery) ([]caldav.CalendarObject, error) { + debug.Printf("filesystem.QueryCalendarObjects(%v)", query) + + var propFilter []string + if query != nil && !query.CompRequest.AllProps { + propFilter = query.CompRequest.Props + } + + result, err := b.loadAllCalendars(ctx, propFilter) + if err != nil { + return result, err + } + + return result, nil + // TODO implement: + //return caldav.Filter(query, result) +} + +func (b *filesystemBackend) PutCalendarObject(ctx context.Context, objPath string, calendar *ical.Calendar, opts *caldav.PutCalendarObjectOptions) (loc string, err error) { + debug.Printf("filesystem.PutCalendarObject(%s, %v, %v)", objPath, calendar, opts) + + _, uid, err := caldav.ValidateCalendarObject(calendar) + if err != nil { + return "", caldav.NewPreconditionError(caldav.PreconditionValidCalendarObjectResource) + } + + // Object always get saved as .ics + dirname, _ := path.Split(objPath) + objPath = path.Join(dirname, uid+".ics") + + localPath, err := b.localCalDAVPath(ctx, objPath) + if err != nil { + return "", err + } + + if _, err := os.Stat(localPath); !os.IsNotExist(err) { + return "", caldav.NewPreconditionError(caldav.PreconditionNoUIDConflict) + } + + f, err := os.Create(localPath) + if err != nil { + return "", err + } + defer f.Close() + + enc := ical.NewEncoder(f) + err = enc.Encode(calendar) + if err != nil { + return "", err + } + + return objPath, nil + return "", nil +} diff --git a/storage/postgresql.go b/storage/postgresql.go index 2b3a945..b2a9c87 100644 --- a/storage/postgresql.go +++ b/storage/postgresql.go @@ -4,6 +4,7 @@ import ( "context" "github.com/emersion/go-vcard" + "github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/carddav" ) @@ -11,8 +12,16 @@ type psqlBackend struct{} var _ carddav.Backend = (*psqlBackend)(nil) -func NewPostgreSQL() carddav.Backend { - return &psqlBackend{} +func NewPostgreSQL() (caldav.Backend, carddav.Backend, error) { + return nil, &psqlBackend{}, nil +} + +func (*psqlBackend) CurrentUserPrincipal(ctx context.Context) (string, error) { + panic("TODO") +} + +func (*psqlBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) { + panic("TODO") } func (*psqlBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) { @@ -31,7 +40,7 @@ func (*psqlBackend) QueryAddressObjects(ctx context.Context, query *carddav.Addr panic("TODO") } -func (*psqlBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card) (loc string, err error) { +func (*psqlBackend) PutAddressObject(ctx context.Context, path string, card vcard.Card, opts *carddav.PutAddressObjectOptions) (loc string, err error) { panic("TODO") } diff --git a/storage/url.go b/storage/url.go index 2cdd891..bb5dbd9 100644 --- a/storage/url.go +++ b/storage/url.go @@ -4,21 +4,23 @@ import ( "fmt" "net/url" + "github.com/emersion/go-webdav" + "github.com/emersion/go-webdav/caldav" "github.com/emersion/go-webdav/carddav" ) -func NewFromURL(storageURL string) (carddav.Backend, error) { +func NewFromURL(storageURL, caldavPrefix, carddavPrefix string, userPrincipalBackend webdav.UserPrincipalBackend) (caldav.Backend, carddav.Backend, error) { u, err := url.Parse(storageURL) if err != nil { - return nil, fmt.Errorf("error parsing storage URL: %s", err.Error()) + return nil, nil, fmt.Errorf("error parsing storage URL: %s", err.Error()) } switch u.Scheme { case "file": - return NewFilesystem(u.Path) + return NewFilesystem(u.Path, caldavPrefix, carddavPrefix, userPrincipalBackend) case "postgresql": - return NewPostgreSQL(), nil + return NewPostgreSQL() default: - return nil, fmt.Errorf("no storage provider found for %s:// URL", u.Scheme) + return nil, nil, fmt.Errorf("no storage provider found for %s:// URL", u.Scheme) } }