diff --git a/.github/workflows/golang-build-debug.yml b/.github/workflows/golang-build-debug.yml
index 596a332..43253a5 100644
--- a/.github/workflows/golang-build-debug.yml
+++ b/.github/workflows/golang-build-debug.yml
@@ -18,8 +18,6 @@ jobs:
cache: false
- name: Checkout code
uses: actions/checkout@v4
- - name: Run all Go tests
- run: make test
- name: Vendor the application for debug
run: make vendor
- name: Build the application for debug
diff --git a/.github/workflows/golang-build.yml b/.github/workflows/golang-build.yml
index c8bbb5a..4f50719 100644
--- a/.github/workflows/golang-build.yml
+++ b/.github/workflows/golang-build.yml
@@ -18,8 +18,6 @@ jobs:
cache: false
- name: Checkout code
uses: actions/checkout@v4
- - name: Run all Go tests
- run: make test
- name: Vendor the application for production
run: make vendor
- name: Build the application for production
diff --git a/.github/workflows/zipfuse-cli.yml b/.github/workflows/zipfuse-cli.yml
new file mode 100644
index 0000000..538a0e8
--- /dev/null
+++ b/.github/workflows/zipfuse-cli.yml
@@ -0,0 +1,237 @@
+name: zipfuse-cli
+on:
+ push:
+ branches:
+ - master
+ - main
+ pull_request:
+permissions:
+ contents: read
+jobs:
+ zipfuse:
+ name: cli
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.25.1'
+
+ - name: Install FUSE
+ 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
+
+ - name: Build debug binaries
+ run: make debug
+
+ - name: Install debug binaries
+ run: |
+ sudo cp zipfuse /bin/zipfuse
+ sudo chmod +x /bin/zipfuse
+ sudo cp mount.zipfuse /sbin/mount.zipfuse
+ sudo chmod +x /sbin/mount.zipfuse
+
+ - name: Create test environment
+ run: |
+ mkdir -p source
+ mkdir -p mountpoint
+
+ mkdir -p test1/subdir1/subdir2
+ echo "Hello from test1.txt" > test1/test1.txt
+ echo "Second file in test1" > test1/file2.txt
+ echo "File in subdir1" > test1/subdir1/nested1.txt
+ echo "File in subdir2" > test1/subdir1/subdir2/nested2.txt
+ (cd test1 && zip -r ../source/archive1.zip .)
+
+ mkdir -p test2/docs/reports
+ echo "Hello from test2.txt" > test2/test2.txt
+ echo "Another file in test2" > test2/another.txt
+ echo "Document file" > test2/docs/document.txt
+ echo "Report file" > test2/docs/reports/report.txt
+ (cd test2 && zip -r ../source/archive2.zip .)
+
+ rm -rf test1 test2
+
+ - name: Mount filesystem
+ run: |
+ sudo sh -c 'nohup zipfuse source mountpoint --allow-other &>/var/log/zipfuse.log &'
+
+ for i in {1..20}; do
+ if mountpoint -q mountpoint; then
+ echo "Filesystem mounted successfully"
+ break
+ fi
+ if [ $i -eq 30 ]; then
+ echo "Mount timeout"
+ exit 1
+ fi
+ sleep 0.5
+ done
+
+ mount | grep zipfuse || true
+ ls -la mountpoint/
+
+ - name: Test filesystem
+ run: |
+ echo "Testing archive1..."
+ if [ ! -f "mountpoint/archive1/test1.txt" ]; then
+ echo "ERROR: test1.txt not found in archive1"
+ exit 1
+ fi
+
+ CONTENT1=$(cat mountpoint/archive1/test1.txt)
+ if [ "$CONTENT1" != "Hello from test1.txt" ]; then
+ echo "ERROR: Content mismatch in test1.txt"
+ echo "Expected: 'Hello from test1.txt'"
+ echo "Got: '$CONTENT1'"
+ exit 1
+ fi
+ echo "archive1/test1.txt content verified"
+
+ CONTENT2=$(cat mountpoint/archive1/file2.txt)
+ if [ "$CONTENT2" != "Second file in test1" ]; then
+ echo "ERROR: Content mismatch in file2.txt"
+ echo "Expected: 'Second file in test1'"
+ echo "Got: '$CONTENT2'"
+ exit 1
+ fi
+ echo "archive1/file2.txt content verified"
+
+ CONTENT3=$(cat mountpoint/archive1/subdir1/nested1.txt)
+ if [ "$CONTENT3" != "File in subdir1" ]; then
+ echo "ERROR: Content mismatch in subdir1/nested1.txt"
+ echo "Expected: 'File in subdir1'"
+ echo "Got: '$CONTENT3'"
+ exit 1
+ fi
+ echo "archive1/subdir1/nested1.txt content verified"
+
+ CONTENT4=$(cat mountpoint/archive1/subdir1/subdir2/nested2.txt)
+ if [ "$CONTENT4" != "File in subdir2" ]; then
+ echo "ERROR: Content mismatch in subdir1/subdir2/nested2.txt"
+ echo "Expected: 'File in subdir2'"
+ echo "Got: '$CONTENT4'"
+ exit 1
+ fi
+ echo "archive1/subdir1/subdir2/nested2.txt content verified"
+
+ echo "Testing archive2..."
+ if [ ! -f "mountpoint/archive2/test2.txt" ]; then
+ echo "ERROR: test2.txt not found in archive2"
+ exit 1
+ fi
+
+ CONTENT5=$(cat mountpoint/archive2/test2.txt)
+ if [ "$CONTENT5" != "Hello from test2.txt" ]; then
+ echo "ERROR: Content mismatch in test2.txt"
+ echo "Expected: 'Hello from test2.txt'"
+ echo "Got: '$CONTENT5'"
+ exit 1
+ fi
+ echo "archive2/test2.txt content verified"
+
+ CONTENT6=$(cat mountpoint/archive2/another.txt)
+ if [ "$CONTENT6" != "Another file in test2" ]; then
+ echo "ERROR: Content mismatch in another.txt"
+ echo "Expected: 'Another file in test2'"
+ echo "Got: '$CONTENT6'"
+ exit 1
+ fi
+ echo "archive2/another.txt content verified"
+
+ CONTENT7=$(cat mountpoint/archive2/docs/document.txt)
+ if [ "$CONTENT7" != "Document file" ]; then
+ echo "ERROR: Content mismatch in docs/document.txt"
+ echo "Expected: 'Document file'"
+ echo "Got: '$CONTENT7'"
+ exit 1
+ fi
+ echo "archive2/docs/document.txt content verified"
+
+ CONTENT8=$(cat mountpoint/archive2/docs/reports/report.txt)
+ if [ "$CONTENT8" != "Report file" ]; then
+ echo "ERROR: Content mismatch in docs/reports/report.txt"
+ echo "Expected: 'Report file'"
+ echo "Got: '$CONTENT8'"
+ exit 1
+ fi
+ echo "archive2/docs/reports/report.txt content verified"
+
+ echo "All file content tests passed!"
+
+ - name: Unmount filesystem
+ if: always()
+ run: |
+ sudo killall zipfuse || true
+
+ for i in {1..10}; do
+ if ! mountpoint -q mountpoint; then
+ echo "Filesystem unmounted successfully"
+ break
+ fi
+ sleep 0.5
+ done
+
+ for i in {1..10}; do
+ if ! pgrep -fa zipfuse &>/dev/null; then
+ echo "Filesystem binary exited successfully"
+ exit 0
+ fi
+ sleep 0.5
+ done
+
+ echo "ERROR: Filesystem binary seems stuck after unmount"
+ exit 1
+
+ - name: Verify unmounted
+ if: always()
+ run: |
+ if mountpoint -q mountpoint; then
+ echo "ERROR: Filesystem still mounted after killall"
+ mount | grep zipfuse || true
+ exit 1
+ fi
+
+ if pgrep -fa zipfuse &>/dev/null; then
+ echo "ERROR: Filesystem binary still alive after unmount"
+ pgrep -fa zipfuse || true
+ exit 1
+ fi
+
+ if [ -f "mountpoint/archive1/test1.txt" ]; then
+ echo "ERROR: Files still accessible after unmount"
+ exit 1
+ fi
+
+ echo "Filesystem properly unmounted"
+
+ - name: Show logs on failure
+ if: failure()
+ run: |
+ echo "=== Mount points ==="
+ mount | grep zipfuse || echo "No zipfuse mounts found"
+ echo ""
+
+ echo "=== Mountpoint directory ==="
+ ls -la mountpoint/ || true
+ echo ""
+
+ echo "=== Process list ==="
+ ps aux | grep zipfuse || echo "No zipfuse processes found"
+ echo ""
+
+ echo "=== Dmesg (last 25 lines) ==="
+ sudo dmesg | tail -n 25 || true
+ echo ""
+
+ echo "=== ZipFUSE Events (last 25 lines) ==="
+ sudo cat /var/log/zipfuse.log | tail -n 25 || true
diff --git a/.github/workflows/zipfuse-fstab.yml b/.github/workflows/zipfuse-fstab.yml
new file mode 100644
index 0000000..e1c2706
--- /dev/null
+++ b/.github/workflows/zipfuse-fstab.yml
@@ -0,0 +1,243 @@
+name: zipfuse-fstab
+on:
+ push:
+ branches:
+ - master
+ - main
+ pull_request:
+permissions:
+ contents: read
+jobs:
+ zipfuse:
+ name: fstab
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.25.1'
+
+ - name: Install FUSE
+ 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
+
+ - name: Build debug binaries
+ run: make debug
+
+ - name: Install debug binaries
+ run: |
+ sudo cp zipfuse /bin/zipfuse
+ sudo chmod +x /bin/zipfuse
+ sudo cp mount.zipfuse /sbin/mount.zipfuse
+ sudo chmod +x /sbin/mount.zipfuse
+
+ - name: Create test environment
+ run: |
+ mkdir -p source
+ mkdir -p mountpoint
+
+ mkdir -p test1/subdir1/subdir2
+ echo "Hello from test1.txt" > test1/test1.txt
+ echo "Second file in test1" > test1/file2.txt
+ echo "File in subdir1" > test1/subdir1/nested1.txt
+ echo "File in subdir2" > test1/subdir1/subdir2/nested2.txt
+ (cd test1 && zip -r ../source/archive1.zip .)
+
+ mkdir -p test2/docs/reports
+ echo "Hello from test2.txt" > test2/test2.txt
+ echo "Another file in test2" > test2/another.txt
+ echo "Document file" > test2/docs/document.txt
+ echo "Report file" > test2/docs/reports/report.txt
+ (cd test2 && zip -r ../source/archive2.zip .)
+
+ rm -rf test1 test2
+
+ - name: Mount filesystem
+ run: |
+ if ! sudo mount -t zipfuse source mountpoint -o allow_other; then
+ echo "ERROR: Filesystem mount has failed"
+ exit 1
+ fi
+
+ for i in {1..20}; do
+ if mountpoint -q mountpoint; then
+ echo "Filesystem mounted successfully"
+ break
+ fi
+ if [ $i -eq 30 ]; then
+ echo "Mount timeout"
+ exit 1
+ fi
+ sleep 0.5
+ done
+
+ mount | grep zipfuse || true
+ ls -la mountpoint/
+
+ - name: Test filesystem
+ run: |
+ echo "Testing archive1..."
+ if [ ! -f "mountpoint/archive1/test1.txt" ]; then
+ echo "ERROR: test1.txt not found in archive1"
+ exit 1
+ fi
+
+ CONTENT1=$(cat mountpoint/archive1/test1.txt)
+ if [ "$CONTENT1" != "Hello from test1.txt" ]; then
+ echo "ERROR: Content mismatch in test1.txt"
+ echo "Expected: 'Hello from test1.txt'"
+ echo "Got: '$CONTENT1'"
+ exit 1
+ fi
+ echo "archive1/test1.txt content verified"
+
+ CONTENT2=$(cat mountpoint/archive1/file2.txt)
+ if [ "$CONTENT2" != "Second file in test1" ]; then
+ echo "ERROR: Content mismatch in file2.txt"
+ echo "Expected: 'Second file in test1'"
+ echo "Got: '$CONTENT2'"
+ exit 1
+ fi
+ echo "archive1/file2.txt content verified"
+
+ CONTENT3=$(cat mountpoint/archive1/subdir1/nested1.txt)
+ if [ "$CONTENT3" != "File in subdir1" ]; then
+ echo "ERROR: Content mismatch in subdir1/nested1.txt"
+ echo "Expected: 'File in subdir1'"
+ echo "Got: '$CONTENT3'"
+ exit 1
+ fi
+ echo "archive1/subdir1/nested1.txt content verified"
+
+ CONTENT4=$(cat mountpoint/archive1/subdir1/subdir2/nested2.txt)
+ if [ "$CONTENT4" != "File in subdir2" ]; then
+ echo "ERROR: Content mismatch in subdir1/subdir2/nested2.txt"
+ echo "Expected: 'File in subdir2'"
+ echo "Got: '$CONTENT4'"
+ exit 1
+ fi
+ echo "archive1/subdir1/subdir2/nested2.txt content verified"
+
+ echo "Testing archive2..."
+ if [ ! -f "mountpoint/archive2/test2.txt" ]; then
+ echo "ERROR: test2.txt not found in archive2"
+ exit 1
+ fi
+
+ CONTENT5=$(cat mountpoint/archive2/test2.txt)
+ if [ "$CONTENT5" != "Hello from test2.txt" ]; then
+ echo "ERROR: Content mismatch in test2.txt"
+ echo "Expected: 'Hello from test2.txt'"
+ echo "Got: '$CONTENT5'"
+ exit 1
+ fi
+ echo "archive2/test2.txt content verified"
+
+ CONTENT6=$(cat mountpoint/archive2/another.txt)
+ if [ "$CONTENT6" != "Another file in test2" ]; then
+ echo "ERROR: Content mismatch in another.txt"
+ echo "Expected: 'Another file in test2'"
+ echo "Got: '$CONTENT6'"
+ exit 1
+ fi
+ echo "archive2/another.txt content verified"
+
+ CONTENT7=$(cat mountpoint/archive2/docs/document.txt)
+ if [ "$CONTENT7" != "Document file" ]; then
+ echo "ERROR: Content mismatch in docs/document.txt"
+ echo "Expected: 'Document file'"
+ echo "Got: '$CONTENT7'"
+ exit 1
+ fi
+ echo "archive2/docs/document.txt content verified"
+
+ CONTENT8=$(cat mountpoint/archive2/docs/reports/report.txt)
+ if [ "$CONTENT8" != "Report file" ]; then
+ echo "ERROR: Content mismatch in docs/reports/report.txt"
+ echo "Expected: 'Report file'"
+ echo "Got: '$CONTENT8'"
+ exit 1
+ fi
+ echo "archive2/docs/reports/report.txt content verified"
+
+ echo "All file content tests passed!"
+
+ - name: Unmount filesystem
+ if: always()
+ run: |
+ if ! sudo fusermount3 -u mountpoint; then
+ echo "ERROR: Filesystem unmount has failed"
+ exit 1
+ fi
+
+ for i in {1..10}; do
+ if ! mountpoint -q mountpoint; then
+ echo "Filesystem unmounted successfully"
+ break
+ fi
+ sleep 0.5
+ done
+
+ for i in {1..10}; do
+ if ! pgrep -fa zipfuse &>/dev/null; then
+ echo "Filesystem binary exited successfully"
+ exit 0
+ fi
+ sleep 0.5
+ done
+
+ echo "ERROR: Filesystem binary seems stuck after unmount"
+ exit 1
+
+ - name: Verify unmounted
+ if: always()
+ run: |
+ if mountpoint -q mountpoint; then
+ echo "ERROR: Filesystem still mounted after fusermount3"
+ mount | grep zipfuse || true
+ exit 1
+ fi
+
+ if pgrep -fa zipfuse &>/dev/null; then
+ echo "ERROR: Filesystem binary still alive after unmount"
+ pgrep -fa zipfuse || true
+ exit 1
+ fi
+
+ if [ -f "mountpoint/archive1/test1.txt" ]; then
+ echo "ERROR: Files still accessible after unmount"
+ exit 1
+ fi
+
+ echo "Filesystem properly unmounted"
+
+ - name: Show logs on failure
+ if: failure()
+ run: |
+ echo "=== Mount points ==="
+ mount | grep zipfuse || echo "No zipfuse mounts found"
+ echo ""
+
+ echo "=== Mountpoint directory ==="
+ ls -la mountpoint/ || true
+ echo ""
+
+ echo "=== Process list ==="
+ ps aux | grep zipfuse || echo "No zipfuse processes found"
+ echo ""
+
+ echo "=== Dmesg (last 25 lines) ==="
+ sudo dmesg | tail -n 25 || true
+ echo ""
+
+ echo "=== ZipFUSE Events (last 25 lines) ==="
+ sudo cat /var/log/zipfuse.log | tail -n 25 || true
diff --git a/README.md b/README.md
index afab0f4..c192060 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,17 @@
>**Note: This software is under active development.**
@@ -138,6 +138,14 @@ sudo mount -t zipfuse /home/alice/zips /home/alice/zipfuse -o setuid=alice,allow
/home/alice/zips /home/alice/zipfuse zipfuse setuid=alice,allow_other,webserver=:8000 0 0
```
+**Additional mount options to control mount helper behavior itself:**
+```
+setuid=USER (as username or UID; overrides executing user)
+xbin=/full/path/to/zipfuse/binary (overrides filesystem binary)
+xlog=/full/path/to/writeable/logfile (overrides filesystem logfile)
+xtim=SECS (numeric and in seconds; overrides filesystem mount timeout)
+```
+
**As you can see, program options (read more below) need format conversion:**
`--allow-other --webserver :8000` is turning into `allow_other,webserver=:8000`
diff --git a/cmd/mount.zipfuse/exec.go b/cmd/mount.zipfuse/exec.go
index ac8d275..95d4466 100644
--- a/cmd/mount.zipfuse/exec.go
+++ b/cmd/mount.zipfuse/exec.go
@@ -1,8 +1,9 @@
-//nolint:mnd,noctx
+//nolint:mnd,noctx,err113
package main
import (
"bufio"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -16,7 +17,15 @@ import (
"al.essio.dev/pkg/shellescape"
)
-var errMountTimeout = errors.New("mount timeout")
+const (
+ signalMountSuccess byte = 0
+ signalMountFailed byte = 1
+)
+
+var (
+ errMountTimeout = errors.New("mount timeout")
+ errMountFailed = errors.New("mount failed")
+)
func (mh *mountHelper) BuildCommand() []string {
var parts []string
@@ -75,13 +84,15 @@ func (mh *mountHelper) Execute() error {
fdnull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
- return fmt.Errorf("failed to open /dev/null: %w", err)
+ return fmt.Errorf("failed to open \"/dev/null\": %w", err)
}
defer fdnull.Close()
- fdlog, err := os.OpenFile(mountLog, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o640)
+ fdlog, err := os.OpenFile(mh.Logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o640)
if err != nil {
- fmt.Fprintf(os.Stderr, "warning: failed to open %q: %v (falling back to '/dev/null')\n", mountLog, err)
+ fmt.Fprintf(os.Stderr, `mount.zipfuse warning: failed to open %q: %v (falling back to "/dev/null").
+Do try to pass "xlog=/full/path/to/writeable/logfile" as a mount option.
+`, mh.Logfile, err)
cmd.Stdin, cmd.Stdout, cmd.Stderr = fdnull, fdnull, fdnull
} else {
defer fdlog.Close()
@@ -136,7 +147,8 @@ func (mh *mountHelper) setUID(spa *syscall.SysProcAttr, cmd *exec.Cmd, cmdArgs [
Gid: gid,
}
} else {
- fmt.Fprintf(os.Stderr, "warning: failed to resolve setuid %q: %v (falling back to 'su')\n", mh.Setuid, err)
+ fmt.Fprintf(os.Stderr, "mount.zipfuse warning: failed to resolve user %q: %v (falling back to \"su\")\n",
+ mh.Setuid, err)
safeCmdArgs := make([]string, len(cmdArgs))
for i, arg := range cmdArgs {
@@ -156,28 +168,21 @@ func (mh *mountHelper) setUID(spa *syscall.SysProcAttr, cmd *exec.Cmd, cmdArgs [
}
func (mh *mountHelper) waitForMount(r io.Reader) error {
- signalDone := make(chan error, 1)
- go func() {
- defer close(signalDone)
- buf := make([]byte, 1)
- _, err := r.Read(buf)
- if err == nil {
- signalDone <- nil
- } else {
- signalDone <- err
- }
- }()
+ signalDone := mh.waitForSignal(r)
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
- totalTimeout := time.After(mountTimeout)
+ totalTimeout := time.After(mh.Timeout)
for {
select {
case signalErr := <-signalDone:
if signalErr == nil {
return nil
+ } else if errors.Is(signalErr, errMountFailed) {
+ return signalErr
}
+ fmt.Fprintf(os.Stderr, "mount.zipfuse warning: %v\n", signalErr)
signalDone = nil
case <-ticker.C:
@@ -195,10 +200,61 @@ func (mh *mountHelper) waitForMount(r io.Reader) error {
}
}
+func (mh *mountHelper) waitForSignal(r io.Reader) <-chan error {
+ signalChan := make(chan error, 1)
+
+ go func() {
+ defer func() {
+ rec := recover()
+ if rec != nil {
+ select {
+ case signalChan <- fmt.Errorf("panic recovered: %v", rec):
+ default:
+ }
+ }
+ close(signalChan)
+ }()
+
+ status := make([]byte, 1)
+ if _, err := io.ReadFull(r, status); err != nil {
+ signalChan <- fmt.Errorf("failed to read from pipe: %w", err)
+
+ return
+ }
+
+ switch status[0] {
+ case signalMountSuccess:
+ signalChan <- nil
+
+ case signalMountFailed:
+ scanner := bufio.NewScanner(r)
+ if scanner.Scan() {
+ var msg string
+ err := json.Unmarshal(scanner.Bytes(), &msg)
+ if err != nil {
+ signalChan <- fmt.Errorf("failed to unmarshal from pipe: %w", err)
+
+ return
+ }
+ signalChan <- fmt.Errorf("%w: %s", errMountFailed, msg)
+ } else if err := scanner.Err(); err != nil {
+ signalChan <- fmt.Errorf("failed to parse from pipe: %w", err)
+ } else {
+ signalChan <- errors.New("malformed signal from pipe: unknown error")
+ }
+
+ default:
+ signalChan <- errors.New("malformed signal from pipe: unknown status")
+ }
+ }()
+
+ return signalChan
+}
+
func (mh *mountHelper) checkMountTable() (bool, error) {
f, err := os.Open("/proc/self/mountinfo")
if err != nil {
- return false, fmt.Errorf("cannot open /proc/self/mountinfo: %w", err)
+ return false, fmt.Errorf("cannot open \"/proc/self/mountinfo\": %w", err)
}
defer f.Close()
@@ -211,7 +267,7 @@ func (mh *mountHelper) checkMountTable() (bool, error) {
}
if err := scanner.Err(); err != nil {
- return false, fmt.Errorf("error reading /proc/self/mountinfo: %w", err)
+ return false, fmt.Errorf("error reading \"/proc/self/mountinfo\": %w", err)
}
return false, nil
diff --git a/cmd/mount.zipfuse/help.go b/cmd/mount.zipfuse/help.go
new file mode 100644
index 0000000..d7c52c9
--- /dev/null
+++ b/cmd/mount.zipfuse/help.go
@@ -0,0 +1,41 @@
+package main
+
+const (
+ helpTextLong = `%s (%s) - FUSE mount helper
+
+This program is a helper for the mount/fstab mechanism.
+It is normally located in /sbin or another directory
+searched by mount(8) for filesystem helpers, and is
+not intended to be invoked directly by the end users.
+
+Usage:
+ %s source mountpoint [-o key[=value],key[=value],...]
+
+For running the filesystem as another (e.g. unprivileged) user:
+ %s source mountpoint -o setuid=USER[,key[=value],...]
+
+Example (fstab entry):
+ /mnt/zips /mnt/zipfuse zipfuse allow_other,webserver=:8000 0 0
+
+Additional mount options to control mount helper behavior itself:
+ setuid=USER (as username or UID; overrides executing user)
+ xbin=/full/path/to/zipfuse/binary (overrides filesystem binary)
+ xlog=/full/path/to/writeable/logfile (overrides filesystem logfile)
+ xtim=SECS (numeric and in seconds; overrides filesystem mount timeout)
+
+Filesystem-specific options need to be adapted into this format:
+ --webserver :8000 --strict-cache => webserver=:8000,strict_cache
+
+Note that FUSE mount helper events are printed to standard error (stderr).
+Filesystem events are printed to %q (if it is writeable).`
+
+ helpErrNotFound = `mount.zipfuse error: zipfuse not found within $PATH dirs.
+Perhaps you installed it into some non-standard directory?
+Some operating systems also mangle the environment variable.
+Do try to pass "xbin=/full/path/to/binary" as a mount option.`
+
+ helpErrMountTimeout = `mount.zipfuse error: mount did not appear within %d seconds.
+You can raise this timeout by passing "xtim=SECS" as a mount option.
+But beware default timeouts usually suffice and indicate error conditions.
+So first do try checking %q for more (error) information.`
+)
diff --git a/cmd/mount.zipfuse/main.go b/cmd/mount.zipfuse/main.go
index e94809e..28351d5 100644
--- a/cmd/mount.zipfuse/main.go
+++ b/cmd/mount.zipfuse/main.go
@@ -15,11 +15,17 @@ For running the filesystem as another (e.g. unprivileged) user:
Example (fstab entry):
/mnt/zips /mnt/zipfuse zipfuse allow_other,webserver=:8000 0 0
+Additional mount options to control mount helper behavior itself:
+ setuid=USER (as username or UID; overrides executing user)
+ xbin=/full/path/to/zipfuse/binary (overrides filesystem binary)
+ xlog=/full/path/to/writeable/logfile (overrides filesystem logfile)
+ xtim=SECS (numeric and in seconds; overrides filesystem mount timeout)
+
Filesystem-specific options need to be adapted into this format:
--webserver :8000 --strict-cache => webserver=:8000,strict_cache
-Mount helper events are printed to standard error (stderr).
-FS events are printed to '/var/log/zipfuse.log' (if writeable).
+Note that FUSE mount helper events are printed to standard error (stderr).
+Filesystem events are printed to "/var/log/zipfuse.log" (if it is writeable).
*/
//nolint:mnd,err113
package main
@@ -30,13 +36,15 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strconv"
"strings"
"time"
)
const (
- mountLog = "/var/log/zipfuse.log"
- mountTimeout = 20 * time.Second
+ defaultType = "zipfuse"
+ defaultLogfile = "/var/log/zipfuse.log"
+ defaultTimeout = 20 * time.Second
)
var (
@@ -70,15 +78,19 @@ type mountHelper struct {
Mountpoint string
Options map[string]string
Setuid string
+ Logfile string
+ Timeout time.Duration
}
func newMountHelper(args []string) (*mountHelper, error) {
mh := &mountHelper{
Program: args[0],
Source: args[1],
- Type: "zipfuse",
+ Type: defaultType,
Mountpoint: args[2],
Options: make(map[string]string),
+ Logfile: defaultLogfile,
+ Timeout: defaultTimeout,
}
if mh.Source == "" {
@@ -139,11 +151,29 @@ func (mh *mountHelper) parseOptions(args []string) error {
key := parts[0]
val := parts[1]
- if key == "bin" {
+ _, ok := allowedKeys[key]
+
+ switch {
+ case key == "xbin":
mh.Binary = val
- } else if key == "setuid" {
+
+ case key == "xlog":
+ mh.Logfile = val
+
+ case key == "xtim":
+ secs, err := strconv.Atoi(val)
+ if err != nil {
+ return fmt.Errorf("failed to parse %q value %q: %w", key, val, err)
+ }
+ if secs <= 0 {
+ return fmt.Errorf("failed to use %q value %q: must be > 0", key, val)
+ }
+ mh.Timeout = time.Duration(secs) * time.Second
+
+ case key == "setuid":
mh.Setuid = val
- } else if _, ok := allowedKeys[key]; ok {
+
+ case ok:
mh.Options[key] = val
}
} else { // key
@@ -160,7 +190,7 @@ func (mh *mountHelper) parseOptions(args []string) error {
func (mh *mountHelper) deriveTypeFromArg(i *int, args []string) error {
*i++
if *i >= len(args) {
- return errors.New("missing value to argument '-t'")
+ return errors.New("missing type value to argument \"-t\"")
}
t := args[*i]
if after, ok := strings.CutPrefix(t, "fuse."); ok {
@@ -169,7 +199,7 @@ func (mh *mountHelper) deriveTypeFromArg(i *int, args []string) error {
t = after0
}
if t == "" {
- return errors.New("missing value to argument '-t'")
+ return errors.New("empty type value to argument \"-t\"")
}
mh.Type = t
@@ -183,14 +213,14 @@ func (mh *mountHelper) deriveTypeFromSource() error {
mh.Type = parts[0]
mh.Source = parts[1]
} else {
- return errors.New("source argument is not in format 'type#source'")
+ return errors.New("source argument is not in format \"type#source\"")
}
if mh.Type == "" {
- return errors.New("empty type before '#' in source argument")
+ return errors.New("empty type value before '#' in source argument")
}
if mh.Source == "" {
- return errors.New("empty source after '#' in source argument")
+ return errors.New("empty source value after '#' in source argument")
}
return nil
@@ -199,28 +229,8 @@ func (mh *mountHelper) deriveTypeFromSource() error {
func main() {
if len(os.Args) < 3 {
progName := filepath.Base(os.Args[0])
- fmt.Fprintf(os.Stderr, `%s (%s) - FUSE mount helper
-
-This program is a helper for the mount/fstab mechanism.
-It is normally located in /sbin or another directory
-searched by mount(8) for filesystem helpers, and is
-not intended to be invoked directly by the end users.
-
-Usage:
- %s source mountpoint [-o key[=value],key[=value],...]
-
-For running the filesystem as another (e.g. unprivileged) user:
- %s source mountpoint -o setuid=USER[,key[=value],...]
-
-Example (fstab entry):
- /mnt/zips /mnt/zipfuse zipfuse allow_other,webserver=:8000 0 0
-
-Filesystem-specific options need to be adapted into this format:
- --webserver :8000 --strict-cache => webserver=:8000,strict_cache
-
-Mount helper events are printed to standard error (stderr).
-FS events are printed to '%s' (if writeable).
-`, progName, Version, progName, progName, mountLog)
+ fmt.Fprintf(os.Stderr, helpTextLong+"\n",
+ progName, Version, progName, progName, defaultLogfile)
os.Exit(1)
}
@@ -234,15 +244,11 @@ FS events are printed to '%s' (if writeable).
if err != nil {
switch {
case errors.Is(err, exec.ErrNotFound):
- fmt.Fprintln(os.Stderr, `mount.zipfuse error: zipfuse not found within $PATH dirs.
-Perhaps you installed it into some non-standard directory?
-Some operating systems also mangle the environment variable.
-Do try to pass 'bin=/full/path/to/binary' as a mount option.`)
+ fmt.Fprintln(os.Stderr, helpErrNotFound)
case errors.Is(err, errMountTimeout):
- fmt.Fprintf(os.Stderr, `mount.zipfuse error: mount did not appear within %d seconds.
-Do try checking '/var/log/zipfuse.log' for more information.
-`, int(mountTimeout.Seconds()))
+ fmt.Fprintf(os.Stderr, helpErrMountTimeout+"\n",
+ int(helper.Timeout.Seconds()), helper.Logfile)
default:
fmt.Fprintf(os.Stderr, "mount.zipfuse error: %v\n", err)
diff --git a/cmd/mount.zipfuse/main_test.go b/cmd/mount.zipfuse/main_test.go
index 2eb4ab2..cde2dc3 100644
--- a/cmd/mount.zipfuse/main_test.go
+++ b/cmd/mount.zipfuse/main_test.go
@@ -237,7 +237,7 @@ func Test_MountHelper_BuildCommand_Success(t *testing.T) {
},
{
name: "explicit binary path",
- args: []string{"mount.zipfuse", "./source", "./dest", "-o", "bin=/bin/zipfuze"},
+ args: []string{"mount.zipfuse", "./source", "./dest", "-o", "xbin=/bin/zipfuze"},
want: []string{"/bin/zipfuze", "./source", "./dest"},
},
{
@@ -280,6 +280,11 @@ func Test_MountHelper_BuildCommand_Success(t *testing.T) {
args: []string{"mount", "/mnt/a", "/mnt/b", "-t"},
wantErr: true,
},
+ {
+ name: "invalid xtim value",
+ args: []string{"mount", "/mnt/a", "/mnt/b", "-o", "xtim=0"},
+ wantErr: true,
+ },
}
for _, tt := range tests {
diff --git a/cmd/zipfuse/help.go b/cmd/zipfuse/help.go
index 829ae81..bf20a8c 100644
--- a/cmd/zipfuse/help.go
+++ b/cmd/zipfuse/help.go
@@ -23,4 +23,10 @@ When enabled, the diagnostics dashboard exposes the following routes:
- "/set/must-crc32/" for adapting forced integrity checking
- "/set/fd-cache-bypass/" for bypassing the file descriptor cache
- "/set/stream-threshold/" for adapting of the streaming threshold`
+
+ helpErrOptionsArg = `You have invoked this program with an "-o" flag, which is not supported.
+Most likely you tried mounting as "fuse.zipfuse" using mount(8) or fstab?
+If you wish to mount using mount(8) or fstab, use only "zipfuse" as type.
+However that requires the helper "mount.zipfuse" be installed in "/sbin".
+For more information, please read the INSTALL instructions or the README.`
)
diff --git a/cmd/zipfuse/main.go b/cmd/zipfuse/main.go
index 67cd9a6..0a8bc9c 100644
--- a/cmd/zipfuse/main.go
+++ b/cmd/zipfuse/main.go
@@ -23,6 +23,7 @@ When enabled, the diagnostics server exposes the following routes over HTTP:
package main
import (
+ "encoding/json"
"errors"
"fmt"
"net/http"
@@ -42,13 +43,19 @@ import (
)
const (
- stackTraceBufferSize = 1 << 24
+ stackTraceBufferSize int = 1 << 24
+ signalMountSuccess byte = 0
+ signalMountFailed byte = 1
+ signalDelimiter byte = '\n'
)
var (
// Version is the program version (filled in from the Makefile).
Version string
+ // mountHelperNotified is for when the FUSE mount helper was notified.
+ mountHelperNotified bool
+
// errInvalidArgument is for an invalid CLI argument/value provided.
errInvalidArgument = errors.New("invalid argument")
@@ -162,6 +169,7 @@ func mountFilesystem(opts cliOptions, fsys *filesystem.FS) (*fuse.Conn, error) {
mountOpts := []fuse.MountOption{
fuse.FSName("zipfuse"),
fuse.ReadOnly(),
+ fuse.DefaultPermissions(),
fuse.MaxReadahead(uint32(opts.streamPoolSize)),
}
if opts.allowOther {
@@ -177,7 +185,14 @@ func mountFilesystem(opts cliOptions, fsys *filesystem.FS) (*fuse.Conn, error) {
return conn, nil
}
-func notifyMountHelper() error {
+// 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.
+func notifyMountHelper(e error) error {
+ if mountHelperNotified {
+ return nil
+ }
+
fdStr := os.Getenv("ZIPFUSE_HELPER_FD")
if fdStr == "" {
return nil
@@ -185,17 +200,34 @@ func notifyMountHelper() error {
fd, err := strconv.Atoi(fdStr)
if err != nil {
- return fmt.Errorf("fd conversion failed: %w", err)
+ return fmt.Errorf("fd conversion failed for %q: %w", fdStr, err)
}
f := os.NewFile(uintptr(fd), "helper-pipe")
+ if f == nil {
+ return fmt.Errorf("opening pipe failed for %q (fd=%d): (nil)", fdStr, fd) //nolint:err113
+ }
defer f.Close()
- _, err = f.Write([]byte{1})
- if err != nil {
- return fmt.Errorf("write to pipe failed: %w", err)
+ if e == nil {
+ if _, err := f.Write([]byte{signalMountSuccess}); err != nil {
+ return fmt.Errorf("write success status to pipe failed: %w", err)
+ }
+ } else {
+ if _, err := f.Write([]byte{signalMountFailed}); err != nil {
+ return fmt.Errorf("write error status to pipe failed: %w", err)
+ }
+ msg, err := json.Marshal(e.Error())
+ if err != nil {
+ return fmt.Errorf("marshal error message for pipe failed: %w", err)
+ }
+ if _, err := f.Write(append(msg, signalDelimiter)); err != nil {
+ return fmt.Errorf("write error message to pipe failed: %w", err)
+ }
}
+ mountHelperNotified = true
+
return nil
}
@@ -250,7 +282,21 @@ func cleanupMount(mountDir string, conn *fuse.Conn, fsys *filesystem.FS) {
}
func main() {
+ for _, arg := range os.Args {
+ if arg == "-o" {
+ fmt.Fprintln(os.Stderr, helpErrOptionsArg)
+ err := notifyMountHelper(fmt.Errorf("%w: \"-o\" is not supported", errInvalidArgument))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to notify mount helper: %v\n", err)
+ }
+ os.Exit(1)
+ }
+ }
if err := rootCmd().Execute(); err != nil {
+ err := notifyMountHelper(err)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to notify mount helper: %v\n", err)
+ }
os.Exit(1)
}
}
@@ -274,7 +320,7 @@ func run(opts cliOptions) error {
}
defer cleanupMount(opts.mountDir, conn, fsys)
- err = notifyMountHelper()
+ err = notifyMountHelper(nil)
if err != nil {
rbuf.Printf("failed to notify mount helper: %v\n", err)
}
diff --git a/cmd/zipfuse/util.go b/cmd/zipfuse/util.go
index 9b4d62f..d682db4 100644
--- a/cmd/zipfuse/util.go
+++ b/cmd/zipfuse/util.go
@@ -44,7 +44,7 @@ func fdLimits() (fsLimit int, cacheLimit int, err error) {
cacheLimit = (fsLimit * 70) / 100 // 70% of FS limit
if fsLimit < 1 || cacheLimit < 1 {
- return 0, 0, fmt.Errorf("calculations too small (soft=%d)", osLimit)
+ return 0, 0, fmt.Errorf("calculations too small (os=%d)", osLimit)
}
return fsLimit, cacheLimit, nil