From 163fa1656d988bd7758730f822002ad6461c5f34 Mon Sep 17 00:00:00 2001 From: emersion Date: Sun, 3 Sep 2017 20:11:36 +0200 Subject: [PATCH] carddav: first commit --- carddav/backend.go | 23 ++++ carddav/carddav.go | 316 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 carddav/backend.go create mode 100644 carddav/carddav.go diff --git a/carddav/backend.go b/carddav/backend.go new file mode 100644 index 0000000..516088a --- /dev/null +++ b/carddav/backend.go @@ -0,0 +1,23 @@ +package carddav + +// TODO: add context support + +import ( + "errors" + + "github.com/emersion/go-vcard" +) + +var ( + ErrNotFound = errors.New("carddav: not found") +) + +type AddressObject interface { + ID() string + Card() (vcard.Card, error) +} + +type AddressBook interface { + GetAddressObject(id string) (AddressObject, error) + ListAddressObjects() ([]AddressObject, error) +} diff --git a/carddav/carddav.go b/carddav/carddav.go new file mode 100644 index 0000000..3cc4870 --- /dev/null +++ b/carddav/carddav.go @@ -0,0 +1,316 @@ +package carddav + +import ( + "bytes" + "encoding/xml" + "errors" + "net/http" + "os" + "strings" + "time" + + "github.com/emersion/go-vcard" + "github.com/emersion/go-webdav" + "golang.org/x/net/context" + + "log" +) + +var ( + errNotYetImplemented = errors.New("not yet implemented") + errUnsupported = errors.New("unsupported") +) + +const nsDAV = "DAV:" + +var ( + resourcetype = xml.Name{Space: nsDAV, Local: "resourcetype"} + displayname = xml.Name{Space: nsDAV, Local: "displayname"} + getcontenttype = xml.Name{Space: nsDAV, Local: "getcontenttype"} +) + +const nsCardDAV = "urn:ietf:params:xml:ns:carddav" + +var ( + addressBookDescription = xml.Name{Space: nsCardDAV, Local: "addressbook-description"} + addressBookSupportedAddressData = xml.Name{Space: nsCardDAV, Local: "supported-address-data"} + addressBookMaxResourceSize = xml.Name{Space: nsCardDAV, Local: "max-resource-size"} + addressBookHomeSet = xml.Name{Space: nsCardDAV, Local: "addressbook-home-set"} +) + +type fileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi *fileInfo) Name() string { + return fi.name +} + +func (fi *fileInfo) Size() int64 { + return fi.size +} + +func (fi *fileInfo) Mode() os.FileMode { + return fi.mode +} + +func (fi *fileInfo) ModTime() time.Time { + return fi.modTime +} + +func (fi *fileInfo) IsDir() bool { + return fi.mode.IsDir() +} + +func (fi *fileInfo) Sys() interface{} { + return nil +} + +type file struct { + *bytes.Reader + fs *fileSystem + name string + ao AddressObject +} + +func (f *file) Close() error { + return nil +} + +func (f *file) Read(b []byte) (int, error) { + if f.Reader == nil { + card, err := f.ao.Card() + if err != nil { + return 0, err + } + + var b bytes.Buffer + if err := vcard.NewEncoder(&b).Encode(card); err != nil { + return 0, err + } + + f.Reader = bytes.NewReader(b.Bytes()) + } + + return f.Reader.Read(b) +} + +func (f *file) Write(b []byte) (int, error) { + return 0, errUnsupported +} + +func (f *file) Seek(offset int64, whence int) (int64, error) { + if f.Reader == nil { + if _, err := f.Read(nil); err != nil { + return 0, err + } + } + + return f.Reader.Seek(offset, whence) +} + +func (f *file) Readdir(count int) ([]os.FileInfo, error) { + return nil, errUnsupported +} + +func (f *file) Stat() (os.FileInfo, error) { + return &fileInfo{ + name: f.name, + mode: os.ModePerm, + }, nil +} + +// TODO: getcontenttype for file + +type dir struct { + fs *fileSystem + name string + files []os.FileInfo + + n int +} + +func (d *dir) Close() error { + return nil +} + +func (d *dir) Read(b []byte) (int, error) { + return 0, errUnsupported +} + +func (d *dir) Write(b []byte) (int, error) { + return 0, errUnsupported +} + +func (d *dir) Seek(offset int64, whence int) (int64, error) { + return 0, errUnsupported +} + +func (d *dir) Readdir(count int) ([]os.FileInfo, error) { + if d.files == nil { + aos, err := d.fs.ab.ListAddressObjects() + if err != nil { + return nil, err + } + + d.files = make([]os.FileInfo, len(aos)) + for i, ao := range aos { + d.files[i] = &fileInfo{ + name: ao.ID() + ".vcf", + mode: os.ModePerm, + } + } + } + + if count == 0 { + count = len(d.files) - d.n + } + if d.n >= len(d.files) { + return nil, nil + } + + from := d.n + d.n += count + if d.n > len(d.files) { + d.n = len(d.files) + } + + return d.files[from:d.n], nil +} + +func (d *dir) Stat() (os.FileInfo, error) { + return &fileInfo{ + name: d.name, + mode: os.ModeDir | os.ModePerm, + }, nil +} + +func (d *dir) DeadProps() (map[xml.Name]webdav.Property, error) { + return map[xml.Name]webdav.Property{ + resourcetype: webdav.Property{ + XMLName: resourcetype, + InnerXML: []byte(``), + }, + displayname: webdav.Property{ + XMLName: displayname, + InnerXML: []byte("Test"), + }, + addressBookDescription: webdav.Property{ + XMLName: addressBookDescription, + InnerXML: []byte("C'est juste un test mdr."), + }, + addressBookSupportedAddressData: webdav.Property{ + XMLName: addressBookSupportedAddressData, + InnerXML: []byte(``), + }, + addressBookMaxResourceSize: webdav.Property{ + XMLName: addressBookMaxResourceSize, + InnerXML: []byte("102400"), + }, + addressBookHomeSet: webdav.Property{ + XMLName: addressBookHomeSet, + InnerXML: []byte(`/`), + }, + }, nil +} + +func (d *dir) Patch([]webdav.Proppatch) ([]webdav.Propstat, error) { + return nil, errUnsupported +} + +type fileSystem struct { + ab AddressBook +} + +func (fs *fileSystem) Mkdir(ctx context.Context, name string, perm os.FileMode) error { + return errNotYetImplemented +} + +func (fs *fileSystem) addressObjectID(name string) string { + return strings.TrimRight(strings.TrimLeft(name, "/"), ".vcf") +} + +func (fs *fileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { + if name == "/" { + return &dir{ + fs: fs, + name: name, + }, nil + } + + id := fs.addressObjectID(name) + ao, err := fs.ab.GetAddressObject(id) + if err != nil { + return nil, err + } + + return &file{ + fs: fs, + name: name, + ao: ao, + }, nil +} + +func (fs *fileSystem) RemoveAll(ctx context.Context, name string) error { + return errNotYetImplemented +} + +func (fs *fileSystem) Rename(ctx context.Context, oldName, newName string) error { + return errNotYetImplemented +} + +func (fs *fileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error) { + if name == "/" { + return &fileInfo{ + name: name, + mode: os.ModeDir | os.ModePerm, + }, nil + } + + id := fs.addressObjectID(name) + _, err := fs.ab.GetAddressObject(id) + if err != nil { + return nil, err + } + + return &fileInfo{ + name: name, + mode: os.ModePerm, + }, nil +} + +type Handler struct { + webdav *webdav.Handler +} + +func NewHandler(ab AddressBook) *Handler { + return &Handler{&webdav.Handler{ + FileSystem: &fileSystem{ab}, + Logger: func(req *http.Request, err error) { + if err != nil { + log.Println("ERROR", req, err) + } + }, + }} +} + +type responseWriter struct { + http.ResponseWriter +} + +func (w responseWriter) Write(b []byte) (int, error) { + os.Stdout.Write(b) + return w.ResponseWriter.Write(b) +} + +func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + log.Printf("%+v\n", req) + if req.Method == http.MethodOptions { + resp.Header().Add("DAV", "addressbook") + } + //h.webdav.ServeHTTP(resp, req) + h.webdav.ServeHTTP(responseWriter{resp}, req) +}