Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/zipfuse-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:

- name: Mount filesystem
run: |
sudo sh -c 'nohup zipfuse source mountpoint --allow-other &>/var/log/zipfuse.log &'
sudo sh -c 'nohup zipfuse source mountpoint &>/var/log/zipfuse.log &'

for i in {1..20}; do
if mountpoint -q mountpoint; then
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/zipfuse-fstab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y fuse3
sudo sed -i 's/#user_allow_other/user_allow_other/' /etc/fuse.conf

- name: Vendor dependencies
run: make vendor
Expand Down Expand Up @@ -63,7 +62,7 @@ jobs:

- name: Mount filesystem
run: |
if ! sudo mount -t zipfuse source mountpoint -o allow_other; then
if ! sudo mount -t zipfuse source mountpoint; then
echo "ERROR: Filesystem mount has failed"
exit 1
fi
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ zipfuse <source> <mountpoint> [flags]

| Flag | Shorthand | Default | Description |
|------|-----------|---------|-------------|
| --allow-other `<bool>` | -a | false | Allow other system users to access the mounted filesystem. |
| --allow-other `<bool>` | -a | (true if root; false if not) | Allow other system users to access the mounted filesystem. |
| --dry-run `<bool>` | -d | false | Do not mount; instead print all would-be inodes and paths to standard output. |
| --fd-cache-bypass `<bool>` | (none) | false | Disable file descriptor caching; open/close a new file descriptor on every single request. |
| --fd-cache-size `<int>` | (none) | (70% of `fd-limit`) | Maximum open file descriptors to retain in cache (for more performant re-accessing). |
Expand Down
16 changes: 16 additions & 0 deletions cmd/mount.zipfuse/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var (
errMountFailed = errors.New("mount failed")
)

