carddav: PUT support

This commit is contained in:
emersion 2017-09-13 19:02:12 +02:00
parent 64f8396654
commit 2a7d999100
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
3 changed files with 118 additions and 36 deletions

View File

@ -21,12 +21,14 @@ type AddressBookInfo struct {
type AddressObject interface { type AddressObject interface {
ID() string ID() string
Card() (vcard.Card, error)
Stat() (os.FileInfo, error) // can return nil, nil Stat() (os.FileInfo, error) // can return nil, nil
Card() (vcard.Card, error)
SetCard(vcard.Card) error
} }
type AddressBook interface { type AddressBook interface {
Info() (*AddressBookInfo, error) Info() (*AddressBookInfo, error)
GetAddressObject(id string) (AddressObject, error) GetAddressObject(id string) (AddressObject, error)
ListAddressObjects() ([]AddressObject, error) ListAddressObjects() ([]AddressObject, error)
CreateAddressObject(vcard.Card) (AddressObject, error)
} }

View File

@ -38,6 +38,10 @@ var (
addressBookHomeSet = xml.Name{Space: nsCardDAV, Local: "addressbook-home-set"} addressBookHomeSet = xml.Name{Space: nsCardDAV, Local: "addressbook-home-set"}
) )
func addressObjectName(ao AddressObject) string {
return ao.ID() + ".vcf"
}
type fileInfo struct { type fileInfo struct {
name string name string
size int64 size int64
@ -45,6 +49,13 @@ type fileInfo struct {
modTime time.Time modTime time.Time
} }
func addressObjectFileInfo(ao AddressObject) *fileInfo {
return &fileInfo{
name: addressObjectName(ao),
mode: os.ModePerm,
}
}
func (fi *fileInfo) Name() string { func (fi *fileInfo) Name() string {
return fi.name return fi.name
} }
@ -70,18 +81,34 @@ func (fi *fileInfo) Sys() interface{} {
} }
type file struct { type file struct {
*bytes.Reader r *bytes.Reader
w *bytes.Buffer
fs *fileSystem fs *fileSystem
name string
ao AddressObject ao AddressObject
} }
func (f *file) Close() error { func (f *file) Close() error {
if f.w != nil {
defer func() {
f.w = nil
}()
card, err := vcard.NewDecoder(f.w).Decode()
if err != nil {
return err
}
if err := f.ao.SetCard(card); err != nil {
return err
}
}
f.r = nil
return nil return nil
} }
func (f *file) Read(b []byte) (int, error) { func (f *file) Read(b []byte) (int, error) {
if f.Reader == nil { if f.r == nil {
card, err := f.ao.Card() card, err := f.ao.Card()
if err != nil { if err != nil {
return 0, err return 0, err
@ -92,24 +119,27 @@ func (f *file) Read(b []byte) (int, error) {
return 0, err return 0, err
} }
f.Reader = bytes.NewReader(b.Bytes()) f.r = bytes.NewReader(b.Bytes())
} }
return f.Reader.Read(b) return f.r.Read(b)
} }
func (f *file) Write(b []byte) (int, error) { func (f *file) Write(b []byte) (int, error) {
return 0, errUnsupported if f.w == nil {
f.w = &bytes.Buffer{}
}
return f.w.Write(b)
} }
func (f *file) Seek(offset int64, whence int) (int64, error) { func (f *file) Seek(offset int64, whence int) (int64, error) {
if f.Reader == nil { if f.r == nil {
if _, err := f.Read(nil); err != nil { if _, err := f.Read(nil); err != nil {
return 0, err return 0, err
} }
} }
return f.Reader.Seek(offset, whence) return f.r.Seek(offset, whence)
} }
func (f *file) Readdir(count int) ([]os.FileInfo, error) { func (f *file) Readdir(count int) ([]os.FileInfo, error) {
@ -121,15 +151,71 @@ func (f *file) Stat() (os.FileInfo, error) {
if info != nil || err != nil { if info != nil || err != nil {
return info, err return info, err
} }
return addressObjectFileInfo(f.ao), nil
return &fileInfo{
name: f.name,
mode: os.ModePerm,
}, nil
} }
// TODO: getcontenttype for file // TODO: getcontenttype for file
type newFile struct {
buf bytes.Buffer
fs *fileSystem
ao AddressObject
}
func (f *newFile) Close() error {
if f.ao == nil {
defer f.buf.Reset()
card, err := vcard.NewDecoder(&f.buf).Decode()
if err != nil {
return err
}
ao, err := f.fs.ab.CreateAddressObject(card)
if err != nil {
return err
}
f.ao = ao
}
return nil
}
func (f *newFile) Read(b []byte) (int, error) {
return 0, errUnsupported
}
func (f *newFile) Write(b []byte) (int, error) {
// TODO: limit amount of data in f.buf
if f.ao != nil {
return 0, errUnsupported
}
return f.buf.Write(b)
}
func (f *newFile) Seek(offset int64, whence int) (int64, error) {
return 0, errUnsupported
}
func (f *newFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, errUnsupported
}
func (f *newFile) Stat() (os.FileInfo, error) {
// Only available after a successful call to Close
if f.ao == nil {
return nil, errUnsupported
}
info, err := f.ao.Stat()
if info != nil || err != nil {
return info, err
}
return addressObjectFileInfo(f.ao), nil
}
type dir struct { type dir struct {
fs *fileSystem fs *fileSystem
name string name string
@ -163,18 +249,7 @@ func (d *dir) Readdir(count int) ([]os.FileInfo, error) {
d.files = make([]os.FileInfo, len(aos)) d.files = make([]os.FileInfo, len(aos))
for i, ao := range aos { for i, ao := range aos {
f := &file{ d.files[i] = addressObjectFileInfo(ao)
fs: d.fs,
name: ao.ID() + ".vcf",
ao: ao,
}
info, err := f.Stat()
if err != nil {
return nil, err
}
d.files[i] = info
} }
} }
@ -262,13 +337,14 @@ func (fs *fileSystem) OpenFile(ctx context.Context, name string, flag int, perm
id := fs.addressObjectID(name) id := fs.addressObjectID(name)
ao, err := fs.ab.GetAddressObject(id) ao, err := fs.ab.GetAddressObject(id)
if err != nil { if err == ErrNotFound && flag & os.O_CREATE != 0 {
return &newFile{fs: fs}, nil
} else if err != nil {
return nil, err return nil, err
} }
return &file{ return &file{
fs: fs, fs: fs,
name: name,
ao: ao, ao: ao,
}, nil }, nil
} }
@ -300,8 +376,5 @@ func (fs *fileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error
return info, err return info, err
} }
return &fileInfo{ return addressObjectFileInfo(ao), nil
name: name,
mode: os.ModePerm,
}, nil
} }

