From 8e0cdda24ed423affc8f35c241e5e9b16180338e Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 25 Apr 2018 09:40:29 +0100 Subject: [PATCH] webdav: allow the user to override the ETag and ContentType properties Before this commit it was not possible to override the the ContentType and ETag properties. Since these properties aren't directly read from the os.FileInfo objects returned by the FileSystem it seems reasonable that the user might have a different policy for computing them. For instance the underlying FileSystem may already know the ContentType or want to use an MD5 Hash for the ETag. This commit introduces two new optional interfaces ETager and ContentTyper which, when defined on the os.FileInfo objects returned by the FileSystem methods, allows the user of this library to override the ETag and ContentType generation. Fixes golang/go#22577 Change-Id: Ib42e126db3fcc0a93463e61db85fde59be85cca5 Reviewed-on: https://go-review.googlesource.com/109217 Reviewed-by: Brad Fitzpatrick Run-TryBot: Brad Fitzpatrick TryBot-Result: Gobot Gobot --- webdav/prop.go | 52 ++++++++++++++++++++++ webdav/prop_test.go | 104 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) diff --git a/webdav/prop.go b/webdav/prop.go index ba595d7ac..4d7be3492 100644 --- a/webdav/prop.go +++ b/webdav/prop.go @@ -7,6 +7,7 @@ package webdav import ( "bytes" "encoding/xml" + "errors" "fmt" "io" "mime" @@ -379,7 +380,34 @@ func findLastModified(ctx context.Context, fs FileSystem, ls LockSystem, name st return fi.ModTime().UTC().Format(http.TimeFormat), nil } +// ErrNotImplemented should be returned by optional interfaces if they +// want the original implementation to be used. +var ErrNotImplemented = errors.New("not implemented") + +// ContentTyper is an optional interface for the os.FileInfo +// objects returned by the FileSystem. +// +// If this interface is defined then it will be used to read the +// content type from the object. +// +// If this interface is not defined the file will be opened and the +// content type will be guessed from the initial contents of the file. +type ContentTyper interface { + // ContentType returns the content type for the file. + // + // If this returns error ErrNotImplemented then the error will + // be ignored and the base implementation will be used + // instead. + ContentType(ctx context.Context) (string, error) +} + func findContentType(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) { + if do, ok := fi.(ContentTyper); ok { + ctype, err := do.ContentType(ctx) + if err != ErrNotImplemented { + return ctype, err + } + } f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0) if err != nil { return "", err @@ -402,7 +430,31 @@ func findContentType(ctx context.Context, fs FileSystem, ls LockSystem, name str return ctype, err } +// ETager is an optional interface for the os.FileInfo objects +// returned by the FileSystem. +// +// If this interface is defined then it will be used to read the ETag +// for the object. +// +// If this interface is not defined an ETag will be computed using the +// ModTime() and the Size() methods of the os.FileInfo object. +type ETager interface { + // ETag returns an ETag for the file. This should be of the + // form "value" or W/"value" + // + // If this returns error ErrNotImplemented then the error will + // be ignored and the base implementation will be used + // instead. + ETag(ctx context.Context) (string, error) +} + func findETag(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) { + if do, ok := fi.(ETager); ok { + etag, err := do.ETag(ctx) + if err != ErrNotImplemented { + return etag, err + } + } // The Apache http 2.4 web server by default concatenates the // modification time and size of a file. We replicate the heuristic // with nanosecond granularity. diff --git a/webdav/prop_test.go b/webdav/prop_test.go index 914f9504d..bc42b9132 100644 --- a/webdav/prop_test.go +++ b/webdav/prop_test.go @@ -10,6 +10,7 @@ import ( "net/http" "os" "reflect" + "regexp" "sort" "testing" @@ -611,3 +612,106 @@ func (f noDeadPropsFile) Readdir(count int) ([]os.FileInfo, error) { return f.f func (f noDeadPropsFile) Seek(off int64, whence int) (int64, error) { return f.f.Seek(off, whence) } func (f noDeadPropsFile) Stat() (os.FileInfo, error) { return f.f.Stat() } func (f noDeadPropsFile) Write(p []byte) (int, error) { return f.f.Write(p) } + +type overrideContentType struct { + os.FileInfo + contentType string + err error +} + +func (o *overrideContentType) ContentType(ctx context.Context) (string, error) { + return o.contentType, o.err +} + +func TestFindContentTypeOverride(t *testing.T) { + fs, err := buildTestFS([]string{"touch /file"}) + if err != nil { + t.Fatalf("cannot create test filesystem: %v", err) + } + ctx := context.Background() + fi, err := fs.Stat(ctx, "/file") + if err != nil { + t.Fatalf("cannot Stat /file: %v", err) + } + + // Check non overridden case + originalContentType, err := findContentType(ctx, fs, nil, "/file", fi) + if err != nil { + t.Fatalf("findContentType /file failed: %v", err) + } + if originalContentType != "text/plain; charset=utf-8" { + t.Fatalf("ContentType wrong want %q got %q", "text/plain; charset=utf-8", originalContentType) + } + + // Now try overriding the ContentType + o := &overrideContentType{fi, "OverriddenContentType", nil} + ContentType, err := findContentType(ctx, fs, nil, "/file", o) + if err != nil { + t.Fatalf("findContentType /file failed: %v", err) + } + if ContentType != o.contentType { + t.Fatalf("ContentType wrong want %q got %q", o.contentType, ContentType) + } + + // Now return ErrNotImplemented and check we get the original content type + o = &overrideContentType{fi, "OverriddenContentType", ErrNotImplemented} + ContentType, err = findContentType(ctx, fs, nil, "/file", o) + if err != nil { + t.Fatalf("findContentType /file failed: %v", err) + } + if ContentType != originalContentType { + t.Fatalf("ContentType wrong want %q got %q", originalContentType, ContentType) + } +} + +type overrideETag struct { + os.FileInfo + eTag string + err error +} + +func (o *overrideETag) ETag(ctx context.Context) (string, error) { + return o.eTag, o.err +} + +func TestFindETagOverride(t *testing.T) { + fs, err := buildTestFS([]string{"touch /file"}) + if err != nil { + t.Fatalf("cannot create test filesystem: %v", err) + } + ctx := context.Background() + fi, err := fs.Stat(ctx, "/file") + if err != nil { + t.Fatalf("cannot Stat /file: %v", err) + } + + // Check non overridden case + originalETag, err := findETag(ctx, fs, nil, "/file", fi) + if err != nil { + t.Fatalf("findETag /file failed: %v", err) + } + matchETag := regexp.MustCompile(`^"-?[0-9a-f]{6,}"$`) + if !matchETag.MatchString(originalETag) { + t.Fatalf("ETag wrong, wanted something matching %v got %q", matchETag, originalETag) + } + + // Now try overriding the ETag + o := &overrideETag{fi, `"OverriddenETag"`, nil} + ETag, err := findETag(ctx, fs, nil, "/file", o) + if err != nil { + t.Fatalf("findETag /file failed: %v", err) + } + if ETag != o.eTag { + t.Fatalf("ETag wrong want %q got %q", o.eTag, ETag) + } + + // Now return ErrNotImplemented and check we get the original Etag + o = &overrideETag{fi, `"OverriddenETag"`, ErrNotImplemented} + ETag, err = findETag(ctx, fs, nil, "/file", o) + if err != nil { + t.Fatalf("findETag /file failed: %v", err) + } + if ETag != originalETag { + t.Fatalf("ETag wrong want %q got %q", originalETag, ETag) + } +}