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:
Sybren A. Stüvel 2022-06-27 14:50:54 +02:00
parent 7f2cf384b0
commit 7b028df8ac
6 changed files with 104 additions and 11 deletions

1
.gitignore vendored

@ -20,3 +20,4 @@ __pycache__
.openapi-generator/
web/manager-api/dist/
web/static/

@ -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:

@ -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

@ -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}
}