View File

@ -282,24 +282,31 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
if err != nil { if err != nil {
return http.StatusNotFound, err return http.StatusNotFound, err
} }
_, copyErr := io.Copy(f, r.Body) _, copyErr := io.Copy(f, r.Body)
fi, statErr := f.Stat()
closeErr := f.Close() closeErr := f.Close()
fi, statErr := f.Stat()
// TODO(rost): Returning 405 Method Not Allowed might not be appropriate. // TODO(rost): Returning 405 Method Not Allowed might not be appropriate.
if copyErr != nil { if copyErr != nil {
return http.StatusMethodNotAllowed, copyErr return http.StatusMethodNotAllowed, copyErr
} }
if statErr != nil {
return http.StatusMethodNotAllowed, statErr
}
if closeErr != nil { if closeErr != nil {
return http.StatusMethodNotAllowed, closeErr return http.StatusMethodNotAllowed, closeErr
} }
if statErr != nil {
return http.StatusMethodNotAllowed, statErr
}
if path.Base(reqPath) != fi.Name() {
w.Header().Set("Location", path.Join(path.Dir(reqPath), fi.Name()))
}
etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi) etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi)
if err != nil { if err != nil {
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} }
w.Header().Set("ETag", etag) w.Header().Set("ETag", etag)
return http.StatusCreated, nil return http.StatusCreated, nil
} }