feat: add file upload support for file handler (issue #854)

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.
This commit is contained in:
ginuerzh
2026-05-31 22:48:17 +08:00
parent dd19e4387a
commit 5a09a48b2d
2 changed files with 64 additions and 1 deletions
+62 -1
View File
@@ -4,9 +4,14 @@ package file
import ( import (
"context" "context"
"io"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"os"
"path"
"path/filepath"
"strings"
"sync" "sync"
"time" "time"
@@ -52,7 +57,18 @@ func (h *fileHandler) Init(md md.Metadata) (err error) {
return 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{ h.server = &http.Server{
Handler: http.HandlerFunc(h.handleFunc), Handler: http.HandlerFunc(h.handleFunc),
} }
@@ -101,6 +117,51 @@ func (h *fileHandler) Close() error {
return h.server.Close() 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) { func (h *fileHandler) handleFunc(w http.ResponseWriter, r *http.Request) {
start := time.Now() start := time.Now()
+2
View File
@@ -7,9 +7,11 @@ import (
type metadata struct { type metadata struct {
dir string dir string
put bool
} }
func (h *fileHandler) parseMetadata(md mdata.Metadata) (err error) { func (h *fileHandler) parseMetadata(md mdata.Metadata) (err error) {
h.md.dir = mdutil.GetString(md, "file.dir", "dir") h.md.dir = mdutil.GetString(md, "file.dir", "dir")
h.md.put = mdutil.GetBool(md, "file.put", "put")
return return
} }