5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 00:09:56 +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 != "" { if assetConfig.Assets == nil && frontendDevServerURL != "" {
myLogger.Warning("No AssetServer.Assets has been defined but a frontend DevServer, the frontend DevServer will not be used.") 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. // 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) assetdir, err = tryInferAssetDirFromFS(assetConfig.Assets)
if err != nil { 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 ( import (
"bytes" "bytes"
"context" "context"
_ "embed" "embed"
"errors"
"fmt" "fmt"
"io" "io"
iofs "io/fs" iofs "io/fs"
@ -47,6 +48,16 @@ func NewAssetHandler(ctx context.Context, options assetserver.Options) (http.Han
subDir, err := fs.FindPathToFile(vfs, indexHTML) subDir, err := fs.FindPathToFile(vfs, indexHTML)
if err != nil { 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 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) { func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
url := req.URL.Path
handler := d.handler handler := d.handler
if strings.EqualFold(req.Method, http.MethodGet) { if strings.EqualFold(req.Method, http.MethodGet) {
filename := strings.TrimPrefix(req.URL.Path, "/") filename := path.Clean(strings.TrimPrefix(url, "/"))
if filename == "" {
filename = indexHTML
}
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 err := d.serveFSFile(rw, req, filename); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if handler != nil { 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) handler.ServeHTTP(rw, req)
err = nil err = nil
} else if filename == indexHTML {
err = serveFile(rw, filename, defaultHTML)
} else { } else {
rw.WriteHeader(http.StatusNotFound) rw.WriteHeader(http.StatusNotFound)
err = nil err = nil
@ -98,7 +105,7 @@ func (d *assetHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
} }
} }
} else if handler != nil { } 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) handler.ServeHTTP(rw, req)
} else { } else {
rw.WriteHeader(http.StatusMethodNotAllowed) rw.WriteHeader(http.StatusMethodNotAllowed)
@ -122,6 +129,29 @@ func (d *assetHandler) serveFSFile(rw http.ResponseWriter, req *http.Request, fi
return err 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 var buf [512]byte
n, err := file.Read(buf[:]) n, err := file.Read(buf[:])
if err != nil && err != io.EOF { 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) { 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) { 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 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) 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: case runtimeJSPath:
d.writeBlob(rw, path, d.runtimeJS) d.writeBlob(rw, path, d.runtimeJS)

View File

@ -13,19 +13,22 @@ import (
"golang.org/x/net/html" "golang.org/x/net/html"
) )
func BuildAssetServerConfig(options *options.App) assetserver.Options { func BuildAssetServerConfig(appOptions *options.App) (assetserver.Options, error) {
if opts := options.AssetServer; opts != nil { var options assetserver.Options
if options.Assets != nil || options.AssetsHandler != nil { 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.") 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{ return options, options.Validate()
Assets: options.Assets,
Handler: options.AssetsHandler,
}
} }
const ( 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/reload", d.handleReload)
d.server.GET("/wails/ipc", d.handleIPCWebSocket) 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 assetHandler http.Handler
var wsHandler http.Handler var wsHandler http.Handler

View File

@ -2,6 +2,7 @@ package fs
import ( import (
"crypto/md5" "crypto/md5"
"embed"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
@ -376,7 +377,7 @@ func FindPathToFile(fsys fs.FS, file string) (string, error) {
path, _ := filepath.Split(indexFiles.AsSlice()[0]) path, _ := filepath.Split(indexFiles.AsSlice()[0])
return path, nil 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. // 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 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 package assetserver
import ( import (
"fmt"
"io/fs" "io/fs"
"net/http" "net/http"
) )
@ -33,3 +34,12 @@ type Options struct {
// ChainMiddleware(middleware ...Middleware) Middleware // ChainMiddleware(middleware ...Middleware) 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) - 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) - 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 `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 ### Fixed
- The `noreload` flag in wails dev wasn't applied. Fixed by @stffabi in this [PR](https://github.com/wailsapp/wails/pull/2081) - 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) - `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) - 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 ## v2.2.0 - 2022-11-09
### Added ### Added