webdav: add support for ETag to client & server

This commit is contained in:
Simon Ser 2020-01-22 11:51:05 +01:00
parent 3268102d5a
commit f04c1c9421
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
5 changed files with 43 additions and 12 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"time" "time"
"github.com/emersion/go-webdav/internal" "github.com/emersion/go-webdav/internal"
@ -45,6 +46,14 @@ func (c *Client) FindCurrentUserPrincipal() (string, error) {
return prop.Href.Path, nil 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) { func fileInfoFromResponse(resp *internal.Response) (*FileInfo, error) {
path, err := resp.Path() path, err := resp.Path()
if err != nil { if err != nil {
@ -75,22 +84,24 @@ func fileInfoFromResponse(resp *internal.Response) (*FileInfo, error) {
return nil, err 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.Size = getLen.Length
fi.ModTime = time.Time(getMod.LastModified) fi.ModTime = time.Time(getMod.LastModified)
fi.MIMEType = getType.Type fi.MIMEType = getType.Type
fi.ETag = etag
} }
return fi, nil 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) { func (c *Client) Stat(name string) (*FileInfo, error) {
resp, err := c.ic.PropfindFlat(name, fileInfoPropfind) resp, err := c.ic.PropfindFlat(name, fileInfoPropfind)
if err != nil { if err != nil {

View File

@ -1,6 +1,7 @@
package webdav package webdav
import ( import (
"fmt"
"io" "io"
"mime" "mime"
"net/http" "net/http"
@ -49,6 +50,12 @@ func fileInfoFromOS(p string, fi os.FileInfo) *FileInfo {
IsDir: fi.IsDir(), IsDir: fi.IsDir(),
// TODO: fallback to http.DetectContentType? // TODO: fallback to http.DetectContentType?
MIMEType: mime.TypeByExtension(path.Ext(p)), 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()),
} }
} }

View File

@ -339,6 +339,12 @@ type GetLastModified struct {
LastModified Time `xml:",chardata"` 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 // https://tools.ietf.org/html/rfc4918#section-14.5
type Error struct { type Error struct {
XMLName xml.Name `xml:"DAV: error"` XMLName xml.Name `xml:"DAV: error"`

View File

@ -2,6 +2,7 @@ package webdav
import ( import (
"encoding/xml" "encoding/xml"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
@ -87,13 +88,16 @@ func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
} }
defer f.Close() defer f.Close()
w.Header().Set("Content-Length", strconv.FormatInt(fi.Size, 10))
if fi.MIMEType != "" { if fi.MIMEType != "" {
w.Header().Set("Content-Type", fi.MIMEType) w.Header().Set("Content-Type", fi.MIMEType)
} }
if !fi.ModTime.IsZero() { if !fi.ModTime.IsZero() {
w.Header().Set("Last-Modified", fi.ModTime.UTC().Format(http.TimeFormat)) 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 rs, ok := f.(io.ReadSeeker); ok {
// If it's an io.Seeker, use http.ServeContent which supports ranges // 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) return internal.NewPropfindResponse(fi.Path, propfind, props)

View File

@ -7,12 +7,11 @@ import (
"time" "time"
) )
// TODO: add ETag to FileInfo
type FileInfo struct { type FileInfo struct {
Path string Path string
Size int64 Size int64
ModTime time.Time ModTime time.Time
IsDir bool IsDir bool
MIMEType string MIMEType string
ETag string
} }