[module] kffs
This commit is contained in:
parent
38cfe30644
commit
14e5731922
|
@ -0,0 +1,363 @@
|
||||||
|
// This package allows for grouping embedded filesystems into one multiFS,
|
||||||
|
// and provides a simple interface for reading from them
|
||||||
|
// and protections for reading from the local game filesystem
|
||||||
|
package KFFS
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: add in some more security features to prevent directory traversal attacks and other security vulnerabilities.
|
||||||
|
// this package should be an easy to use viable alternative to the standard os and fs packages, while providing
|
||||||
|
// some ease of mind for end user security.
|
||||||
|
|
||||||
|
var localFS fs.FS
|
||||||
|
var localIsValid bool
|
||||||
|
|
||||||
|
// IsRunningDebug TODO: make sure this is removed before any production builds
|
||||||
|
func IsRunningDebug() bool {
|
||||||
|
return os.Getenv("DEBUG_RUN") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
localIsValid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there is a game directory at the root of the executable
|
||||||
|
dataDir := filepath.Join(filepath.Dir(ex), "data")
|
||||||
|
if IsRunningDebug() {
|
||||||
|
dataDir, err = filepath.Abs("data")
|
||||||
|
if err != nil {
|
||||||
|
localIsValid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
localIsValid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it is a directory
|
||||||
|
if !info.IsDir() {
|
||||||
|
localIsValid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localFS = os.DirFS(dataDir)
|
||||||
|
|
||||||
|
// DEBUG: REMOVE BELOW
|
||||||
|
// walk the directory to check if it is valid by printing out the files
|
||||||
|
err = fs.WalkDir(localFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Println(path)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
localIsValid = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// DEBUG: REMOVE ABOVE
|
||||||
|
|
||||||
|
localIsValid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration is a configuration struct for the multiFS that allows specifying the filesystems to use.
|
||||||
|
// new file systems can be added by calling EmbedFS or and embed.FS or a fs.FS
|
||||||
|
type Configuration struct {
|
||||||
|
FSNames []string
|
||||||
|
LocalFS bool
|
||||||
|
OnlyLocal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfiguration creates a new configuration for the multiFS if local is true is checked first.
|
||||||
|
// by leaving fsNames empty all filesystems will be checked in no particular order (it loops through a map[string]fs.FS),
|
||||||
|
// with most fuctions except walk.
|
||||||
|
// returning the first matching file, Walk will walk all filesystems if it is called by an empty fsNames,
|
||||||
|
// or only the specified filesystems if fsNames is not empty.
|
||||||
|
// you can also manually set OnlyLocal to true after creating the configuration to only check the local filesystem.
|
||||||
|
func NewConfiguration(useLocal bool, fsNames ...string) Configuration {
|
||||||
|
return Configuration{
|
||||||
|
LocalFS: useLocal,
|
||||||
|
FSNames: fsNames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiFS is a list of filesystems.
|
||||||
|
type multiFS map[string]fs.FS
|
||||||
|
|
||||||
|
func (m multiFS) Open(path string, config Configuration) (fs.File, error) {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
if (config.LocalFS || config.OnlyLocal) && localIsValid {
|
||||||
|
file, err := localFS.Open(path)
|
||||||
|
if err == nil {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
} else if !localIsValid && config.OnlyLocal {
|
||||||
|
return nil, errors.New("local filesystem is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.OnlyLocal {
|
||||||
|
if len(config.FSNames) > 0 {
|
||||||
|
for _, name := range config.FSNames {
|
||||||
|
// check if the name exists in the filesystems
|
||||||
|
fsys, ok := m[name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file, err := fsys.Open(path)
|
||||||
|
if err == nil {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, fsys := range m {
|
||||||
|
file, err := fsys.Open(path)
|
||||||
|
if err == nil {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat returns the fileInfo for the first matching file info in the filesystem.
|
||||||
|
func (m multiFS) Stat(path string, config Configuration) (fs.FileInfo, error) {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
if (config.LocalFS || config.OnlyLocal) && localIsValid {
|
||||||
|
fileInfo, err := fs.Stat(localFS, path)
|
||||||
|
if err == nil {
|
||||||
|
return fileInfo, nil
|
||||||
|
}
|
||||||
|
} else if !localIsValid && config.OnlyLocal {
|
||||||
|
return nil, errors.New("local filesystem is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.OnlyLocal {
|
||||||
|
if len(config.FSNames) > 0 {
|
||||||
|
for _, name := range config.FSNames {
|
||||||
|
// check if the name exists in the filesystems
|
||||||
|
fsys, ok := m[name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file, err := fsys.Open(path)
|
||||||
|
if err == nil {
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err == nil {
|
||||||
|
return fileInfo, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, fsys := range m {
|
||||||
|
file, err := fsys.Open(path)
|
||||||
|
if err == nil {
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err == nil {
|
||||||
|
return fileInfo, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile reads the first matching file in the filesystems and returns the contents as a byte slice
|
||||||
|
func (m multiFS) ReadFile(path string, config Configuration) ([]byte, error) {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
if (config.LocalFS || config.OnlyLocal) && localIsValid {
|
||||||
|
data, err := fs.ReadFile(localFS, path)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
} else if !localIsValid && config.OnlyLocal {
|
||||||
|
return nil, errors.New("local filesystem is not valid")
|
||||||
|
}
|
||||||
|
if !config.OnlyLocal {
|
||||||
|
if len(config.FSNames) > 0 {
|
||||||
|
for _, name := range config.FSNames {
|
||||||
|
//Check if the name exists in the filesystems
|
||||||
|
fsys, ok := m[name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := fs.ReadFile(fsys, path)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, fsys := range m {
|
||||||
|
data, err := fs.ReadFile(fsys, path)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir reads the first matching directory in the filesystems and returns a list of directory entries
|
||||||
|
func (m multiFS) ReadDir(path string, config Configuration) ([]fs.DirEntry, error) {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
if (config.LocalFS || config.OnlyLocal) && localIsValid {
|
||||||
|
data, err := fs.ReadDir(localFS, path)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
} else if !localIsValid && config.OnlyLocal {
|
||||||
|
return nil, errors.New("local filesystem is not valid")
|
||||||
|
}
|
||||||
|
if !config.OnlyLocal {
|
||||||
|
if len(config.FSNames) > 0 {
|
||||||
|
for _, name := range config.FSNames {
|
||||||
|
//Check if the name exists in the filesystems
|
||||||
|
fsys, ok := m[name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := fs.ReadDir(fsys, path)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, fsys := range m {
|
||||||
|
data, err := fs.ReadDir(fsys, path)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk walks each filesystem in the multiFS performing the walkFn on each file or directory
|
||||||
|
func (m multiFS) Walk(path string, walkFn fs.WalkDirFunc, config Configuration) error {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
var err error
|
||||||
|
if (config.LocalFS || config.OnlyLocal) && localIsValid {
|
||||||
|
walkErr := fs.WalkDir(localFS, path, walkFn)
|
||||||
|
if walkErr != nil {
|
||||||
|
errors.Join(err, walkErr)
|
||||||
|
}
|
||||||
|
} else if !localIsValid && config.OnlyLocal {
|
||||||
|
errors.Join(err, errors.New("local filesystem is not valid"))
|
||||||
|
}
|
||||||
|
if !config.OnlyLocal {
|
||||||
|
if len(config.FSNames) > 0 {
|
||||||
|
for _, name := range config.FSNames {
|
||||||
|
// check if the name exists in the filesystems
|
||||||
|
fsys, ok := m[name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
walkErr := fs.WalkDir(fsys, path, walkFn)
|
||||||
|
if walkErr != nil {
|
||||||
|
errors.Join(err, walkErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, fsys := range m {
|
||||||
|
walkErr := fs.WalkDir(fsys, path, walkFn)
|
||||||
|
if walkErr != nil {
|
||||||
|
errors.Join(err, walkErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var embeddedFS multiFS
|
||||||
|
|
||||||
|
// EmbedFS sets the embedded filesystem to use for loading files
|
||||||
|
// This function can be called multiple times to add multiple embedded filesystems
|
||||||
|
// However, please note that unless you use Walk, the functions acting on these added filesystems
|
||||||
|
// will only return the first matching result. So make sure your filenames/directory names are unique between the filesystems
|
||||||
|
// Or specify the filesystem to use when calling the function
|
||||||
|
func EmbedFS(fs embed.FS, names ...string) {
|
||||||
|
//Check if a name is provided otherwise set name to "embedded" plus a number until it is unique
|
||||||
|
name := "embedded"
|
||||||
|
if len(names) > 0 {
|
||||||
|
name = names[0]
|
||||||
|
}
|
||||||
|
for i := 0; embeddedFS[name] != nil; i++ {
|
||||||
|
name = "embedded" + strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
embeddedFS[name] = fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk is an extension of fs.WalkDir for any specified fileSystems
|
||||||
|
// that have been embedded with EmbedFS
|
||||||
|
// or all of them if no names are specified(It will return the first matching file)
|
||||||
|
// and optionally the local game filesystem
|
||||||
|
// Configuration can be created with NewConfiguration
|
||||||
|
func Walk(dir string, config Configuration, walkFn fs.WalkDirFunc) error {
|
||||||
|
return embeddedFS.Walk(dir, walkFn, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile is an extension of fs.ReadFile for any specified fileSystems
|
||||||
|
// that have been embedded with EmbedFS
|
||||||
|
// or all of them if no names are specified(It will return the first matching file)
|
||||||
|
// and optionally the local game filesystem
|
||||||
|
// Configuration can be created with NewConfiguration
|
||||||
|
func ReadFile(path string, config Configuration) ([]byte, error) {
|
||||||
|
return embeddedFS.ReadFile(path, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadDir is an extension of fs.ReadDir for any specified fileSystems
|
||||||
|
// that have been embedded with EmbedFS
|
||||||
|
// or all of them if no names are specified(It will return the first matching file)
|
||||||
|
// and optionally the local game filesystem
|
||||||
|
// Configuration can be created with NewConfiguration
|
||||||
|
func ReadDir(path string, config Configuration) ([]fs.DirEntry, error) {
|
||||||
|
return embeddedFS.ReadDir(path, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open is an extension of os.Open for any specified fileSystems
|
||||||
|
// that have been embedded with EmbedFS
|
||||||
|
// or all of them if no names are specified(It will return the first matching file)
|
||||||
|
// and optionally the local game filesystem
|
||||||
|
// Configuration can be created with NewConfiguration
|
||||||
|
func Open(path string, config Configuration) (fs.File, error) {
|
||||||
|
return embeddedFS.Open(path, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat is an extension of fs.Stat for any specified fileSystems
|
||||||
|
// that have been embedded with EmbedFS
|
||||||
|
// or all of them if no names are specified(It will return the first matching file)
|
||||||
|
// and optionally the local game filesystem
|
||||||
|
// Configuration can be created with NewConfiguration
|
||||||
|
func Stat(path string, config Configuration) (fs.FileInfo, error) {
|
||||||
|
return embeddedFS.Stat(path, config)
|
||||||
|
}
|
Loading…
Reference in New Issue