storage: break filesystem.go into multiple files
This commit is contained in:
parent
68de660456
commit
03fce79e57
@ -1,24 +1,16 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/emersion/go-ical"
|
|
||||||
"github.com/emersion/go-vcard"
|
|
||||||
"github.com/emersion/go-webdav"
|
"github.com/emersion/go-webdav"
|
||||||
"github.com/emersion/go-webdav/caldav"
|
"github.com/emersion/go-webdav/caldav"
|
||||||
"github.com/emersion/go-webdav/carddav"
|
"github.com/emersion/go-webdav/carddav"
|
||||||
@ -35,6 +27,7 @@ type filesystemBackend struct {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
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]+)?$`)
|
||||||
|
defaultResourceName = "default"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewFilesystem(fsPath, caldavPrefix, carddavPrefix string, userPrincipalBackend webdav.UserPrincipalBackend) (caldav.Backend, carddav.Backend, error) {
|
func NewFilesystem(fsPath, caldavPrefix, carddavPrefix string, userPrincipalBackend webdav.UserPrincipalBackend) (caldav.Backend, carddav.Backend, error) {
|
||||||
@ -54,24 +47,6 @@ func NewFilesystem(fsPath, caldavPrefix, carddavPrefix string, userPrincipalBack
|
|||||||
return backend, backend, nil
|
return backend, backend, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *filesystemBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
|
|
||||||
upPath, err := b.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.CurrentUserPrincipal(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.Join(upPath, b.caldavPrefix) + "/", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureLocalDir(path string) error {
|
func ensureLocalDir(path string) error {
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
err = os.MkdirAll(path, 0755)
|
err = os.MkdirAll(path, 0755)
|
||||||
@ -84,7 +59,7 @@ func ensureLocalDir(path string) error {
|
|||||||
|
|
||||||
// don't use this directly, use localCalDAVPath or localCardDAVPath instead.
|
// don't use this directly, use localCalDAVPath or localCardDAVPath instead.
|
||||||
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, "default")
|
localPath := filepath.Join(b.path, homeSetPath, defaultResourceName)
|
||||||
if err := ensureLocalDir(localPath); err != nil {
|
if err := ensureLocalDir(localPath); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -97,12 +72,12 @@ func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (s
|
|||||||
// TODO this changes once multiple addess books are supported
|
// TODO this changes once multiple addess books are supported
|
||||||
dir, file := path.Split(urlPath)
|
dir, file := path.Split(urlPath)
|
||||||
// only accept resources in default calendar/adress book for now
|
// only accept resources in default calendar/adress book for now
|
||||||
if path.Clean(dir) != path.Join(homeSetPath, "default") {
|
if path.Clean(dir) != path.Join(homeSetPath, defaultResourceName) {
|
||||||
if strings.HasPrefix(dir, homeSetPath) {
|
if strings.HasPrefix(dir, homeSetPath) {
|
||||||
err := fmt.Errorf("invalid request path: %s (%s is not %s)", urlPath, dir, path.Join(homeSetPath, "default"))
|
err := fmt.Errorf("invalid request path: %s (%s is not %s)", urlPath, dir, path.Join(homeSetPath, defaultResourceName))
|
||||||
return "", webdav.NewHTTPError(400, err)
|
return "", webdav.NewHTTPError(400, err)
|
||||||
} else {
|
} else {
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,24 +92,6 @@ func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (s
|
|||||||
return filepath.Join(localPath, file), nil
|
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) {
|
func etagForFile(path string) (string, error) {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -150,533 +107,3 @@ func etagForFile(path string) (string, error) {
|
|||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(csum[:]), nil
|
return base64.StdEncoding.EncodeToString(csum[:]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func vcardPropFilter(card vcard.Card, props []string) vcard.Card {
|
|
||||||
if card == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(props) == 0 {
|
|
||||||
return card
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(vcard.Card)
|
|
||||||
result["VERSION"] = card["VERSION"]
|
|
||||||
for _, prop := range props {
|
|
||||||
value, ok := card[prop]
|
|
||||||
if ok {
|
|
||||||
result[prop] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func vcardFromFile(path string, propFilter []string) (vcard.Card, error) {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
dec := vcard.NewDecoder(f)
|
|
||||||
card, err := dec.Decode()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return vcardPropFilter(card, propFilter), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// TODO implement
|
|
||||||
//return icalPropFilter(cal, propFilter), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDefaultAddressBook(path, localPath string) error {
|
|
||||||
// TODO what should the default address book look like?
|
|
||||||
defaultAB := carddav.AddressBook{
|
|
||||||
Path: path,
|
|
||||||
Name: "My contacts",
|
|
||||||
Description: "Default address book",
|
|
||||||
MaxResourceSize: 1024,
|
|
||||||
SupportedAddressData: nil,
|
|
||||||
}
|
|
||||||
blob, err := json.MarshalIndent(defaultAB, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error creating default address book: %s", err.Error())
|
|
||||||
}
|
|
||||||
err = os.WriteFile(localPath, blob, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error writing default address book: %s", err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) {
|
|
||||||
debug.Printf("filesystem.AddressBook()")
|
|
||||||
localPath, err := b.localCardDAVPath(ctx, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
localPath = filepath.Join(localPath, "addressbook.json")
|
|
||||||
|
|
||||||
debug.Printf("loading addressbook from %s", localPath)
|
|
||||||
|
|
||||||
data, readErr := ioutil.ReadFile(localPath)
|
|
||||||
if os.IsNotExist(readErr) {
|
|
||||||
urlPath, err := b.AddressbookHomeSetPath(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
urlPath = path.Join(urlPath, "default") + "/"
|
|
||||||
debug.Printf("creating default addressbook (URL:path): %s:%s", urlPath, localPath)
|
|
||||||
err = createDefaultAddressBook(urlPath, localPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data, readErr = ioutil.ReadFile(localPath)
|
|
||||||
}
|
|
||||||
if readErr != nil {
|
|
||||||
return nil, fmt.Errorf("error opening address book: %s", readErr.Error())
|
|
||||||
}
|
|
||||||
var addressBook carddav.AddressBook
|
|
||||||
err = json.Unmarshal(data, &addressBook)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading address book: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &addressBook, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := os.Stat(localPath)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
|
||||||
return nil, webdav.NewHTTPError(404, err)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var propFilter []string
|
|
||||||
if req != nil && !req.AllProp {
|
|
||||||
propFilter = req.Props
|
|
||||||
}
|
|
||||||
|
|
||||||
card, err := vcardFromFile(localPath, propFilter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
etag, err := etagForFile(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
obj := carddav.AddressObject{
|
|
||||||
Path: objPath,
|
|
||||||
ModTime: info.ModTime(),
|
|
||||||
ContentLength: info.Size(),
|
|
||||||
ETag: etag,
|
|
||||||
Card: card,
|
|
||||||
}
|
|
||||||
return &obj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *filesystemBackend) loadAllContacts(ctx context.Context, propFilter []string) ([]carddav.AddressObject, error) {
|
|
||||||
var result []carddav.AddressObject
|
|
||||||
|
|
||||||
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 !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, "default", filepath.Base(filename)),
|
|
||||||
ModTime: info.ModTime(),
|
|
||||||
ContentLength: info.Size(),
|
|
||||||
ETag: etag,
|
|
||||||
Card: card,
|
|
||||||
}
|
|
||||||
result = append(result, obj)
|
|
||||||
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, "default", filepath.Base(filename)),
|
|
||||||
ModTime: info.ModTime(),
|
|
||||||
ContentLength: info.Size(),
|
|
||||||
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.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.loadAllContacts(ctx, propFilter)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return carddav.Filter(query, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Object always get saved as <UID>.vcf
|
|
||||||
dirname, _ := path.Split(objPath)
|
|
||||||
objPath = path.Join(dirname, card.Value(vcard.FieldUID)+".vcf")
|
|
||||||
|
|
||||||
localPath, err := b.localCardDAVPath(ctx, objPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
|
|
||||||
// TODO handle IfNoneMatch == ETag
|
|
||||||
if opts.IfNoneMatch.IsWildcard() {
|
|
||||||
// Make sure we're not overwriting an existing file
|
|
||||||
flags |= os.O_EXCL
|
|
||||||
} else if opts.IfMatch.IsWildcard() {
|
|
||||||
// Make sure we _are_ overwriting an existing file
|
|
||||||
flags &= ^os.O_CREATE
|
|
||||||
} else if opts.IfMatch.IsSet() {
|
|
||||||
// Make sure we overwrite the _right_ file
|
|
||||||
etag, err := etagForFile(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
|
||||||
}
|
|
||||||
want, err := opts.IfMatch.ETag()
|
|
||||||
if err != nil {
|
|
||||||
return "", webdav.NewHTTPError(http.StatusBadRequest, err)
|
|
||||||
}
|
|
||||||
if want != etag {
|
|
||||||
err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag)
|
|
||||||
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.OpenFile(localPath, flags, 0666)
|
|
||||||
if os.IsExist(err) {
|
|
||||||
return "", carddav.NewPreconditionError(carddav.PreconditionNoUIDConflict)
|
|
||||||
} else if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
enc := vcard.NewEncoder(f)
|
|
||||||
err = enc.Encode(card)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return objPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string) error {
|
|
||||||
debug.Printf("filesystem.DeleteAddressObject(%s)", path)
|
|
||||||
|
|
||||||
localPath, err := b.localCardDAVPath(ctx, path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.Remove(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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()")
|
|
||||||
|
|
||||||
localPath, err := b.localCalDAVPath(ctx, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
localPath = filepath.Join(localPath, "calendar.json")
|
|
||||||
|
|
||||||
debug.Printf("loading calendar from %s", localPath)
|
|
||||||
|
|
||||||
data, readErr := ioutil.ReadFile(localPath)
|
|
||||||
if os.IsNotExist(readErr) {
|
|
||||||
urlPath, err := b.CalendarHomeSetPath(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
urlPath = path.Join(urlPath, "default") + "/"
|
|
||||||
debug.Printf("creating default calendar (URL:path): %s:%s", urlPath, localPath)
|
|
||||||
err = createDefaultCalendar(urlPath, localPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data, readErr = ioutil.ReadFile(localPath)
|
|
||||||
}
|
|
||||||
if readErr != nil {
|
|
||||||
return nil, fmt.Errorf("error opening calendar: %s", readErr.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, webdav.NewHTTPError(404, err)
|
|
||||||
}
|
|
||||||
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(),
|
|
||||||
ContentLength: info.Size(),
|
|
||||||
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 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 <UID>.ics
|
|
||||||
dirname, _ := path.Split(objPath)
|
|
||||||
objPath = path.Join(dirname, uid+".ics")
|
|
||||||
|
|
||||||
localPath, err := b.localCalDAVPath(ctx, objPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
|
|
||||||
// TODO handle IfNoneMatch == ETag
|
|
||||||
if opts.IfNoneMatch.IsWildcard() {
|
|
||||||
// Make sure we're not overwriting an existing file
|
|
||||||
flags |= os.O_EXCL
|
|
||||||
} else if opts.IfMatch.IsWildcard() {
|
|
||||||
// Make sure we _are_ overwriting an existing file
|
|
||||||
flags &= ^os.O_CREATE
|
|
||||||
} else if opts.IfMatch.IsSet() {
|
|
||||||
// Make sure we overwrite the _right_ file
|
|
||||||
etag, err := etagForFile(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
|
||||||
}
|
|
||||||
want, err := opts.IfMatch.ETag()
|
|
||||||
if err != nil {
|
|
||||||
return "", webdav.NewHTTPError(http.StatusBadRequest, err)
|
|
||||||
}
|
|
||||||
if want != etag {
|
|
||||||
err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag)
|
|
||||||
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := os.OpenFile(localPath, flags, 0666)
|
|
||||||
if os.IsExist(err) {
|
|
||||||
return "", caldav.NewPreconditionError(caldav.PreconditionNoUIDConflict)
|
|
||||||
} else if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
enc := ical.NewEncoder(f)
|
|
||||||
err = enc.Encode(calendar)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return objPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *filesystemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
|
|
||||||
debug.Printf("filesystem.DeleteCalendarObject(%s)", path)
|
|
||||||
|
|
||||||
localPath, err := b.localCalDAVPath(ctx, path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.Remove(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
303
storage/filesystem_caldav.go
Normal file
303
storage/filesystem_caldav.go
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/emersion/go-ical"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
|
"github.com/emersion/go-webdav/caldav"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/tokidoki/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *filesystemBackend) CalendarHomeSetPath(ctx context.Context) (string, error) {
|
||||||
|
upPath, err := b.CurrentUserPrincipal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(upPath, b.caldavPrefix) + "/", 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 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
|
||||||
|
// TODO implement
|
||||||
|
//return icalPropFilter(cal, propFilter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error accessing %s: %s", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, defaultResourceName, filepath.Base(filename)),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
ContentLength: info.Size(),
|
||||||
|
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 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()")
|
||||||
|
|
||||||
|
localPath, err := b.localCalDAVPath(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
localPath = filepath.Join(localPath, "calendar.json")
|
||||||
|
|
||||||
|
debug.Printf("loading calendar from %s", localPath)
|
||||||
|
|
||||||
|
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) + "/"
|
||||||
|
debug.Printf("creating default calendar (URL:path): %s:%s", urlPath, localPath)
|
||||||
|
err = createDefaultCalendar(urlPath, localPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, readErr = ioutil.ReadFile(localPath)
|
||||||
|
}
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("error opening calendar: %s", readErr.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, webdav.NewHTTPError(404, err)
|
||||||
|
}
|
||||||
|
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(),
|
||||||
|
ContentLength: info.Size(),
|
||||||
|
ETag: etag,
|
||||||
|
Data: calendar,
|
||||||
|
}
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 <UID>.ics
|
||||||
|
dirname, _ := path.Split(objPath)
|
||||||
|
objPath = path.Join(dirname, uid+".ics")
|
||||||
|
|
||||||
|
localPath, err := b.localCalDAVPath(ctx, objPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
|
||||||
|
// TODO handle IfNoneMatch == ETag
|
||||||
|
if opts.IfNoneMatch.IsWildcard() {
|
||||||
|
// Make sure we're not overwriting an existing file
|
||||||
|
flags |= os.O_EXCL
|
||||||
|
} else if opts.IfMatch.IsWildcard() {
|
||||||
|
// Make sure we _are_ overwriting an existing file
|
||||||
|
flags &= ^os.O_CREATE
|
||||||
|
} else if opts.IfMatch.IsSet() {
|
||||||
|
// Make sure we overwrite the _right_ file
|
||||||
|
etag, err := etagForFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
||||||
|
}
|
||||||
|
want, err := opts.IfMatch.ETag()
|
||||||
|
if err != nil {
|
||||||
|
return "", webdav.NewHTTPError(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
if want != etag {
|
||||||
|
err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag)
|
||||||
|
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(localPath, flags, 0666)
|
||||||
|
if os.IsExist(err) {
|
||||||
|
return "", caldav.NewPreconditionError(caldav.PreconditionNoUIDConflict)
|
||||||
|
} else if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
enc := ical.NewEncoder(f)
|
||||||
|
err = enc.Encode(calendar)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *filesystemBackend) DeleteCalendarObject(ctx context.Context, path string) error {
|
||||||
|
debug.Printf("filesystem.DeleteCalendarObject(%s)", path)
|
||||||
|
|
||||||
|
localPath, err := b.localCalDAVPath(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.Remove(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
310
storage/filesystem_carddav.go
Normal file
310
storage/filesystem_carddav.go
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/emersion/go-vcard"
|
||||||
|
"github.com/emersion/go-webdav"
|
||||||
|
"github.com/emersion/go-webdav/carddav"
|
||||||
|
|
||||||
|
"git.sr.ht/~sircmpwn/tokidoki/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *filesystemBackend) AddressbookHomeSetPath(ctx context.Context) (string, error) {
|
||||||
|
upPath, err := b.CurrentUserPrincipal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(upPath, b.carddavPrefix) + "/", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 vcardPropFilter(card vcard.Card, props []string) vcard.Card {
|
||||||
|
if card == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(props) == 0 {
|
||||||
|
return card
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(vcard.Card)
|
||||||
|
result["VERSION"] = card["VERSION"]
|
||||||
|
for _, prop := range props {
|
||||||
|
value, ok := card[prop]
|
||||||
|
if ok {
|
||||||
|
result[prop] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func vcardFromFile(path string, propFilter []string) (vcard.Card, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
dec := vcard.NewDecoder(f)
|
||||||
|
card, err := dec.Decode()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return vcardPropFilter(card, propFilter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDefaultAddressBook(path, localPath string) error {
|
||||||
|
// TODO what should the default address book look like?
|
||||||
|
defaultAB := carddav.AddressBook{
|
||||||
|
Path: path,
|
||||||
|
Name: "My contacts",
|
||||||
|
Description: "Default address book",
|
||||||
|
MaxResourceSize: 1024,
|
||||||
|
SupportedAddressData: nil,
|
||||||
|
}
|
||||||
|
blob, err := json.MarshalIndent(defaultAB, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating default address book: %s", err.Error())
|
||||||
|
}
|
||||||
|
err = os.WriteFile(localPath, blob, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error writing default address book: %s", err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *filesystemBackend) AddressBook(ctx context.Context) (*carddav.AddressBook, error) {
|
||||||
|
debug.Printf("filesystem.AddressBook()")
|
||||||
|
localPath, err := b.localCardDAVPath(ctx, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
localPath = filepath.Join(localPath, "addressbook.json")
|
||||||
|
|
||||||
|
debug.Printf("loading addressbook from %s", localPath)
|
||||||
|
|
||||||
|
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) + "/"
|
||||||
|
debug.Printf("creating default addressbook (URL:path): %s:%s", urlPath, localPath)
|
||||||
|
err = createDefaultAddressBook(urlPath, localPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, readErr = ioutil.ReadFile(localPath)
|
||||||
|
}
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, fmt.Errorf("error opening address book: %s", readErr.Error())
|
||||||
|
}
|
||||||
|
var addressBook carddav.AddressBook
|
||||||
|
err = json.Unmarshal(data, &addressBook)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading address book: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &addressBook, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(localPath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil, webdav.NewHTTPError(404, err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var propFilter []string
|
||||||
|
if req != nil && !req.AllProp {
|
||||||
|
propFilter = req.Props
|
||||||
|
}
|
||||||
|
|
||||||
|
card, err := vcardFromFile(localPath, propFilter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
etag, err := etagForFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := carddav.AddressObject{
|
||||||
|
Path: objPath,
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
ContentLength: info.Size(),
|
||||||
|
ETag: etag,
|
||||||
|
Card: card,
|
||||||
|
}
|
||||||
|
return &obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *filesystemBackend) loadAllContacts(ctx context.Context, propFilter []string) ([]carddav.AddressObject, error) {
|
||||||
|
var result []carddav.AddressObject
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
debug.Printf("filesystem.loadAllContacts() 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.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.loadAllContacts(ctx, propFilter)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return carddav.Filter(query, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Object always get saved as <UID>.vcf
|
||||||
|
dirname, _ := path.Split(objPath)
|
||||||
|
objPath = path.Join(dirname, card.Value(vcard.FieldUID)+".vcf")
|
||||||
|
|
||||||
|
localPath, err := b.localCardDAVPath(ctx, objPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
|
||||||
|
// TODO handle IfNoneMatch == ETag
|
||||||
|
if opts.IfNoneMatch.IsWildcard() {
|
||||||
|
// Make sure we're not overwriting an existing file
|
||||||
|
flags |= os.O_EXCL
|
||||||
|
} else if opts.IfMatch.IsWildcard() {
|
||||||
|
// Make sure we _are_ overwriting an existing file
|
||||||
|
flags &= ^os.O_CREATE
|
||||||
|
} else if opts.IfMatch.IsSet() {
|
||||||
|
// Make sure we overwrite the _right_ file
|
||||||
|
etag, err := etagForFile(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
||||||
|
}
|
||||||
|
want, err := opts.IfMatch.ETag()
|
||||||
|
if err != nil {
|
||||||
|
return "", webdav.NewHTTPError(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
if want != etag {
|
||||||
|
err = fmt.Errorf("If-Match does not match current ETag (%s/%s)", want, etag)
|
||||||
|
return "", webdav.NewHTTPError(http.StatusPreconditionFailed, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.OpenFile(localPath, flags, 0666)
|
||||||
|
if os.IsExist(err) {
|
||||||
|
return "", carddav.NewPreconditionError(carddav.PreconditionNoUIDConflict)
|
||||||
|
} else if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
enc := vcard.NewEncoder(f)
|
||||||
|
err = enc.Encode(card)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *filesystemBackend) DeleteAddressObject(ctx context.Context, path string) error {
|
||||||
|
debug.Printf("filesystem.DeleteAddressObject(%s)", path)
|
||||||
|
|
||||||
|
localPath, err := b.localCardDAVPath(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.Remove(localPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user