diff --git a/webdav/prop.go b/webdav/prop.go index 4ab49190..41c1eaba 100644 --- a/webdav/prop.go +++ b/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()) +} diff --git a/webdav/prop_test.go b/webdav/prop_test.go index d5f8e787..fd01aee4 100644 --- a/webdav/prop_test.go +++ b/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"}, diff --git a/webdav/webdav.go b/webdav/webdav.go index f4acc65c..53d8704a 100644 --- a/webdav/webdav.go +++ b/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},