You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package assetfs
  4. import (
  5. "context"
  6. "fmt"
  7. "io"
  8. "io/fs"
  9. "net/http"
  10. "os"
  11. "path/filepath"
  12. "sort"
  13. "time"
  14. "code.gitea.io/gitea/modules/container"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/process"
  17. "code.gitea.io/gitea/modules/util"
  18. "github.com/fsnotify/fsnotify"
  19. )
  20. // Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem
  21. type Layer struct {
  22. name string
  23. fs http.FileSystem
  24. localPath string
  25. }
  26. func (l *Layer) Name() string {
  27. return l.name
  28. }
  29. // Open opens the named file. The caller is responsible for closing the file.
  30. func (l *Layer) Open(name string) (http.File, error) {
  31. return l.fs.Open(name)
  32. }
  33. // Local returns a new Layer with the given name, it serves files from the given local path.
  34. func Local(name, base string, sub ...string) *Layer {
  35. // TODO: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
  36. // Ideally, the caller should guarantee the base is absolute, guessing a relative path based on the current working directory is unreliable.
  37. base, err := filepath.Abs(base)
  38. if err != nil {
  39. // This should never happen in a real system. If it happens, the user must have already been in trouble: the system is not able to resolve its own paths.
  40. panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err))
  41. }
  42. root := util.FilePathJoinAbs(base, sub...)
  43. return &Layer{name: name, fs: http.Dir(root), localPath: root}
  44. }
  45. // Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
  46. func Bindata(name string, fs http.FileSystem) *Layer {
  47. return &Layer{name: name, fs: fs}
  48. }
  49. // LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers.
  50. // The first layer is the top layer, and it will be used first.
  51. // If the file is not found in the top layer, it will be searched in the next layer.
  52. type LayeredFS struct {
  53. layers []*Layer
  54. }
  55. // Layered returns a new LayeredFS with the given layers. The first layer is the top layer.
  56. func Layered(layers ...*Layer) *LayeredFS {
  57. return &LayeredFS{layers: layers}
  58. }
  59. // Open opens the named file. The caller is responsible for closing the file.
  60. func (l *LayeredFS) Open(name string) (http.File, error) {
  61. for _, layer := range l.layers {
  62. f, err := layer.Open(name)
  63. if err == nil || !os.IsNotExist(err) {
  64. return f, err
  65. }
  66. }
  67. return nil, fs.ErrNotExist
  68. }
  69. // ReadFile reads the named file.
  70. func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) {
  71. bs, _, err := l.ReadLayeredFile(elems...)
  72. return bs, err
  73. }
  74. // ReadLayeredFile reads the named file, and returns the layer name.
  75. func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
  76. name := util.PathJoinRel(elems...)
  77. for _, layer := range l.layers {
  78. f, err := layer.Open(name)
  79. if os.IsNotExist(err) {
  80. continue
  81. } else if err != nil {
  82. return nil, layer.name, err
  83. }
  84. bs, err := io.ReadAll(f)
  85. _ = f.Close()
  86. return bs, layer.name, err
  87. }
  88. return nil, "", fs.ErrNotExist
  89. }
  90. func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
  91. if util.CommonSkip(info.Name()) {
  92. return false
  93. }
  94. if len(fileMode) == 0 {
  95. return true
  96. } else if len(fileMode) == 1 {
  97. return fileMode[0] == !info.Mode().IsDir()
  98. }
  99. panic("too many arguments for fileMode in shouldInclude")
  100. }
  101. func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
  102. f, err := layer.Open(name)
  103. if os.IsNotExist(err) {
  104. return nil, nil
  105. } else if err != nil {
  106. return nil, err
  107. }
  108. defer f.Close()
  109. return f.Readdir(-1)
  110. }
  111. // ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
  112. // * omitted: all files and directories will be returned.
  113. // * true: only files will be returned.
  114. // * false: only directories will be returned.
  115. // The returned files are sorted by name.
  116. func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
  117. fileSet := make(container.Set[string])
  118. for _, layer := range l.layers {
  119. infos, err := readDir(layer, name)
  120. if err != nil {
  121. return nil, err
  122. }
  123. for _, info := range infos {
  124. if shouldInclude(info, fileMode...) {
  125. fileSet.Add(info.Name())
  126. }
  127. }
  128. }
  129. files := fileSet.Values()
  130. sort.Strings(files)
  131. return files, nil
  132. }
  133. // ListAllFiles returns files/directories in the given directory, including subdirectories, recursively.
  134. // The fileMode controls the returned files:
  135. // * omitted: all files and directories will be returned.
  136. // * true: only files will be returned.
  137. // * false: only directories will be returned.
  138. // The returned files are sorted by name.
  139. func (l *LayeredFS) ListAllFiles(name string, fileMode ...bool) ([]string, error) {
  140. return listAllFiles(l.layers, name, fileMode...)
  141. }
  142. func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, error) {
  143. fileSet := make(container.Set[string])
  144. var list func(dir string) error
  145. list = func(dir string) error {
  146. for _, layer := range layers {
  147. infos, err := readDir(layer, dir)
  148. if err != nil {
  149. return err
  150. }
  151. for _, info := range infos {
  152. path := util.PathJoinRelX(dir, info.Name())
  153. if shouldInclude(info, fileMode...) {
  154. fileSet.Add(path)
  155. }
  156. if info.IsDir() {
  157. if err = list(path); err != nil {
  158. return err
  159. }
  160. }
  161. }
  162. }
  163. return nil
  164. }
  165. if err := list(name); err != nil {
  166. return nil, err
  167. }
  168. files := fileSet.Values()
  169. sort.Strings(files)
  170. return files, nil
  171. }
  172. // WatchLocalChanges watches local changes in the file-system. It's used to help to reload assets when the local file-system changes.
  173. func (l *LayeredFS) WatchLocalChanges(ctx context.Context, callback func()) {
  174. ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Asset Local FileSystem Watcher", process.SystemProcessType, true)
  175. defer finished()
  176. watcher, err := fsnotify.NewWatcher()
  177. if err != nil {
  178. log.Error("Unable to create watcher for asset local file-system: %v", err)
  179. return
  180. }
  181. defer watcher.Close()
  182. for _, layer := range l.layers {
  183. if layer.localPath == "" {
  184. continue
  185. }
  186. layerDirs, err := listAllFiles([]*Layer{layer}, ".", false)
  187. if err != nil {
  188. log.Error("Unable to list directories for asset local file-system %q: %v", layer.localPath, err)
  189. continue
  190. }
  191. layerDirs = append(layerDirs, ".")
  192. for _, dir := range layerDirs {
  193. if err = watcher.Add(util.FilePathJoinAbs(layer.localPath, dir)); err != nil && !os.IsNotExist(err) {
  194. log.Error("Unable to watch directory %s: %v", dir, err)
  195. }
  196. }
  197. }
  198. debounce := util.Debounce(100 * time.Millisecond)
  199. for {
  200. select {
  201. case <-ctx.Done():
  202. return
  203. case event, ok := <-watcher.Events:
  204. if !ok {
  205. return
  206. }
  207. log.Trace("Watched asset local file-system had event: %v", event)
  208. debounce(callback)
  209. case err, ok := <-watcher.Errors:
  210. if !ok {
  211. return
  212. }
  213. log.Error("Watched asset local file-system had error: %v", err)
  214. }
  215. }
  216. }
  217. // GetFileLayerName returns the name of the first-seen layer that contains the given file.
  218. func (l *LayeredFS) GetFileLayerName(elems ...string) string {
  219. name := util.PathJoinRel(elems...)
  220. for _, layer := range l.layers {
  221. f, err := layer.Open(name)
  222. if os.IsNotExist(err) {
  223. continue
  224. } else if err != nil {
  225. return ""
  226. }
  227. _ = f.Close()
  228. return layer.name
  229. }
  230. return ""
  231. }