From 50c025bd092971f668819c2875cfc5596b4b5876 Mon Sep 17 00:00:00 2001 From: zicla Date: Sat, 20 Apr 2019 22:21:49 +0800 Subject: [PATCH] Finish the first version of propfind. --- build/doc/webdav/PROPFIND.md | 267 +++++++++++++++++++++++++++++++++++ rest/dav/file.go | 12 +- rest/dav/file_test.go | 2 +- rest/dav/lock.go | 6 +- rest/dav/prop.go | 6 +- rest/dav/webdav_test.go | 4 +- rest/dav_controller.go | 4 +- rest/dav_model.go | 99 +++++++++++++ rest/dav_service.go | 58 ++++++-- rest/matter_model.go | 8 ++ 10 files changed, 438 insertions(+), 28 deletions(-) create mode 100644 build/doc/webdav/PROPFIND.md diff --git a/build/doc/webdav/PROPFIND.md b/build/doc/webdav/PROPFIND.md new file mode 100644 index 0000000..1c88869 --- /dev/null +++ b/build/doc/webdav/PROPFIND.md @@ -0,0 +1,267 @@ +#### PROPFIND 列出目录情况 +request method +``` +PROPFIND +``` + +request header +``` +Authorization=Basic YWRtaW46YWRtaW4= +Content-Type=text/xml +Accept-Encoding=gzip +Depth=infinity +``` + +request body +``` + + + + + + + + + +``` + +response body +``` + + + + /api/dav/ + + + dav + Tue, 16 Apr 2019 17:50:59 GMT + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + + /api/dav/api/ + + + api + Tue, 16 Apr 2019 17:51:03 GMT + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + + /api/dav/api/dav/ + + + dav + Tue, 16 Apr 2019 17:51:38 GMT + + + + + + + + + + + + + + + HTTP/1.1 200 OK + + + + /api/dav/api/dav/body.txt + + + body.txt + Tue, 16 Apr 2019 17:51:38 GMT + text/plain; charset=utf-8 + "159605ccc1d0f3c410" + + + + + + + + + + + + 16 + + HTTP/1.1 200 OK + + + + /api/dav/api/dav/cat.txt + + + + 24 + Tue, 16 Apr 2019 17:51:19 GMT + text/plain; charset=utf-8 + "159605c862b0d64c18" + + + + + + + + + + + cat.txt + + HTTP/1.1 200 OK + + + + /api/dav/cat/ + + + Sat, 13 Apr 2019 16:55:54 GMT + + + + + + + + + + + cat + + + + + HTTP/1.1 200 OK + + + + /api/dav/cat/dog/ + + + Sat, 13 Apr 2019 16:55:58 GMT + + + + + + + + + + + dog + + + + + HTTP/1.1 200 OK + + + + /api/dav/cat/dog/pig/ + + + + + + Sat, 13 Apr 2019 16:56:08 GMT + + + + + + + + + + + pig + + HTTP/1.1 200 OK + + + + /api/dav/cat/dog/pig/hi.txt + + + Sat, 13 Apr 2019 16:56:08 GMT + text/plain; charset=utf-8 + "15951707dc1116d87" + + + + + + + + + + + hi.txt + + 7 + + HTTP/1.1 200 OK + + + + /api/dav/morning.txt + + + + 13 + morning.txt + Sat, 13 Apr 2019 16:52:08 GMT + text/plain; charset=utf-8 + "159516cfe790beecd" + + + + + + + + + + + + HTTP/1.1 200 OK + + + +``` \ No newline at end of file diff --git a/rest/dav/file.go b/rest/dav/file.go index 6e9c982..a67c7d8 100644 --- a/rest/dav/file.go +++ b/rest/dav/file.go @@ -17,9 +17,9 @@ import ( "time" ) -// slashClean is equivalent to but slightly more efficient than +// SlashClean is equivalent to but slightly more efficient than // path.Clean("/" + name). -func slashClean(name string) string { +func SlashClean(name string) string { if name == "" || name[0] != '/' { name = "/" + name } @@ -74,7 +74,7 @@ func (d Dir) resolve(name string) string { if dir == "" { dir = "." } - return filepath.Join(dir, filepath.FromSlash(slashClean(name))) + return filepath.Join(dir, filepath.FromSlash(SlashClean(name))) } func (d Dir) Mkdir(ctx context.Context, name string, perm os.FileMode) error { @@ -166,7 +166,7 @@ type memFS struct { // ends at that root node. func (fs *memFS) walk(op, fullname string, f func(dir *memFSNode, frag string, final bool) error) error { original := fullname - fullname = slashClean(fullname) + fullname = SlashClean(fullname) // Strip any leading "/"s to make fullname a relative path, as the walk // starts at fs.root. @@ -335,8 +335,8 @@ func (fs *memFS) Rename(ctx context.Context, oldName, newName string) error { fs.mu.Lock() defer fs.mu.Unlock() - oldName = slashClean(oldName) - newName = slashClean(newName) + oldName = SlashClean(oldName) + newName = SlashClean(newName) if oldName == newName { return nil } diff --git a/rest/dav/file_test.go b/rest/dav/file_test.go index bbbf47b..8af8644 100644 --- a/rest/dav/file_test.go +++ b/rest/dav/file_test.go @@ -37,7 +37,7 @@ func TestSlashClean(t *testing.T) { "a/b/c", } for _, tc := range testCases { - got := slashClean(tc) + got := SlashClean(tc) want := path.Clean("/" + tc) if got != want { t.Errorf("tc=%q: got %q, want %q", tc, got, want) diff --git a/rest/dav/lock.go b/rest/dav/lock.go index 47b8248..65d3f25 100644 --- a/rest/dav/lock.go +++ b/rest/dav/lock.go @@ -151,12 +151,12 @@ func (m *memLS) Confirm(now time.Time, name0, name1 string, conditions ...Condit var n0, n1 *memLSNode if name0 != "" { - if n0 = m.lookup(slashClean(name0), conditions...); n0 == nil { + if n0 = m.lookup(SlashClean(name0), conditions...); n0 == nil { return nil, ErrConfirmationFailed } } if name1 != "" { - if n1 = m.lookup(slashClean(name1), conditions...); n1 == nil { + if n1 = m.lookup(SlashClean(name1), conditions...); n1 == nil { return nil, ErrConfirmationFailed } } @@ -233,7 +233,7 @@ func (m *memLS) Create(now time.Time, details LockDetails) (string, error) { m.mu.Lock() defer m.mu.Unlock() m.collectExpiredNodes(now) - details.Root = slashClean(details.Root) + details.Root = SlashClean(details.Root) if !m.canCreate(details.Root, details.ZeroDepth) { return "", ErrLocked diff --git a/rest/dav/prop.go b/rest/dav/prop.go index a1173f6..28582b0 100644 --- a/rest/dav/prop.go +++ b/rest/dav/prop.go @@ -336,7 +336,7 @@ loop: return []Propstat{pstat}, nil } -func escapeXML(s string) string { +func EscapeXML(s string) string { for i := 0; i < len(s); i++ { // As an optimization, if s contains only ASCII letters, digits or a // few special characters, the escaped value is s itself and we don't @@ -364,11 +364,11 @@ func findResourceType(ctx context.Context, fs FileSystem, ls LockSystem, name st } func findDisplayName(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) { - if slashClean(name) == "/" { + if SlashClean(name) == "/" { // Hide the real name of a possibly prefixed root directory. return "", nil } - return escapeXML(fi.Name()), nil + return EscapeXML(fi.Name()), nil } func findContentLength(ctx context.Context, fs FileSystem, ls LockSystem, name string, fi os.FileInfo) (string, error) { diff --git a/rest/dav/webdav_test.go b/rest/dav/webdav_test.go index 51efc40..e4b13c2 100644 --- a/rest/dav/webdav_test.go +++ b/rest/dav/webdav_test.go @@ -208,7 +208,7 @@ func TestEscapeXML(t *testing.T) { // These test cases aren't exhaustive, and there is more than one way to // escape e.g. a quot (as """ or """) or an apos. We presume that // the encoding/xml package tests xml.EscapeText more thoroughly. This test - // here is just a sanity check for this package's escapeXML function, and + // here is just a sanity check for this package's EscapeXML function, and // its attempt to provide a fast path (and avoid a bytes.Buffer allocation) // when escaping filenames is obviously a no-op. testCases := map[string]string{ @@ -236,7 +236,7 @@ func TestEscapeXML(t *testing.T) { } for in, want := range testCases { - if got := escapeXML(in); got != want { + if got := EscapeXML(in); got != want { t.Errorf("in=%q: got %q, want %q", in, got, want) } } diff --git a/rest/dav_controller.go b/rest/dav_controller.go index b9d5167..ad23ad3 100644 --- a/rest/dav_controller.go +++ b/rest/dav_controller.go @@ -1,6 +1,7 @@ package rest import ( + "fmt" "net/http" "regexp" "tank/rest/dav" @@ -80,7 +81,8 @@ func (this *DavController) HandleRoutes(writer http.ResponseWriter, request *htt path := request.URL.Path //匹配 /api/dav{subPath} - reg := regexp.MustCompile(`^/api/dav(.*)$`) + pattern := fmt.Sprintf(`^%s(.*)$`, WEBDAV_PREFFIX) + reg := regexp.MustCompile(pattern) strs := reg.FindStringSubmatch(path) if len(strs) == 2 { var f = func(writer http.ResponseWriter, request *http.Request) { diff --git a/rest/dav_model.go b/rest/dav_model.go index c9f4f37..8b88f1d 100644 --- a/rest/dav_model.go +++ b/rest/dav_model.go @@ -1,3 +1,102 @@ package rest +import ( + "encoding/xml" + "fmt" + "net/http" + "strconv" + "tank/rest/dav" +) +//访问前缀,这个是特殊入口 +var WEBDAV_PREFFIX = "/api/dav" + +//动态的文件属性 +type LiveProp struct { + findFn func(matter *Matter) string + dir bool +} + +//所有的动态属性定义及其值的获取方式 +var LivePropMap = map[xml.Name]LiveProp{ + {Space: "DAV:", Local: "resourcetype"}: { + findFn: func(matter *Matter) string { + if matter.Dir { + return `` + } else { + return "" + } + }, + dir: true, + }, + {Space: "DAV:", Local: "displayname"}: { + findFn: func(matter *Matter) string { + if dav.SlashClean(matter.Name) == "/" { + return "" + } else { + return dav.EscapeXML(matter.Name) + } + }, + dir: true, + }, + {Space: "DAV:", Local: "getcontentlength"}: { + findFn: func(matter *Matter) string { + return strconv.FormatInt(matter.Size, 10) + }, + dir: false, + }, + {Space: "DAV:", Local: "getlastmodified"}: { + findFn: func(matter *Matter) string { + return matter.UpdateTime.UTC().Format(http.TimeFormat) + }, + // http://webdav.org/specs/rfc4918.html#PROPERTY_getlastmodified + // suggests that getlastmodified should only apply to GETable + // resources, and this package does not support GET on directories. + // + // Nonetheless, some WebDAV clients expect child directories to be + // sortable by getlastmodified date, so this value is true, not false. + // See golang.org/issue/15334. + dir: true, + }, + {Space: "DAV:", Local: "creationdate"}: { + findFn: nil, + dir: false, + }, + {Space: "DAV:", Local: "getcontentlanguage"}: { + findFn: nil, + dir: false, + }, + {Space: "DAV:", Local: "getcontenttype"}: { + findFn: func(matter *Matter) string { + if matter.Dir { + return "" + } else { + return dav.EscapeXML(matter.Name) + } + }, + dir: false, + }, + {Space: "DAV:", Local: "getetag"}: { + findFn: func(matter *Matter) string { + return fmt.Sprintf(`"%x%x"`, matter.UpdateTime.UnixNano(), matter.Size) + }, + // findETag 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. + dir: false, + }, + // TODO: The lockdiscovery property requires LockSystem to list the + // active locks on a resource. + {Space: "DAV:", Local: "lockdiscovery"}: {}, + {Space: "DAV:", Local: "supportedlock"}: { + findFn: func(matter *Matter) string { + return `` + + `` + + `` + + `` + + `` + }, + dir: true, + }, +} diff --git a/rest/dav_service.go b/rest/dav_service.go index eb5a958..7f2cc86 100644 --- a/rest/dav_service.go +++ b/rest/dav_service.go @@ -58,6 +58,48 @@ func (this *DavService) PropNames(matter *Matter) []xml.Name { } + +//从一个matter中获取其 []dav.Propstat +func (this *DavService) Propstats(matter *Matter, propfind dav.Propfind) []dav.Propstat { + + propstats := make([]dav.Propstat, 0) + if propfind.Propname != nil { + this.PanicBadRequest("propfind.Propname != nil 尚未处理") + } else if propfind.Allprop != nil { + this.PanicBadRequest("propfind.Allprop != nil 尚未处理") + } else { + + var properties []dav.Property + + for _, prop := range propfind.Prop { + //TODO: deadprops尚未考虑 + + // Otherwise, it must either be a live property or we don't know it. + if liveProp := LivePropMap[prop]; liveProp.findFn != nil && (liveProp.dir || !matter.Dir) { + innerXML := liveProp.findFn(matter) + + properties = append(properties, dav.Property{ + XMLName: prop, + InnerXML: []byte(innerXML), + }) + } else { + //TODO: 某一项请求的prop没有对应的结果 + } + } + + if len(properties) == 0 { + this.PanicBadRequest("请求的属性项无法解析!") + } + + okPropstat := dav.Propstat{Status: http.StatusOK, Props: properties} + + propstats = append(propstats, okPropstat) + } + + return propstats + +} + //处理 方法 func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http.Request, subPath string) { @@ -84,19 +126,11 @@ func (this *DavService) HandlePropfind(writer http.ResponseWriter, request *http for _, matter := range matters { - fmt.Printf("开始分析 %s\n", matter.Name) + fmt.Printf("开始分析 %s\n", matter.Path) - var propstats []dav.Propstat - var props = make([]dav.Property, 0) - props = append(props, dav.Property{ - XMLName: xml.Name{Space: "DAV:"}, - }) - propstats = append(propstats, dav.Propstat{ - Props: props, - ResponseDescription: "有点问题", - }) - - response := this.makePropstatResponse("/eyeblue/ready/go", propstats) + propstats := this.Propstats(matter, propfind) + path := fmt.Sprintf("%s%s", WEBDAV_PREFFIX, matter.Path) + response := this.makePropstatResponse(path, propstats) err = multiStatusWriter.Write(response) this.PanicError(err) diff --git a/rest/matter_model.go b/rest/matter_model.go index 6635a60..3948039 100644 --- a/rest/matter_model.go +++ b/rest/matter_model.go @@ -29,11 +29,19 @@ func (Matter) TableName() string { return TABLE_PREFIX + "matter" } + // 获取该Matter的绝对路径。path代表的是相对路径。 func (this *Matter) AbsolutePath() string { return GetUserFileRootDir(this.Username) + this.Path } + +// 获取该Matter的MimeType +func (this *Matter) MimeType() string { + return GetMimeType(GetExtension(this.Name)) +} + + //创建一个 ROOT 的matter,主要用于统一化处理移动复制等内容。 func NewRootMatter(user *User) *Matter { matter := &Matter{}