[v3, assetServer] Remove fs.FS from options and always use a http.Handler

This commit is contained in:
stffabi
2024-02-16 07:38:27 +01:00
parent 471d626043
commit 65251cdafa
21 changed files with 154 additions and 231 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ func main() {
&GreetService{},
},
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
+1 -1
View File
@@ -17,7 +17,7 @@ func main() {
Name: "Context Menu Demo",
Description: "A demo of the Context Menu API",
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
+1 -1
View File
@@ -16,7 +16,7 @@ func main() {
Name: "dev",
Description: "A demo of using raw HTML & CSS",
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
+1 -1
View File
@@ -18,7 +18,7 @@ func main() {
Name: "Drag-n-drop Demo",
Description: "A demo of the Drag-n-drop API",
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
+1 -1
View File
@@ -19,7 +19,7 @@ func main() {
Name: "Events Demo",
Description: "A demo of the Events API",
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
+1 -1
View File
@@ -17,7 +17,7 @@ func main() {
Name: "Frameless Demo",
Description: "A demo of frameless windows",
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
+4 -3
View File
@@ -3,12 +3,13 @@ package main
import (
"embed"
_ "embed"
"log"
"os"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/github"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/plugins/oauth"
"log"
"os"
)
//go:embed assets
@@ -34,7 +35,7 @@ func main() {
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
Plugins: map[string]application.Plugin{
"github.com/wailsapp/wails/v3/plugins/oauth": oAuthPlugin,
+1 -1
View File
@@ -46,7 +46,7 @@ func main() {
"start_at_login": start_at_login.NewPlugin(start_at_login.Config{}),
},
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
})
+1 -1
View File
@@ -26,7 +26,7 @@ func main() {
WebviewBrowserPath: "",
},
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
})
+1 -1
View File
@@ -30,7 +30,7 @@ func main() {
}),
},
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
})
go func() {
+1 -1
View File
@@ -20,7 +20,7 @@ func main() {
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
Assets: application.AssetOptions{
FS: assets,
Handler: application.AssetFileServerFS(assets),
},
})
@@ -2,12 +2,13 @@ package assetserver
import (
"bytes"
"context"
"embed"
"errors"
"fmt"
"io"
"io/fs"
iofs "io/fs"
"log/slog"
"net/http"
"os"
"path"
@@ -18,91 +19,53 @@ const (
indexHTML = "index.html"
)
type assetHandler struct {
fs iofs.FS
handler http.Handler
logger *slog.Logger
retryMissingFiles bool
type assetFileServer struct {
fs iofs.FS
err error
}
func NewDefaultAssetHandler(options *Options) (http.Handler, error) {
vfs := options.Assets
if vfs != nil {
if _, err := vfs.Open("."); err != nil {
return nil, err
}
subDir, err := FindPathToFile(vfs, indexHTML)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
msg := "no `index.html` could be found in your Assets fs.FS"
if embedFs, isEmbedFs := vfs.(embed.FS); isEmbedFs {
rootFolder, _ := FindEmbedRootPath(embedFs)
msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder)
}
return nil, fmt.Errorf(msg)
func newAssetFileServerFS(vfs fs.FS) http.Handler {
subDir, err := findPathToFile(vfs, indexHTML)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
msg := "no `index.html` could be found in your Assets fs.FS"
if embedFs, isEmbedFs := vfs.(embed.FS); isEmbedFs {
rootFolder, _ := findEmbedRootPath(embedFs)
msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder)
}
return nil, err
err = fmt.Errorf(msg)
}
vfs, err = iofs.Sub(vfs, path.Clean(subDir))
if err != nil {
return nil, err
}
}
var result http.Handler = &assetHandler{
fs: vfs,
handler: options.Handler,
logger: options.Logger,
}
if middleware := options.Middleware; middleware != nil {
result = middleware(result)
}
return result, nil
}
func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
url := req.URL.Path
handler := d.handler
if strings.EqualFold(req.Method, http.MethodGet) {
filename := path.Clean(strings.TrimPrefix(url, "/"))
d.logInfo("Handling request", "url", url, "file", filename)
if err := d.serveFSFile(rw, req, filename); err != nil {
if os.IsNotExist(err) {
if handler != nil {
d.logInfo("File not found. Deferring to AssetHandler", "filename", filename, "url", url)
handler.ServeHTTP(rw, req)
err = nil
} else {
rw.WriteHeader(http.StatusNotFound)
err = nil
}
}
if err != nil {
d.logError("Unable to handle request '%s': %s", url, err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
} else if handler != nil {
d.logInfo("Non-GET request. Deferring to AssetHandler", "url", url)
handler.ServeHTTP(rw, req)
} else {
rw.WriteHeader(http.StatusMethodNotAllowed)
vfs, err = iofs.Sub(vfs, path.Clean(subDir))
}
return &assetFileServer{fs: vfs, err: err}
}
func (d *assetFileServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
url := req.URL.Path
err := d.err
if err == nil {
filename := path.Clean(strings.TrimPrefix(url, "/"))
d.logInfo(ctx, "Handling request", "url", url, "file", filename)
err = d.serveFSFile(rw, req, filename)
if os.IsNotExist(err) {
rw.WriteHeader(http.StatusNotFound)
return
}
}
if err != nil {
d.logError(ctx, "Unable to handle request", "url", url, "err", err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
}
// serveFile will try to load the file from the fs.FS and write it to the response
func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error {
func (d *assetFileServer) serveFSFile(rw http.ResponseWriter, req *http.Request, filename string) error {
if d.fs == nil {
return os.ErrNotExist
}
@@ -182,10 +145,10 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi
return err
}
func (d *assetHandler) logInfo(message string, args ...interface{}) {
d.logger.Debug("[AssetHandler] "+message, args...)
func (d *assetFileServer) logInfo(ctx context.Context, message string, args ...interface{}) {
logInfo(ctx, "[AssetFileServerFS] "+message, args...)
}
func (d *assetHandler) logError(message string, args ...interface{}) {
d.logger.Error("[AssetHandler] "+message, args...)
func (d *assetFileServer) logError(ctx context.Context, message string, args ...interface{}) {
logError(ctx, "[AssetFileServerFS] "+message, args...)
}
+26 -24
View File
@@ -6,7 +6,6 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"path"
"strings"
@@ -29,34 +28,43 @@ type RuntimeHandler interface {
type AssetServer struct {
options *Options
handler http.Handler
wsHandler *httputil.ReverseProxy
handler http.Handler
pluginScripts map[string]string
devServerURL string
assetServerWebView
}
func NewAssetServer(options *Options) (*AssetServer, error) {
result := &AssetServer{
options: options,
result := &AssetServer{options: options}
userHandler := options.Handler
if userHandler == nil {
userHandler = http.NotFoundHandler()
}
var err error
result.handler, err = result.setupHandler()
if err != nil {
return nil, err
handler := http.Handler(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
result.serveHTTP(w, r, userHandler)
}))
if middleware := options.Middleware; middleware != nil {
handler = middleware(handler)
}
result.handler = handler
return result, nil
}
func (a *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
start := time.Now()
wrapped := &contentTypeSniffer{rw: rw}
a.serveHTTP(wrapped, req)
req = req.WithContext(contextWithLogger(req.Context(), a.options.Logger))
a.handler.ServeHTTP(wrapped, req)
a.options.Logger.Info(
"Asset Request:",
"windowName", req.Header.Get(webViewRequestHeaderWindowName),
@@ -68,17 +76,11 @@ func (a *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
)
}
func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request) {
if a.wsHandler != nil {
a.wsHandler.ServeHTTP(rw, req)
func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userHandler http.Handler) {
if isWebSocket(req) {
// WebSockets are not supported by the AssetServer
rw.WriteHeader(http.StatusNotImplemented)
return
} else {
if isWebSocket(req) {
// WebSockets are not supported by the AssetServer
rw.WriteHeader(http.StatusNotImplemented)
return
}
}
header := rw.Header()
@@ -91,7 +93,7 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request) {
switch path {
case "", "/", "/index.html":
recorder := httptest.NewRecorder()
a.handler.ServeHTTP(recorder, req)
userHandler.ServeHTTP(recorder, req)
for k, v := range recorder.Result().Header {
header[k] = v
}
@@ -126,7 +128,7 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request) {
if script, ok := a.pluginScripts[path]; ok {
a.writeBlob(rw, path, []byte(script))
} else {
a.handler.ServeHTTP(rw, req)
userHandler.ServeHTTP(rw, req)
return
}
}
+2 -3
View File
@@ -4,12 +4,11 @@ package assetserver
func (a *AssetServer) LogDetails() {
var info = []any{
"assetsFS", a.options.Assets != nil,
"middleware", a.options.Middleware != nil,
"handler", a.options.Handler != nil,
}
if a.devServerURL != "" {
info = append(info, "devServerURL", a.devServerURL)
if devServerURL := GetDevServerURL(); devServerURL != "" {
info = append(info, "devServerURL", devServerURL)
}
a.options.Logger.Info("AssetServer Info:", info...)
}
+15 -70
View File
@@ -4,8 +4,7 @@ package assetserver
import (
_ "embed"
"errors"
"fmt"
"io/fs"
"net/http"
"net/http/httputil"
"net/url"
@@ -19,83 +18,29 @@ func defaultIndexHTML() []byte {
return defaultHTML
}
func (a *AssetServer) setupHandler() (http.Handler, error) {
// Do we have an external dev server URL?
a.devServerURL = GetDevServerURL()
if a.devServerURL == "" {
return NewDefaultAssetHandler(a.options)
func NewAssetFileServer(vfs fs.FS) http.Handler {
devServerURL := GetDevServerURL()
if devServerURL == "" {
return newAssetFileServerFS(vfs)
}
// Parse the URL
parsedURL, err := url.Parse(a.devServerURL)
parsedURL, err := url.Parse(devServerURL)
if err != nil {
return nil, fmt.Errorf("invalid FRONTEND_DEVSERVER_URL. Should be valid URL: %s", err.Error())
return http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
logError(req.Context(), "[ExternalAssetHandler] Invalid FRONTEND_DEVSERVER_URL. Should be valid URL", "error", err.Error())
http.Error(rw, err.Error(), http.StatusInternalServerError)
})
}
baseHandler := a.options.Handler
errSkipProxy := fmt.Errorf("skip proxying")
proxy := httputil.NewSingleHostReverseProxy(parsedURL)
baseDirector := proxy.Director
proxy.Director = func(r *http.Request) {
baseDirector(r)
if a.options.Logger != nil {
a.options.Logger.Debug("ExternalAssetHandler: loading", "url", r.URL)
}
}
proxy.ModifyResponse = func(res *http.Response) error {
if baseHandler == nil {
return nil
}
if res.StatusCode == http.StatusSwitchingProtocols {
return nil
}
if res.StatusCode == http.StatusNotFound || res.StatusCode == http.StatusMethodNotAllowed {
return errSkipProxy
}
return nil
}
proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
if baseHandler != nil && errors.Is(err, errSkipProxy) {
if a.options.Logger != nil {
a.options.Logger.Debug("ExternalAssetHandler: Loading file failed, using original AssetHandler", "url", r.URL)
}
baseHandler.ServeHTTP(rw, r)
} else {
if a.options.Logger != nil {
a.options.Logger.Error("ExternalAssetHandler: Proxy error", "error", err.Error())
}
rw.WriteHeader(http.StatusBadGateway)
}
logError(r.Context(), "[ExternalAssetHandler] Proxy error", "error", err.Error())
rw.WriteHeader(http.StatusBadGateway)
}
var result http.Handler = http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet {
proxy.ServeHTTP(rw, req)
return
}
if baseHandler != nil {
baseHandler.ServeHTTP(rw, req)
return
}
rw.WriteHeader(http.StatusMethodNotAllowed)
})
if middleware := a.options.Middleware; middleware != nil {
result = middleware(result)
}
return result, nil
return proxy
}
func GetDevServerURL() string {
+6 -3
View File
@@ -2,14 +2,17 @@
package assetserver
import "net/http"
import (
"io/fs"
"net/http"
)
func defaultIndexHTML() []byte {
return []byte{}
}
func (a *AssetServer) setupHandler() (http.Handler, error) {
return NewDefaultAssetHandler(a.options)
func NewAssetFileServer(vfs fs.FS) http.Handler {
return newAssetFileServerFS(vfs)
}
func GetDevServerURL() string {
+22
View File
@@ -2,8 +2,10 @@ package assetserver
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
)
@@ -20,6 +22,10 @@ const (
WailsUserAgentValue = "wails.io"
)
var (
assetServerLogger = struct{}{}
)
func serveFile(rw http.ResponseWriter, filename string, blob []byte) error {
header := rw.Header()
header.Set(HeaderContentLength, fmt.Sprintf("%d", len(blob)))
@@ -37,3 +43,19 @@ func isWebSocket(req *http.Request) bool {
upgrade := req.Header.Get(HeaderUpgrade)
return strings.EqualFold(upgrade, "websocket")
}
func contextWithLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, assetServerLogger, logger)
}
func logInfo(ctx context.Context, message string, args ...interface{}) {
if logger, _ := ctx.Value(assetServerLogger).(*slog.Logger); logger != nil {
logger.Info(message, args...)
}
}
func logError(ctx context.Context, message string, args ...interface{}) {
if logger, _ := ctx.Value(assetServerLogger).(*slog.Logger); logger != nil {
logger.Error(message, args...)
}
}
+3 -3
View File
@@ -10,8 +10,8 @@ import (
"strings"
)
// FindEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files.
func FindEmbedRootPath(fileSystem embed.FS) (string, error) {
// findEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files.
func findEmbedRootPath(fileSystem embed.FS) (string, error) {
stopErr := fmt.Errorf("files or multiple dirs found")
fPath := ""
@@ -39,7 +39,7 @@ func FindEmbedRootPath(fileSystem embed.FS) (string, error) {
return fPath, nil
}
func FindPathToFile(fileSystem fs.FS, file string) (string, error) {
func findPathToFile(fileSystem fs.FS, file string) (string, error) {
stat, _ := fs.Stat(fileSystem, file)
if stat != nil {
return ".", nil
+5 -16
View File
@@ -2,26 +2,13 @@ package assetserver
import (
"fmt"
"io/fs"
"log/slog"
"net/http"
)
// Options defines the configuration of the AssetServer.
type Options struct {
// Assets defines the static assets to be used. A GET request is first tried to be served from this Assets. If the Assets returns
// `os.ErrNotExist` for that file, the request handling will fallback to the Handler and tries to serve the GET
// request from it.
//
// If set to nil, all GET requests will be forwarded to Handler.
Assets fs.FS
// Handler will be called for every GET request that can't be served from Assets, due to `os.ErrNotExist`. Furthermore all
// non GET requests will always be served from this Handler.
//
// If not defined, the result is the following in cases where the Handler would have been called:
// GET request: `http.StatusNotFound`
// Other request: `http.StatusMethodNotAllowed`
// Handler which serves all the content to the WebView.
Handler http.Handler
// Middleware is a HTTP Middleware which allows to hook into the AssetServer request chain. It allows to skip the default
@@ -29,6 +16,8 @@ type Options struct {
// The Middleware is called to build a new `http.Handler` used by the AssetSever and it also receives the default
// handler used by the AssetServer as an argument.
//
// This middleware injects itself before any of Wails internal middlewares.
//
// If not defined, the default AssetServer request chain is executed.
//
// Multiple Middlewares can be chained together with:
@@ -50,8 +39,8 @@ type Options struct {
// Validate the options
func (o Options) Validate() error {
if o.Assets == nil && o.Handler == nil && o.Middleware == nil {
return fmt.Errorf("AssetServer options invalid: either Assets, Handler or Middleware must be set")
if o.Handler == nil && o.Middleware == nil {
return fmt.Errorf("AssetServer options invalid: either Handler or Middleware must be set")
}
return nil
+5 -5
View File
@@ -3,9 +3,6 @@ package application
import (
"embed"
"encoding/json"
"github.com/pkg/browser"
"github.com/samber/lo"
"github.com/wailsapp/wails/v3/internal/signal"
"io"
"log"
"log/slog"
@@ -15,6 +12,10 @@ import (
"strconv"
"sync"
"github.com/pkg/browser"
"github.com/samber/lo"
"github.com/wailsapp/wails/v3/internal/signal"
"github.com/wailsapp/wails/v3/internal/assetserver"
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
"github.com/wailsapp/wails/v3/internal/capabilities"
@@ -29,7 +30,7 @@ var globalApplication *App
// AlphaAssets is the default assets for the alpha application
var AlphaAssets = AssetOptions{
FS: alphaAssets,
Handler: AssetFileServerFS(alphaAssets),
}
func init() {
@@ -76,7 +77,6 @@ func New(appOptions Options) *App {
result.Events = NewWailsEventProcessor(result.dispatchEventToWindows)
opts := &assetserver.Options{
Assets: appOptions.Assets.FS,
Handler: appOptions.Assets.Handler,
Middleware: assetserver.Middleware(appOptions.Assets.Middleware),
Logger: result.Logger,

Some files were not shown because too many files have changed in this diff Show More