diff --git a/.gitignore b/.gitignore index c00edc63..67fde929 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ __pycache__ .openapi-generator/ web/manager-api/dist/ +web/static/ diff --git a/Makefile b/Makefile index 0f3ead56..e391440f 100644 --- a/Makefile +++ b/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: diff --git a/README.md b/README.md index 904ffbee..c6343b02 100644 --- a/README.md +++ b/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 diff --git a/cmd/flamenco-manager/main.go b/cmd/flamenco-manager/main.go index 16552571..bb09279a 100644 --- a/cmd/flamenco-manager/main.go +++ b/cmd/flamenco-manager/main.go @@ -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:") diff --git a/web/app/vite.config.js b/web/app/vite.config.js index 116273fb..27afca90 100644 --- a/web/app/vite.config.js +++ b/web/app/vite.config.js @@ -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)) } - } + }, }) diff --git a/web/web_app.go b/web/web_app.go new file mode 100644 index 00000000..57af3dfa --- /dev/null +++ b/web/web_app.go @@ -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} +}