echo icon indicating copy to clipboard operation
echo copied to clipboard

Expose FileFS

Open pcfreak30 opened this issue 4 months ago • 12 comments

I have hit a situation where I want to re-use FileFS manually in my own abstractions which wraps fsFile but its not in the interface and is only allowed via echo.FileFS.

	if indexFile == "" {
		indexFile = DefaultIndexFile
	}

	echoRouter.GET("/*", func(c echo.Context) error {
		if strings.HasPrefix(c.Request().URL.Path, "/api/") {
			return echo.ErrNotFound
		}
		return c.FileFS(indexFile,fsys )
	})
	return nil
}

I want return c.FileFS(indexFile ,fsys ) but im having to copy your fsFile into my library...

pcfreak30 avatar Sep 01 '25 12:09 pcfreak30

You want to specify your own index file name?

something like

func FsFile(c Context, file string, filesystem fs.FS, indexFile string) error {
	f, err := filesystem.Open(file)
	if err != nil {
		return ErrNotFound
	}
	defer f.Close()

	fi, _ := f.Stat()
	if fi.IsDir() {
		file = filepath.ToSlash(filepath.Join(file, cmp.Or(indexFile, indexPage))) // ToSlash is necessary for Windows. fs.Open and os.Open are different in that aspect.
		f, err = filesystem.Open(file)
		if err != nil {
			return ErrNotFound
		}
		defer f.Close()
		if fi, err = f.Stat(); err != nil {
			return err
		}
	}
	ff, ok := f.(io.ReadSeeker)
	if !ok {
		return errors.New("file does not implement io.ReadSeeker")
	}
	http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff)
	return nil
}

aldas avatar Sep 01 '25 19:09 aldas

You want to specify your own index file name?

something like

