// 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/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 ) {
fileMap := map [ string ] bool { }
for _ , layer := range l . layers {
infos , err := readDir ( layer , name )
if err != nil {
return nil , err
}
for _ , info := range infos {
if shouldInclude ( info , fileMode ... ) {
fileMap [ info . Name ( ) ] = true
}
}
}
files := make ( [ ] string , 0 , len ( fileMap ) )
for file := range fileMap {
files = append ( files , file )
}
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 ) {
fileMap := map [ string ] bool { }
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 ... ) {
fileMap [ path ] = true
}
if info . IsDir ( ) {
if err = list ( path ) ; err != nil {
return err
}
}
}
}
return nil
}
if err := list ( name ) ; err != nil {
return nil , err
}
var files [ ] string
for file := range fileMap {
files = append ( files , file )
}
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 ""
}