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/
|
.openapi-generator/
|
||||||
|
|
||||||
web/manager-api/dist/
|
web/manager-api/dist/
|
||||||
|
web/static/
|
||||||
|
9
Makefile
9
Makefile
@ -28,6 +28,7 @@ with-deps:
|
|||||||
application: flamenco-manager flamenco-worker webapp
|
application: flamenco-manager flamenco-worker webapp
|
||||||
|
|
||||||
flamenco-manager:
|
flamenco-manager:
|
||||||
|
$(MAKE) webapp-static
|
||||||
go build -v ${BUILD_FLAGS} ${PKG}/cmd/flamenco-manager
|
go build -v ${BUILD_FLAGS} ${PKG}/cmd/flamenco-manager
|
||||||
|
|
||||||
flamenco-worker:
|
flamenco-worker:
|
||||||
@ -42,6 +43,14 @@ flamenco-worker_race:
|
|||||||
webapp:
|
webapp:
|
||||||
yarn --cwd web/app install
|
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: generate-go generate-py generate-js
|
||||||
|
|
||||||
generate-go:
|
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
|
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
|
make webapp
|
||||||
yarn install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run the frontend development server with:
|
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
|
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
|
using `localhost`, and if you're not testing on `localhost` this stands out
|
||||||
more.
|
more.
|
||||||
|
|
||||||
|
The web interface is also "baked" into the `flamenco-manager` binary when using
|
||||||
|
`make flamenco-manager`.
|
||||||
|
|
||||||
|
|
||||||
## Generating Code
|
## Generating Code
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ import (
|
|||||||
"git.blender.org/flamenco/internal/upnp_ssdp"
|
"git.blender.org/flamenco/internal/upnp_ssdp"
|
||||||
"git.blender.org/flamenco/pkg/api"
|
"git.blender.org/flamenco/pkg/api"
|
||||||
"git.blender.org/flamenco/pkg/shaman"
|
"git.blender.org/flamenco/pkg/shaman"
|
||||||
|
"git.blender.org/flamenco/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
var cliArgs struct {
|
var cliArgs struct {
|
||||||
@ -285,12 +286,6 @@ func buildWebService(
|
|||||||
return c.JSON(http.StatusOK, swagger)
|
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.
|
// Serve UPnP service descriptions.
|
||||||
if ssdp != nil {
|
if ssdp != nil {
|
||||||
e.GET(ssdp.DescriptionPath(), func(c echo.Context) error {
|
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
|
// Log available routes
|
||||||
routeLogger := log.Level(zerolog.TraceLevel)
|
routeLogger := log.Level(zerolog.TraceLevel)
|
||||||
routeLogger.Trace().Msg("available routes:")
|
routeLogger.Trace().Msg("available routes:")
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { fileURLToPath, URL } from 'url'
|
import { fileURLToPath, URL } from 'url'
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import { resolve } from 'path'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
@ -10,5 +11,5 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': 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…
x
Reference in New Issue
Block a user