Served the frontend's build output from server.frontend.dir
internal/frontend serves a Vite build from an fs.FS: hashed files under /assets/ are cached for a year, HTML and other files get no-cache, ETags are computed (and cached for a build that can't change), precompressed variants are served when the client accepts them, and a path without an extension falls back to index.html for client-side routes. Only GET and HEAD are allowed. server.WithFrontend serves it for every path outside /api and /ws, behind compression and the frontend group's hooks. job-search serve passes the directory in server.frontend.dir, checked for an index.html at start; empty means no frontend. Its option building moved into serverOptions, with the access log.
This commit is contained in:
1 parent
155b84caea
commit
9905ffb4cb
7 files changed
+430
-36
No files matched your search
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -16,19 +15,23 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.avsdev.uk/Go/job-search/internal/cli"
|
||||
"git.avsdev.uk/Go/job-search/internal/config"
|
||||
"git.avsdev.uk/Go/job-search/internal/events"
|
||||
"git.avsdev.uk/Go/job-search/internal/fetcher"
|
||||
"git.avsdev.uk/Go/job-search/internal/frontend"
|
||||
"git.avsdev.uk/Go/job-search/internal/logging"
|
||||
"git.avsdev.uk/Go/job-search/internal/server"
|
||||
)
|
||||
|
||||
// NewCommand returns the serve command, which serves the REST API and the
|
||||
// WebSocket endpoint, and runs a worker for every enabled source.
|
||||
// NewCommand returns the serve command, which serves the REST API, the
|
||||
// WebSocket endpoint and the frontend, and runs a worker for every enabled
|
||||
// source.
|
||||
func NewCommand(opts *cli.Options) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "Serve the REST API (/api) and WebSocket (/ws), and fetch from sources",
|
||||
Long: `Serve the REST API under /api and the WebSocket endpoint under /ws.
|
||||
Short: "Serve the REST API, WebSocket and frontend, and fetch from sources",
|
||||
Long: `Serve the REST API under /api, the WebSocket endpoint under /ws, and the
|
||||
frontend's files from server.frontend.dir under / if it is set.
|
||||
|
||||
It also runs a worker for every enabled source, as fetcher service does,
|
||||
saving what they fetch to database.file if it is set, and pushing an
|
||||
@@ -82,23 +85,13 @@ set, the API serves the stored opportunities under /api/opportunities.`,
|
||||
conf.Server.AccessLog = accessLogOverride
|
||||
}
|
||||
|
||||
var options []server.Option
|
||||
if conf.Server.AccessLog != "" {
|
||||
var (
|
||||
accessLog *log.Logger
|
||||
closeAccessLog func() error
|
||||
)
|
||||
accessLog, closeAccessLog, err = logging.OpenAccessLog(
|
||||
conf.Server.AccessLog,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, closeAccessLog())
|
||||
}()
|
||||
options = append(options, server.WithAccessLog(accessLog))
|
||||
options, closeAccessLog, err := serverOptions(&conf.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, closeAccessLog())
|
||||
}()
|
||||
|
||||
// ctx is cancelled on the first SIGINT (Ctrl+C) or SIGTERM.
|
||||
ctx, stop := signal.NotifyContext(
|
||||
@@ -201,3 +194,38 @@ set, the API serves the stored opportunities under /api/opportunities.`,
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// serverOptions returns the server's options for conf: the access log, if
|
||||
// server.access_log is set, and the frontend's files, if server.frontend.dir
|
||||
// is (checked first). closeAccessLog closes the access log; call it once the
|
||||
// server has stopped.
|
||||
func serverOptions(
|
||||
conf *config.JSCServer,
|
||||
) ([]server.Option, func() error, error) {
|
||||
var options []server.Option
|
||||
closeAccessLog := func() error {
|
||||
return nil
|
||||
}
|
||||
if conf.AccessLog != "" {
|
||||
accessLog, closeFile, err := logging.OpenAccessLog(conf.AccessLog)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
closeAccessLog = closeFile
|
||||
options = append(options, server.WithAccessLog(accessLog))
|
||||
}
|
||||
|
||||
if dir := conf.Frontend.Dir; dir != "" {
|
||||
files := os.DirFS(dir)
|
||||
err := frontend.CheckFS(files)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Join(
|
||||
fmt.Errorf("server.frontend.dir %s: %w", dir, err),
|
||||
closeAccessLog(),
|
||||
)
|
||||
}
|
||||
options = append(options, server.WithFrontend(files))
|
||||
}
|
||||
|
||||
return options, closeAccessLog, nil
|
||||
}
|
||||
Reference in new issue
Block a user