From f04c1c94211379ea97bdc30606e28cf5dc698dec Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 22 Jan 2020 11:51:05 +0100 Subject: [PATCH] webdav: add support for ETag to client & server --- client.go | 27 +++++++++++++++++++-------- fs_local.go | 7 +++++++ internal/elements.go | 6 ++++++ server.go | 12 ++++++++++-- webdav.go | 3 +-- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/client.go b/client.go index f49e564..e37858d 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net/http" + "strconv" "time" "github.com/emersion/go-webdav/internal" @@ -45,6 +46,14 @@ func (c *Client) FindCurrentUserPrincipal() (string, error) { return prop.Href.Path, nil } +var fileInfoPropfind = internal.NewPropNamePropfind( + internal.ResourceTypeName, + internal.GetContentLengthName, + internal.GetLastModifiedName, + internal.GetContentTypeName, + internal.GetETagName, +) + func fileInfoFromResponse(resp *internal.Response) (*FileInfo, error) { path, err := resp.Path() if err != nil { @@ -75,22 +84,24 @@ func fileInfoFromResponse(resp *internal.Response) (*FileInfo, error) { return nil, err } + var getETag internal.GetETag + if err := resp.DecodeProp(&getETag); err != nil && !internal.IsNotFound(err) { + return nil, err + } + etag, err := strconv.Unquote(getETag.ETag) + if err != nil { + return nil, fmt.Errorf("webdav: failed to unquote ETag: %v", err) + } + fi.Size = getLen.Length fi.ModTime = time.Time(getMod.LastModified) fi.MIMEType = getType.Type + fi.ETag = etag } return fi, nil } -// TODO: getetag -var fileInfoPropfind = internal.NewPropNamePropfind( - internal.ResourceTypeName, - internal.GetContentLengthName, - internal.GetLastModifiedName, - internal.GetContentTypeName, -) - func (c *Client) Stat(name string) (*FileInfo, error) { resp, err := c.ic.PropfindFlat(name, fileInfoPropfind) if err != nil { diff --git a/fs_local.go b/fs_local.go index 73c5dc8..0cc692b 100644 --- a/fs_local.go +++ b/fs_local.go @@ -1,6 +1,7 @@ package webdav import ( + "fmt" "io" "mime" "net/http" @@ -49,6 +50,12 @@ func fileInfoFromOS(p string, fi os.FileInfo) *FileInfo { IsDir: fi.IsDir(), // TODO: fallback to http.DetectContentType? MIMEType: mime.TypeByExtension(path.Ext(p)), + // RFC 2616 section 13.3.3 describes strong ETags. Ideally these would + // be checksums or sequence numbers, however these are expensive to + // compute. The modification time with nanosecond granularity is good + // enough, as it's very unlikely for the same file to be modified twice + // during a single nanosecond. + ETag: fmt.Sprintf("%x%x", fi.ModTime().UnixNano(), fi.Size()), } } diff --git a/internal/elements.go b/internal/elements.go index 88e23d7..accb6de 100644 --- a/internal/elements.go +++ b/internal/elements.go @@ -339,6 +339,12 @@ type GetLastModified struct { LastModified Time `xml:",chardata"` } +// https://tools.ietf.org/html/rfc4918#section-15.6 +type GetETag struct { + XMLName xml.Name `xml:"DAV: getetag"` + ETag string `xml:",chardata"` +} + // https://tools.ietf.org/html/rfc4918#section-14.5 type Error struct { XMLName xml.Name `xml:"DAV: error"` diff --git a/server.go b/server.go index 693dda2..10d80ac 100644 --- a/server.go +++ b/server.go @@ -2,6 +2,7 @@ package webdav import ( "encoding/xml" + "fmt" "io" "net/http" "os" @@ -87,13 +88,16 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error { } defer f.Close() + w.Header().Set("Content-Length", strconv.FormatInt(fi.Size, 10)) if fi.MIMEType != "" { w.Header().Set("Content-Type", fi.MIMEType) } if !fi.ModTime.IsZero() { w.Header().Set("Last-Modified", fi.ModTime.UTC().Format(http.TimeFormat)) } - w.Header().Set("Content-Length", strconv.FormatInt(fi.Size, 10)) + if fi.ETag != "" { + w.Header().Set("ETag", fmt.Sprintf("%q", fi.ETag)) + } if rs, ok := f.(io.ReadSeeker); ok { // If it's an io.Seeker, use http.ServeContent which supports ranges @@ -171,7 +175,11 @@ func (b *backend) propfindFile(propfind *internal.Propfind, fi *FileInfo) (*inte } } - // TODO: getetag + if fi.ETag != "" { + props[internal.GetETagName] = func(*internal.RawXMLValue) (interface{}, error) { + return &internal.GetETag{ETag: fmt.Sprintf("%q", fi.ETag)}, nil + } + } } return internal.NewPropfindResponse(fi.Path, propfind, props) diff --git a/webdav.go b/webdav.go index 0166990..4dd369c 100644 --- a/webdav.go +++ b/webdav.go @@ -7,12 +7,11 @@ import ( "time" ) -// TODO: add ETag to FileInfo - type FileInfo struct { Path string Size int64 ModTime time.Time IsDir bool MIMEType string + ETag string }