diff --git a/oa/builtin/webdav/dir_render.go b/oa/builtin/webdav/dir_render.go new file mode 100644 index 0000000..3fc20b8 --- /dev/null +++ b/oa/builtin/webdav/dir_render.go @@ -0,0 +1,89 @@ +// +// dir_render.go +// Copyright (C) 2024 veypi +// 2024-10-18 19:35 +// Distributed under terms of the GPL license. +// + +package webdav + +import ( + "fmt" + "io/fs" + "net/http" + "net/url" + "sort" + "strings" + + "github.com/veypi/utils/logv" +) + +type anyDirs interface { + len() int + name(i int) string + isDir(i int) bool +} + +type fileInfoDirs []fs.FileInfo + +func (d fileInfoDirs) len() int { return len(d) } +func (d fileInfoDirs) isDir(i int) bool { return d[i].IsDir() } +func (d fileInfoDirs) name(i int) string { return d[i].Name() } + +type dirEntryDirs []fs.DirEntry + +func (d dirEntryDirs) len() int { return len(d) } +func (d dirEntryDirs) isDir(i int) bool { return d[i].IsDir() } +func (d dirEntryDirs) name(i int) string { return d[i].Name() } + +func dirList(w http.ResponseWriter, r *http.Request, f File) { + + // Prefer to use ReadDir instead of Readdir, + // because the former doesn't require calling + // Stat on every entry of a directory on Unix. + var dirs anyDirs + var err error + if d, ok := f.(fs.ReadDirFile); ok { + var list dirEntryDirs + list, err = d.ReadDir(-1) + dirs = list + } else { + var list fileInfoDirs + list, err = f.Readdir(-1) + dirs = list + } + + if err != nil { + logv.Warn().Msgf("http: error reading directory: %v", err) + http.Error(w, "Error reading directory", http.StatusInternalServerError) + return + } + sort.Slice(dirs, func(i, j int) bool { return dirs.name(i) < dirs.name(j) }) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintf(w, "\n") + fmt.Fprintf(w, "\n") + fmt.Fprintf(w, "
\n")
+	for i, n := 0, dirs.len(); i < n; i++ {
+		name := dirs.name(i)
+		if dirs.isDir(i) {
+			name += "/"
+		}
+		// name may contain '?' or '#', which must be escaped to remain
+		// part of the URL path, and not indicate the start of a query
+		// string or fragment.
+		url := url.URL{Path: name}
+		fmt.Fprintf(w, "%s\n", url.String(), htmlReplacer.Replace(name))
+	}
+	fmt.Fprintf(w, "
\n") +} + +var htmlReplacer = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + // """ is shorter than """. + `"`, """, + // "'" is shorter than "'" and apos was not in HTML until HTML5. + "'", "'", +) diff --git a/oa/builtin/webdav/webdav.go b/oa/builtin/webdav/webdav.go index 03fbd96..71bf80a 100644 --- a/oa/builtin/webdav/webdav.go +++ b/oa/builtin/webdav/webdav.go @@ -20,16 +20,21 @@ import ( func NewWebdav(p string) func(http.ResponseWriter, *http.Request) { fs := &Handler{ - FileSystem: Dir(p), - LockSystem: NewMemLS(), + FileSystem: Dir(p), + LockSystem: NewMemLS(), + EnableDirRender: true, + GenSubPathFunc: func(r *http.Request) string { + return "" + }, } return fs.ServeHTTP } type Handler struct { // Prefix is the URL path prefix to strip from WebDAV resource paths. - Prefix string - GenSubPathFunc func(*http.Request) string + Prefix string + EnableDirRender bool + GenSubPathFunc func(*http.Request) string // FileSystem is the virtual file system. FileSystem FileSystem // LockSystem is the lock management system. @@ -39,14 +44,22 @@ type Handler struct { Logger func(*http.Request, error) } -func (h *Handler) stripPrefix(p string) (string, int, error) { - if h.Prefix == "" { - return p, http.StatusOK, nil +func (h *Handler) stripPrefix(p string, r *http.Request) (np string, ns int, ne error) { + np = p + ns = http.StatusOK + if h.Prefix != "" { + if tmp := strings.TrimPrefix(p, h.Prefix); len(tmp) < len(p) { + np = tmp + } else { + return p, http.StatusNotFound, errPrefixMismatch + } } - if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) { - return r, http.StatusOK, nil + if h.GenSubPathFunc != nil { + if tmp := h.GenSubPathFunc(r); tmp != "" && tmp != "/" { + np = tmp + np + } } - return p, http.StatusNotFound, errPrefixMismatch + return } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -158,7 +171,7 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func() if u.Host != r.Host { continue } - lsrc, status, err = h.stripPrefix(u.Path) + lsrc, status, err = h.stripPrefix(u.Path, r) if err != nil { return nil, status, err } @@ -180,7 +193,7 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func() } func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) { - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } @@ -202,7 +215,7 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status } func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) { - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } @@ -218,6 +231,10 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta return http.StatusNotFound, err } if fi.IsDir() { + if h.EnableDirRender { + dirList(w, r, f) + return 0, nil + } return http.StatusMethodNotAllowed, nil } etag, err := findETag(ctx, h.FileSystem, h.LockSystem, reqPath, fi) @@ -231,7 +248,7 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta } func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) { - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } @@ -261,7 +278,7 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status i } func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) { - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } @@ -303,7 +320,7 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, } func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) { - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } @@ -340,12 +357,12 @@ func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status return http.StatusBadGateway, errInvalidDestination } - src, status, err := h.stripPrefix(r.URL.Path) + src, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } - dst, status, err := h.stripPrefix(u.Path) + dst, status, err := h.stripPrefix(u.Path, r) if err != nil { return status, err } @@ -446,7 +463,7 @@ func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus return http.StatusBadRequest, errInvalidDepth } } - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } @@ -520,7 +537,7 @@ 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) { - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } @@ -589,7 +606,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status } func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) { - reqPath, status, err := h.stripPrefix(r.URL.Path) + reqPath, status, err := h.stripPrefix(r.URL.Path, r) if err != nil { return status, err } diff --git a/oa/builtin/webdav/xml_test.go b/oa/builtin/webdav/xml_test.go index ac2f247..5f38242 100644 --- a/oa/builtin/webdav/xml_test.go +++ b/oa/builtin/webdav/xml_test.go @@ -16,7 +16,7 @@ import ( "strings" "testing" - ixml "golang.org/x/net/webdav/internal/xml" + ixml "oa/builtin/webdav/internal/xml" ) func TestReadLockInfo(t *testing.T) {