diff --git a/webdav/memlock.go b/webdav/memlock.go index 276b798..dabdd71 100644 --- a/webdav/memlock.go +++ b/webdav/memlock.go @@ -17,6 +17,7 @@ import ( type MemLock struct { mu sync.RWMutex locks map[string]*lock + stop chan struct{} } type lock struct { @@ -30,21 +31,33 @@ type lock struct { func NewMemLock() *MemLock { l := &MemLock{ locks: make(map[string]*lock), + stop: make(chan struct{}), } go l.cleanup() return l } +// Close stops the cleanup goroutine. +func (l *MemLock) Close() { + close(l.stop) +} + func (l *MemLock) cleanup() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() for { - time.Sleep(1 * time.Minute) - l.mu.Lock() - for token, lock := range l.locks { - if time.Now().After(lock.expires) { - delete(l.locks, token) + select { + case <-ticker.C: + l.mu.Lock() + for token, lock := range l.locks { + if time.Now().After(lock.expires) { + delete(l.locks, token) + } } + l.mu.Unlock() + case <-l.stop: + return } - l.mu.Unlock() } } diff --git a/webdav/osfs.go b/webdav/osfs.go index a4dfb4f..cf4c62e 100644 --- a/webdav/osfs.go +++ b/webdav/osfs.go @@ -26,18 +26,37 @@ func NewOSFS(rootDir string) (*OSFS, error) { } func (fs *OSFS) resolve(name string) (string, error) { - if filepath.IsAbs(name) { + if filepath.IsAbs(name) || strings.Contains(name, "..") { return "", os.ErrPermission } + path := filepath.Join(fs.RootDir, name) - rel, err := filepath.Rel(fs.RootDir, path) - if err != nil { + // Evaluate symlinks, but only if the path exists. + if _, err := os.Lstat(path); err == nil { + path, err = filepath.EvalSymlinks(path) + if err != nil { + return "", err + } + } else if !os.IsNotExist(err) { return "", err + // For non-existent paths (like for PUT or MKCOL), we can't EvalSymlinks the full path. + // Instead, we resolve the parent and ensure it's within the root. + } else { + parentDir := filepath.Dir(path) + if _, err := os.Stat(parentDir); err == nil { + parentDir, err = filepath.EvalSymlinks(parentDir) + if err != nil { + return "", err + } + path = filepath.Join(parentDir, filepath.Base(path)) + } } - if strings.HasPrefix(rel, "..") { + + if !strings.HasPrefix(path, fs.RootDir) { return "", os.ErrPermission } + return path, nil } diff --git a/webdav/webdav.go b/webdav/webdav.go index e6f4d5f..2bad373 100644 --- a/webdav/webdav.go +++ b/webdav/webdav.go @@ -284,7 +284,39 @@ func (h *Handler) handleGetHead(c *touka.Context) { func (h *Handler) handleDelete(c *touka.Context) { path, _ := c.Get("webdav_path") - if err := h.FileSystem.RemoveAll(c.Context(), path.(string)); err != nil { + pathStr := path.(string) + + info, err := h.FileSystem.Stat(c.Context(), pathStr) + if err != nil { + if os.IsNotExist(err) { + c.Status(http.StatusNotFound) + } else { + c.Status(http.StatusInternalServerError) + } + return + } + + if info.IsDir() { + file, err := h.FileSystem.OpenFile(c.Context(), pathStr, os.O_RDONLY, 0) + if err != nil { + c.Status(http.StatusInternalServerError) + return + } + defer file.Close() + + // Check if the directory has any children. Readdir(1) is enough. + children, err := file.Readdir(1) + if err != nil && err != io.EOF { + c.Status(http.StatusInternalServerError) + return + } + if len(children) > 0 { + c.Status(http.StatusConflict) // 409 Conflict for non-empty collection + return + } + } + + if err := h.FileSystem.RemoveAll(c.Context(), pathStr); err != nil { if os.IsNotExist(err) { c.Status(http.StatusNotFound) } else { @@ -347,11 +379,13 @@ func (h *Handler) handleCopy(c *touka.Context) { overwrite = "T" // Default is to overwrite } - if overwrite == "F" { - if _, err := h.FileSystem.Stat(c.Context(), destPath); err == nil { - c.Status(http.StatusPreconditionFailed) - return - } + // Check for existence before the operation to determine status code later. + _, err = h.FileSystem.Stat(c.Context(), destPath) + existed := err == nil + + if overwrite == "F" && existed { + c.Status(http.StatusPreconditionFailed) + return } if err := h.copy(c.Context(), srcPath.(string), destPath); err != nil { @@ -359,7 +393,11 @@ func (h *Handler) handleCopy(c *touka.Context) { return } - c.Status(http.StatusCreated) + if existed { + c.Status(http.StatusNoContent) + } else { + c.Status(http.StatusCreated) + } } func (h *Handler) handleMove(c *touka.Context) { @@ -382,11 +420,13 @@ func (h *Handler) handleMove(c *touka.Context) { overwrite = "T" // Default is to overwrite } - if overwrite == "F" { - if _, err := h.FileSystem.Stat(c.Context(), destPath); err == nil { - c.Status(http.StatusPreconditionFailed) - return - } + // Check for existence before the operation to determine status code later. + _, err = h.FileSystem.Stat(c.Context(), destPath) + existed := err == nil + + if overwrite == "F" && existed { + c.Status(http.StatusPreconditionFailed) + return } if err := h.FileSystem.Rename(c.Context(), srcPath.(string), destPath); err != nil { @@ -394,7 +434,11 @@ func (h *Handler) handleMove(c *touka.Context) { return } - c.Status(http.StatusCreated) + if existed { + c.Status(http.StatusNoContent) + } else { + c.Status(http.StatusCreated) + } } func (h *Handler) copy(ctx context.Context, src, dest string) error {