package webdav import ( "encoding/json" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/metazla/meta-core/internal/config" "golang.org/x/net/webdav" ) // Handler handles WebDAV requests (caching is handled by nginx proxy_cache) type Handler struct { config *config.Config filesPath string webdavFS webdav.Handler } // NewHandler creates a new WebDAV handler func NewHandler(cfg *config.Config) *Handler { h := &Handler{ config: cfg, filesPath: cfg.FilesPath, } // Create standard WebDAV handler for write operations h.webdavFS = webdav.Handler{ FileSystem: webdav.Dir(cfg.FilesPath), LockSystem: webdav.NewMemLS(), Logger: func(r *http.Request, err error) { if err != nil { log.Printf("[WebDAV] %s %s: %v", r.Method, r.URL.Path, err) } }, } return h } // ServeHTTP handles WebDAV requests func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Remove /webdav prefix to get the file path path := strings.TrimPrefix(r.URL.Path, "/webdav") if path == "" { path = "/" } log.Printf("[WebDAV] %s %s", r.Method, path) switch r.Method { case "GET", "HEAD": h.handleRead(w, r, path) case "PUT": h.handlePut(w, r, path) case "DELETE": h.handleDelete(w, r, path) case "MKCOL": h.handleMkcol(w, r, path) case "COPY", "MOVE": h.handleCopyMove(w, r, path) case "PROPFIND": h.handlePropfind(w, r, path) case "OPTIONS": h.handleOptions(w, r) default: // Pass to standard WebDAV handler h.webdavFS.ServeHTTP(w, r) } } // handleRead handles GET and HEAD requests (caching is done by nginx proxy_cache) func (h *Handler) handleRead(w http.ResponseWriter, r *http.Request, path string) { fullPath := filepath.Join(h.filesPath, path) // Check if it's a directory info, err := os.Stat(fullPath) if err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } if info.IsDir() { // Return directory listing as JSON h.handleDirListing(w, r, fullPath, path) return } // Serve file directly (nginx handles caching via proxy_cache) http.ServeFile(w, r, fullPath) } // handleDirListing returns a directory listing as JSON func (h *Handler) handleDirListing(w http.ResponseWriter, r *http.Request, fullPath, relPath string) { entries, err := os.ReadDir(fullPath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } type dirEntry struct { Name string `json:"name"` Type string `json:"type"` MTime string `json:"mtime"` Size int64 `json:"size,omitempty"` } var result []dirEntry for _, entry := range entries { info, err := entry.Info() if err != nil { continue } entryType := "file" if entry.IsDir() { entryType = "directory" } result = append(result, dirEntry{ Name: entry.Name(), Type: entryType, MTime: info.ModTime().Format("2006-01-02T15:04:05Z"), Size: info.Size(), }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // handlePut handles PUT requests (file upload) func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request, path string) { fullPath := filepath.Join(h.filesPath, path) // Ensure parent directory exists if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Create/overwrite file file, err := os.Create(fullPath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer file.Close() // Copy request body to file _, err = io.Copy(file, r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Note: nginx proxy_cache handles invalidation via TTL (1h inactive) w.WriteHeader(http.StatusCreated) } // handleDelete handles DELETE requests func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request, path string) { fullPath := filepath.Join(h.filesPath, path) // Check if exists _, err := os.Stat(fullPath) if err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // Remove file or directory if err := os.RemoveAll(fullPath); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Note: nginx proxy_cache handles invalidation via TTL (1h inactive) w.WriteHeader(http.StatusNoContent) } // handleMkcol handles MKCOL requests (create directory) func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request, path string) { fullPath := filepath.Join(h.filesPath, path) // Check if already exists if _, err := os.Stat(fullPath); err == nil { http.Error(w, "Already exists", http.StatusMethodNotAllowed) return } // Create directory if err := os.MkdirAll(fullPath, 0755); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) } // handleCopyMove handles COPY and MOVE requests func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request, path string) { // Get destination from header dest := r.Header.Get("Destination") if dest == "" { http.Error(w, "Destination header required", http.StatusBadRequest) return } // Parse destination URL to get path destPath := strings.TrimPrefix(dest, "/webdav") if strings.HasPrefix(dest, "http") { // Full URL provided - extract path parts := strings.SplitN(dest, "/webdav", 2) if len(parts) == 2 { destPath = parts[1] } } srcFullPath := filepath.Join(h.filesPath, path) destFullPath := filepath.Join(h.filesPath, destPath) // Check if source exists if _, err := os.Stat(srcFullPath); err != nil { if os.IsNotExist(err) { http.Error(w, "Source not found", http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } // Ensure destination directory exists if err := os.MkdirAll(filepath.Dir(destFullPath), 0755); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if r.Method == "MOVE" { // Move file if err := os.Rename(srcFullPath, destFullPath); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Note: nginx proxy_cache handles invalidation via TTL (1h inactive) } else { // Copy file if err := copyFile(srcFullPath, destFullPath); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } w.WriteHeader(http.StatusCreated) } // handlePropfind handles PROPFIND requests func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request, path string) { fullPath := filepath.Join(h.filesPath, path) info, err := os.Stat(fullPath) if err != nil { if os.IsNotExist(err) { http.Error(w, "Not found", http.StatusNotFound) } else { http.Error(w, err.Error(), http.StatusInternalServerError) } return } depth := r.Header.Get("Depth") if depth == "" { depth = "infinity" } // Build XML response w.Header().Set("Content-Type", "application/xml; charset=utf-8") w.WriteHeader(http.StatusMultiStatus) w.Write([]byte(`` + "\n")) w.Write([]byte(`` + "\n")) // Add entry for the requested resource h.writePropfindEntry(w, path, info) // If directory and depth > 0, add children if info.IsDir() && depth != "0" { entries, err := os.ReadDir(fullPath) if err == nil { for _, entry := range entries { childInfo, err := entry.Info() if err != nil { continue } childPath := filepath.Join(path, entry.Name()) h.writePropfindEntry(w, childPath, childInfo) } } } w.Write([]byte(`` + "\n")) } // writePropfindEntry writes a single PROPFIND response entry func (h *Handler) writePropfindEntry(w http.ResponseWriter, path string, info os.FileInfo) { href := "/webdav" + path if info.IsDir() && !strings.HasSuffix(href, "/") { href += "/" } resourceType := "" if info.IsDir() { resourceType = "" } w.Write([]byte(` ` + "\n")) w.Write([]byte(` ` + href + `` + "\n")) w.Write([]byte(` ` + "\n")) w.Write([]byte(` ` + "\n")) w.Write([]byte(` ` + resourceType + `` + "\n")) if !info.IsDir() { w.Write([]byte(` ` + formatInt64(info.Size()) + `` + "\n")) } w.Write([]byte(` ` + info.ModTime().Format("Mon, 02 Jan 2006 15:04:05 GMT") + `` + "\n")) w.Write([]byte(` ` + "\n")) w.Write([]byte(` HTTP/1.1 200 OK` + "\n")) w.Write([]byte(` ` + "\n")) w.Write([]byte(` ` + "\n")) } // handleOptions handles OPTIONS requests func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) { w.Header().Set("Allow", "OPTIONS, GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND") w.Header().Set("DAV", "1, 2") w.Header().Set("MS-Author-Via", "DAV") w.WriteHeader(http.StatusOK) } // copyFile copies a file from src to dst func copyFile(src, dst string) error { srcFile, err := os.Open(src) if err != nil { return err } defer srcFile.Close() dstFile, err := os.Create(dst) if err != nil { return err } defer dstFile.Close() _, err = io.Copy(dstFile, srcFile) return err } // formatInt64 formats an int64 as a string func formatInt64(n int64) string { return strconv.FormatInt(n, 10) }