فهرست منبع

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>
Robert Stepanek 11 سال پیش
والد
کامیت
84ba27dd5b
3فایلهای تغییر یافته به همراه175 افزوده شده و 33 حذف شده
  1. 84 28
      webdav/prop.go
  2. 48 2
      webdav/prop_test.go
  3. 43 3
      webdav/webdav.go

+ 84 - 28
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())
+}

+ 48 - 2
webdav/prop_test.go

@@ -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"},

+ 43 - 3
webdav/webdav.go

@@ -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},