Compare commits

..

No commits in common. "b92f1face5d19fa1f6800a3aa6c2f8f80e3f2278" and "85409ba803344a9fdad0a4519f9f99f1c04a542a" have entirely different histories.

8 changed files with 48 additions and 175 deletions

View file

@ -17,10 +17,18 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
// Serve the "public" directory on the "/webdav/" route. // Create a new WebDAV handler with the OS file system.
if err := webdav.Serve(r, "/webdav", "public"); err != nil { fs, err := webdav.NewOSFS("public")
if err != nil {
log.Fatal(err) 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...") log.Println("Touka WebDAV Server starting on :8080...")
if err := r.RunShutdown(":8080", 10*time.Second); err != nil { if err := r.RunShutdown(":8080", 10*time.Second); err != nil {

View file

@ -67,8 +67,6 @@ var (
MethodMkcol = "MKCOL" MethodMkcol = "MKCOL"
MethodCopy = "COPY" MethodCopy = "COPY"
MethodMove = "MOVE" MethodMove = "MOVE"
MethodLock = "LOCK"
MethodUnlock = "UNLOCK"
) )
var MethodsSet = map[string]struct{}{ var MethodsSet = map[string]struct{}{
@ -86,6 +84,4 @@ var MethodsSet = map[string]struct{}{
MethodMkcol: {}, MethodMkcol: {},
MethodCopy: {}, MethodCopy: {},
MethodMove: {}, MethodMove: {},
MethodLock: {},
MethodUnlock: {},
} }

View file

@ -1,48 +0,0 @@
// 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
}

View file

@ -1,50 +0,0 @@
// 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)
}
}

View file

@ -10,10 +10,7 @@ import (
"path" "path"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/infinite-iroha/touka"
) )
// MemFS is an in-memory file system for WebDAV using a tree structure. // MemFS is an in-memory file system for WebDAV using a tree structure.
@ -88,7 +85,7 @@ func (fs *MemFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error
} }
// OpenFile opens a file in the in-memory file system. // OpenFile opens a file in the in-memory file system.
func (fs *MemFS) OpenFile(c *touka.Context, name string, flag int, perm os.FileMode) (File, error) { func (fs *MemFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) {
fs.mu.Lock() fs.mu.Lock()
defer fs.mu.Unlock() defer fs.mu.Unlock()
@ -114,16 +111,15 @@ func (fs *MemFS) OpenFile(c *touka.Context, name string, flag int, perm os.FileM
if flag&os.O_TRUNC != 0 { if flag&os.O_TRUNC != 0 {
node.data = nil node.data = nil
atomic.StoreInt64(&node.size, 0) node.size = 0
} }
mf := memFilePool.Get().(*memFile) return &memFile{
mf.node = node node: node,
mf.fs = fs fs: fs,
mf.offset = 0 offset: 0,
mf.fullPath = name fullPath: name,
mf.contentLength = c.Request.ContentLength }, nil
return mf, nil
} }
// RemoveAll removes a file or directory from the in-memory file system. // RemoveAll removes a file or directory from the in-memory file system.
@ -198,32 +194,20 @@ type memNode struct {
} }
func (n *memNode) Name() string { return n.name } func (n *memNode) Name() string { return n.name }
func (n *memNode) Size() int64 { return atomic.LoadInt64(&n.size) } func (n *memNode) Size() int64 { return n.size }
func (n *memNode) Mode() os.FileMode { return n.mode } func (n *memNode) Mode() os.FileMode { return n.mode }
func (n *memNode) ModTime() time.Time { return n.modTime } func (n *memNode) ModTime() time.Time { return n.modTime }
func (n *memNode) IsDir() bool { return n.isDir } func (n *memNode) IsDir() bool { return n.isDir }
func (n *memNode) Sys() interface{} { return nil } func (n *memNode) Sys() interface{} { return nil }
type memFile struct { type memFile struct {
node *memNode node *memNode
fs *MemFS fs *MemFS
offset int64 offset int64
fullPath string fullPath string
contentLength int64
} }
var memFilePool = sync.Pool{ func (f *memFile) Close() error { return nil }
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) Stat() (ObjectInfo, error) { return f.node, nil }
func (f *memFile) Read(p []byte) (n int, err error) { func (f *memFile) Read(p []byte) (n int, err error) {
@ -240,17 +224,17 @@ func (f *memFile) Read(p []byte) (n int, err error) {
func (f *memFile) Write(p []byte) (n int, err error) { func (f *memFile) Write(p []byte) (n int, err error) {
f.fs.mu.Lock() f.fs.mu.Lock()
defer f.fs.mu.Unlock() defer f.fs.mu.Unlock()
newSize := f.offset + int64(len(p)) if f.offset+int64(len(p)) > int64(len(f.node.data)) {
if newSize > int64(cap(f.node.data)) { newSize := f.offset + int64(len(p))
newData := make([]byte, newSize) newData := make([]byte, newSize)
copy(newData, f.node.data) copy(newData, f.node.data)
f.node.data = newData f.node.data = newData
} else {
f.node.data = f.node.data[:newSize]
} }
n = copy(f.node.data[f.offset:], p) n = copy(f.node.data[f.offset:], p)
f.offset += int64(n) f.offset += int64(n)
atomic.StoreInt64(&f.node.size, newSize) if f.offset > f.node.size {
f.node.size = f.offset
}
return n, nil return n, nil
} }

View file

@ -9,8 +9,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/infinite-iroha/touka"
) )
// OSFS is a WebDAV FileSystem that uses the local OS file system. // OSFS is a WebDAV FileSystem that uses the local OS file system.
@ -55,11 +53,7 @@ func (fs *OSFS) resolve(name string) (string, error) {
} }
} }
rel, err := filepath.Rel(fs.RootDir, path) if !strings.HasPrefix(path, fs.RootDir) {
if err != nil {
return "", err
}
if strings.HasPrefix(rel, "..") {
return "", os.ErrPermission return "", os.ErrPermission
} }
@ -104,7 +98,7 @@ func (f *osFile) Readdir(count int) ([]ObjectInfo, error) {
} }
// OpenFile opens a file. // OpenFile opens a file.
func (fs *OSFS) OpenFile(c *touka.Context, name string, flag int, perm os.FileMode) (File, error) { func (fs *OSFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) {
path, err := fs.resolve(name) path, err := fs.resolve(name)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -14,7 +14,6 @@ import (
"os" "os"
"path" "path"
"strings" "strings"
"sync"
"time" "time"
"github.com/infinite-iroha/touka" "github.com/infinite-iroha/touka"
@ -25,7 +24,7 @@ import (
// abstracting the underlying storage from the WebDAV protocol logic. // abstracting the underlying storage from the WebDAV protocol logic.
type FileSystem interface { type FileSystem interface {
Mkdir(ctx context.Context, name string, perm os.FileMode) error Mkdir(ctx context.Context, name string, perm os.FileMode) error
OpenFile(c *touka.Context, name string, flag int, perm os.FileMode) (File, error) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error)
RemoveAll(ctx context.Context, name string) error RemoveAll(ctx context.Context, name string) error
Rename(ctx context.Context, oldName, newName string) error Rename(ctx context.Context, oldName, newName string) error
Stat(ctx context.Context, name string) (ObjectInfo, error) Stat(ctx context.Context, name string) (ObjectInfo, error)
@ -150,14 +149,6 @@ type Response struct {
Propstats []Propstat `xml:"DAV: propstat"` 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 // Propstat groups properties with their corresponding HTTP status in a
// single response, indicating success or failure for those properties. // single response, indicating success or failure for those properties.
type Propstat struct { type Propstat struct {
@ -266,7 +257,7 @@ func (h *Handler) handleOptions(c *touka.Context) {
func (h *Handler) handleGetHead(c *touka.Context) { func (h *Handler) handleGetHead(c *touka.Context) {
path, _ := c.Get("webdav_path") path, _ := c.Get("webdav_path")
file, err := h.FileSystem.OpenFile(c, path.(string), os.O_RDONLY, 0) file, err := h.FileSystem.OpenFile(c.Context(), path.(string), os.O_RDONLY, 0)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
c.Status(http.StatusNotFound) c.Status(http.StatusNotFound)
@ -306,7 +297,7 @@ func (h *Handler) handleDelete(c *touka.Context) {
} }
if info.IsDir() { if info.IsDir() {
file, err := h.FileSystem.OpenFile(c, pathStr, os.O_RDONLY, 0) file, err := h.FileSystem.OpenFile(c.Context(), pathStr, os.O_RDONLY, 0)
if err != nil { if err != nil {
c.Status(http.StatusInternalServerError) c.Status(http.StatusInternalServerError)
return return
@ -338,7 +329,7 @@ func (h *Handler) handleDelete(c *touka.Context) {
func (h *Handler) handlePut(c *touka.Context) { func (h *Handler) handlePut(c *touka.Context) {
path, _ := c.Get("webdav_path") path, _ := c.Get("webdav_path")
file, err := h.FileSystem.OpenFile(c, path.(string), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) file, err := h.FileSystem.OpenFile(c.Context(), path.(string), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil { if err != nil {
c.Status(http.StatusInternalServerError) c.Status(http.StatusInternalServerError)
return return
@ -397,7 +388,7 @@ func (h *Handler) handleCopy(c *touka.Context) {
return return
} }
if err := h.copy(c, srcPath.(string), destPath); err != nil { if err := h.copy(c.Context(), srcPath.(string), destPath); err != nil {
c.Status(http.StatusInternalServerError) c.Status(http.StatusInternalServerError)
return return
} }
@ -450,18 +441,18 @@ func (h *Handler) handleMove(c *touka.Context) {
} }
} }
func (h *Handler) copy(c *touka.Context, src, dest string) error { func (h *Handler) copy(ctx context.Context, src, dest string) error {
info, err := h.FileSystem.Stat(c.Context(), src) info, err := h.FileSystem.Stat(ctx, src)
if err != nil { if err != nil {
return err return err
} }
if info.IsDir() { if info.IsDir() {
if err := h.FileSystem.Mkdir(c.Context(), dest, info.Mode()); err != nil { if err := h.FileSystem.Mkdir(ctx, dest, info.Mode()); err != nil {
return err return err
} }
srcFile, err := h.FileSystem.OpenFile(c, src, os.O_RDONLY, 0) srcFile, err := h.FileSystem.OpenFile(ctx, src, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
@ -473,20 +464,20 @@ func (h *Handler) copy(c *touka.Context, src, dest string) error {
} }
for _, child := range children { for _, child := range children {
if err := h.copy(c, path.Join(src, child.Name()), path.Join(dest, child.Name())); err != nil { if err := h.copy(ctx, path.Join(src, child.Name()), path.Join(dest, child.Name())); err != nil {
return err return err
} }
} }
return nil return nil
} }
srcFile, err := h.FileSystem.OpenFile(c, src, os.O_RDONLY, 0) srcFile, err := h.FileSystem.OpenFile(ctx, src, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }
defer srcFile.Close() defer srcFile.Close()
destFile, err := h.FileSystem.OpenFile(c, dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) destFile, err := h.FileSystem.OpenFile(ctx, dest, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode())
if err != nil { if err != nil {
return err return err
} }
@ -516,11 +507,9 @@ func (h *Handler) handlePropfind(c *touka.Context) {
} }
} }
ms := multistatusPool.Get().(*Multistatus) ms := &Multistatus{
defer func() { Responses: make([]*Response, 0),
ms.Responses = ms.Responses[:0] }
multistatusPool.Put(ms)
}()
depth := c.GetReqHeader("Depth") depth := c.GetReqHeader("Depth")
if depth == "" { if depth == "" {
@ -536,7 +525,7 @@ func (h *Handler) handlePropfind(c *touka.Context) {
return nil return nil
} }
file, err := h.FileSystem.OpenFile(c, p, os.O_RDONLY, 0) file, err := h.FileSystem.OpenFile(c.Context(), p, os.O_RDONLY, 0)
if err != nil { if err != nil {
return err return err
} }

View file

@ -75,7 +75,7 @@ func TestHandlePropfind(t *testing.T) {
// Create a test directory and a test file // Create a test directory and a test file
fs.Mkdir(nil, "/testdir", 0755) fs.Mkdir(nil, "/testdir", 0755)
file, _ := fs.OpenFile(&touka.Context{Request: &http.Request{}}, "/testdir/testfile", os.O_CREATE|os.O_WRONLY, 0644) file, _ := fs.OpenFile(nil, "/testdir/testfile", os.O_CREATE|os.O_WRONLY, 0644)
file.Write([]byte("test content")) file.Write([]byte("test content"))
file.Close() file.Close()