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  }