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