Skip to content

Commit

Permalink
webdav: allow the user to override the ETag and ContentType properties
Browse files Browse the repository at this point in the history
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 <[email protected]>
Run-TryBot: Brad Fitzpatrick <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
  • Loading branch information
ncw authored and bradfitz committed May 21, 2018
1 parent 5706520 commit 8e0cdda
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 0 deletions.
52 changes: 52 additions & 0 deletions webdav/prop.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package webdav
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
"mime"
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
104 changes: 104 additions & 0 deletions webdav/prop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"reflect"
"regexp"
"sort"
"testing"

Expand Down Expand Up @@ -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)
}
}

0 comments on commit 8e0cdda

Please sign in to comment.