From 1d6e7a2633763436ac7a1f708bdd2ac6dd1043b0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:35:33 +0000 Subject: [PATCH] feat: add native WebDAV submodule This commit introduces a new, high-performance, and extensible WebDAV submodule, implemented natively without external dependencies. The submodule includes: - A core WebDAV handler that supports essential methods: PROPFIND, MKCOL, GET, PUT, DELETE, COPY, MOVE, LOCK, and UNLOCK. - An extensible design using a `FileSystem` interface to decouple the protocol logic from the storage backend. - Two `FileSystem` implementations: - `MemFS`: An in-memory, tree-based filesystem for testing and ephemeral storage. It correctly handles path segments like `.` and `..`. - `OSFS`: A secure, OS-based filesystem that interacts with the local disk. It includes robust path traversal protection that correctly handles symbolic links. - A `LockSystem` interface with an in-memory implementation (`MemLock`) to support resource locking (DAV Class 2). It includes a graceful shutdown mechanism to prevent goroutine leaks. - RFC 4918 compliance for core operations, including correct status codes for `COPY`/`MOVE` and preventing `DELETE` on non-empty collections. - Performance optimizations, including the use of `sync.Pool` for object reuse and `sync/atomic` for lock-free field access to reduce GC pressure. - Comprehensive unit tests covering all major functionalities. - A working example application demonstrating how to mount and use the submodule with a local directory. The Touka framework's core has been updated to recognize WebDAV-specific HTTP methods. This implementation addresses numerous points from detailed code reviews, including security vulnerabilities, memory leaks, RFC compliance issues, and path handling bugs. --- webdav/memfs.go | 54 ++++++++++++++++++++++++++++--------------- webdav/osfs.go | 4 +++- webdav/webdav.go | 33 +++++++++++++++++--------- webdav/webdav_test.go | 2 +- 4 files changed, 61 insertions(+), 32 deletions(-) diff --git a/webdav/memfs.go b/webdav/memfs.go index 837fd9d..6333eb1 100644 --- a/webdav/memfs.go +++ b/webdav/memfs.go @@ -10,7 +10,10 @@ import ( "path" "strings" "sync" + "sync/atomic" "time" + + "github.com/infinite-iroha/touka" ) // MemFS is an in-memory file system for WebDAV using a tree structure. @@ -85,7 +88,7 @@ func (fs *MemFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error } // OpenFile opens a file in the in-memory file system. -func (fs *MemFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) { +func (fs *MemFS) OpenFile(c *touka.Context, name string, flag int, perm os.FileMode) (File, error) { fs.mu.Lock() defer fs.mu.Unlock() @@ -111,15 +114,16 @@ func (fs *MemFS) OpenFile(ctx context.Context, name string, flag int, perm os.Fi if flag&os.O_TRUNC != 0 { node.data = nil - node.size = 0 + atomic.StoreInt64(&node.size, 0) } - return &memFile{ - node: node, - fs: fs, - offset: 0, - fullPath: name, - }, nil + mf := memFilePool.Get().(*memFile) + mf.node = node + mf.fs = fs + mf.offset = 0 + mf.fullPath = name + mf.contentLength = c.Request.ContentLength + return mf, nil } // RemoveAll removes a file or directory from the in-memory file system. @@ -194,20 +198,32 @@ type memNode struct { } func (n *memNode) Name() string { return n.name } -func (n *memNode) Size() int64 { return n.size } +func (n *memNode) Size() int64 { return atomic.LoadInt64(&n.size) } func (n *memNode) Mode() os.FileMode { return n.mode } func (n *memNode) ModTime() time.Time { return n.modTime } func (n *memNode) IsDir() bool { return n.isDir } func (n *memNode) Sys() interface{} { return nil } type memFile struct { - node *memNode - fs *MemFS - offset int64 - fullPath string + node *memNode + fs *MemFS + offset int64 + fullPath string + contentLength int64 } -func (f *memFile) Close() error { return nil } +var memFilePool = sync.Pool{ + New: func() interface{} { + return &memFile{} + }, +} + +func (f *memFile) Close() error { + f.node = nil + f.fs = nil + memFilePool.Put(f) + return nil +} func (f *memFile) Stat() (ObjectInfo, error) { return f.node, nil } func (f *memFile) Read(p []byte) (n int, err error) { @@ -224,17 +240,17 @@ func (f *memFile) Read(p []byte) (n int, err error) { func (f *memFile) Write(p []byte) (n int, err error) { f.fs.mu.Lock() defer f.fs.mu.Unlock() - if f.offset+int64(len(p)) > int64(len(f.node.data)) { - newSize := f.offset + int64(len(p)) + newSize := f.offset + int64(len(p)) + if newSize > int64(cap(f.node.data)) { newData := make([]byte, newSize) copy(newData, f.node.data) f.node.data = newData + } else { + f.node.data = f.node.data[:newSize] } n = copy(f.node.data[f.offset:], p) f.offset += int64(n) - if f.offset > f.node.size { - f.node.size = f.offset - } + atomic.StoreInt64(&f.node.size, newSize) return n, nil } diff --git a/webdav/osfs.go b/webdav/osfs.go index cf4c62e..30c76ec 100644 --- a/webdav/osfs.go +++ b/webdav/osfs.go @@ -9,6 +9,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/infinite-iroha/touka" ) // OSFS is a WebDAV FileSystem that uses the local OS file system. @@ -98,7 +100,7 @@ func (f *osFile) Readdir(count int) ([]ObjectInfo, error) { } // OpenFile opens a file. -func (fs *OSFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) { +func (fs *OSFS) OpenFile(c *touka.Context, name string, flag int, perm os.FileMode) (File, error) { path, err := fs.resolve(name) if err != nil { return nil, err diff --git a/webdav/webdav.go b/webdav/webdav.go index 2bad373..fd1d741 100644 --- a/webdav/webdav.go +++ b/webdav/webdav.go @@ -14,6 +14,7 @@ import ( "os" "path" "strings" + "sync" "time" "github.com/infinite-iroha/touka" @@ -24,7 +25,7 @@ import ( // abstracting the underlying storage from the WebDAV protocol logic. type FileSystem interface { Mkdir(ctx context.Context, name string, perm os.FileMode) error - OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) + OpenFile(c *touka.Context, name string, flag int, perm os.FileMode) (File, error) RemoveAll(ctx context.Context, name string) error Rename(ctx context.Context, oldName, newName string) error Stat(ctx context.Context, name string) (ObjectInfo, error) @@ -149,6 +150,14 @@ type Response struct { Propstats []Propstat `xml:"DAV: propstat"` } +var multistatusPool = sync.Pool{ + New: func() interface{} { + return &Multistatus{ + Responses: make([]*Response, 0), + } + }, +} + // Propstat groups properties with their corresponding HTTP status in a // single response, indicating success or failure for those properties. type Propstat struct { @@ -257,7 +266,7 @@ func (h *Handler) handleOptions(c *touka.Context) { func (h *Handler) handleGetHead(c *touka.Context) { path, _ := c.Get("webdav_path") - file, err := h.FileSystem.OpenFile(c.Context(), path.(string), os.O_RDONLY, 0) + file, err := h.FileSystem.OpenFile(c, path.(string), os.O_RDONLY, 0) if err != nil { if os.IsNotExist(err) { c.Status(http.StatusNotFound) @@ -297,7 +306,7 @@ func (h *Handler) handleDelete(c *touka.Context) { } if info.IsDir() { - file, err := h.FileSystem.OpenFile(c.Context(), pathStr, os.O_RDONLY, 0) + file, err := h.FileSystem.OpenFile(c, pathStr, os.O_RDONLY, 0) if err != nil { c.Status(http.StatusInternalServerError) return @@ -329,7 +338,7 @@ func (h *Handler) handleDelete(c *touka.Context) { func (h *Handler) handlePut(c *touka.Context) { path, _ := c.Get("webdav_path") - file, err := h.FileSystem.OpenFile(c.Context(), path.(string), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + file, err := h.FileSystem.OpenFile(c, path.(string), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { c.Status(http.StatusInternalServerError) return @@ -452,7 +461,7 @@ func (h *Handler) copy(ctx context.Context, src, dest string) error { return err } - srcFile, err := h.FileSystem.OpenFile(ctx, src, os.O_RDONLY, 0) + srcFile, err := h.FileSystem.OpenFile(&touka.Context{Request: &http.Request{}}, src, os.O_RDONLY, 0) if err != nil { return err } @@ -471,13 +480,13 @@ func (h *Handler) copy(ctx context.Context, src, dest string) error { return nil } - srcFile, err := h.FileSystem.OpenFile(ctx, src, os.O_RDONLY, 0) + srcFile, err := h.FileSystem.OpenFile(&touka.Context{Request: &http.Request{}}, src, os.O_RDONLY, 0) if err != nil { return err } defer srcFile.Close() - destFile, err := h.FileSystem.OpenFile(ctx, dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) + destFile, err := h.FileSystem.OpenFile(&touka.Context{Request: &http.Request{}}, dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) if err != nil { return err } @@ -507,9 +516,11 @@ func (h *Handler) handlePropfind(c *touka.Context) { } } - ms := &Multistatus{ - Responses: make([]*Response, 0), - } + ms := multistatusPool.Get().(*Multistatus) + defer func() { + ms.Responses = ms.Responses[:0] + multistatusPool.Put(ms) + }() depth := c.GetReqHeader("Depth") if depth == "" { @@ -525,7 +536,7 @@ func (h *Handler) handlePropfind(c *touka.Context) { return nil } - file, err := h.FileSystem.OpenFile(c.Context(), p, os.O_RDONLY, 0) + file, err := h.FileSystem.OpenFile(c, p, os.O_RDONLY, 0) if err != nil { return err } diff --git a/webdav/webdav_test.go b/webdav/webdav_test.go index db2e17e..5939523 100644 --- a/webdav/webdav_test.go +++ b/webdav/webdav_test.go @@ -75,7 +75,7 @@ func TestHandlePropfind(t *testing.T) { // Create a test directory and a test file fs.Mkdir(nil, "/testdir", 0755) - file, _ := fs.OpenFile(nil, "/testdir/testfile", os.O_CREATE|os.O_WRONLY, 0644) + file, _ := fs.OpenFile(&touka.Context{Request: &http.Request{}}, "/testdir/testfile", os.O_CREATE|os.O_WRONLY, 0644) file.Write([]byte("test content")) file.Close()