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 <nigeltao@golang.org>
diff --git a/webdav/file.go b/webdav/file.go
index aa4ea6a..2f62dc4 100644
--- a/webdav/file.go
+++ b/webdav/file.go
@@ -672,3 +672,54 @@
}
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 0d06b3f..5d327db 100644
--- a/webdav/file_test.go
+++ b/webdav/file_test.go
@@ -823,3 +823,226 @@
}
}
}
+
+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 e75e01e..8aea999 100644
--- a/webdav/litmus_test_server.go
+++ b/webdav/litmus_test_server.go
@@ -32,9 +32,12 @@
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 0000000..4ab4919
--- /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 `<collection xmlns="DAV:"/>`, 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 0000000..d5f8e78
--- /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(`<collection xmlns="DAV:"/>`),
+ }, {
+ 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(`<collection xmlns="DAV:"/>`),
+ }},
+ }},
+ }, {
+ 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 45484b6..f4acc65 100644
--- a/webdav/webdav.go
+++ b/webdav/webdav.go
@@ -9,6 +9,7 @@
import (
"errors"
+ "fmt"
"io"
"net/http"
"net/url"
@@ -16,15 +17,12 @@
"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 @@
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 @@
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) 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 @@
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 @@
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 b6534be..0bfceea 100644
--- a/webdav/xml.go
+++ b/webdav/xml.go
@@ -114,6 +114,7 @@
}
}
+// 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.