Manager: serve static files of the webapp at /app/
Vue Router generates URLs for which there are no static files on the filesystem (like `/jobs/{job ID}`). To make this work, the webapp's `index.html` has to be served for such requests. The client-side JavaScript then figures out how things fit together, and can even render a nice 404 page if necessary. This shouldn't happen for non-webapp URLs, though. Because of this, the entire webapp (including the "serve `index.html` if file not found logic) is moved to a `/app/` base URL. `make flamenco-manager` now also builds the webapp and embeds the static files into the binary. `make flamenco-manager_race` does NOT rebuild the static web files, to help speed up of debug cycles. Run `make webapp-static` to rebuild the webapp itself, if necessary, or run a separate web development server with `yarn --cwd web/app run dev --host`.
This commit is contained in:
parent
7f2cf384b0
commit
7b028df8ac
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,3 +20,4 @@ __pycache__
|
||||
.openapi-generator/
|
||||
|
||||
web/manager-api/dist/
|
||||
web/static/
|
||||
|
9
Makefile
9
Makefile
@ -28,6 +28,7 @@ with-deps:
|
||||
application: flamenco-manager flamenco-worker webapp
|
||||
|
||||
flamenco-manager:
|
||||
$(MAKE) webapp-static
|
||||
go build -v ${BUILD_FLAGS} ${PKG}/cmd/flamenco-manager
|
||||
|
||||
flamenco-worker:
|
||||
@ -42,6 +43,14 @@ flamenco-worker_race:
|
||||
webapp:
|
||||
yarn --cwd web/app install
|
||||
|
||||
webapp-static:
|
||||
rm -rf web/static
|
||||
# When changing the base URL, also update the line
|
||||
# e.GET("/app/*", echo.WrapHandler(webAppHandler))
|
||||
# in `cmd/flamenco-manager/main.go`
|
||||
yarn --cwd web/app build --outDir ../static --base=/app/
|
||||
@echo "Web app has been installed into web/static"
|
||||
|
||||
generate: generate-go generate-py generate-js
|
||||
|
||||
generate-go:
|
||||
|
10
README.md
10
README.md
@ -36,16 +36,15 @@ The web UI is built with Vue, Bootstrap, and Socket.IO for communication with th
|
||||
sudo snap install node --classic --channel=16
|
||||
```
|
||||
|
||||
This also gives you the Yarn package manager, which can be used to install web dependencies and build the frontend files.
|
||||
This also gives you the Yarn package manager, which can be used to install web dependencies and build the frontend files via:
|
||||
|
||||
```
|
||||
cd web/app
|
||||
yarn install
|
||||
make webapp
|
||||
```
|
||||
|
||||
Then run the frontend development server with:
|
||||
```
|
||||
yarn run dev --host
|
||||
yarn --cwd web/app run dev --host
|
||||
```
|
||||
|
||||
The `--host` parameter is optional but recommended. The downside is that it
|
||||
@ -54,6 +53,9 @@ easier to detect configuration issues. The generated OpenAPI client defaults to
|
||||
using `localhost`, and if you're not testing on `localhost` this stands out
|
||||
more.
|
||||
|
||||
The web interface is also "baked" into the `flamenco-manager` binary when using
|
||||
`make flamenco-manager`.
|
||||
|
||||
|
||||
## Generating Code
|
||||
|
||||
|
@ -44,6 +44,7 @@ import (
|
||||
"git.blender.org/flamenco/internal/upnp_ssdp"
|
||||
"git.blender.org/flamenco/pkg/api"
|
||||
"git.blender.org/flamenco/pkg/shaman"
|
||||
"git.blender.org/flamenco/web"
|
||||
)
|
||||
|
||||
var cliArgs struct {
|
||||
@ -285,12 +286,6 @@ func buildWebService(
|
||||
return c.JSON(http.StatusOK, swagger)
|
||||
})
|
||||
|
||||
// Temporarily redirect the index page to the Swagger UI, so that at least you
|
||||
// can see something.
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/api/swagger-ui/")
|
||||
})
|
||||
|
||||
// Serve UPnP service descriptions.
|
||||
if ssdp != nil {
|
||||
e.GET(ssdp.DescriptionPath(), func(c echo.Context) error {
|
||||
@ -298,6 +293,21 @@ func buildWebService(
|
||||
})
|
||||
}
|
||||
|
||||
// Serve static files for the webapp on /app/.
|
||||
webAppHandler, err := web.WebAppHandler()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("unable to set up HTTP server for embedded web app")
|
||||
}
|
||||
e.GET("/app/*", echo.WrapHandler(http.StripPrefix("/app", webAppHandler)))
|
||||
e.GET("/app", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/app/")
|
||||
})
|
||||
|
||||
// Redirect / to the webapp.
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/app/")
|
||||
})
|
||||
|
||||
// Log available routes
|
||||
routeLogger := log.Level(zerolog.TraceLevel)
|
||||
routeLogger.Trace().Msg("available routes:")
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { fileURLToPath, URL } from 'url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import { resolve } from 'path'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
@ -10,5 +11,5 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
70
web/web_app.go
Normal file
70
web/web_app.go
Normal file
@ -0,0 +1,70 @@
|
||||
package web
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
var webStaticFS embed.FS
|
||||
|
||||
// WebAppHandler returns a HTTP handler to serve the static files of the Flamenco Manager web app.
|
||||
func WebAppHandler() (http.Handler, error) {
|
||||
// Strip the 'static/' directory off of the embedded filesystem.
|
||||
fs, err := fs.Sub(webStaticFS, "static")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to wrap embedded filesystem: %w", err)
|
||||
}
|
||||
|
||||
// Serve `index.html` from the root directory if the requested file cannot be
|
||||
// found.
|
||||
wrappedFS := WrapFS(fs, "index.html")
|
||||
|
||||
return http.FileServer(http.FS(wrappedFS)), nil
|
||||
}
|
||||
|
||||
// FSWrapper wraps a filesystem and falls back to serving a specific file when
|
||||
// the requested file cannot be found.
|
||||
//
|
||||
// This is necesasry for compatibility with Vue Router, as that generates URL
|
||||
// paths to files that don't exist on the filesystem, like
|
||||
// `/workers/c441766a-5d28-47cb-9589-b0caa4269065`. Serving `/index.html` in
|
||||
// such cases makes Vue Router understand what's going on again.
|
||||
type FSWrapper struct {
|
||||
fs fs.FS
|
||||
fallback string
|
||||
}
|
||||
|
||||
func (w *FSWrapper) Open(name string) (fs.File, error) {
|
||||
file, err := w.fs.Open(name)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
return file, nil
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
fallbackFile, fallbackErr := w.fs.Open(w.fallback)
|
||||
if fallbackErr != nil {
|
||||
log.Error().
|
||||
Str("name", name).
|
||||
Str("fallback", w.fallback).
|
||||
Err(err).
|
||||
Str("fallbackErr", fallbackErr.Error()).
|
||||
Msg("static web server: error opening fallback file")
|
||||
return file, err
|
||||
}
|
||||
return fallbackFile, nil
|
||||
}
|
||||
|
||||
return file, err
|
||||
}
|
||||
|
||||
func WrapFS(fs fs.FS, fallback string) *FSWrapper {
|
||||
return &FSWrapper{fs: fs, fallback: fallback}
|
||||
}
|
Loading…
Reference in New Issue
Block a user