perf(panel): speed up file browsing and preserve loading forms

This commit is contained in:
Lemon-miaow committed 2026-10-04 20:18:11 +08:00
1 parent 4d8e9af240
commit 2c98e8b2ab
27 files changed
+1387 -145

No files matched your search

+3
View File
@@ -99,6 +99,7 @@ type API struct {
// InternalBaseURL is where that Job reaches felis-api's internal face to do so
// (handleUploadFile). Uploads report 503 unless both are set.
FileStage *fileedit.Stage
FileBrowser *fileedit.Browser
InternalBaseURL string
// Exporter starts the Job behind a world or backup download (exports.go),
@@ -476,6 +477,8 @@ func (a *API) internalAPIRoutes() []apiRoute {
// with the upload is the check (handlers_files.go).
{Method: "GET", Pattern: "/api/v1/internal/file-uploads/{id}", Public: true, h: a.handleInternalFileUpload},
{Method: "DELETE", Pattern: "/api/v1/internal/file-uploads/{id}", Public: true, h: a.handleInternalFileUploadLanded},
// Read-only file Jobs pull commands with their own scoped bearer token.
{Method: "POST", Pattern: "/api/v1/internal/file-browser/{id}", Public: true, h: a.handleInternalFileBrowser},
// An export Job's archive, held open until the owner's browser downloads
// it. Public for the same reason as file uploads: the Job holds no service
+9 -1
View File
@@ -30,7 +30,7 @@ import (
// internal/fileedit, which explains why that transport needs no RBAC felis-api
// does not already hold. Unlike Restorer and Backuper these calls are
// SYNCHRONOUS: the caller wants the listing or the bytes, so the handler blocks on
// the Job (seconds, dominated by Pod scheduling) rather than answering 202.
// the result rather than answering 202.
//
// It is an interface so the handlers are unit-tested against a fake; the
// production implementation is *fileedit.Editor. Using fileedit.Entry directly
@@ -61,6 +61,14 @@ type FileEditor interface {
Ops(ctx context.Context, server string) ([]fileedit.OpState, error)
}
func (a *API) handleInternalFileBrowser(w http.ResponseWriter, r *http.Request) {
if a.FileBrowser == nil {
writeError(w, r, newError(http.StatusServiceUnavailable, "files_unavailable", "file browser is not configured"))
return
}
a.FileBrowser.ServeHTTP(w, r)
}
// writeFileRequest is the PUT /servers/{name}/file body. Content is []byte, so
// encoding/json requires it to be base64 — which is what makes the write path
// binary-safe: a config file with CRLF line endings, a UTF-8 BOM, or a stray
+18
View File
@@ -53,6 +53,24 @@ type fakeFileEditor struct {
ops []fileedit.OpState
}
func TestFileBrowserStaysOnInternalFaceWithScopedAuthentication(t *testing.T) {
a := newTestAPI(newFakeRepo(), newFakeCluster())
a.Internal = CallerTokens{CallerVelocity: "service-token"}
a.External = staticExternal{p: &Principal{UserID: "owner", Role: "owner", ViaAdminAccess: true}}
a.FileBrowser = &fileedit.Browser{}
for _, h := range []http.Handler{a.InternalHandler(), a.ExternalHandler()} {
w := do(h, "POST", fileedit.BrowserRoute+"unknown", `{}`, map[string]string{"Authorization": "Bearer service-token"})
if w.Code != http.StatusNotFound {
t.Fatalf("service token opened a browser: status %d (%s)", w.Code, w.Body.String())
}
}
a.FileBrowser = nil
w := do(a.InternalHandler(), "POST", fileedit.BrowserRoute+"unknown", `{}`, nil)
if w.Code != http.StatusServiceUnavailable || decodeErr(t, w) != "files_unavailable" {
t.Fatalf("unconfigured browser: status %d (%s)", w.Code, w.Body.String())
}
}
func (f *fakeFileEditor) List(_ context.Context, server, path string) (fileedit.Listing, error) {
f.calls++
f.gotServer, f.gotPath = server, path