Skip to content

Commit

Permalink
webdav: add support for (custom) ETags and Content-Type.
Browse files Browse the repository at this point in the history
This change adds ETag headers to GET/HEAD/POST and PUT responses. It does
not update the existing If-header request handling. The ETag header and
DAV property value can be overriden by implementing a custom property
system. A similar scheme is provided for Content-Type.

This CL makes net/webdav pass three more litmus ‘locks’ test cases
successfully.

Before: Summary for `locks': of 30 tests run: 27 passed, 3 failed. 90.0%
After:  Summary for `locks': of 34 tests run: 30 passed, 4 failed. 88.2%

Change-Id: I5102b9ac18d20844deaaa630b62cc3611b3f0740
Reviewed-on: https://go-review.googlesource.com/4903
Reviewed-by: Nigel Tao <[email protected]>
  • Loading branch information
rsto authored and nigeltao committed Apr 9, 2015
1 parent 6460565 commit 84ba27d
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 33 deletions.
112 changes: 84 additions & 28 deletions webdav/prop.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ package webdav

import (
"encoding/xml"
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strconv"
)

Expand Down Expand Up @@ -79,25 +83,54 @@ func NewMemPS(fs FileSystem, ls LockSystem) PropSystem {
return &memPS{fs: fs, ls: ls}
}

type propfindFn func(*memPS, string, os.FileInfo) (string, error)