func FsFile(c Context, file string, filesystem fs.FS, indexFile string) error { f, err := filesystem.Open(file) if err != nil { return ErrNotFound } defer f.Close()

fi, _ := f.Stat() if fi.IsDir() { file = filepath.ToSlash(filepath.Join(file, cmp.Or(indexFile, indexPage))) // ToSlash is necessary for Windows. fs.Open and os.Open are different in that aspect. f, err = filesystem.Open(file) if err != nil { return ErrNotFound } defer f.Close() if fi, err = f.Stat(); err != nil { return err } } ff, ok := f.(io.ReadSeeker) if !ok { return errors.New("file does not implement io.ReadSeeker") } http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), ff) return nil }

Just expose FileFS on the interface b/c you already implement in in the context struct. Nothing more is needed

pcfreak30 avatar Sep 01 '25 19:09 pcfreak30

contect.FileFs does not allow you to specify index page (line 36) to use when file points to directory and as I understand this is what you seek

https://github.com/labstack/echo/blob/5ac2f11f21b7884903db6126630e6786c8c22661/context_fs.go#L23-L37

aldas avatar Sep 01 '25 19:09 aldas

ot allow you to specify index page (line 36)

thats a minor detail for me atm since it will be the same default anyways.

pcfreak30 avatar Sep 01 '25 19:09 pcfreak30

to be honest. I do not get why exposing that function func fsFile(c Context, file string, filesystem fs.FS) error { would be useful if you still need to pass in context - meaning you still need to have context around. If you have context around why not call c.FileFs(filename, fsys). changit it to echo.FileFs(c, filename, fsys) makes no difference.

aldas avatar Sep 01 '25 19:09 aldas

to be honest. I do not get why exposing that function func fsFile(c Context, file string, filesystem fs.FS) error { would be useful if you still need to pass in context - meaning you still need to have context around. If you have context around why not call c.FileFs(filename, fsys). changit it to echo.FileFs(c, filename, fsys) makes no difference.

FileFs is not exposed on the interface. its public on the context, but the context struct is private. so I have to copy the entire fsFile b/c i can't use echos without using a full route, and i am using my own route registration as I need more control.

pcfreak30 avatar Sep 01 '25 19:09 pcfreak30

  1. and filename is not static actually, as your current example is?

because if it is you could do

	indexFile := "index.html"
	indexFs := os.DirFS("main/website/")
	indexHandler := echo.StaticFileHandler(indexFile, indexFs)

	e.GET("/*", func(c echo.Context) error {
		if strings.HasPrefix(c.Request().URL.Path, "/api/") {
			return echo.ErrNotFound
		}
		return indexHandler(c)
	})
  1. and there are more than one filesystem to read from

if it single fs that does not change, you could just do

	indexFile := "index.html"
	e.Filesystem = os.DirFS("main/website/")

	e.GET("/*", func(c echo.Context) error {
		if strings.HasPrefix(c.Request().URL.Path, "/api/") {
			return echo.ErrNotFound
		}
		return c.File(indexFile) // <-- uses `e.Filesystem`
	})

aldas avatar Sep 01 '25 20:09 aldas

  1. and filename is not static actually, as your current example is?

because if it is you could do

indexFile := "index.html" indexFs := os.DirFS("main/website/") indexHandler := echo.StaticFileHandler(indexFile, indexFs)

e.GET("/*", func(c echo.Context) error { if strings.HasPrefix(c.Request().URL.Path, "/api/") { return echo.ErrNotFound } return indexHandler(c) }) 2. and there are more than one filesystem to read from

if it single fs that does not change, you could just do

indexFile := "index.html" e.Filesystem = os.DirFS("main/website/")

e.GET("/*", func(c echo.Context) error { if strings.HasPrefix(c.Request().URL.Path, "/api/") { return echo.ErrNotFound } return c.File(indexFile) // <-- uses e.Filesystem })

My files can be any fs.FS, and we can't just clobber e.Filesystem

See https://github.com/LumeWeb/portal-router/blob/d69faa4e67d41ee6b81339e4816c84baee11143b/static_helpers.go#L354

pcfreak30 avatar Sep 02 '25 04:09 pcfreak30

My files can be any fs.FS, and we can't just clobber e.Filesystem

but e.Filesystem type is fs.FS

so you can use other fs.FS implementations like embed.FS

//go:embed static/*
var content embed.FS

func main() {
	e := echo.New()

	e.Filesystem = content

	e.GET("/*", func(c echo.Context) error {
		if strings.HasPrefix(c.Request().URL.Path, "/api/") {
			return echo.ErrNotFound
		}
		return c.File("static/index.html") // <-- embed fs needs prefix to work
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		e.Logger.Fatal(err)
	}
}

aldas avatar Sep 02 '25 20:09 aldas

My files can be any fs.FS, and we can't just clobber e.Filesystem

but e.Filesystem type is fs.FS

so you can use other fs.FS implementations like embed.FS

//go:embed static/* var content embed.FS

func main() { e := echo.New()

e.Filesystem = content

e.GET("/*", func(c echo.Context) error { if strings.HasPrefix(c.Request().URL.Path, "/api/") { return echo.ErrNotFound } return c.File("static/index.html") // <-- embed fs needs prefix to work })

if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) { e.Logger.Fatal(err) } }

My point is, there can be potentially multiple routes setup so assuming a singleton e.Filesystem for all uses just is a bad assumption. Im a bit bewildered as to why your against exposing an API thats already implemented, into the interface. That's all im requesting.

pcfreak30 avatar Sep 02 '25 20:09 pcfreak30

I am reluctant because that fsFile does more that your requirements are probably needing - it knows how to serve index page when the "filename" points to the folder. But your requirements seem to be to serve single static/hardcoded file/page from any fs.

moreover if we are to expose that function we should add additional parameter to it, to provide way to specify indexPage as this would be something that potentially needs also be configured.


another suggestion. Cast context to interface that has FileFS method that supports fs.FS like that, so you can access the context.FileFS method, which is public.

		cfs := c.(interface {
			FileFS(file string, filesystem fs.FS) error
		})

full example:

//go:embed website/*
var embedFS embed.FS

func main() {
	e := echo.New()

	e.GET("/*", func(c echo.Context) error {
		if strings.HasPrefix(c.Request().URL.Path, "/api/") {
			return echo.ErrNotFound
		}
		cfs := c.(interface {
			FileFS(file string, filesystem fs.FS) error
		})
		return cfs.FileFS("website/index.html", embedFS)
	})

	if err := e.Start(":8080"); err != nil && !errors.Is(err, http.ErrServerClosed) {
		e.Logger.Fatal(err)
	}
}

aldas avatar Sep 03 '25 05:09 aldas

I am reluctant because that fsFile does more that your requirements are probably needing - it knows how to serve index page when the "filename" points to the folder. But your requirements seem to be to serve single static/hardcoded file/page from any fs.

Well... what I have deployed now works... so IMO your kind of arguing with me over details I don't consider relevant right now 🙃 .

And... I forgot golang allows defining implicit interfaces for anything 😅 . That will likely solve my need, however it would still be simplist to expose this, and change its API if you feel thats warranted.

pcfreak30 avatar Sep 03 '25 05:09 pcfreak30