diff --git a/examples/webdav/main.go b/examples/webdav/main.go index ecbfe15..87bb00e 100644 --- a/examples/webdav/main.go +++ b/examples/webdav/main.go @@ -17,18 +17,10 @@ func main() { log.Fatal(err) } - // Create a new WebDAV handler with the OS file system. - fs, err := webdav.NewOSFS("public") - if err != nil { + // Serve the "public" directory on the "/webdav/" route. + if err := webdav.Serve(r, "/webdav", "public"); err != nil { log.Fatal(err) } - handler := webdav.NewHandler("/webdav", fs, webdav.NewMemLock(), log.New(os.Stdout, "", 0)) - - // Mount the WebDAV handler on the "/webdav/" route. - webdavMethods := []string{ - "OPTIONS", "GET", "HEAD", "DELETE", "PUT", "MKCOL", "COPY", "MOVE", "PROPFIND", "PROPPATCH", "LOCK", "UNLOCK", - } - r.HandleFunc(webdavMethods, "/webdav/*path", handler.ServeTouka) log.Println("Touka WebDAV Server starting on :8080...") if err := r.RunShutdown(":8080", 10*time.Second); err != nil { diff --git a/touka.go b/touka.go index 898a8b3..441212c 100644 --- a/touka.go +++ b/touka.go @@ -67,6 +67,8 @@ var ( MethodMkcol = "MKCOL" MethodCopy = "COPY" MethodMove = "MOVE" + MethodLock = "LOCK" + MethodUnlock = "UNLOCK" ) var MethodsSet = map[string]struct{}{ @@ -84,4 +86,6 @@ var MethodsSet = map[string]struct{}{ MethodMkcol: {}, MethodCopy: {}, MethodMove: {}, + MethodLock: {}, + MethodUnlock: {}, } diff --git a/webdav/easy.go b/webdav/easy.go new file mode 100644 index 0000000..56d1eb0 --- /dev/null +++ b/webdav/easy.go @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// Copyright 2024 WJQSERVER. All rights reserved. +// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. +package webdav + +import ( + "log" + "os" + + "github.com/infinite-iroha/touka" +) + +// Config is a configuration for the WebDAV handler. +type Config struct { + FileSystem FileSystem + LockSystem LockSystem + Logger Logger +} + +// Register registers a WebDAV handler on the given router. +func Register(engine *touka.Engine, prefix string, cfg *Config) { + if cfg.LockSystem == nil { + cfg.LockSystem = NewMemLock() + } + + handler := NewHandler(prefix, cfg.FileSystem, cfg.LockSystem, cfg.Logger) + + webdavMethods := []string{ + "OPTIONS", "GET", "HEAD", "DELETE", "PUT", "MKCOL", "COPY", "MOVE", "PROPFIND", "PROPPATCH", "LOCK", "UNLOCK", + } + engine.HandleFunc(webdavMethods, prefix+"/*path", handler.ServeTouka) +} + +// Serve serves a local directory via WebDAV. +func Serve(engine *touka.Engine, prefix string, rootDir string) error { + fs, err := NewOSFS(rootDir) + if err != nil { + return err + } + + cfg := &Config{ + FileSystem: fs, + Logger: log.New(os.Stdout, "", 0), + } + Register(engine, prefix, cfg) + return nil +} diff --git a/webdav/easy_test.go b/webdav/easy_test.go new file mode 100644 index 0000000..bf44441 --- /dev/null +++ b/webdav/easy_test.go @@ -0,0 +1,50 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// Copyright 2024 WJQSERVER. All rights reserved. +// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization. +package webdav + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/infinite-iroha/touka" +) + +func TestRegister(t *testing.T) { + r := touka.New() + cfg := &Config{ + FileSystem: NewMemFS(), + } + Register(r, "/dav", cfg) + + // Check if a WebDAV method is registered + req, _ := http.NewRequest("PROPFIND", "/dav/", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code == http.StatusNotFound { + t.Errorf("Expected PROPFIND to be registered, but got 404") + } +} + +func TestServe(t *testing.T) { + r := touka.New() + dir, _ := os.MkdirTemp("", "webdav") + defer os.RemoveAll(dir) + + if err := Serve(r, "/serve", dir); err != nil { + t.Fatalf("Serve failed: %v", err) + } + + // Check if a WebDAV method is registered + req, _ := http.NewRequest("OPTIONS", "/serve/", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected OPTIONS to return 200, but got %d", w.Code) + } +} 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..3d54a30 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. @@ -53,7 +55,11 @@ func (fs *OSFS) resolve(name string) (string, error) { } } - if !strings.HasPrefix(path, fs.RootDir) { + rel, err := filepath.Rel(fs.RootDir, path) + if err != nil { + return "", err + } + if strings.HasPrefix(rel, "..") { return "", os.ErrPermission } @@ -98,7 +104,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..b438c24 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 @@ -388,7 +397,7 @@ func (h *Handler) handleCopy(c *touka.Context) { return } - if err := h.copy(c.Context(), srcPath.(string), destPath); err != nil { + if err := h.copy(c, srcPath.(string), destPath); err != nil { c.Status(http.StatusInternalServerError) return } @@ -441,18 +450,18 @@ func (h *Handler) handleMove(c *touka.Context) { } } -func (h *Handler) copy(ctx context.Context, src, dest string) error { - info, err := h.FileSystem.Stat(ctx, src) +func (h *Handler) copy(c *touka.Context, src, dest string) error { + info, err := h.FileSystem.Stat(c.Context(), src) if err != nil { return err } if info.IsDir() { - if err := h.FileSystem.Mkdir(ctx, dest, info.Mode()); err != nil { + if err := h.FileSystem.Mkdir(c.Context(), dest, info.Mode()); err != nil { return err } - srcFile, err := h.FileSystem.OpenFile(ctx, src, os.O_RDONLY, 0) + srcFile, err := h.FileSystem.OpenFile(c, src, os.O_RDONLY, 0) if err != nil { return err } @@ -464,20 +473,20 @@ func (h *Handler) copy(ctx context.Context, src, dest string) error { } for _, child := range children { - if err := h.copy(ctx, path.Join(src, child.Name()), path.Join(dest, child.Name())); err != nil { + if err := h.copy(c, path.Join(src, child.Name()), path.Join(dest, child.Name())); err != nil { return err } } return nil } - srcFile, err := h.FileSystem.OpenFile(ctx, src, os.O_RDONLY, 0) + srcFile, err := h.FileSystem.OpenFile(c, 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(c, 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()