From 878979af70e0e93678a809bb010472d1e8f719fc Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 5 Nov 2026 10:11:52 -0800 Subject: [PATCH] filer: hide write controls in the UI on the read-only port (#12429) * filer: hide write controls in the UI on the read-only port The read-only port accepts only GET and HEAD, but its directory page offered New Folder, Upload, drag-and-drop, rename and delete, each of which failed with 405. Requests that come through the read-only handler now render the page without those controls or the upload script. Co-Authored-By: Claude Opus 5.5 * filer: test the directory page controls on each port Co-Authored-By: Claude Opus 5.5 * filer: match the rendered row buttons in the port test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- weed/server/filer_server_handlers.go | 2 +- weed/server/filer_server_handlers_read_dir.go | 6 ++ .../filer_server_handlers_read_dir_test.go | 69 +++++++++++++++++++ weed/server/filer_ui/filer.html | 8 +++ 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 weed/server/filer_server_handlers_read_dir_test.go diff --git a/weed/server/filer_server_handlers.go b/weed/server/filer_server_handlers.go index 4bdd5d87f9..34e6fe3ad7 100644 --- a/weed/server/filer_server_handlers.go +++ b/weed/server/filer_server_handlers.go @@ -194,7 +194,7 @@ func (fs *FilerServer) readonlyFilerHandler(w http.ResponseWriter, r *http.Reque switch r.Method { case http.MethodGet, http.MethodHead: - fs.GetOrHeadHandler(w, r) + fs.GetOrHeadHandler(w, r.WithContext(context.WithValue(r.Context(), readOnlyListingKey{}, true))) default: requestMethod = "INVALID" w.WriteHeader(http.StatusMethodNotAllowed) diff --git a/weed/server/filer_server_handlers_read_dir.go b/weed/server/filer_server_handlers_read_dir.go index 380816a4de..348f422ea3 100644 --- a/weed/server/filer_server_handlers_read_dir.go +++ b/weed/server/filer_server_handlers_read_dir.go @@ -13,6 +13,10 @@ import ( "github.com/seaweedfs/seaweedfs/weed/util/version" ) +// readOnlyListingKey marks a request served on the read-only port, whose +// listing page must not offer write operations. +type readOnlyListingKey struct{} + // listDirectoryHandler lists directories and folders under a directory // files are sorted by name and paginated via "lastFileName" and "limit". // sub directories are listed on the first page, when "lastFileName" @@ -91,6 +95,7 @@ func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Reque ShouldDisplayLoadMore bool EmptyFolder bool ShowDirectoryDelete bool + ReadOnly bool }{ version.Version(), path, @@ -101,6 +106,7 @@ func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Reque shouldDisplayLoadMore, emptyFolder, fs.option.ShowUIDirectoryDelete, + ctx.Value(readOnlyListingKey{}) != nil, }) if err != nil { glog.V(0).InfofCtx(ctx, "Template Execute Error: %v", err) diff --git a/weed/server/filer_server_handlers_read_dir_test.go b/weed/server/filer_server_handlers_read_dir_test.go new file mode 100644 index 0000000000..31cb1f1bc2 --- /dev/null +++ b/weed/server/filer_server_handlers_read_dir_test.go @@ -0,0 +1,69 @@ +package weed_server + +import ( + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/filer" + "github.com/seaweedfs/seaweedfs/weed/security" +) + +func TestListDirectoryWriteControlsFollowPort(t *testing.T) { + mtime := time.Unix(1700000000, 0) + store := newRenameTestStore() + store.entries["/docs"] = &filer.Entry{FullPath: "/docs", Attr: filer.Attr{Mtime: mtime, Mode: os.ModeDir | 0755}} + store.entries["/docs/sub"] = &filer.Entry{FullPath: "/docs/sub", Attr: filer.Attr{Mtime: mtime, Mode: os.ModeDir | 0755}} + store.entries["/docs/a.txt"] = &filer.Entry{FullPath: "/docs/a.txt", Attr: filer.Attr{Mtime: mtime, Mode: 0644}} + store.entries["/empty"] = &filer.Entry{FullPath: "/empty", Attr: filer.Attr{Mtime: mtime, Mode: os.ModeDir | 0755}} + fs := &FilerServer{ + filer: newRenameTestFiler(t, store), + option: &FilerOption{ExposeDirectoryData: true, DirListingLimit: 100, ShowUIDirectoryDelete: true}, + filerGuard: security.NewGuard(nil, "", 0, "", 0), + } + + writeControls := []string{ + `onclick="handleCreateDir()"`, + `for="fileElem"`, + `for="dirElem"`, + `onclick="handleRename(`, + `onclick="handleDelete(`, + `class="row add-files"`, + "function uploadFile(", + } + + render := func(t *testing.T, handler http.HandlerFunc, path string) string { + t.Helper() + rec := httptest.NewRecorder() + handler(rec, httptest.NewRequest(http.MethodGet, path, nil)) + if rec.Code != http.StatusOK { + t.Fatalf("GET %s: status %d, body %s", path, rec.Code, rec.Body.String()) + } + return rec.Body.String() + } + + for _, path := range []string{"/docs/", "/empty/"} { + readOnly := render(t, fs.readonlyFilerHandler, path) + if !strings.Contains(readOnly, "
    ") { + t.Fatalf("read-only listing of %s is not the directory page:\n%s", path, readOnly) + } + for _, control := range writeControls { + if strings.Contains(readOnly, control) { + t.Errorf("read-only listing of %s offers %q", path, control) + } + } + } + if readOnly := render(t, fs.readonlyFilerHandler, "/docs/"); !strings.Contains(readOnly, "a.txt") || !strings.Contains(readOnly, "sub") { + t.Errorf("read-only listing lost its entries:\n%s", readOnly) + } + + writable := render(t, fs.GetOrHeadHandler, "/docs/") + render(t, fs.GetOrHeadHandler, "/empty/") + for _, control := range writeControls { + if !strings.Contains(writable, control) { + t.Errorf("listing on the main port is missing %q", control) + } + } +} diff --git a/weed/server/filer_ui/filer.html b/weed/server/filer_ui/filer.html index 5653f41dd8..383ad29b40 100644 --- a/weed/server/filer_ui/filer.html +++ b/weed/server/filer_ui/filer.html @@ -87,6 +87,7 @@ +{{ if not .ReadOnly }} +{{ end }}