fs.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. package context
  2. import (
  3. "embed"
  4. "fmt"
  5. "io/fs"
  6. "net/http"
  7. "os"
  8. "path"
  9. "path/filepath"
  10. )
  11. // ResolveFS accepts a single input argument of any type
  12. // and tries to cast it to fs.FS.
  13. //
  14. // It affects the view engine's filesystem resolver.
  15. //
  16. // This package-level variable can be modified on initialization.
  17. var ResolveFS = func(fsOrDir interface{}) fs.FS {
  18. if fsOrDir == nil {
  19. return noOpFS{}
  20. }
  21. switch v := fsOrDir.(type) {
  22. case string:
  23. if v == "" {
  24. return noOpFS{}
  25. }
  26. return os.DirFS(v)
  27. case fs.FS:
  28. return v
  29. case http.FileSystem: // handles go-bindata.
  30. return &httpFS{v}
  31. default:
  32. panic(fmt.Errorf(`unexpected "fsOrDir" argument type of %T (string or fs.FS or embed.FS or http.FileSystem)`, v))
  33. }
  34. }
  35. type noOpFS struct{}
  36. func (fileSystem noOpFS) Open(name string) (fs.File, error) { return nil, nil }
  37. // IsNoOpFS reports whether the given "fileSystem" is a no operation fs.
  38. func IsNoOpFS(fileSystem fs.FS) bool {
  39. _, ok := fileSystem.(noOpFS)
  40. return ok
  41. }
  42. type httpFS struct {
  43. fs http.FileSystem
  44. }
  45. func (f *httpFS) Open(name string) (fs.File, error) {
  46. if name == "." {
  47. name = "/"
  48. }
  49. return f.fs.Open(filepath.ToSlash(name))
  50. }
  51. func (f *httpFS) ReadDir(name string) ([]fs.DirEntry, error) {
  52. name = filepath.ToSlash(name)
  53. if name == "." {
  54. name = "/"
  55. }
  56. file, err := f.fs.Open(name)
  57. if err != nil {
  58. return nil, err
  59. }
  60. defer file.Close()
  61. infos, err := file.Readdir(-1)
  62. if err != nil {
  63. return nil, err
  64. }
  65. entries := make([]fs.DirEntry, 0, len(infos))
  66. for _, info := range infos {
  67. if info.IsDir() { // http file's does not return the whole tree, so read it.
  68. sub, err := f.ReadDir(info.Name())
  69. if err != nil {
  70. return nil, err
  71. }
  72. entries = append(entries, sub...)
  73. continue
  74. }
  75. entry := fs.FileInfoToDirEntry(info)
  76. entries = append(entries, entry)
  77. }
  78. return entries, nil
  79. }
  80. // ResolveHTTPFS accepts a single input argument of any type
  81. // and tries to cast it to http.FileSystem.
  82. //
  83. // It affects the Application's API Builder's `HandleDir` method.
  84. //
  85. // This package-level variable can be modified on initialization.
  86. var ResolveHTTPFS = func(fsOrDir interface{}) http.FileSystem {
  87. var fileSystem http.FileSystem
  88. switch v := fsOrDir.(type) {
  89. case string:
  90. fileSystem = http.Dir(v)
  91. case http.FileSystem:
  92. fileSystem = v
  93. case embed.FS:
  94. direEtries, err := v.ReadDir(".")
  95. if err != nil {
  96. panic(err)
  97. }
  98. if len(direEtries) == 0 {
  99. panic("HandleDir: no directories found under the embedded file system")
  100. }
  101. subfs, err := fs.Sub(v, direEtries[0].Name())
  102. if err != nil {
  103. panic(err)
  104. }
  105. fileSystem = http.FS(subfs)
  106. case fs.FS:
  107. fileSystem = http.FS(v)
  108. default:
  109. panic(fmt.Sprintf(`unexpected "fsOrDir" argument type of %T (string or http.FileSystem or embed.FS or fs.FS)`, v))
  110. }
  111. return fileSystem
  112. }
  113. // FindNames accepts a "http.FileSystem" and a root name and returns
  114. // the list containing its file names.
  115. func FindNames(fileSystem http.FileSystem, name string) ([]string, error) {
  116. f, err := fileSystem.Open(name)
  117. if err != nil {
  118. return nil, err
  119. }
  120. defer f.Close()
  121. fi, err := f.Stat()
  122. if err != nil {
  123. return nil, err
  124. }
  125. if !fi.IsDir() {
  126. return []string{name}, nil
  127. }
  128. fileinfos, err := f.Readdir(-1)
  129. if err != nil {
  130. return nil, err
  131. }
  132. files := make([]string, 0)
  133. for _, info := range fileinfos {
  134. // Note:
  135. // go-bindata has absolute names with os.Separator,
  136. // http.Dir the basename.
  137. filename := toBaseName(info.Name())
  138. fullname := path.Join(name, filename)
  139. if fullname == name { // prevent looping through itself.
  140. continue
  141. }
  142. rfiles, err := FindNames(fileSystem, fullname)
  143. if err != nil {
  144. return nil, err
  145. }
  146. files = append(files, rfiles...)
  147. }
  148. return files, nil
  149. }
  150. // Instead of path.Base(filepath.ToSlash(s))
  151. // let's do something like that, it is faster
  152. // (used to list directories on serve-time too):
  153. func toBaseName(s string) string {
  154. n := len(s) - 1
  155. for i := n; i >= 0; i-- {
  156. if c := s[i]; c == '/' || c == '\\' {
  157. if i == n {
  158. // "s" ends with a slash, remove it and retry.
  159. return toBaseName(s[:n])
  160. }
  161. return s[i+1:] // return the rest, trimming the slash.
  162. }
  163. }
  164. return s
  165. }