Flamenco/web/web_app.go
Sybren A. Stüvel 7b028df8ac 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`.
2022-06-27 14:53:42 +02:00

71 lines
1.8 KiB
Go

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