gui: don't run fetch-gui on make
- Fail gracefully if `make fetch-gui` hasn't been run - Return errors instead of panic or fatal errrors - Don't run `make fetch-gui` on every make since we have it in the workflow
This commit is contained in:
@@ -20,5 +20,3 @@ __pycache__
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
resource_windows_*.syso
|
resource_windows_*.syso
|
||||||
.devcontainer
|
.devcontainer
|
||||||
# GUI dist is fetched at build time via `make fetch-gui`
|
|
||||||
cmd/gui/dist/
|
|
||||||
@@ -47,7 +47,7 @@ LDFLAGS=--ldflags "-s -X github.com/rclone/rclone/fs.Version=$(TAG)"
|
|||||||
|
|
||||||
.PHONY: rclone test_all vars version fetch-gui
|
.PHONY: rclone test_all vars version fetch-gui
|
||||||
|
|
||||||
rclone: fetch-gui
|
rclone:
|
||||||
ifeq ($(GO_OS),windows)
|
ifeq ($(GO_OS),windows)
|
||||||
go run bin/resource_windows.go -version $(TAG) -syso resource_windows_`go env GOARCH`.syso
|
go run bin/resource_windows.go -version $(TAG) -syso resource_windows_`go env GOARCH`.syso
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ rm -rf "${DEST}"
|
|||||||
mkdir -p "${DEST}"
|
mkdir -p "${DEST}"
|
||||||
unzip -q "${TMPFILE}" -d "${DEST}"
|
unzip -q "${TMPFILE}" -d "${DEST}"
|
||||||
|
|
||||||
|
# Restore marker files
|
||||||
|
git checkout "${DEST}"/.gitignore
|
||||||
|
git checkout "${DEST}"/README.md
|
||||||
|
|
||||||
# Write tag for cache comparison
|
# Write tag for cache comparison
|
||||||
echo -n "${TAG}" > "${TAG_FILE}"
|
echo -n "${TAG}" > "${TAG_FILE}"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# This directory gets the web gui release which we ignore
|
||||||
|
*
|
||||||
|
# Except this file which we use to keep the directory available
|
||||||
|
!.gitignore
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
Use `make fetch-gui` to populate this directory.
|
||||||
+25
-20
@@ -80,11 +80,11 @@ Use --no-auth to disable authentication entirely:
|
|||||||
"versionIntroduced": "v1.74",
|
"versionIntroduced": "v1.74",
|
||||||
"groups": "RC",
|
"groups": "RC",
|
||||||
},
|
},
|
||||||
Run: func(command *cobra.Command, args []string) {
|
RunE: func(command *cobra.Command, args []string) error {
|
||||||
cmd.CheckArgs(0, 0, command, args)
|
cmd.CheckArgs(0, 0, command, args)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// --- 1. Create the GUI server (binds port eagerly, before Serve) ---
|
// Create the GUI server (binds port eagerly, before Serve)
|
||||||
guiCfg := libhttp.DefaultCfg()
|
guiCfg := libhttp.DefaultCfg()
|
||||||
if command.Flags().Changed("addr") {
|
if command.Flags().Changed("addr") {
|
||||||
guiCfg.ListenAddr = guiAddr
|
guiCfg.ListenAddr = guiAddr
|
||||||
@@ -93,13 +93,13 @@ Use --no-auth to disable authentication entirely:
|
|||||||
}
|
}
|
||||||
guiServer, err := libhttp.NewServer(ctx, libhttp.WithConfig(guiCfg))
|
guiServer, err := libhttp.NewServer(ctx, libhttp.WithConfig(guiCfg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Fatalf(nil, "Failed to create GUI server: %v", err)
|
return fmt.Errorf("failed to create GUI server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the GUI origin from the bound address (available before Serve).
|
// Read the GUI origin from the bound address (available before Serve).
|
||||||
guiOrigin := originFromURL(guiServer.URLs()[0])
|
guiOrigin := originFromURL(guiServer.URLs()[0])
|
||||||
|
|
||||||
// --- 2. Configure the RC API server ---
|
// Configure the RC API server
|
||||||
opt := rc.Opt // copy global defaults
|
opt := rc.Opt // copy global defaults
|
||||||
opt.Enabled = true
|
opt.Enabled = true
|
||||||
opt.WebUI = false
|
opt.WebUI = false
|
||||||
@@ -110,7 +110,7 @@ Use --no-auth to disable authentication entirely:
|
|||||||
} else {
|
} else {
|
||||||
port, err := freePort()
|
port, err := freePort()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Fatalf(nil, "Failed to find a free port for RC: %v", err)
|
return fmt.Errorf("failed to find a free port for RC: %w", err)
|
||||||
}
|
}
|
||||||
opt.HTTP.ListenAddr = []string{fmt.Sprintf("localhost:%d", port)}
|
opt.HTTP.ListenAddr = []string{fmt.Sprintf("localhost:%d", port)}
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ Use --no-auth to disable authentication entirely:
|
|||||||
opt.EnableMetrics = enableMetrics
|
opt.EnableMetrics = enableMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Generate credentials if needed ---
|
// Generate credentials if needed
|
||||||
if command.Flags().Changed("user") {
|
if command.Flags().Changed("user") {
|
||||||
opt.Auth.BasicUser = user
|
opt.Auth.BasicUser = user
|
||||||
}
|
}
|
||||||
@@ -142,28 +142,28 @@ Use --no-auth to disable authentication entirely:
|
|||||||
if opt.Auth.BasicPass == "" {
|
if opt.Auth.BasicPass == "" {
|
||||||
randomPass, err := random.Password(128)
|
randomPass, err := random.Password(128)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fs.Fatalf(nil, "Failed to make password: %v", err)
|
return fmt.Errorf("failed to make password: %w", err)
|
||||||
}
|
}
|
||||||
opt.Auth.BasicPass = randomPass
|
opt.Auth.BasicPass = randomPass
|
||||||
fs.Infof(nil, "No password specified. Using random password: %s", randomPass)
|
fs.Infof(nil, "No password specified. Using random password: %s", randomPass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. Start the RC server (unchanged rcserver.Start) ---
|
// Start the RC server (unchanged rcserver.Start)
|
||||||
rcServer, err := rcserver.Start(ctx, &opt)
|
rcServer, err := rcserver.Start(ctx, &opt)
|
||||||
if err != nil {
|
if err != nil || rcServer == nil {
|
||||||
fs.Fatalf(nil, "Failed to start RC server: %v", err)
|
return fmt.Errorf("failed to start RC server: %w", err)
|
||||||
}
|
|
||||||
if rcServer == nil {
|
|
||||||
fs.Fatal(nil, "RC server not configured")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the RC URL from the address we configured (rcserver.Server
|
// Build the RC URL from the address we configured (rcserver.Server
|
||||||
// does not expose URLs, and we know the address we passed in).
|
// does not expose URLs, and we know the address we passed in).
|
||||||
rcURL := "http://" + opt.HTTP.ListenAddr[0] + "/"
|
rcURL := "http://" + opt.HTTP.ListenAddr[0] + "/"
|
||||||
|
|
||||||
// --- 5. Mount the embedded GUI handler and start serving ---
|
// Mount the embedded GUI handler and start serving
|
||||||
spaHandler := guiHandler()
|
spaHandler, err := guiHandler()
|
||||||
|
if err != nil || spaHandler == nil {
|
||||||
|
return fmt.Errorf("failed to start GUI handler: %w", err)
|
||||||
|
}
|
||||||
guiServer.Router().Get("/*", spaHandler.ServeHTTP)
|
guiServer.Router().Get("/*", spaHandler.ServeHTTP)
|
||||||
guiServer.Router().Head("/*", spaHandler.ServeHTTP)
|
guiServer.Router().Head("/*", spaHandler.ServeHTTP)
|
||||||
guiServer.Serve()
|
guiServer.Serve()
|
||||||
@@ -171,7 +171,7 @@ Use --no-auth to disable authentication entirely:
|
|||||||
guiURL := guiServer.URLs()[0]
|
guiURL := guiServer.URLs()[0]
|
||||||
fs.Logf(nil, "Serving GUI on %s", guiURL)
|
fs.Logf(nil, "Serving GUI on %s", guiURL)
|
||||||
|
|
||||||
// --- 6. Open browser ---
|
// Open browser
|
||||||
loginURL := buildLoginURL(guiURL, rcURL, opt.Auth.BasicUser, opt.Auth.BasicPass, opt.NoAuth)
|
loginURL := buildLoginURL(guiURL, rcURL, opt.Auth.BasicUser, opt.Auth.BasicPass, opt.NoAuth)
|
||||||
|
|
||||||
fs.Logf(nil, "GUI available at %s", loginURL)
|
fs.Logf(nil, "GUI available at %s", loginURL)
|
||||||
@@ -181,7 +181,7 @@ Use --no-auth to disable authentication entirely:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 7. Wait for either server to exit, then shut both down ---
|
// Wait for either server to exit, then shut both down
|
||||||
defer systemd.Notify()()
|
defer systemd.Notify()()
|
||||||
done := make(chan struct{}, 2)
|
done := make(chan struct{}, 2)
|
||||||
go func() { rcServer.Wait(); done <- struct{}{} }()
|
go func() { rcServer.Wait(); done <- struct{}{} }()
|
||||||
@@ -189,6 +189,7 @@ Use --no-auth to disable authentication entirely:
|
|||||||
<-done
|
<-done
|
||||||
_ = rcServer.Shutdown()
|
_ = rcServer.Shutdown()
|
||||||
_ = guiServer.Shutdown()
|
_ = guiServer.Shutdown()
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,10 +215,14 @@ func originFromURL(rawURL string) string {
|
|||||||
|
|
||||||
// guiHandler returns an http.Handler that serves the embedded GUI bundle
|
// guiHandler returns an http.Handler that serves the embedded GUI bundle
|
||||||
// with SPA fallback: paths that don't match a real file return index.html.
|
// with SPA fallback: paths that don't match a real file return index.html.
|
||||||
func guiHandler() http.Handler {
|
func guiHandler() (http.Handler, error) {
|
||||||
sub, err := iofs.Sub(assets, "dist")
|
sub, err := iofs.Sub(assets, "dist")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("gui: embedded dist missing: " + err.Error())
|
return nil, fmt.Errorf("embedded GUI dir not found: was `make fetch-gui` run before building?: %w", err)
|
||||||
|
}
|
||||||
|
_, err = iofs.Stat(sub, "index.html")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("embedded GUI not found: was `make fetch-gui` run before building?: %w", err)
|
||||||
}
|
}
|
||||||
fileServer := http.FileServer(http.FS(sub))
|
fileServer := http.FileServer(http.FS(sub))
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -233,7 +238,7 @@ func guiHandler() http.Handler {
|
|||||||
// client-side routing (e.g. /login) works.
|
// client-side routing (e.g. /login) works.
|
||||||
r.URL.Path = "/"
|
r.URL.Path = "/"
|
||||||
fileServer.ServeHTTP(w, r)
|
fileServer.ServeHTTP(w, r)
|
||||||
})
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// guiBaseURL is the GUI server's URL. rcURL is the RC API server's URL.
|
// guiBaseURL is the GUI server's URL. rcURL is the RC API server's URL.
|
||||||
|
|||||||
+15
-4
@@ -94,8 +94,19 @@ func TestFreePort(t *testing.T) {
|
|||||||
assert.Less(t, port, 65536)
|
assert.Less(t, port, 65536)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newTestHandler returns a guiHandler, or skips the test if the embedded
|
||||||
|
// GUI bundle is not present (i.e. `make fetch-gui` has not been run).
|
||||||
|
func newTestHandler(t *testing.T) http.Handler {
|
||||||
|
t.Helper()
|
||||||
|
h, err := guiHandler()
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("skipping: GUI dist not embedded (run `make fetch-gui`): %v", err)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandlerServesIndexHTML(t *testing.T) {
|
func TestHandlerServesIndexHTML(t *testing.T) {
|
||||||
h := guiHandler()
|
h := newTestHandler(t)
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h.ServeHTTP(w, req)
|
h.ServeHTTP(w, req)
|
||||||
@@ -109,7 +120,7 @@ func TestHandlerServesIndexHTML(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerServesStaticAssets(t *testing.T) {
|
func TestHandlerServesStaticAssets(t *testing.T) {
|
||||||
h := guiHandler()
|
h := newTestHandler(t)
|
||||||
req := httptest.NewRequest("GET", "/icon.svg", nil)
|
req := httptest.NewRequest("GET", "/icon.svg", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h.ServeHTTP(w, req)
|
h.ServeHTTP(w, req)
|
||||||
@@ -123,7 +134,7 @@ func TestHandlerServesStaticAssets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerSPAFallback(t *testing.T) {
|
func TestHandlerSPAFallback(t *testing.T) {
|
||||||
h := guiHandler()
|
h := newTestHandler(t)
|
||||||
|
|
||||||
// /login is not a real file — it should fall back to index.html
|
// /login is not a real file — it should fall back to index.html
|
||||||
req := httptest.NewRequest("GET", "/login", nil)
|
req := httptest.NewRequest("GET", "/login", nil)
|
||||||
@@ -140,7 +151,7 @@ func TestHandlerSPAFallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandlerSPAFallbackDeepPath(t *testing.T) {
|
func TestHandlerSPAFallbackDeepPath(t *testing.T) {
|
||||||
h := guiHandler()
|
h := newTestHandler(t)
|
||||||
|
|
||||||
req := httptest.NewRequest("GET", "/some/deep/route", nil)
|
req := httptest.NewRequest("GET", "/some/deep/route", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
Reference in New Issue
Block a user