mirror of
https://github.com/golang/net.git
synced 2026-03-31 10:27:08 +09:00
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:
committed by
Nigel Tao
parent
2ad74281c4
commit
7dbad50ab5
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
195
webdav/prop.go
Normal 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
304
webdav/prop_test.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user