2024-07-06 15:59:33 -04:00
|
|
|
package embededFUSE
|
2024-07-03 04:33:12 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"embed"
|
|
|
|
"fmt"
|
2024-07-06 15:59:33 -04:00
|
|
|
"io"
|
2024-07-03 04:33:12 -04:00
|
|
|
"io/fs"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path"
|
|
|
|
"strings"
|
2024-07-03 12:06:00 -04:00
|
|
|
"syscall"
|
2024-07-03 04:33:12 -04:00
|
|
|
"time"
|
2024-07-03 12:06:00 -04:00
|
|
|
"unicode"
|
2024-07-03 04:33:12 -04:00
|
|
|
|
|
|
|
"bazil.org/fuse"
|
|
|
|
fuseFS "bazil.org/fuse/fs"
|
|
|
|
)
|
|
|
|
|
2024-07-03 12:06:00 -04:00
|
|
|
var ServeInitFailTimeoutMS = 500
|
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
func getFSName() string {
|
2024-07-03 12:06:00 -04:00
|
|
|
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 {
|
2024-07-06 15:59:33 -04:00
|
|
|
progName = "generic-goembed"
|
2024-07-03 12:06:00 -04:00
|
|
|
}
|
2024-07-06 15:59:33 -04:00
|
|
|
return strings.Map(func(r rune) rune {
|
2024-07-03 12:06:00 -04:00
|
|
|
if !unicode.In(r, unicode.Hyphen, unicode.Letter) {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
return r
|
|
|
|
}, progName)
|
|
|
|
}
|
|
|
|
|
2024-07-03 04:33:12 -04:00
|
|
|
type File struct {
|
2024-07-06 15:59:33 -04:00
|
|
|
fs embed.FS
|
|
|
|
path string
|
|
|
|
size uint64
|
|
|
|
executable bool
|
2024-07-03 04:33:12 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func (f *File) Attr(ctx context.Context, attr *fuse.Attr) error {
|
2024-07-03 12:06:00 -04:00
|
|
|
attr.Mode = 0o400
|
2024-07-06 15:59:33 -04:00
|
|
|
if f.executable {
|
|
|
|
attr.Mode = 0o500
|
|
|
|
}
|
2024-07-03 04:33:12 -04:00
|
|
|
attr.Size = f.size
|
|
|
|
attr.Mtime = time.Now()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-03 12:06:00 -04:00
|
|
|
func (f *File) ReadAll() ([]byte, error) {
|
2024-07-06 15:59:33 -04:00
|
|
|
buff := make([]byte, f.size, f.size)
|
|
|
|
file, err := f.fs.Open(f.path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fuse.ToErrno(err)
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
n, err := file.Read(buff)
|
2024-07-03 12:06:00 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, fuse.ToErrno(err)
|
|
|
|
}
|
|
|
|
if uint64(n) < f.size {
|
|
|
|
return nil, fuse.ToErrno(syscall.EIO)
|
|
|
|
}
|
2024-07-06 15:59:33 -04:00
|
|
|
return buff, nil
|
2024-07-03 12:06:00 -04:00
|
|
|
}
|
2024-07-03 04:33:12 -04:00
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
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)
|
|
|
|
file, err := f.fs.Open(f.path)
|
|
|
|
if err != nil {
|
|
|
|
return fuse.ToErrno(err)
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
secReader := io.NewSectionReader(file.(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
|
|
|
|
}
|
|
|
|
|
|
|
|
type EmbeddedFUSEConfig struct {
|
2024-07-03 04:33:12 -04:00
|
|
|
EmbedFS embed.FS
|
|
|
|
DefaultExecutable string
|
|
|
|
OtherExecutables []string
|
|
|
|
Mountpoint string
|
2024-07-06 15:59:33 -04:00
|
|
|
Prefix string
|
2024-07-03 04:33:12 -04:00
|
|
|
}
|
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
type EmbeddedFUSE interface {
|
2024-07-03 04:33:12 -04:00
|
|
|
Mount() error
|
|
|
|
Unmount() error
|
|
|
|
Execute(...string) error
|
2024-07-06 15:59:33 -04:00
|
|
|
Ls() ([]os.DirEntry, error)
|
2024-07-03 04:33:12 -04:00
|
|
|
}
|
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
type embededFUSE struct {
|
2024-07-03 04:33:12 -04:00
|
|
|
fs embed.FS
|
|
|
|
conn *fuse.Conn
|
2024-07-06 15:59:33 -04:00
|
|
|
tree *fuseFS.Tree
|
2024-07-03 04:33:12 -04:00
|
|
|
mounted bool
|
|
|
|
mountpoint string
|
|
|
|
defaultExecutable string
|
|
|
|
executables map[string]struct{}
|
2024-07-03 12:06:00 -04:00
|
|
|
ctx context.Context
|
|
|
|
cancel context.CancelFunc
|
|
|
|
srvErrChan chan error
|
2024-07-03 04:33:12 -04:00
|
|
|
}
|
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
func (g *embededFUSE) Mount() error {
|
2024-07-03 04:33:12 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2024-07-03 12:06:00 -04:00
|
|
|
if conn, err := fuse.Mount(
|
|
|
|
g.mountpoint,
|
|
|
|
fuse.AllowNonEmptyMount(),
|
|
|
|
fuse.CacheSymlinks(),
|
2024-07-06 15:59:33 -04:00
|
|
|
fuse.FSName(getFSName()),
|
2024-07-03 12:06:00 -04:00
|
|
|
fuse.ReadOnly(),
|
2024-07-06 15:59:33 -04:00
|
|
|
fuse.Subtype("goembed"),
|
2024-07-03 12:06:00 -04:00
|
|
|
); err == nil {
|
2024-07-03 04:33:12 -04:00
|
|
|
g.conn = conn
|
|
|
|
} else {
|
|
|
|
return err
|
|
|
|
}
|
2024-07-03 12:06:00 -04:00
|
|
|
g.ctx, g.cancel = context.WithCancel(context.Background())
|
|
|
|
g.srvErrChan = make(chan error, 1)
|
|
|
|
go func() {
|
|
|
|
select {
|
|
|
|
case <-g.ctx.Done():
|
|
|
|
return
|
|
|
|
default:
|
2024-07-06 15:59:33 -04:00
|
|
|
if err := fuseFS.Serve(g.conn, g.tree); err != nil {
|
2024-07-03 12:06:00 -04:00
|
|
|
g.srvErrChan <- err
|
|
|
|
g.cancel()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
time.Sleep(time.Millisecond * time.Duration(ServeInitFailTimeoutMS))
|
|
|
|
if len(g.srvErrChan) > 0 {
|
|
|
|
return <-g.srvErrChan
|
2024-07-03 04:33:12 -04:00
|
|
|
}
|
|
|
|
g.mounted = true
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
func (g *embededFUSE) Unmount() error {
|
2024-07-03 12:06:00 -04:00
|
|
|
g.cancel()
|
2024-07-03 04:33:12 -04:00
|
|
|
if err := fuse.Unmount(g.mountpoint); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-07-03 12:15:29 -04:00
|
|
|
g.conn.Close()
|
2024-07-03 12:06:00 -04:00
|
|
|
return os.Remove(g.mountpoint)
|
2024-07-03 04:33:12 -04:00
|
|
|
}
|
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
func (g *embededFUSE) Execute(execAndArgs ...string) error {
|
2024-07-03 04:33:12 -04:00
|
|
|
if !g.mounted {
|
2024-07-06 15:59:33 -04:00
|
|
|
return fmt.Errorf("EmbeddedFUSE is not mounted.")
|
2024-07-03 04:33:12 -04:00
|
|
|
}
|
|
|
|
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)
|
2024-07-03 12:06:00 -04:00
|
|
|
cmd := exec.Command(execName, args...)
|
2024-07-06 15:59:33 -04:00
|
|
|
cmd.Stdout = os.Stdout
|
2024-07-03 04:33:12 -04:00
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-07-06 15:59:33 -04:00
|
|
|
func (g *embededFUSE) Ls() ([]os.DirEntry, error) {
|
|
|
|
return os.ReadDir(g.mountpoint)
|
|
|
|
}
|
|
|
|
|
|
|
|
func New(conf EmbeddedFUSEConfig) (EmbeddedFUSE, error) {
|
2024-07-03 04:33:12 -04:00
|
|
|
if conf.OtherExecutables == nil {
|
|
|
|
conf.OtherExecutables = []string{}
|
|
|
|
if conf.DefaultExecutable != "" {
|
|
|
|
conf.OtherExecutables = append(conf.OtherExecutables, conf.DefaultExecutable)
|
|
|
|
}
|
|
|
|
}
|
2024-07-06 15:59:33 -04:00
|
|
|
g := embededFUSE{
|
2024-07-03 04:33:12 -04:00
|
|
|
fs: conf.EmbedFS,
|
2024-07-06 15:59:33 -04:00
|
|
|
tree: &fuseFS.Tree{},
|
2024-07-03 04:33:12 -04:00
|
|
|
mountpoint: conf.Mountpoint,
|
|
|
|
defaultExecutable: conf.DefaultExecutable,
|
|
|
|
executables: map[string]struct{}{},
|
|
|
|
}
|
2024-07-06 15:59:33 -04:00
|
|
|
|
2024-07-03 04:33:12 -04:00
|
|
|
for _, e := range conf.OtherExecutables {
|
|
|
|
g.executables[e] = struct{}{}
|
|
|
|
}
|
2024-07-06 15:59:33 -04:00
|
|
|
|
|
|
|
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
|
|
|
|
})
|
|
|
|
|
2024-07-03 04:33:12 -04:00
|
|
|
return &g, nil
|
|
|
|
}
|