diff --git a/v2/internal/app/app_dev.go b/v2/internal/app/app_dev.go index 35c02678f..91a47fbf1 100644 --- a/v2/internal/app/app_dev.go +++ b/v2/internal/app/app_dev.go @@ -91,7 +91,10 @@ func CreateApp(appoptions *options.App) (*App, error) { } } - assetConfig := assetserver.BuildAssetServerConfig(appoptions) + assetConfig, err := assetserver.BuildAssetServerConfig(appoptions) + if err != nil { + return nil, err + } if assetConfig.Assets == nil && frontendDevServerURL != "" { myLogger.Warning("No AssetServer.Assets has been defined but a frontend DevServer, the frontend DevServer will not be used.") @@ -118,7 +121,7 @@ func CreateApp(appoptions *options.App) (*App, error) { // If no assetdir has been defined, let's try to infer it from the project root and the asset FS. assetdir, err = tryInferAssetDirFromFS(assetConfig.Assets) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to infer the AssetDir from your Assets fs.FS: %w", err) } } diff --git a/v2/internal/frontend/assetserver/assethandler.go b/v2/internal/frontend/assetserver/assethandler.go index 796fdc3db..00487d179 100644 --- a/v2/internal/frontend/assetserver/assethandler.go +++ b/v2/internal/frontend/assetserver/assethandler.go @@ -3,7 +3,8 @@ package assetserver import ( "bytes" "context" - _ "embed" + "embed" + "errors" "fmt" "io" iofs "io/fs" @@ -47,6 +48,16 @@ func NewAssetHandler(ctx context.Context, options assetserver.Options) (http.Han subDir, err := fs.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, _ := fs.FindEmbedRootPath(embedFs) + msg += fmt.Sprintf(", please make sure the embedded directory '%s' is correct and contains your assets", rootFolder) + } + + return nil, fmt.Errorf(msg) + } + return nil, err } @@ -70,22 +81,18 @@ func NewAssetHandler(ctx context.Context, options assetserver.Options) (http.Han } 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 := strings.TrimPrefix(req.URL.Path, "/") - if filename == "" { - filename = indexHTML - } + filename := path.Clean(strings.TrimPrefix(url, "/")) - d.logDebug("Loading file '%s'", filename) + d.logDebug("Handling request '%s' (file='%s')", url, filename) if err := d.serveFSFile(rw, req, filename); err != nil { if os.IsNotExist(err) { if handler != nil { - d.logDebug("File '%s' not found, serving '%s' by AssetHandler", filename, req.URL) + d.logDebug("File '%s' not found, serving '%s' by AssetHandler", filename, url) handler.ServeHTTP(rw, req) err = nil - } else if filename == indexHTML { - err = serveFile(rw, filename, defaultHTML) } else { rw.WriteHeader(http.StatusNotFound) err = nil @@ -98,7 +105,7 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } } else if handler != nil { - d.logDebug("No GET request, serving '%s' by AssetHandler", req.URL) + d.logDebug("No GET request, serving '%s' by AssetHandler", url) handler.ServeHTTP(rw, req) } else { rw.WriteHeader(http.StatusMethodNotAllowed) @@ -122,6 +129,29 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi return err } + if statInfo.IsDir() { + url := req.URL.Path + if url != "" && url[len(url)-1] != '/' { + // If the URL doesn't end in a slash normally a http.redirect should be done, but that currently doesn't work on + // WebKit WebVies (macOS/Linux). + // So we handle this as a file that could not be found. + return os.ErrNotExist + } + + filename = path.Join(filename, indexHTML) + + file, err = d.fs.Open(filename) + if err != nil { + return err + } + defer file.Close() + + statInfo, err = file.Stat() + if err != nil { + return err + } + } + var buf [512]byte n, err := file.Read(buf[:]) if err != nil && err != io.EOF { diff --git a/v2/internal/frontend/assetserver/assetserver.go b/v2/internal/frontend/assetserver/assetserver.go index fd66d46bd..64ae77002 100644 --- a/v2/internal/frontend/assetserver/assetserver.go +++ b/v2/internal/frontend/assetserver/assetserver.go @@ -34,7 +34,11 @@ type AssetServer struct { } func NewAssetServerMainPage(ctx context.Context, bindingsJSON string, options *options.App) (*AssetServer, error) { - return NewAssetServer(ctx, bindingsJSON, BuildAssetServerConfig(options)) + assetOptions, err := BuildAssetServerConfig(options) + if err != nil { + return nil, err + } + return NewAssetServer(ctx, bindingsJSON, assetOptions) } func NewAssetServer(ctx context.Context, bindingsJSON string, options assetserver.Options) (*AssetServer, error) { @@ -96,19 +100,23 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { header[k] = v } - if recorder.Code != http.StatusOK { + switch recorder.Code { + case http.StatusOK: + content, err := d.processIndexHTML(recorder.Body.Bytes()) + if err != nil { + d.serveError(rw, err, "Unable to processIndexHTML") + return + } + d.writeBlob(rw, indexHTML, content) + + case http.StatusNotFound: + d.writeBlob(rw, indexHTML, defaultHTML) + + default: rw.WriteHeader(recorder.Code) - return - } - content, err := d.processIndexHTML(recorder.Body.Bytes()) - if err != nil { - d.serveError(rw, err, "Unable to processIndexHTML") - return } - d.writeBlob(rw, "/index.html", content) - case runtimeJSPath: d.writeBlob(rw, path, d.runtimeJS) diff --git a/v2/internal/frontend/assetserver/common.go b/v2/internal/frontend/assetserver/common.go index ffe80577a..01e51f2be 100644 --- a/v2/internal/frontend/assetserver/common.go +++ b/v2/internal/frontend/assetserver/common.go @@ -13,19 +13,22 @@ import ( "golang.org/x/net/html" ) -func BuildAssetServerConfig(options *options.App) assetserver.Options { - if opts := options.AssetServer; opts != nil { - if options.Assets != nil || options.AssetsHandler != nil { +func BuildAssetServerConfig(appOptions *options.App) (assetserver.Options, error) { + var options assetserver.Options + if opt := appOptions.AssetServer; opt != nil { + if appOptions.Assets != nil || appOptions.AssetsHandler != nil { panic("It's not possible to use the deprecated Assets and AssetsHandler options and the new AssetServer option at the same time. Please migrate all your Assets options to the AssetServer option.") } - return *opts + options = *opt + } else { + options = assetserver.Options{ + Assets: appOptions.Assets, + Handler: appOptions.AssetsHandler, + } } - return assetserver.Options{ - Assets: options.Assets, - Handler: options.AssetsHandler, - } + return options, options.Validate() } const ( diff --git a/v2/internal/frontend/devserver/devserver.go b/v2/internal/frontend/devserver/devserver.go index 6dc18b8e0..88a2d971e 100644 --- a/v2/internal/frontend/devserver/devserver.go +++ b/v2/internal/frontend/devserver/devserver.go @@ -54,7 +54,10 @@ func (d *DevWebServer) Run(ctx context.Context) error { d.server.GET("/wails/reload", d.handleReload) d.server.GET("/wails/ipc", d.handleIPCWebSocket) - assetServerConfig := assetserver.BuildAssetServerConfig(d.appoptions) + assetServerConfig, err := assetserver.BuildAssetServerConfig(d.appoptions) + if err != nil { + return err + } var assetHandler http.Handler var wsHandler http.Handler diff --git a/v2/internal/fs/fs.go b/v2/internal/fs/fs.go index 7deae9810..778c8704a 100644 --- a/v2/internal/fs/fs.go +++ b/v2/internal/fs/fs.go @@ -2,6 +2,7 @@ package fs import ( "crypto/md5" + "embed" "fmt" "io" "io/fs" @@ -376,7 +377,7 @@ func FindPathToFile(fsys fs.FS, file string) (string, error) { path, _ := filepath.Split(indexFiles.AsSlice()[0]) return path, nil } - return "", fmt.Errorf("no index.html found") + return "", fmt.Errorf("%s: %w", file, os.ErrNotExist) } // FindFileInParents searches for a file in the current directory and all parent directories. @@ -402,3 +403,32 @@ func FindFileInParents(path string, filename string) string { } return pathToFile } + +// FindEmbedRootPath finds the root path in the embed FS. It's the directory which contains all the files. +func FindEmbedRootPath(fsys embed.FS) (string, error) { + stopErr := fmt.Errorf("files or multiple dirs found") + + fPath := "" + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + fPath = path + if entries, dErr := fs.ReadDir(fsys, path); dErr != nil { + return dErr + } else if len(entries) <= 1 { + return nil + } + } + + return stopErr + }) + + if err != nil && err != stopErr { + return "", err + } + + return fPath, nil +} diff --git a/v2/pkg/options/assetserver/options.go b/v2/pkg/options/assetserver/options.go index 0be9a1d3e..674451a0c 100644 --- a/v2/pkg/options/assetserver/options.go +++ b/v2/pkg/options/assetserver/options.go @@ -1,6 +1,7 @@ package assetserver import ( + "fmt" "io/fs" "net/http" ) @@ -33,3 +34,12 @@ type Options struct { // ChainMiddleware(middleware ...Middleware) Middleware Middleware Middleware } + +// 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") + } + + return nil +} diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 9dc9d0956..d15f568dd 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -20,12 +20,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The [AssetServer](/docs/reference/options#assetserver) now supports handling range-requests if the [Assets](/docs/reference/options/#assets-1) `fs.FS` provides an `io.ReadSeeker`. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2091) - Add new property for the `wails.json` config file - `bindings`. More information on the new property can be found in the updated [schema](/schemas/config.v2.json). Properties `prefix` and `suffix` allow you to control the generated TypeScript entity name in the `model.ts` file. Added by @OlegGulevskyy in [PR](https://github.com/wailsapp/wails/pull/2101) - The `WindowSetAlwaysOnTop` method is now exposed in the JS runtime. Fixed by @gotid in [PR](https://github.com/wailsapp/wails/pull/2128) +- The [AssetServer](/docs/reference/options#assetserver) now supports serving the index.html file when requesting a directory. Added by @stffabi in [PR](https://github.com/wailsapp/wails/pull/2110) ### Fixed - The `noreload` flag in wails dev wasn't applied. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2081) - `build/bin` folder was duplicating itself on each reload in `wails dev` mode. Fixed by @OlegGulevskyy in this [PR](https://github.com/wailsapp/wails/pull/2103) - Prevent a thin white line at the bottom of a frameless window on Windows. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2111) +### Changed +- Improve error message if no `index.html` could be found in the assets and validate assetserver options. Changed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2110) + ## v2.2.0 - 2022-11-09 ### Added