From 5a09a48b2d276760bac8d8a0710ef63d57decce2 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 31 May 2026 22:48:17 +0800 Subject: [PATCH] feat: add file upload support for file handler (issue #854) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PUT method support to the file:// handler, allowing file uploads via curl -T. Upload is opt-in, disabled by default — enable with ?put=true metadata flag. Includes path traversal protection. --- handler/file/handler.go | 63 +++++++++++++++++++++++++++++++++++++++- handler/file/metadata.go | 2 ++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/handler/file/handler.go b/handler/file/handler.go index 76cc4bda..9729139d 100644 --- a/handler/file/handler.go +++ b/handler/file/handler.go @@ -4,9 +4,14 @@ package file import ( "context" + "io" "net" "net/http" "net/http/httputil" + "os" + "path" + "path/filepath" + "strings" "sync" "time" @@ -52,7 +57,18 @@ func (h *fileHandler) Init(md md.Metadata) (err error) { return } - h.handler = http.FileServer(http.Dir(h.md.dir)) + fs := http.FileServer(http.Dir(h.md.dir)) + h.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut { + if !h.md.put { + http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) + return + } + h.handlePUT(w, r) + return + } + fs.ServeHTTP(w, r) + }) h.server = &http.Server{ Handler: http.HandlerFunc(h.handleFunc), } @@ -101,6 +117,51 @@ func (h *fileHandler) Close() error { return h.server.Close() } +func (h *fileHandler) handlePUT(w http.ResponseWriter, r *http.Request) { + // Reject path traversal attempts. + target := path.Clean(r.URL.Path) + if strings.Contains(target, "..") || !strings.HasPrefix(target, "/") { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + + dest := filepath.Join(h.md.dir, filepath.FromSlash(target)) + // Verify the resolved path is within the base directory. + base, err := filepath.Abs(h.md.dir) + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + absDest, err := filepath.Abs(dest) + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + if !strings.HasPrefix(absDest, base) { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + if err := os.MkdirAll(filepath.Dir(absDest), 0755); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + f, err := os.Create(absDest) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer f.Close() + + if _, err := io.Copy(f, r.Body); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) +} + func (h *fileHandler) handleFunc(w http.ResponseWriter, r *http.Request) { start := time.Now() diff --git a/handler/file/metadata.go b/handler/file/metadata.go index 064b34ca..9ff28f08 100644 --- a/handler/file/metadata.go +++ b/handler/file/metadata.go @@ -7,9 +7,11 @@ import ( type metadata struct { dir string + put bool } func (h *fileHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.dir = mdutil.GetString(md, "file.dir", "dir") + h.md.put = mdutil.GetBool(md, "file.put", "put") return }