// BuildCommand constructs the full command slice from the parsed mount options.
func (mh *mountHelper) BuildCommand() []string {
var parts []string

Expand All @@ -43,6 +44,7 @@ func (mh *mountHelper) BuildCommand() []string {
return parts
}

// BuildOptions constructs the full options slice from the parsed mount options.
func (mh *mountHelper) BuildOptions() []string {
var parts []string

Expand All @@ -67,6 +69,10 @@ func (mh *mountHelper) BuildOptions() []string {
return parts
}

// Execute handles the execution of the ZipFUSE filesystem binary.
// It handles setting up the environment, forking, and executing the
// filesystem process - waiting for a mount success or failure signal.
// The signaling is realized with an [io.Pipe] to the filesystem binary.
func (mh *mountHelper) Execute() error {
// We must always set up our "parent" environment first,
// because [exec.Command] internally requires a sane $PATH.
Expand Down Expand Up @@ -122,6 +128,8 @@ Do try to pass "xlog=/full/path/to/writeable/logfile" as a mount option.
return nil
}

// setupEnvironment establishes the environment for the FUSE mount helper,
// later to be inherited by the executed ZipFUSE filesystem binary itself.
func (mh *mountHelper) setupEnvironment() {
currentPath := os.Getenv("PATH")
additionalPath := "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Expand All @@ -136,6 +144,8 @@ func (mh *mountHelper) setupEnvironment() {
}
}

// setUID handles the execution of the ZipFUSE filesystem binary under another
// configured user account and fallback if the user account cannot be resolved.
func (mh *mountHelper) setUID(spa *syscall.SysProcAttr, cmd *exec.Cmd, cmdArgs []string) (*exec.Cmd, *syscall.SysProcAttr) {
home, uid, gid, err := resolveUser(mh.Setuid)
if err == nil {
Expand Down Expand Up @@ -167,6 +177,9 @@ func (mh *mountHelper) setUID(spa *syscall.SysProcAttr, cmd *exec.Cmd, cmdArgs [
return cmd, spa
}

// waitForMount takes the read-end of an [io.Pipe] and waits for the mount
// success or failure signal communicated by the ZipFUSE filesystem binary.
// "/proc/self/mountinfo" is also observed under "first-come, first-serve".
func (mh *mountHelper) waitForMount(r io.Reader) error {
signalDone := mh.waitForSignal(r)

Expand Down Expand Up @@ -200,6 +213,8 @@ func (mh *mountHelper) waitForMount(r io.Reader) error {
}
}

// waitForSignal observes the [io.Reader] (as read-end of the [io.Pipe])
// for the mount success or mount failure signal of the filesystem binary.
func (mh *mountHelper) waitForSignal(r io.Reader) <-chan error {
signalChan := make(chan error, 1)

Expand Down Expand Up @@ -251,6 +266,7 @@ func (mh *mountHelper) waitForSignal(r io.Reader) <-chan error {
return signalChan
}

// checkMountTable checks "/proc/self/mountinfo" for the configured mountpoint.
func (mh *mountHelper) checkMountTable() (bool, error) {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
Expand Down
6 changes: 6 additions & 0 deletions cmd/mount.zipfuse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var (
// Version is the program version (filled in from the Makefile).
Version string

// allowedKeys is a map of known arguments to the ZipFUSE program.
allowedKeys = map[string]struct{}{
"fd-cache-bypass": {},
"force-unicode": {},
Expand All @@ -70,6 +71,7 @@ var (
}
)

// mountHelper is the principal implementation of the FUSE mount helper.
type mountHelper struct {
Program string
Binary string
Expand All @@ -82,6 +84,7 @@ type mountHelper struct {
Timeout time.Duration
}

// newMountHelper parses arguments and returns a new [mountHelper] on success.
func newMountHelper(args []string) (*mountHelper, error) {
mh := &mountHelper{
Program: args[0],
Expand Down Expand Up @@ -122,6 +125,7 @@ func newMountHelper(args []string) (*mountHelper, error) {
return mh, nil
}

// parseOptions parses the mount options from the provided argument slice.
func (mh *mountHelper) parseOptions(args []string) error {
for i := 0; i < len(args); i++ { //nolint:intrange
arg := args[i]
Expand Down Expand Up @@ -187,6 +191,7 @@ func (mh *mountHelper) parseOptions(args []string) error {
return nil
}

// deriveTypeFromArg tries to deduct the filesystem type from a "-t" argument.
func (mh *mountHelper) deriveTypeFromArg(i *int, args []string) error {
*i++
if *i >= len(args) {
Expand All @@ -206,6 +211,7 @@ func (mh *mountHelper) deriveTypeFromArg(i *int, args []string) error {
return nil
}

// deriveTypeFromSource tries to deduct the filesystem type from the source.
func (mh *mountHelper) deriveTypeFromSource() error {
parts := strings.SplitN(mh.Source, "#", 2)

Expand Down
2 changes: 1 addition & 1 deletion cmd/mount.zipfuse/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"testing"
)

// Expectation: The expected command should be built from the given arguments.
// Expectation: The command slice should be built from the given argument slice.
//
//nolint:maintidx
func Test_MountHelper_BuildCommand_Success(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions cmd/mount.zipfuse/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strconv"
)

// resolveUser is a helper function to resolve a user on the operating system.
// It returns the user's home directory, their UID, their GID, and any errors.
func resolveUser(spec string) (string, uint32, uint32, error) {
var resolvedUser *user.User

Expand Down
104 changes: 63 additions & 41 deletions cmd/zipfuse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"runtime/debug"
"strconv"
"sync"
"syscall"
"time"

"bazil.org/fuse"
Expand Down Expand Up @@ -63,6 +64,7 @@ var (
errPanicRecovered = errors.New("panic recovered")
)

// cliOptions describes all configurables of the command-line interface.
type cliOptions struct {
allowOther bool
dryRun bool
Expand All @@ -85,10 +87,19 @@ type cliOptions struct {
webserverAddr string
}

// rootCmd is the principal implementation of the command-line interface.
// It describes all configurables and the invocation of the run() function.
//
//nolint:mnd
func rootCmd() *cobra.Command {
var opts cliOptions

var allowOther bool
if euid := syscall.Geteuid(); euid == 0 {
// If the executing user is root, default to true.
allowOther = true
}

fsLimit, cacheLimit, err := fdLimits()
if err != nil {
fsLimit = filesystem.DefaultOptions().FDLimit
Expand Down Expand Up @@ -128,7 +139,7 @@ func rootCmd() *cobra.Command {
cmd.Flags().BoolVar(&opts.forceUnicode, "force-unicode", true, "Unicode (or generated) paths for ZIPs; disabling garbles non-compliant ZIPs")
cmd.Flags().BoolVar(&opts.mustCRC32, "must-crc32", false, "Force integrity verification on non-compressed ZIP files also (at performance cost)")
cmd.Flags().BoolVar(&opts.strictCache, "strict-cache", false, "Do not treat ZIP files/contents as immutable (non-changing) for caching decisions")
cmd.Flags().BoolVarP(&opts.allowOther, "allow-other", "a", false, "Allow other users to access the filesystem")
cmd.Flags().BoolVarP(&opts.allowOther, "allow-other", "a", allowOther, "Allow other users to access the filesystem")
cmd.Flags().BoolVarP(&opts.dryRun, "dry-run", "d", false, "Do not mount, but print all would-be inodes and paths to standard output (stdout)")
cmd.Flags().BoolVarP(&opts.flatMode, "flatten-zips", "f", false, "Flatten ZIP-contained subdirectories and their files into one directory per ZIP")
cmd.Flags().BoolVarP(&opts.fuseVerbose, "verbose", "v", false, "Print all verbose FUSE communication and diagnostics to standard error (stderr)")
Expand All @@ -143,6 +154,49 @@ func rootCmd() *cobra.Command {
return cmd
}

// run is the runtime logic for the program as executed by [cobra.Command].
// It implements the entire lifetime of the program and the served filesystem.
func run(opts cliOptions) error {
rbuf := logging.NewRingBuffer(opts.ringBufferSize, os.Stderr)

fsys, err := setupFilesystem(opts, rbuf)
if err != nil {
return fmt.Errorf("failed to setup fs: %w", err)
}
defer fsys.Destroy()

if opts.dryRun {
return dryWalkFS(fsys)
}

conn, err := mountFilesystem(opts, fsys)
if err != nil {
return fmt.Errorf("failed to mount fs: %w", err)
}
defer cleanupMount(opts.mountDir, conn, fsys)

err = notifyMountHelper(nil)
if err != nil {
rbuf.Printf("failed to notify mount helper: %v\n", err)
}

setupSignalHandlers(fsys, rbuf, opts.mountDir)
wg, errChan := serveFilesystem(conn, fsys, opts.fuseVerbose)

if opts.webserverAddr != "" {
srv, err := serveDashboard(opts.webserverAddr, fsys, rbuf)
if err != nil {
return fmt.Errorf("failed to setup webserver: %w", err)
}
defer srv.Close()
}

wg.Wait()

return <-errChan
}

// setupFilesystem configures and returns the [filesystem.FS] to be served.
func setupFilesystem(opts cliOptions, rbuf *logging.RingBuffer) (*filesystem.FS, error) {
fopts := &filesystem.Options{
FDCacheSize: opts.fdCacheSize,
Expand All @@ -165,6 +219,7 @@ func setupFilesystem(opts cliOptions, rbuf *logging.RingBuffer) (*filesystem.FS,
return fsys, nil
}

// mountFilesystem opens a new [fuse.Conn] for the specified mountpoint.
func mountFilesystem(opts cliOptions, fsys *filesystem.FS) (*fuse.Conn, error) {
mountOpts := []fuse.MountOption{
fuse.FSName("zipfuse"),
Expand All @@ -185,9 +240,13 @@ func mountFilesystem(opts cliOptions, fsys *filesystem.FS) (*fuse.Conn, error) {
return conn, nil
}

// notifyMountHelper handles piped notifications to the FUSE mount helper.
//
// The FUSE mount helper passes the write end of a [os.Pipe] as file descriptor.
// We use the pipe to signal either success or failure to the FUSE mount helper.
// The argument is the error (or nil = success) that will be sent over the pipe.
//
// If the FUSE mount helper is not involved, a call to this function is a no-op.
func notifyMountHelper(e error) error {
if mountHelperNotified {
return nil
Expand Down Expand Up @@ -231,6 +290,7 @@ func notifyMountHelper(e error) error {
return nil
}

// serveFilesystem serves the [filesystem.FS] on the [fuse.Conn] for the mountpoint.
func serveFilesystem(conn *fuse.Conn, fsys *filesystem.FS, verbose bool) (*sync.WaitGroup, <-chan error) {
var wg sync.WaitGroup
errChan := make(chan error, 1)
Expand Down Expand Up @@ -264,6 +324,7 @@ func serveFilesystem(conn *fuse.Conn, fsys *filesystem.FS, verbose bool) (*sync.
return &wg, errChan
}

// serveDashboard sets up a [http.Server] and starts serving a [webserver.FSDashboard].
func serveDashboard(addr string, fsys *filesystem.FS, rbuf *logging.RingBuffer) (*http.Server, error) {
dashboard, err := webserver.NewFSDashboard(fsys, rbuf, Version)
if err != nil {
Expand All @@ -273,6 +334,7 @@ func serveDashboard(addr string, fsys *filesystem.FS, rbuf *logging.RingBuffer)
return dashboard.Serve(addr), nil
}

// cleanupMount runs FS cleanup, unmounts and eventually closes the [fuse.Conn].
func cleanupMount(mountDir string, conn *fuse.Conn, fsys *filesystem.FS) {
defer conn.Close()
defer fuse.Unmount(mountDir) //nolint:errcheck
Expand Down Expand Up @@ -300,43 +362,3 @@ func main() {
os.Exit(1)
}
}

func run(opts cliOptions) error {
rbuf := logging.NewRingBuffer(opts.ringBufferSize, os.Stderr)

fsys, err := setupFilesystem(opts, rbuf)
if err != nil {
return fmt.Errorf("failed to setup fs: %w", err)
}
defer fsys.Destroy()

if opts.dryRun {
return dryWalkFS(fsys)
}

conn, err := mountFilesystem(opts, fsys)
if err != nil {
return fmt.Errorf("failed to mount fs: %w", err)
}
defer cleanupMount(opts.mountDir, conn, fsys)

err = notifyMountHelper(nil)
if err != nil {
rbuf.Printf("failed to notify mount helper: %v\n", err)
}

setupSignalHandlers(fsys, rbuf, opts.mountDir)
wg, errChan := serveFilesystem(conn, fsys, opts.fuseVerbose)

if opts.webserverAddr != "" {
srv, err := serveDashboard(opts.webserverAddr, fsys, rbuf)
if err != nil {
return fmt.Errorf("failed to setup webserver: %w", err)
}
defer srv.Close()
}

wg.Wait()

return <-errChan
}
Loading
Loading