mirror of
https://github.com/golang/net.git
synced 2026-03-31 02:17:08 +09:00
webdav: add support for (custom) ETags and Content-Type.
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 <nigeltao@golang.org>
This commit is contained in:
committed by
Nigel Tao
parent
6460565bec
commit
84ba27dd5b
112
webdav/prop.go
112
webdav/prop.go
@@ -6,8 +6,12 @@ package webdav
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
@@ -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"},
|
||||
},
|
||||
}},
|
||||
}, {
|
||||
@@ -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"),
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
@@ -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.
|
||||
}},
|
||||
}},
|
||||
}, {
|
||||
@@ -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{{
|
||||
@@ -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"},
|
||||
|
||||
@@ -8,6 +8,7 @@ package webdav // import "golang.org/x/net/webdav"
|
||||
// TODO: ETag, properties.
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user