Commit 8e0cdda2 authored by Nick Craig-Wood's avatar Nick Craig-Wood Committed by Brad Fitzpatrick

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/109217Reviewed-by: 's avatarBrad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 57065200
...@@ -7,6 +7,7 @@ package webdav ...@@ -7,6 +7,7 @@ package webdav
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"io" "io"
"mime" "mime"
...@@ -379,7 +380,34 @@ func findLastModified(ctx context.Context, fs FileSystem, ls LockSystem, name st ...@@ -379,7 +380,34 @@ func findLastModified(ctx context.Context, fs FileSystem, ls LockSystem, name st
return fi.ModTime().UTC().Format(http.TimeFormat), nil 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) { 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) f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
if err != nil { if err != nil {
return "", err return "", err
...@@ -402,7 +430,31 @@ func findContentType(ctx context.Context, fs FileSystem, ls LockSystem, name str ...@@ -402,7 +430,31 @@ func findContentType(ctx context.Context, fs FileSystem, ls LockSystem, name str
return ctype, err 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) { 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 // The Apache http 2.4 web server by default concatenates the
// modification time and size of a file. We replicate the heuristic // modification time and size of a file. We replicate the heuristic
// with nanosecond granularity. // with nanosecond granularity.
......
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
"net/http" "net/http"
"os" "os"
"reflect" "reflect"
"regexp"
"sort" "sort"
"testing" "testing"
...@@ -611,3 +612,106 @@ func (f noDeadPropsFile) Readdir(count int) ([]os.FileInfo, error) { return f.f ...@@ -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) 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) Stat() (os.FileInfo, error) { return f.f.Stat() }
func (f noDeadPropsFile) Write(p []byte) (int, error) { return f.f.Write(p) } 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)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment