diff --git a/service/fs/default.go b/service/fs/default.go new file mode 100644 index 000000000..950cf5e3a --- /dev/null +++ b/service/fs/default.go @@ -0,0 +1,52 @@ +package fs + +import ( + "errors" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/yaoapp/gou/application" +) + +// Dir http root path +type Dir string + +// Open implements FileSystem using os.Open, opening files for reading rooted +// and relative to the directory d. +func (d Dir) Open(name string) (http.File, error) { + if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { + return nil, errors.New("http: invalid character in file path") + } + + dir := string(d) + if dir == "" { + dir = "." + } + + name = filepath.FromSlash(path.Clean("/" + name)) + relName := filepath.Join(dir, name) + + // Close dir views Disable directory listing + absName := filepath.Join(application.App.Root(), relName) + stat, err := os.Stat(absName) + if err != nil { + return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) + } + + if stat.IsDir() { + if _, err := os.Stat(filepath.Join(absName, "index.html")); os.IsNotExist(err) { + return nil, mapOpenError(fs.ErrNotExist, relName, filepath.Separator, os.Stat) + } + } + + f, err := application.App.FS(string(d)).Open(name) + if err != nil { + return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) + } + + return f, nil +} diff --git a/service/fs/fs.go b/service/fs/fs.go deleted file mode 100644 index a3b664238..000000000 --- a/service/fs/fs.go +++ /dev/null @@ -1,120 +0,0 @@ -package fs - -import ( - "errors" - "io/fs" - "net/http" - "os" - "path" - "path/filepath" - "strings" - - "github.com/yaoapp/gou/application" -) - -// Dir http root path -type Dir string - -// DirPWA is the PWA path -type DirPWA string - -// Open implements FileSystem using os.Open, opening files for reading rooted -// and relative to the directory d. -func (d Dir) Open(name string) (http.File, error) { - if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { - return nil, errors.New("http: invalid character in file path") - } - - dir := string(d) - if dir == "" { - dir = "." - } - - name = filepath.FromSlash(path.Clean("/" + name)) - relName := filepath.Join(dir, name) - - // Close dir views Disable directory listing - absName := filepath.Join(application.App.Root(), relName) - stat, err := os.Stat(absName) - if err != nil { - return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) - } - - if stat.IsDir() { - if _, err := os.Stat(filepath.Join(absName, "index.html")); os.IsNotExist(err) { - return nil, mapOpenError(fs.ErrNotExist, relName, filepath.Separator, os.Stat) - } - } - - f, err := application.App.FS(string(d)).Open(name) - if err != nil { - return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) - } - - return f, nil -} - -// Open implements FileSystem using os.Open, opening files for reading rooted -// and relative to the directory d. -func (d DirPWA) Open(name string) (http.File, error) { - if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { - return nil, errors.New("http: invalid character in file path") - } - - dir := string(d) - if dir == "" { - dir = "." - } - - name = filepath.FromSlash(path.Clean("/" + name)) - relName := filepath.Join(dir, name) - - if filepath.Ext(relName) == "" && relName != dir { - relName = filepath.Join(dir, "index.html") - name = filepath.Join(string(os.PathSeparator), "index.html") - } - - // Close dir views Disable directory listing - absName := filepath.Join(application.App.Root(), relName) - stat, err := os.Stat(absName) - if err != nil { - return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) - } - - if stat.IsDir() { - if _, err := os.Stat(filepath.Join(absName, "index.html")); os.IsNotExist(err) { - return nil, mapOpenError(fs.ErrNotExist, relName, filepath.Separator, os.Stat) - } - } - - f, err := application.App.FS(string(d)).Open(name) - if err != nil { - return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) - } - - return f, nil -} - -// mapOpenError maps the provided non-nil error from opening name -// to a possibly better non-nil error. In particular, it turns OS-specific errors -// about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552. -func mapOpenError(originalErr error, name string, sep rune, stat func(string) (fs.FileInfo, error)) error { - if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) { - return originalErr - } - - parts := strings.Split(name, string(sep)) - for i := range parts { - if parts[i] == "" { - continue - } - fi, err := stat(strings.Join(parts[:i+1], string(sep))) - if err != nil { - return originalErr - } - if !fi.IsDir() { - return fs.ErrNotExist - } - } - return originalErr -} diff --git a/service/fs/spa.go b/service/fs/spa.go new file mode 100644 index 000000000..3091e6891 --- /dev/null +++ b/service/fs/spa.go @@ -0,0 +1,57 @@ +package fs + +import ( + "errors" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/yaoapp/gou/application" +) + +// DirSPA is the PWA path +type DirSPA string + +// Open implements FileSystem using os.Open, opening files for reading rooted +// and relative to the directory d. +func (d DirSPA) Open(name string) (http.File, error) { + if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { + return nil, errors.New("http: invalid character in file path") + } + + dir := string(d) + if dir == "" { + dir = "." + } + + name = filepath.FromSlash(path.Clean("/" + name)) + relName := filepath.Join(dir, name) + + if filepath.Ext(relName) == "" && relName != dir { + relName = filepath.Join(dir, "index.html") + name = filepath.Join(string(os.PathSeparator), "index.html") + } + + // Close dir views Disable directory listing + absName := filepath.Join(application.App.Root(), relName) + stat, err := os.Stat(absName) + if err != nil { + return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) + } + + if stat.IsDir() { + if _, err := os.Stat(filepath.Join(absName, "index.html")); os.IsNotExist(err) { + return nil, mapOpenError(fs.ErrNotExist, relName, filepath.Separator, os.Stat) + } + } + + f, err := application.App.FS(string(d)).Open(name) + if err != nil { + return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) + } + + return f, nil +} diff --git a/service/fs/sui.go b/service/fs/sui.go new file mode 100644 index 000000000..67ea94648 --- /dev/null +++ b/service/fs/sui.go @@ -0,0 +1,57 @@ +package fs + +import ( + "errors" + "io/fs" + "net/http" + "os" + "path" + "path/filepath" + "strings" + + "github.com/yaoapp/gou/application" +) + +// DirSUI is the PWA path +type DirSUI string + +// Open implements FileSystem using os.Open, opening files for reading rooted +// and relative to the directory d. +func (d DirSUI) Open(name string) (http.File, error) { + if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) { + return nil, errors.New("http: invalid character in file path") + } + + dir := string(d) + if dir == "" { + dir = "." + } + + name = filepath.FromSlash(path.Clean("/" + name)) + relName := filepath.Join(dir, name) + + if filepath.Ext(relName) == "" && relName != dir { + relName = filepath.Join(dir, "index.html") + name = filepath.Join(string(os.PathSeparator), "index.html") + } + + // Close dir views Disable directory listing + absName := filepath.Join(application.App.Root(), relName) + stat, err := os.Stat(absName) + if err != nil { + return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) + } + + if stat.IsDir() { + if _, err := os.Stat(filepath.Join(absName, "index.html")); os.IsNotExist(err) { + return nil, mapOpenError(fs.ErrNotExist, relName, filepath.Separator, os.Stat) + } + } + + f, err := application.App.FS(string(d)).Open(name) + if err != nil { + return nil, mapOpenError(err, relName, filepath.Separator, os.Stat) + } + + return f, nil +} diff --git a/service/fs/utils.go b/service/fs/utils.go new file mode 100644 index 000000000..519186f4e --- /dev/null +++ b/service/fs/utils.go @@ -0,0 +1,31 @@ +package fs + +import ( + "errors" + "io/fs" + "strings" +) + +// mapOpenError maps the provided non-nil error from opening name +// to a possibly better non-nil error. In particular, it turns OS-specific errors +// about opening files in non-directories into fs.ErrNotExist. See Issues 18984 and 49552. +func mapOpenError(originalErr error, name string, sep rune, stat func(string) (fs.FileInfo, error)) error { + if errors.Is(originalErr, fs.ErrNotExist) || errors.Is(originalErr, fs.ErrPermission) { + return originalErr + } + + parts := strings.Split(name, string(sep)) + for i := range parts { + if parts[i] == "" { + continue + } + fi, err := stat(strings.Join(parts[:i+1], string(sep))) + if err != nil { + return originalErr + } + if !fi.IsDir() { + return fs.ErrNotExist + } + } + return originalErr +} diff --git a/service/middleware.go b/service/middleware.go index b6a6fb7c8..9b0786e90 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -7,20 +7,20 @@ import ( ) // Middlewares the middlewares -var Middlewares = []func(c *gin.Context){ +var Middlewares = []gin.HandlerFunc{ + gin.Logger(), withStaticFileServer, } // withStaticFileServer static file server func withStaticFileServer(c *gin.Context) { + // Handle API & websocket length := len(c.Request.URL.Path) - if (length >= 5 && c.Request.URL.Path[0:5] == "/api/") || (length >= 11 && c.Request.URL.Path[0:11] == "/websocket/") { // API & websocket c.Next() return - } // Xgen 1.0 @@ -31,6 +31,7 @@ func withStaticFileServer(c *gin.Context) { return } + // __yao_admin_root if length >= 18 && c.Request.URL.Path[0:18] == "/__yao_admin_root/" { c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, "/__yao_admin_root") XGenFileServerV1.ServeHTTP(c.Writer, c.Request) @@ -38,13 +39,11 @@ func withStaticFileServer(c *gin.Context) { return } - // SPA app static file server - for root, rootLength := range SpaRoots { - if length >= rootLength && c.Request.URL.Path[0:rootLength] == root { - c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, root) - spaFileServers[root].ServeHTTP(c.Writer, c.Request) - c.Abort() - return + // Rewrite + for _, rewrite := range rewriteRules { + if matches := rewrite.Pattern.FindStringSubmatch(c.Request.URL.Path); matches != nil { + c.Request.URL.Path = rewrite.Pattern.ReplaceAllString(c.Request.URL.Path, rewrite.Replacement) + break } } diff --git a/service/service.go b/service/service.go index bea6f62cb..17a2c90ff 100644 --- a/service/service.go +++ b/service/service.go @@ -24,17 +24,16 @@ func Start(cfg config.Config) (*http.Server, error) { } router := gin.New() - router.Use(gin.Logger()) + router.Use(Middlewares...) api.SetGuards(Guards) api.SetRoutes(router, "/api", cfg.AllowFrom...) - srv := http.New(router, http.Option{ Host: cfg.Host, Port: cfg.Port, Root: "/api", Allows: cfg.AllowFrom, Timeout: 5 * time.Second, - }).With(Middlewares...) + }) // Neo API if neo.Neo != nil { @@ -48,6 +47,16 @@ func Start(cfg config.Config) (*http.Server, error) { return srv, nil } +// Restart the yao service +func Restart(srv *http.Server, cfg config.Config) error { + router := gin.New() + router.Use(Middlewares...) + api.SetGuards(Guards) + api.SetRoutes(router, "/api", cfg.AllowFrom...) + srv.Reset(router) + return srv.Restart() +} + // Stop the yao service func Stop(srv *http.Server) error { err := srv.Stop() diff --git a/service/static.go b/service/static.go index 186044ca3..3bb167286 100644 --- a/service/static.go +++ b/service/static.go @@ -3,9 +3,10 @@ package service import ( "fmt" "net/http" - "path/filepath" + "regexp" "strings" + "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/data" "github.com/yaoapp/yao/service/fs" "github.com/yaoapp/yao/share" @@ -14,12 +15,6 @@ import ( // AppFileServer static file server var AppFileServer http.Handler -// spaFileServers spa static file server -var spaFileServers map[string]http.Handler = map[string]http.Handler{} - -// SpaRoots SPA static file server -var SpaRoots map[string]int = map[string]int{} - // XGenFileServerV1 XGen v1.0 var XGenFileServerV1 http.Handler = http.FileServer(data.XgenV1()) @@ -29,42 +24,49 @@ var AdminRoot = "" // AdminRootLen cache var AdminRootLen = 0 -// SetupStatic setup static file server -func SetupStatic() error { - - // SetAdmin Root - adminRoot() +var rewriteRules = []rewriteRule{} - if isPWA() { - AppFileServer = http.FileServer(fs.DirPWA("public")) - return nil - } - - for _, root := range spaApps() { - spaFileServers[root] = http.FileServer(fs.DirPWA(filepath.Join("public", root))) - SpaRoots[root] = len(root) - } +type rewriteRule struct { + Pattern *regexp.Regexp + Replacement string +} +// SetupStatic setup static file server +func SetupStatic() error { + setupAdminRoot() + setupRewrite() AppFileServer = http.FileServer(fs.Dir("public")) return nil } -// rewrite path -func isPWA() bool { - - return share.App.Static.PWA -} - -// rewrite path -func spaApps() []string { - if share.App.Static.Apps == nil { - return []string{} +func setupRewrite() { + if share.App.Static.Rewrite != nil { + for _, rule := range share.App.Static.Rewrite { + + pattern := "" + replacement := "" + for key, value := range rule { + pattern = key + replacement = value + break + } + + re, err := regexp.Compile(pattern) + if err != nil { + log.Error("Invalid rewrite rule: %s", pattern) + continue + } + + rewriteRules = append(rewriteRules, rewriteRule{ + Pattern: re, + Replacement: replacement, + }) + } } - return share.App.Static.Apps } -// SetupAdmin setup admin static root -func adminRoot() (string, int) { +// rewrite path +func setupAdminRoot() (string, int) { if AdminRoot != "" { return AdminRoot, AdminRootLen } diff --git a/service/watch.go b/service/watch.go index fd973e164..e655e14b1 100644 --- a/service/watch.go +++ b/service/watch.go @@ -38,7 +38,7 @@ func Watch(srv *http.Server, interrupt chan uint8) (err error) { // Restart if strings.HasPrefix(name, "/apis") { - err = srv.Restart() + err = Restart(srv, config.Conf) if err != nil { fmt.Println(color.RedString("[Watch] Restart: %s", err.Error())) return diff --git a/share/types.go b/share/types.go index b0aaba2cd..925a42a0e 100644 --- a/share/types.go +++ b/share/types.go @@ -96,8 +96,7 @@ type Moapi struct { // Static setting type Static struct { - PWA bool `json:"pwa,omitempty"` - Apps []string `json:"apps,omitempty"` + Rewrite []map[string]string `json:"rewrite,omitempty"` } // AppStorage 应用存储 diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go index 6b36f0882..7afcd7485 100644 --- a/sui/storages/local/build.go +++ b/sui/storages/local/build.go @@ -104,7 +104,7 @@ func (page *Page) publicFile() string { // writeHTMLTo write the html to file func (page *Page) writeHTML(html []byte) error { - htmlFile := fmt.Sprintf("%s.html", page.publicFile()) + htmlFile := fmt.Sprintf("%s.sui", page.publicFile()) dir := filepath.Dir(htmlFile) if exist, _ := os.Stat(dir); exist == nil { os.MkdirAll(dir, os.ModePerm)