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:
parent
15b7d291f3
commit
993f87af97
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user