go-efuse/embededFUSE.go
2024-07-06 22:37:27 +02:00

285 lines
6.5 KiB
Go

package embededFUSE
import (
"context"
"embed"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path"
"strings"
"syscall"
"time"
"unicode"
"bazil.org/fuse"
fuseFS "bazil.org/fuse/fs"
)
// ServeInitFailTimeoutMS is the time to wait for the Serve function to start.
var ServeInitFailTimeoutMS = 500
func getFSName() string {
var progName string
if name, err := os.Executable(); err != nil {
progName = path.Base(name)
} else if len(os.Args) > 0 {
progName = os.Args[0]
} else {
progName = "generic-goembed"
}
return strings.Map(func(r rune) rune {
if !unicode.In(r, unicode.Hyphen, unicode.Letter) {
return -1
}
return r
}, progName)
}
type file struct {
fs embed.FS
path string
size uint64
executable bool
}
func (f *file) Attr(ctx context.Context, attr *fuse.Attr) error {
attr.Mode = 0o400
if f.executable {
attr.Mode = 0o500
}
attr.Size = f.size
attr.Mtime = time.Now()
return nil
}
func (f *file) ReadAll() ([]byte, error) {
buff := make([]byte, f.size, f.size)
fsFile, err := f.fs.Open(f.path)
if err != nil {
return nil, fuse.ToErrno(err)
}
defer fsFile.Close()
n, err := fsFile.Read(buff)
if err != nil {
return nil, fuse.ToErrno(err)
}
if uint64(n) < f.size {
return nil, fuse.ToErrno(syscall.EIO)
}
return buff, nil
}
func (f *file) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error {
size := int64(f.size)
if req.Offset >= int64(f.size) {
return fuse.ToErrno(syscall.EOVERFLOW)
}
if req.Offset+int64(req.Size) > int64(f.size) {
size = int64(f.size) - req.Offset
}
buff := make([]byte, size, size)
fsFile, err := f.fs.Open(f.path)
if err != nil {
return fuse.ToErrno(err)
}
defer fsFile.Close()
secReader := io.NewSectionReader(fsFile.(io.ReaderAt), req.Offset, size)
_, err = secReader.Read(buff)
if err != nil {
return fuse.ToErrno(err)
}
resp.Data = buff
return nil
}
func (f *file) Ioctl(ctx context.Context, req *fuse.IoctlRequest, resp *fuse.IoctlResponse) error {
return nil
}
// EmbeddedFUSEConfig is the configuration for the EmbeddedFUSE.
type EmbeddedFUSEConfig struct {
// EmbedFS is the embed.FS to be mounted.
EmbedFS embed.FS
// DefaultExecutable is the default executable executed when the Execute method is called.
DefaultExecutable string
// OtherExecutables is the list of other executables that can be executed.
OtherExecutables []string
// Mountpoint is the mountpoint of the FUSE filesystem.
Mountpoint string
// Prefix is the prefix of the files to be mounted from the embed.FS.
Prefix string
}
// EmbeddedFUSE is the interface for a mountable embedded FUSE filesystem.
type EmbeddedFUSE interface {
// Mount mounts the embedded FUSE filesystem.
Mount() error
// Unmount unmounts the embedded FUSE filesystem.
Unmount() error
// Execute executes the default or specified executable.
Execute(...string) error
// Ls lists the files in the mounted FUSE filesystem.
Ls() ([]os.DirEntry, error)
}
type embededFUSE struct {
fs embed.FS
conn *fuse.Conn
tree *fuseFS.Tree
mounted bool
mountpoint string
defaultExecutable string
executables map[string]struct{}
ctx context.Context
cancel context.CancelFunc
srvErrChan chan error
}
func (g *embededFUSE) Mount() error {
if g.mounted {
return fmt.Errorf("Already mounted.")
}
tempDir := os.TempDir()
if strings.HasSuffix(g.mountpoint, "/*") {
tempDir = strings.TrimSuffix(g.mountpoint, "/*")
g.mountpoint = ""
}
if g.mountpoint == "" {
if dir, err := os.MkdirTemp(tempDir, "*"); err == nil {
g.mountpoint = dir
} else {
return err
}
}
if conn, err := fuse.Mount(
g.mountpoint,
fuse.AllowNonEmptyMount(),
fuse.CacheSymlinks(),
fuse.FSName(getFSName()),
fuse.ReadOnly(),
fuse.Subtype("goembed"),
); err == nil {
g.conn = conn
} else {
return err
}
g.ctx, g.cancel = context.WithCancel(context.Background())
g.srvErrChan = make(chan error, 1)
go func() {
select {
case <-g.ctx.Done():
return
default:
if err := fuseFS.Serve(g.conn, g.tree); err != nil {
g.srvErrChan <- err
g.cancel()
}
}
}()
time.Sleep(time.Millisecond * time.Duration(ServeInitFailTimeoutMS))
if len(g.srvErrChan) > 0 {
return <-g.srvErrChan
}
g.mounted = true
return nil
}
func (g *embededFUSE) Unmount() error {
g.cancel()
if err := fuse.Unmount(g.mountpoint); err != nil {
return err
}
g.conn.Close()
return os.Remove(g.mountpoint)
}
func (g *embededFUSE) Execute(execAndArgs ...string) error {
if !g.mounted {
return fmt.Errorf("EmbeddedFUSE is not mounted.")
}
if len(g.executables) == 0 {
return fmt.Errorf("No registerd executables.")
}
execName := g.defaultExecutable
args := []string{}
if len(execAndArgs) > 0 {
execName = execAndArgs[0]
if _, ok := g.executables[execName]; !ok {
return fmt.Errorf("Unregisterd executable.")
}
if len(execAndArgs) > 1 {
args = execAndArgs[1:]
}
}
execName = path.Join(g.mountpoint, execName)
cmd := exec.Command(execName, args...)
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return err
}
return nil
}
func (g *embededFUSE) Ls() ([]os.DirEntry, error) {
return os.ReadDir(g.mountpoint)
}
// New is a constructor for an EmbeddedFUSE object.
func New(conf EmbeddedFUSEConfig) (EmbeddedFUSE, error) {
if conf.OtherExecutables == nil {
conf.OtherExecutables = []string{}
if conf.DefaultExecutable != "" {
conf.OtherExecutables = append(conf.OtherExecutables, conf.DefaultExecutable)
}
}
g := embededFUSE{
fs: conf.EmbedFS,
tree: &fuseFS.Tree{},
mountpoint: conf.Mountpoint,
defaultExecutable: conf.DefaultExecutable,
executables: map[string]struct{}{},
}
for _, e := range conf.OtherExecutables {
g.executables[e] = struct{}{}
}
if conf.Prefix == "" {
conf.Prefix = "."
}
fs.WalkDir(conf.EmbedFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
} else if path == "." {
return nil
} else if strings.HasPrefix(conf.Prefix, path) || path == conf.Prefix {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
if !d.IsDir() {
trimmedPath := strings.TrimPrefix(path, fmt.Sprintf("%s/", conf.Prefix))
executable := false
if _, ok := g.executables[trimmedPath]; ok {
executable = true
}
g.tree.Add(trimmedPath, &file{
fs: conf.EmbedFS,
path: path, size: uint64(info.Size()),
executable: executable,
})
return nil
}
return nil
})
return &g, nil
}