123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- // Copyright 2023 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package assetfs
-
- import (
- "context"
- "fmt"
- "io"
- "io/fs"
- "net/http"
- "os"
- "path/filepath"
- "sort"
- "time"
-
- "code.gitea.io/gitea/modules/container"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/process"
- "code.gitea.io/gitea/modules/util"
-
- "github.com/fsnotify/fsnotify"
- )
-
- // Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem
- type Layer struct {
- name string
- fs http.FileSystem
- localPath string
- }
-
- func (l *Layer) Name() string {
- return l.name
- }
-
- // Open opens the named file. The caller is responsible for closing the file.
- func (l *Layer) Open(name string) (http.File, error) {
- return l.fs.Open(name)
- }
-
- // Local returns a new Layer with the given name, it serves files from the given local path.
- func Local(name, base string, sub ...string) *Layer {
- // TODO: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
- // Ideally, the caller should guarantee the base is absolute, guessing a relative path based on the current working directory is unreliable.
- base, err := filepath.Abs(base)
- if err != nil {
- // 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.
- panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err))
- }
- root := util.FilePathJoinAbs(base, sub...)
- return &Layer{name: name, fs: http.Dir(root), localPath: root}
- }
-
- // Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
- func Bindata(name string, fs http.FileSystem) *Layer {
- return &Layer{name: name, fs: fs}
- }
-
- // LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers.
- // The first layer is the top layer, and it will be used first.
- // If the file is not found in the top layer, it will be searched in the next layer.
- type LayeredFS struct {
- layers []*Layer
- }
-
- // Layered returns a new LayeredFS with the given layers. The first layer is the top layer.
- func Layered(layers ...*Layer) *LayeredFS {
- return &LayeredFS{layers: layers}
- }
-
- // Open opens the named file. The caller is responsible for closing the file.
- func (l *LayeredFS) Open(name string) (http.File, error) {
- for _, layer := range l.layers {
- f, err := layer.Open(name)
- if err == nil || !os.IsNotExist(err) {
- return f, err
- }
- }
- return nil, fs.ErrNotExist
- }
-
- // ReadFile reads the named file.
- func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) {
- bs, _, err := l.ReadLayeredFile(elems...)
- return bs, err
- }
-
- // ReadLayeredFile reads the named file, and returns the layer name.
- func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
- name := util.PathJoinRel(elems...)
- for _, layer := range l.layers {
- f, err := layer.Open(name)
- if os.IsNotExist(err) {
- continue
- } else if err != nil {
- return nil, layer.name, err
- }
- bs, err := io.ReadAll(f)
- _ = f.Close()
- return bs, layer.name, err
- }
- return nil, "", fs.ErrNotExist
- }
-
- func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
- if util.CommonSkip(info.Name()) {
- return false
- }
- if len(fileMode) == 0 {
- return true
- } else if len(fileMode) == 1 {
- return fileMode[0] == !info.Mode().IsDir()
- }
- panic("too many arguments for fileMode in shouldInclude")
- }
-
- func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
- f, err := layer.Open(name)
- if os.IsNotExist(err) {
- return nil, nil
- } else if err != nil {
- return nil, err
- }
- defer f.Close()
- return f.Readdir(-1)
- }
-
- // ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
- // * omitted: all files and directories will be returned.
- // * true: only files will be returned.
- // * false: only directories will be returned.
- // The returned files are sorted by name.
- func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
- fileSet := make(container.Set[string])
- for _, layer := range l.layers {
- infos, err := readDir(layer, name)
- if err != nil {
- return nil, err
- }
- for _, info := range infos {
- if shouldInclude(info, fileMode...) {
- fileSet.Add(info.Name())
- }
- }
- }
- files := fileSet.Values()
- sort.Strings(files)
- return files, nil
- }
-
- // ListAllFiles returns files/directories in the given directory, including subdirectories, recursively.
- // The fileMode controls the returned files:
- // * omitted: all files and directories will be returned.
- // * true: only files will be returned.
- // * false: only directories will be returned.
- // The returned files are sorted by name.
- func (l *LayeredFS) ListAllFiles(name string, fileMode ...bool) ([]string, error) {
- return listAllFiles(l.layers, name, fileMode...)
- }
-
- func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, error) {
- fileSet := make(container.Set[string])
- var list func(dir string) error
- list = func(dir string) error {
- for _, layer := range layers {
- infos, err := readDir(layer, dir)
- if err != nil {
- return err
- }
- for _, info := range infos {
- path := util.PathJoinRelX(dir, info.Name())
- if shouldInclude(info, fileMode...) {
- fileSet.Add(path)
- }
- if info.IsDir() {
- if err = list(path); err != nil {
- return err
- }
- }
- }
- }
- return nil
- }
- if err := list(name); err != nil {
- return nil, err
- }
- files := fileSet.Values()
- sort.Strings(files)
- return files, nil
- }
-
- // WatchLocalChanges watches local changes in the file-system. It's used to help to reload assets when the local file-system changes.
- func (l *LayeredFS) WatchLocalChanges(ctx context.Context, callback func()) {
- ctx, _, finished := process.GetManager().AddTypedContext(ctx, "Asset Local FileSystem Watcher", process.SystemProcessType, true)
- defer finished()
-
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- log.Error("Unable to create watcher for asset local file-system: %v", err)
- return
- }
- defer watcher.Close()
-
- for _, layer := range l.layers {
- if layer.localPath == "" {
- continue
- }
- layerDirs, err := listAllFiles([]*Layer{layer}, ".", false)
- if err != nil {
- log.Error("Unable to list directories for asset local file-system %q: %v", layer.localPath, err)
- continue
- }
- layerDirs = append(layerDirs, ".")
- for _, dir := range layerDirs {
- if err = watcher.Add(util.FilePathJoinAbs(layer.localPath, dir)); err != nil && !os.IsNotExist(err) {
- log.Error("Unable to watch directory %s: %v", dir, err)
- }
- }
- }
-
- debounce := util.Debounce(100 * time.Millisecond)
-
- for {
- select {
- case <-ctx.Done():
- return
- case event, ok := <-watcher.Events:
- if !ok {
- return
- }
- log.Trace("Watched asset local file-system had event: %v", event)
- debounce(callback)
- case err, ok := <-watcher.Errors:
- if !ok {
- return
- }
- log.Error("Watched asset local file-system had error: %v", err)
- }
- }
- }
-
- // GetFileLayerName returns the name of the first-seen layer that contains the given file.
- func (l *LayeredFS) GetFileLayerName(elems ...string) string {
- name := util.PathJoinRel(elems...)
- for _, layer := range l.layers {
- f, err := layer.Open(name)
- if os.IsNotExist(err) {
- continue
- } else if err != nil {
- return ""
- }
- _ = f.Close()
- return layer.name
- }
- return ""
- }
|