5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 13:51:10 +08:00

[assetserver] Add support for serving the index.html file when requesting a directory (#2110)

* [assets] Improve error message if no `index.html` could be found in the assets

* [assetoptions] Valide options that at least one property has been set

* [assetserver] Move defaultHTML handling for 404 from assethandler to assetserver

* [assetserver] Add support for serving the index.html file when requesting a directory

* [docs] Update changelog
This commit is contained in:
stffabi 2022-11-29 09:29:08 +01:00 committed by GitHub
parent 15b7d291f3
commit 993f87af97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 123 additions and 32 deletions

View File

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

View File

@ -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 {

View File

@ -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)

View File

@ -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 (

View File

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

View File

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

View File

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

View File

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