package context import ( "embed" "fmt" "io/fs" "net/http" "os" "path" "path/filepath" ) // ResolveFS accepts a single input argument of any type // and tries to cast it to fs.FS. // // It affects the view engine's filesystem resolver. // // This package-level variable can be modified on initialization. var ResolveFS = func(fsOrDir interface{}) fs.FS { if fsOrDir == nil { return noOpFS{} } switch v := fsOrDir.(type) { case string: if v == "" { return noOpFS{} } return os.DirFS(v) case fs.FS: return v case http.FileSystem: // handles go-bindata. return &httpFS{v} default: panic(fmt.Errorf(`unexpected "fsOrDir" argument type of %T (string or fs.FS or embed.FS or http.FileSystem)`, v)) } } type noOpFS struct{} func (fileSystem noOpFS) Open(name string) (fs.File, error) { return nil, nil } // IsNoOpFS reports whether the given "fileSystem" is a no operation fs. func IsNoOpFS(fileSystem fs.FS) bool { _, ok := fileSystem.(noOpFS) return ok } type httpFS struct { fs http.FileSystem } func (f *httpFS) Open(name string) (fs.File, error) { if name == "." { name = "/" } return f.fs.Open(filepath.ToSlash(name)) } func (f *httpFS) ReadDir(name string) ([]fs.DirEntry, error) { name = filepath.ToSlash(name) if name == "." { name = "/" } file, err := f.fs.Open(name) if err != nil { return nil, err } defer file.Close() infos, err := file.Readdir(-1) if err != nil { return nil, err } entries := make([]fs.DirEntry, 0, len(infos)) for _, info := range infos { if info.IsDir() { // http file's does not return the whole tree, so read it. sub, err := f.ReadDir(info.Name()) if err != nil { return nil, err } entries = append(entries, sub...) continue } entry := fs.FileInfoToDirEntry(info) entries = append(entries, entry) } return entries, nil } // ResolveHTTPFS accepts a single input argument of any type // and tries to cast it to http.FileSystem. // // It affects the Application's API Builder's `HandleDir` method. // // This package-level variable can be modified on initialization. var ResolveHTTPFS = func(fsOrDir interface{}) http.FileSystem { var fileSystem http.FileSystem switch v := fsOrDir.(type) { case string: fileSystem = http.Dir(v) case http.FileSystem: fileSystem = v case embed.FS: direEtries, err := v.ReadDir(".") if err != nil { panic(err) } if len(direEtries) == 0 { panic("HandleDir: no directories found under the embedded file system") } subfs, err := fs.Sub(v, direEtries[0].Name()) if err != nil { panic(err) } fileSystem = http.FS(subfs) case fs.FS: fileSystem = http.FS(v) default: panic(fmt.Sprintf(`unexpected "fsOrDir" argument type of %T (string or http.FileSystem or embed.FS or fs.FS)`, v)) } return fileSystem } // FindNames accepts a "http.FileSystem" and a root name and returns // the list containing its file names. func FindNames(fileSystem http.FileSystem, name string) ([]string, error) { f, err := fileSystem.Open(name) if err != nil { return nil, err } defer f.Close() fi, err := f.Stat() if err != nil { return nil, err } if !fi.IsDir() { return []string{name}, nil } fileinfos, err := f.Readdir(-1) if err != nil { return nil, err } files := make([]string, 0) for _, info := range fileinfos { // Note: // go-bindata has absolute names with os.Separator, // http.Dir the basename. filename := toBaseName(info.Name()) fullname := path.Join(name, filename) if fullname == name { // prevent looping through itself. continue } rfiles, err := FindNames(fileSystem, fullname) if err != nil { return nil, err } files = append(files, rfiles...) } return files, nil } // Instead of path.Base(filepath.ToSlash(s)) // let's do something like that, it is faster // (used to list directories on serve-time too): func toBaseName(s string) string { n := len(s) - 1 for i := n; i >= 0; i-- { if c := s[i]; c == '/' || c == '\\' { if i == n { // "s" ends with a slash, remove it and retry. return toBaseName(s[:n]) } return s[i+1:] // return the rest, trimming the slash. } } return s }