// davProps contains all supported DAV: properties and their optional
// propfind functions. A nil value indicates a hidden, protected property.
var davProps = map[xml.Name]propfindFn{
xml.Name{Space: "DAV:", Local: "resourcetype"}: (*memPS).findResourceType,
xml.Name{Space: "DAV:", Local: "displayname"}: (*memPS).findDisplayName,
xml.Name{Space: "DAV:", Local: "getcontentlength"}: (*memPS).findContentLength,
xml.Name{Space: "DAV:", Local: "getlastmodified"}: (*memPS).findLastModified,
xml.Name{Space: "DAV:", Local: "creationdate"}: nil,
xml.Name{Space: "DAV:", Local: "getcontentlanguage"}: nil,

// TODO(rost) ETag and ContentType will be defined the next CL.
// xml.Name{Space: "DAV:", Local: "getcontenttype"}: (*memPS).findContentType,
// xml.Name{Space: "DAV:", Local: "getetag"}: (*memPS).findEtag,
// propfind functions. A nil findFn indicates a hidden, protected property.
// The dir field indicates if the property applies to directories in addition
// to regular files.
var davProps = map[xml.Name]struct {
findFn func(*memPS, string, os.FileInfo) (string, error)
dir bool
}{
xml.Name{Space: "DAV:", Local: "resourcetype"}: {
findFn: (*memPS).findResourceType,
dir: true,
},
xml.Name{Space: "DAV:", Local: "displayname"}: {
findFn: (*memPS).findDisplayName,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getcontentlength"}: {
findFn: (*memPS).findContentLength,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getlastmodified"}: {
findFn: (*memPS).findLastModified,
dir: true,
},
xml.Name{Space: "DAV:", Local: "creationdate"}: {
findFn: nil,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getcontentlanguage"}: {
findFn: nil,
dir: true,
},
xml.Name{Space: "DAV:", Local: "getcontenttype"}: {
findFn: (*memPS).findContentType,
dir: true,
},
// memPS implements ETag as the concatenated hex values of a file's
// modification time and size. This is not a reliable synchronization
// mechanism for directories, so we do not advertise getetag for
// DAV collections.
xml.Name{Space: "DAV:", Local: "getetag"}: {
findFn: (*memPS).findETag,
dir: false,
},

// TODO(nigeltao) Lock properties will be defined later.
// xml.Name{Space: "DAV:", Local: "lockdiscovery"}: nil, // TODO(rost)
// xml.Name{Space: "DAV:", Local: "supportedlock"}: nil, // TODO(rost)
// xml.Name{Space: "DAV:", Local: "lockdiscovery"}
// xml.Name{Space: "DAV:", Local: "supportedlock"}
}

func (ps *memPS) Find(name string, propnames []xml.Name) ([]Propstat, error) {
Expand All @@ -110,8 +143,8 @@ func (ps *memPS) Find(name string, propnames []xml.Name) ([]Propstat, error) {
for _, pn := range propnames {
p := Property{XMLName: pn}
s := http.StatusNotFound
if fn := davProps[pn]; fn != nil {
xmlvalue, err := fn(ps, name, fi)
if prop := davProps[pn]; prop.findFn != nil && (prop.dir || !fi.IsDir()) {
xmlvalue, err := prop.findFn(ps, name, fi)
if err != nil {
return nil, err
}
Expand All @@ -137,16 +170,8 @@ func (ps *memPS) Propnames(name string) ([]xml.Name, error) {
return nil, err
}
propnames := make([]xml.Name, 0, len(davProps))
for pn, findFn := range davProps {
// TODO(rost) ETag and ContentType will be defined the next CL.
// memPS implements ETag as the concatenated hex values of a file's
// modification time and size. This is not a reliable synchronization
// mechanism for directories, so we do not advertise getetag for
// DAV collections. Other property systems may do how they please.
if fi.IsDir() && pn.Space == "DAV:" && pn.Local == "getetag" {
continue
}
if findFn != nil {
for pn, prop := range davProps {
if prop.findFn != nil && (prop.dir || !fi.IsDir()) {
propnames = append(propnames, pn)
}
}
Expand Down Expand Up @@ -193,3 +218,34 @@ func (ps *memPS) findContentLength(name string, fi os.FileInfo) (string, error)
func (ps *memPS) findLastModified(name string, fi os.FileInfo) (string, error) {
return fi.ModTime().Format(http.TimeFormat), nil
}

func (ps *memPS) findContentType(name string, fi os.FileInfo) (string, error) {
f, err := ps.fs.OpenFile(name, os.O_RDONLY, 0)
if err != nil {
return "", err
}
defer f.Close()
// This implementation is based on serveContent's code in the standard net/http package.
ctype := mime.TypeByExtension(filepath.Ext(name))
if ctype == "" {
// Read a chunk to decide between utf-8 text and binary.
var buf [512]byte
n, _ := io.ReadFull(f, buf[:])
ctype = http.DetectContentType(buf[:n])
// Rewind file.
_, err = f.Seek(0, os.SEEK_SET)
}
return ctype, err
}

func (ps *memPS) findETag(name string, fi os.FileInfo) (string, error) {
return detectETag(fi), nil
}

// detectETag determines the ETag for the file described by fi.
func detectETag(fi os.FileInfo) string {
// 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.
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
}
50 changes: 48 additions & 2 deletions webdav/prop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ func TestMemPS(t *testing.T) {
p.InnerXML = []byte(fi.ModTime().Format(http.TimeFormat))
pst.Props[i] = p
case xml.Name{Space: "DAV:", Local: "getetag"}:
// TODO(rost) ETag will be defined in the next CL.
panic("Not implemented")
if fi.IsDir() {
continue
}
p.InnerXML = []byte(detectETag(fi))
pst.Props[i] = p
}
}
}
Expand Down Expand Up @@ -59,6 +62,7 @@ func TestMemPS(t *testing.T) {
xml.Name{Space: "DAV:", Local: "displayname"},
xml.Name{Space: "DAV:", Local: "getcontentlength"},
xml.Name{Space: "DAV:", Local: "getlastmodified"},
xml.Name{Space: "DAV:", Local: "getcontenttype"},
},
}, {
op: "propname",
Expand All @@ -68,6 +72,8 @@ func TestMemPS(t *testing.T) {
xml.Name{Space: "DAV:", Local: "displayname"},
xml.Name{Space: "DAV:", Local: "getcontentlength"},
xml.Name{Space: "DAV:", Local: "getlastmodified"},
xml.Name{Space: "DAV:", Local: "getcontenttype"},
xml.Name{Space: "DAV:", Local: "getetag"},
},
}},
}, {
Expand All @@ -90,6 +96,9 @@ func TestMemPS(t *testing.T) {
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getlastmodified"},
InnerXML: nil, // Calculated during test.
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getcontenttype"},
InnerXML: []byte("text/plain; charset=utf-8"),
}},
}},
}, {
Expand All @@ -109,6 +118,12 @@ func TestMemPS(t *testing.T) {
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getlastmodified"},
InnerXML: nil, // Calculated during test.
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getcontenttype"},
InnerXML: []byte("text/plain; charset=utf-8"),
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
InnerXML: nil, // Calculated during test.
}},
}},
}, {
Expand All @@ -132,6 +147,12 @@ func TestMemPS(t *testing.T) {
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getlastmodified"},
InnerXML: nil, // Calculated during test.
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getcontenttype"},
InnerXML: []byte("text/plain; charset=utf-8"),
}, {
XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
InnerXML: nil, // Calculated during test.
}}}, {
Status: http.StatusNotFound,
Props: []Property{{
Expand Down Expand Up @@ -189,6 +210,31 @@ func TestMemPS(t *testing.T) {
}},
}},
}},
}, {
"propfind getetag for files but not for directories",
[]string{"mkdir /dir", "touch /file"},
[]propOp{{
op: "propfind",
name: "/dir",
propnames: []xml.Name{{"DAV:", "getetag"}},
wantPropstats: []Propstat{{
Status: http.StatusNotFound,
Props: []Property{{
XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
}},
}},
}, {
op: "propfind",
name: "/file",
propnames: []xml.Name{{"DAV:", "getetag"}},
wantPropstats: []Propstat{{
Status: http.StatusOK,
Props: []Property{{
XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
InnerXML: nil, // Calculated during test.
}},
}},
}},
}, {
"bad: propfind unknown property",
[]string{"mkdir /dir"},
Expand Down
46 changes: 43 additions & 3 deletions webdav/webdav.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package webdav // import "golang.org/x/net/webdav"
// TODO: ETag, properties.

import (
"encoding/xml"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -184,6 +185,14 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
if err != nil {
return http.StatusNotFound, err
}
pstats, err := h.PropSystem.Find(r.URL.Path, []xml.Name{
{Space: "DAV:", Local: "getetag"},
{Space: "DAV:", Local: "getcontenttype"},
})
if err != nil {
return http.StatusInternalServerError, err
}
writeDAVHeaders(w, pstats)
http.ServeContent(w, r, r.URL.Path, fi.ModTime(), f)
return 0, nil
}
Expand Down Expand Up @@ -223,10 +232,21 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
if err != nil {
return http.StatusNotFound, err
}
defer f.Close()
if _, err := io.Copy(f, r.Body); err != nil {
return http.StatusMethodNotAllowed, err
_, copyErr := io.Copy(f, r.Body)
closeErr := f.Close()
if copyErr != nil {
return http.StatusMethodNotAllowed, copyErr
}
if closeErr != nil {
return http.StatusMethodNotAllowed, closeErr
}
pstats, err := h.PropSystem.Find(r.URL.Path, []xml.Name{
{Space: "DAV:", Local: "getetag"},
})
if err != nil {
return http.StatusInternalServerError, err
}
writeDAVHeaders(w, pstats)
return http.StatusCreated, nil
}

Expand Down Expand Up @@ -492,6 +512,26 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
return 0, mw.close()
}

// davHeaderNames maps the names of DAV properties to their corresponding
// HTTP response headers.
var davHeaderNames = map[xml.Name]string{
xml.Name{Space: "DAV:", Local: "getetag"}: "ETag",
xml.Name{Space: "DAV:", Local: "getcontenttype"}: "Content-Type",
}

func writeDAVHeaders(w http.ResponseWriter, pstats []Propstat) {
for _, pst := range pstats {
if pst.Status == http.StatusOK {
for _, p := range pst.Props {
if n, ok := davHeaderNames[p.XMLName]; ok {
w.Header().Set(n, string(p.InnerXML))
}
}
break
}
}
}

func makePropstatResponse(href string, pstats []Propstat) *response {
resp := response{
Href: []string{href},
Expand Down

0 comments on commit 84ba27d

Please sign in to comment.