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 }