Files & File Serving
The filesystem and fileserver packages provide an injectable filesystem and
an HTTP handler for serving files with an SPA-style fallback.
Registering a filesystem
filesystem.Register(ctx, cfg) registers a lazy singleton fs.FS built from a
Config — a type with FS() fs.FS. NewLocalFS(root) builds a config from a
directory path. The generated template uses it for user-uploaded files:
kernel.Register(func(ctx context.Context, c *config.Config) {
filesystem.Register(ctx, c.FileSystem)
})The Config.FileSystem field is wired in config.go:
FileSystem: filesystem.NewLocalFS("./files"),Inside a handler, inject fs.FS and use it like any Go filesystem:
type UploadHandler struct {
FS fs.FS `inject:""`
}
func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
data, err := fs.ReadFile(h.FS, r.PathValue("path"))
// ...
}Serving static files with SPA fallback
fileserver.WithFallback(root, basePath, fallbackPath, data) returns an
http.Handler that serves files from an fs.FS with a catch-all route. When
the requested path matches a real file, its content is served with the correct
MIME type; otherwise the fallback template is rendered with the provided data.
This is the standard pattern for serving a compiled frontend:
r.Handle("/assets", fileserver.WithFallback(resources.Content, "dist", "index.html", nil))basePath is prepended to every URL path before looking up the file, so
embedding files under dist/ and mapping /assets to the handler serves
dist/style.css at /assets/style.css. Directories and missing paths both
fall through to fallbackPath, which receives the data value as template
data.
Build-tag split
The generated project uses //go:build !dev and //go:build dev tags in
resources/ to swap between a production embed.FS and a development
os.DirFS. When running spice dev the dev tag is set, so file changes are
picked up without rebuilding the binary.