From 7dbad50ab5b31073856416cdcfeb2796d682f844 Mon Sep 17 00:00:00 2001 From: Robert Stepanek Date: Wed, 28 Jan 2015 21:50:26 +0100 Subject: [PATCH] webdav: define property system and implement PROPFIND. This change adds support for PROPFIND requests to net/webdav. It contains a proposed PropSystem interface and a preliminary implementation of an in-memory property system. As discussed with nigeltao, this is the first of approximately 4-5 CLs to get property support in the net/webdav package. Current coverage of litmus 'props' test suite: 16 tests were skipped, 14 tests run. 10 passed, 4 failed. 71.4% Change-Id: I0bc5f375422137e911a2f6fb0e99c43a5a52d5ac Reviewed-on: https://go-review.googlesource.com/3417 Reviewed-by: Nigel Tao --- webdav/file.go | 51 ++++++ webdav/file_test.go | 223 +++++++++++++++++++++++++ webdav/litmus_test_server.go | 7 +- webdav/prop.go | 195 ++++++++++++++++++++++ webdav/prop_test.go | 304 +++++++++++++++++++++++++++++++++++ webdav/webdav.go | 95 ++++++++++- webdav/xml.go | 1 + 7 files changed, 869 insertions(+), 7 deletions(-) create mode 100644 webdav/prop.go create mode 100644 webdav/prop_test.go diff --git a/webdav/file.go b/webdav/file.go index aa4ea6a3..2f62dc44 100644 --- a/webdav/file.go +++ b/webdav/file.go @@ -672,3 +672,54 @@ func copyFiles(fs FileSystem, src, dst string, overwrite bool, depth int, recurs } return http.StatusNoContent, nil } + +// walkFS traverses filesystem fs starting at path up to depth levels. +// +// Allowed values for depth are 0, 1 or infiniteDepth. For each visited node, +// walkFS calls walkFn. If a visited file system node is a directory and +// walkFn returns filepath.SkipDir, walkFS will skip traversal of this node. +func walkFS(fs FileSystem, depth int, path string, info os.FileInfo, walkFn filepath.WalkFunc) error { + // This implementation is based on Walk's code in the standard path/filepath package. + err := walkFn(path, info, nil) + if err != nil { + if info.IsDir() && err == filepath.SkipDir { + return nil + } + return err + } + if !info.IsDir() || depth == 0 { + return nil + } + if depth == 1 { + depth = 0 + } + + // Read directory names. + f, err := fs.OpenFile(path, os.O_RDONLY, 0) + if err != nil { + return walkFn(path, info, err) + } + fileInfos, err := f.Readdir(0) + f.Close() + if err != nil { + return walkFn(path, info, err) + } + + for _, fileInfo := range fileInfos { + filename := filepath.Join(path, fileInfo.Name()) + fileInfo, err := fs.Stat(filename) + if err != nil { + if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { + return err + } + } else { + err = walkFS(fs, depth, filename, fileInfo, walkFn) + if err != nil { + if !fileInfo.IsDir() || err != filepath.SkipDir { + return err + } + } + } + } + return nil +} diff --git a/webdav/file_test.go b/webdav/file_test.go index 0d06b3fa..5d327dbd 100644 --- a/webdav/file_test.go +++ b/webdav/file_test.go @@ -823,3 +823,226 @@ func BenchmarkMemFileWrite(b *testing.B) { } } } + +func TestWalkFS(t *testing.T) { + testCases := []struct { + desc string + buildfs []string + startAt string + depth int + walkFn filepath.WalkFunc + want []string + }{{ + "just root", + []string{}, + "/", + infiniteDepth, + nil, + []string{ + "/", + }, + }, { + "infinite walk from root", + []string{ + "mkdir /a", + "mkdir /a/b", + "touch /a/b/c", + "mkdir /a/d", + "mkdir /e", + "touch /f", + }, + "/", + infiniteDepth, + nil, + []string{ + "/", + "/a", + "/a/b", + "/a/b/c", + "/a/d", + "/e", + "/f", + }, + }, { + "infinite walk from subdir", + []string{ + "mkdir /a", + "mkdir /a/b", + "touch /a/b/c", + "mkdir /a/d", + "mkdir /e", + "touch /f", + }, + "/a", + infiniteDepth, + nil, + []string{ + "/a", + "/a/b", + "/a/b/c", + "/a/d", + }, + }, { + "depth 1 walk from root", + []string{ + "mkdir /a", + "mkdir /a/b", + "touch /a/b/c", + "mkdir /a/d", + "mkdir /e", + "touch /f", + }, + "/", + 1, + nil, + []string{ + "/", + "/a", + "/e", + "/f", + }, + }, { + "depth 1 walk from subdir", + []string{ + "mkdir /a", + "mkdir /a/b", + "touch /a/b/c", + "mkdir /a/b/g", + "mkdir /a/b/g/h", + "touch /a/b/g/i", + "touch /a/b/g/h/j", + }, + "/a/b", + 1, + nil, + []string{ + "/a/b", + "/a/b/c", + "/a/b/g", + }, + }, { + "depth 0 walk from subdir", + []string{ + "mkdir /a", + "mkdir /a/b", + "touch /a/b/c", + "mkdir /a/b/g", + "mkdir /a/b/g/h", + "touch /a/b/g/i", + "touch /a/b/g/h/j", + }, + "/a/b", + 0, + nil, + []string{ + "/a/b", + }, + }, { + "infinite walk from file", + []string{ + "mkdir /a", + "touch /a/b", + "touch /a/c", + }, + "/a/b", + 0, + nil, + []string{ + "/a/b", + }, + }, { + "infinite walk with skipped subdir", + []string{ + "mkdir /a", + "mkdir /a/b", + "touch /a/b/c", + "mkdir /a/b/g", + "mkdir /a/b/g/h", + "touch /a/b/g/i", + "touch /a/b/g/h/j", + "touch /a/b/z", + }, + "/", + infiniteDepth, + func(path string, info os.FileInfo, err error) error { + if path == "/a/b/g" { + return filepath.SkipDir + } + return nil + }, + []string{ + "/", + "/a", + "/a/b", + "/a/b/c", + "/a/b/z", + }, + }} + for _, tc := range testCases { + fs, err := buildTestFS(tc.buildfs) + if err != nil { + t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err) + } + var got []string + traceFn := func(path string, info os.FileInfo, err error) error { + if tc.walkFn != nil { + err = tc.walkFn(path, info, err) + if err != nil { + return err + } + } + got = append(got, path) + return nil + } + fi, err := fs.Stat(tc.startAt) + if err != nil { + t.Fatalf("%s: cannot stat: %v", tc.desc, err) + } + err = walkFS(fs, tc.depth, tc.startAt, fi, traceFn) + if err != nil { + t.Errorf("%s:\ngot error %v, want nil", tc.desc, err) + continue + } + sort.Strings(got) + sort.Strings(tc.want) + if !reflect.DeepEqual(got, tc.want) { + t.Errorf("%s:\ngot %q\nwant %q", tc.desc, got, tc.want) + continue + } + } +} + +func buildTestFS(buildfs []string) (FileSystem, error) { + // TODO: Could this be merged with the build logic in TestFS? + + fs := NewMemFS() + for _, b := range buildfs { + op := strings.Split(b, " ") + switch op[0] { + case "mkdir": + err := fs.Mkdir(op[1], os.ModeDir|0777) + if err != nil { + return nil, err + } + case "touch": + f, err := fs.OpenFile(op[1], os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return nil, err + } + f.Close() + case "write": + f, err := fs.OpenFile(op[1], os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return nil, err + } + _, err = f.Write([]byte(op[2])) + f.Close() + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unknown file operation %q", op[0]) + } + } + return fs, nil +} diff --git a/webdav/litmus_test_server.go b/webdav/litmus_test_server.go index e75e01ec..8aea9995 100644 --- a/webdav/litmus_test_server.go +++ b/webdav/litmus_test_server.go @@ -32,9 +32,12 @@ var port = flag.Int("port", 9999, "server port") func main() { flag.Parse() log.SetFlags(0) + fs := webdav.NewMemFS() + ls := webdav.NewMemLS() http.Handle("/", &webdav.Handler{ - FileSystem: webdav.NewMemFS(), - LockSystem: webdav.NewMemLS(), + FileSystem: fs, + LockSystem: ls, + PropSystem: webdav.NewMemPS(fs, ls), Logger: func(r *http.Request, err error) { litmus := r.Header.Get("X-Litmus") if len(litmus) > 19 { diff --git a/webdav/prop.go b/webdav/prop.go new file mode 100644 index 00000000..4ab49190 --- /dev/null +++ b/webdav/prop.go @@ -0,0 +1,195 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package webdav + +import ( + "encoding/xml" + "net/http" + "os" + "strconv" +) + +// PropSystem manages the properties of named resources. It allows finding +// and setting properties as defined in RFC 4918. +// +// The elements in a resource name are separated by slash ('/', U+002F) +// characters, regardless of host operating system convention. +type PropSystem interface { + // Find returns the status of properties named propnames for resource name. + // + // Each Propstat must have a unique status and each property name must + // only be part of one Propstat element. + Find(name string, propnames []xml.Name) ([]Propstat, error) + + // TODO(rost) PROPPATCH. + // TODO(nigeltao) merge Find and Allprop? + + // Allprop returns the properties defined for resource name and the + // properties named in include. The returned Propstats are handled + // as in Find. + // + // Note that RFC 4918 defines 'allprop' to return the DAV: properties + // defined within the RFC plus dead properties. Other live properties + // should only be returned if they are named in 'include'. + // + // See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND + Allprop(name string, include []xml.Name) ([]Propstat, error) + + // Propnames returns the property names defined for resource name. + Propnames(name string) ([]xml.Name, error) + + // TODO(rost) COPY/MOVE/DELETE. +} + +// Propstat describes a XML propstat element as defined in RFC 4918. +// See http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat +type Propstat struct { + // Props contains the properties for which Status applies. + Props []Property + + // Status defines the HTTP status code of the properties in Prop. + // Allowed values include, but are not limited to the WebDAV status + // code extensions for HTTP/1.1. + // http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11 + Status int + + // XMLError contains the XML representation of the optional error element. + // XML content within this field must not rely on any predefined + // namespace declarations or prefixes. If empty, the XML error element + // is omitted. + XMLError string + + // ResponseDescription contains the contents of the optional + // responsedescription field. If empty, the XML element is omitted. + ResponseDescription string +} + +// memPS implements an in-memory PropSystem. It supports all of the mandatory +// live properties of RFC 4918. +type memPS struct { + // TODO(rost) memPS will get writeable in the next CLs. + fs FileSystem + ls LockSystem +} + +// NewMemPS returns a new in-memory PropSystem implementation. +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, + + // 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) +} + +func (ps *memPS) Find(name string, propnames []xml.Name) ([]Propstat, error) { + fi, err := ps.fs.Stat(name) + if err != nil { + return nil, err + } + + pm := make(map[int]Propstat) + for _, pn := range propnames { + p := Property{XMLName: pn} + s := http.StatusNotFound + if fn := davProps[pn]; fn != nil { + xmlvalue, err := fn(ps, name, fi) + if err != nil { + return nil, err + } + s = http.StatusOK + p.InnerXML = []byte(xmlvalue) + } + pstat := pm[s] + pstat.Props = append(pstat.Props, p) + pm[s] = pstat + } + + pstats := make([]Propstat, 0, len(pm)) + for s, pstat := range pm { + pstat.Status = s + pstats = append(pstats, pstat) + } + return pstats, nil +} + +func (ps *memPS) Propnames(name string) ([]xml.Name, error) { + fi, err := ps.fs.Stat(name) + if err != nil { + 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 { + propnames = append(propnames, pn) + } + } + return propnames, nil +} + +func (ps *memPS) Allprop(name string, include []xml.Name) ([]Propstat, error) { + propnames, err := ps.Propnames(name) + if err != nil { + return nil, err + } + // Add names from include if they are not already covered in propnames. + nameset := make(map[xml.Name]bool) + for _, pn := range propnames { + nameset[pn] = true + } + for _, pn := range include { + if !nameset[pn] { + propnames = append(propnames, pn) + } + } + return ps.Find(name, propnames) +} + +func (ps *memPS) findResourceType(name string, fi os.FileInfo) (string, error) { + if fi.IsDir() { + return ``, nil + } + return "", nil +} + +func (ps *memPS) findDisplayName(name string, fi os.FileInfo) (string, error) { + if slashClean(name) == "/" { + // Hide the real name of a possibly prefixed root directory. + return "", nil + } + return fi.Name(), nil +} + +func (ps *memPS) findContentLength(name string, fi os.FileInfo) (string, error) { + return strconv.FormatInt(fi.Size(), 10), nil +} + +func (ps *memPS) findLastModified(name string, fi os.FileInfo) (string, error) { + return fi.ModTime().Format(http.TimeFormat), nil +} diff --git a/webdav/prop_test.go b/webdav/prop_test.go new file mode 100644 index 00000000..d5f8e787 --- /dev/null +++ b/webdav/prop_test.go @@ -0,0 +1,304 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package webdav + +import ( + "encoding/xml" + "fmt" + "net/http" + "reflect" + "sort" + "testing" +) + +func TestMemPS(t *testing.T) { + // calcProps calculates the getlastmodified and getetag DAV: property + // values in pstats for resource name in file-system fs. + calcProps := func(name string, fs FileSystem, pstats []Propstat) error { + fi, err := fs.Stat(name) + if err != nil { + return err + } + for _, pst := range pstats { + for i, p := range pst.Props { + switch p.XMLName { + case xml.Name{Space: "DAV:", Local: "getlastmodified"}: + 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") + } + } + } + return nil + } + + type propOp struct { + op string + name string + propnames []xml.Name + wantNames []xml.Name + wantPropstats []Propstat + } + + testCases := []struct { + desc string + buildfs []string + propOp []propOp + }{{ + "propname", + []string{"mkdir /dir", "touch /file"}, + []propOp{{ + op: "propname", + name: "/dir", + wantNames: []xml.Name{ + xml.Name{Space: "DAV:", Local: "resourcetype"}, + xml.Name{Space: "DAV:", Local: "displayname"}, + xml.Name{Space: "DAV:", Local: "getcontentlength"}, + xml.Name{Space: "DAV:", Local: "getlastmodified"}, + }, + }, { + op: "propname", + name: "/file", + wantNames: []xml.Name{ + xml.Name{Space: "DAV:", Local: "resourcetype"}, + xml.Name{Space: "DAV:", Local: "displayname"}, + xml.Name{Space: "DAV:", Local: "getcontentlength"}, + xml.Name{Space: "DAV:", Local: "getlastmodified"}, + }, + }}, + }, { + "allprop dir and file", + []string{"mkdir /dir", "write /file foobarbaz"}, + []propOp{{ + op: "allprop", + name: "/dir", + wantPropstats: []Propstat{{ + Status: http.StatusOK, + Props: []Property{{ + XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"}, + InnerXML: []byte(``), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "displayname"}, + InnerXML: []byte("dir"), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "getcontentlength"}, + InnerXML: []byte("0"), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "getlastmodified"}, + InnerXML: nil, // Calculated during test. + }}, + }}, + }, { + op: "allprop", + name: "/file", + wantPropstats: []Propstat{{ + Status: http.StatusOK, + Props: []Property{{ + XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"}, + InnerXML: []byte(""), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "displayname"}, + InnerXML: []byte("file"), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "getcontentlength"}, + InnerXML: []byte("9"), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "getlastmodified"}, + InnerXML: nil, // Calculated during test. + }}, + }}, + }, { + op: "allprop", + name: "/file", + propnames: []xml.Name{ + {"DAV:", "resourcetype"}, + {"foo", "bar"}, + }, + wantPropstats: []Propstat{{ + Status: http.StatusOK, + Props: []Property{{ + XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"}, + InnerXML: []byte(""), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "displayname"}, + InnerXML: []byte("file"), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "getcontentlength"}, + InnerXML: []byte("9"), + }, { + XMLName: xml.Name{Space: "DAV:", Local: "getlastmodified"}, + InnerXML: nil, // Calculated during test. + }}}, { + Status: http.StatusNotFound, + Props: []Property{{ + XMLName: xml.Name{Space: "foo", Local: "bar"}, + }}}, + }, + }}, + }, { + "propfind DAV:resourcetype", + []string{"mkdir /dir", "touch /file"}, + []propOp{{ + op: "propfind", + name: "/dir", + propnames: []xml.Name{{"DAV:", "resourcetype"}}, + wantPropstats: []Propstat{{ + Status: http.StatusOK, + Props: []Property{{ + XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"}, + InnerXML: []byte(``), + }}, + }}, + }, { + op: "propfind", + name: "/file", + propnames: []xml.Name{{"DAV:", "resourcetype"}}, + wantPropstats: []Propstat{{ + Status: http.StatusOK, + Props: []Property{{ + XMLName: xml.Name{Space: "DAV:", Local: "resourcetype"}, + InnerXML: []byte(""), + }}, + }}, + }}, + }, { + "propfind unsupported DAV properties", + []string{"mkdir /dir"}, + []propOp{{ + op: "propfind", + name: "/dir", + propnames: []xml.Name{{"DAV:", "getcontentlanguage"}}, + wantPropstats: []Propstat{{ + Status: http.StatusNotFound, + Props: []Property{{ + XMLName: xml.Name{Space: "DAV:", Local: "getcontentlanguage"}, + }}, + }}, + }, { + op: "propfind", + name: "/dir", + propnames: []xml.Name{{"DAV:", "creationdate"}}, + wantPropstats: []Propstat{{ + Status: http.StatusNotFound, + Props: []Property{{ + XMLName: xml.Name{Space: "DAV:", Local: "creationdate"}, + }}, + }}, + }}, + }, { + "bad: propfind unknown property", + []string{"mkdir /dir"}, + []propOp{{ + op: "propfind", + name: "/dir", + propnames: []xml.Name{{"foo:", "bar"}}, + wantPropstats: []Propstat{{ + Status: http.StatusNotFound, + Props: []Property{{ + XMLName: xml.Name{Space: "foo:", Local: "bar"}, + }}, + }}, + }}, + }} + + for _, tc := range testCases { + fs, err := buildTestFS(tc.buildfs) + if err != nil { + t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err) + } + ls := NewMemLS() + ps := NewMemPS(fs, ls) + for _, op := range tc.propOp { + desc := fmt.Sprintf("%s: %s %s", tc.desc, op.op, op.name) + if err = calcProps(op.name, fs, op.wantPropstats); err != nil { + t.Fatalf("%s: calcProps: %v", desc, err) + } + + // Call property system. + var propstats []Propstat + switch op.op { + case "propname": + names, err := ps.Propnames(op.name) + if err != nil { + t.Errorf("%s: got error %v, want nil", desc, err) + continue + } + sort.Sort(byXMLName(names)) + sort.Sort(byXMLName(op.wantNames)) + if !reflect.DeepEqual(names, op.wantNames) { + t.Errorf("%s: names\ngot %q\nwant %q", desc, names, op.wantNames) + } + continue + case "allprop": + propstats, err = ps.Allprop(op.name, op.propnames) + case "propfind": + propstats, err = ps.Find(op.name, op.propnames) + default: + t.Fatalf("%s: %s not implemented", desc, op.op) + } + if err != nil { + t.Errorf("%s: got error %v, want nil", desc, err) + continue + } + // Compare return values from allprop or propfind. + for _, pst := range propstats { + sort.Sort(byPropname(pst.Props)) + } + for _, pst := range op.wantPropstats { + sort.Sort(byPropname(pst.Props)) + } + sort.Sort(byStatus(propstats)) + sort.Sort(byStatus(op.wantPropstats)) + if !reflect.DeepEqual(propstats, op.wantPropstats) { + t.Errorf("%s: propstat\ngot %q\nwant %q", desc, propstats, op.wantPropstats) + } + } + } +} + +func cmpXMLName(a, b xml.Name) bool { + if a.Space != b.Space { + return a.Space < b.Space + } + return a.Local < b.Local +} + +type byXMLName []xml.Name + +func (b byXMLName) Len() int { + return len(b) +} +func (b byXMLName) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} +func (b byXMLName) Less(i, j int) bool { + return cmpXMLName(b[i], b[j]) +} + +type byPropname []Property + +func (b byPropname) Len() int { + return len(b) +} +func (b byPropname) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} +func (b byPropname) Less(i, j int) bool { + return cmpXMLName(b[i].XMLName, b[j].XMLName) +} + +type byStatus []Propstat + +func (b byStatus) Len() int { + return len(b) +} +func (b byStatus) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} +func (b byStatus) Less(i, j int) bool { + return b[i].Status < b[j].Status +} diff --git a/webdav/webdav.go b/webdav/webdav.go index 45484b61..f4acc65c 100644 --- a/webdav/webdav.go +++ b/webdav/webdav.go @@ -9,6 +9,7 @@ package webdav // import "golang.org/x/net/webdav" import ( "errors" + "fmt" "io" "net/http" "net/url" @@ -16,15 +17,12 @@ import ( "time" ) -// TODO: define the PropSystem interface. -type PropSystem interface{} - type Handler struct { // FileSystem is the virtual file system. FileSystem FileSystem // LockSystem is the lock management system. LockSystem LockSystem - // PropSystem is an optional property management system. If non-nil, TODO. + // PropSystem is the property management system. PropSystem PropSystem // Logger is an optional error logger. If non-nil, it will be called // for all HTTP requests. @@ -37,8 +35,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { status, err = http.StatusInternalServerError, errNoFileSystem } else if h.LockSystem == nil { status, err = http.StatusInternalServerError, errNoLockSystem + } else if h.PropSystem == nil { + status, err = http.StatusInternalServerError, errNoPropSystem } else { - // TODO: PROPFIND, PROPPATCH methods. switch r.Method { case "OPTIONS": status, err = h.handleOptions(w, r) @@ -56,6 +55,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { status, err = h.handleLock(w, r) case "UNLOCK": status, err = h.handleUnlock(w, r) + case "PROPFIND": + status, err = h.handlePropfind(w, r) } } @@ -429,6 +430,88 @@ func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status i } } +func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) { + fi, err := h.FileSystem.Stat(r.URL.Path) + if err != nil { + if err == os.ErrNotExist { + return http.StatusNotFound, err + } + return http.StatusMethodNotAllowed, err + } + depth := infiniteDepth + if hdr := r.Header.Get("Depth"); hdr != "" { + depth = parseDepth(hdr) + if depth == invalidDepth { + return http.StatusBadRequest, errInvalidDepth + } + } + pf, status, err := readPropfind(r.Body) + if err != nil { + return status, err + } + + mw := multistatusWriter{w: w} + + walkFn := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + var pstats []Propstat + if pf.Propname != nil { + propnames, err := h.PropSystem.Propnames(path) + if err != nil { + return err + } + pstat := Propstat{Status: http.StatusOK} + for _, xmlname := range propnames { + pstat.Props = append(pstat.Props, Property{XMLName: xmlname}) + } + pstats = append(pstats, pstat) + } else if pf.Allprop != nil { + pstats, err = h.PropSystem.Allprop(path, pf.Prop) + } else { + pstats, err = h.PropSystem.Find(path, pf.Prop) + } + if err != nil { + return err + } + return mw.write(makePropstatResponse(path, pstats)) + } + + err = walkFS(h.FileSystem, depth, r.URL.Path, fi, walkFn) + if mw.enc == nil { + if err == nil { + err = errEmptyMultistatus + } + // Not a single response has been written. + return http.StatusInternalServerError, err + } + if err != nil { + return 0, err + } + return 0, mw.close() +} + +func makePropstatResponse(href string, pstats []Propstat) *response { + resp := response{ + Href: []string{href}, + Propstat: make([]propstat, 0, len(pstats)), + } + for _, p := range pstats { + var xmlErr *xmlError + if p.XMLError != "" { + xmlErr = &xmlError{InnerXML: []byte(p.XMLError)} + } + resp.Propstat = append(resp.Propstat, propstat{ + Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)), + Prop: p.Props, + ResponseDescription: p.ResponseDescription, + Error: xmlErr, + }) + } + return &resp +} + const ( infiniteDepth = -1 invalidDepth = -2 @@ -483,6 +566,7 @@ func StatusText(code int) string { var ( errDestinationEqualsSource = errors.New("webdav: destination equals source") errDirectoryNotEmpty = errors.New("webdav: directory not empty") + errEmptyMultistatus = errors.New("webdav: empty multistatus response") errInvalidDepth = errors.New("webdav: invalid depth") errInvalidDestination = errors.New("webdav: invalid destination") errInvalidIfHeader = errors.New("webdav: invalid If header") @@ -493,6 +577,7 @@ var ( errInvalidTimeout = errors.New("webdav: invalid timeout") errNoFileSystem = errors.New("webdav: no file system") errNoLockSystem = errors.New("webdav: no lock system") + errNoPropSystem = errors.New("webdav: no property system") errNotADirectory = errors.New("webdav: not a directory") errRecursionTooDeep = errors.New("webdav: recursion too deep") errUnsupportedLockInfo = errors.New("webdav: unsupported lock info") diff --git a/webdav/xml.go b/webdav/xml.go index b6534be3..0bfceea3 100644 --- a/webdav/xml.go +++ b/webdav/xml.go @@ -114,6 +114,7 @@ func next(d *xml.Decoder) (xml.Token, error) { } } +// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind) type propnames []xml.Name // UnmarshalXML appends the property names enclosed within start to pn.