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>
This commit is contained in:
Robert Stepanek
2015-01-28 21:50:26 +01:00
committed by Nigel Tao
parent 2ad74281c4
commit 7dbad50ab5
7 changed files with 869 additions and 7 deletions

View File

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

View File

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

View File

@@ -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 {

195
webdav/prop.go Normal file
View File

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

304
webdav/prop_test.go Normal file
View File

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

View File

@@ -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")

View File

@@ -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.