-
Notifications
You must be signed in to change notification settings - Fork 0
Archive Mount as Filesystem (FUSE) #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "os/signal" | ||
| "strings" | ||
| "syscall" | ||
|
|
||
| "github.com/Snider/Borg/pkg/datanode" | ||
| "github.com/Snider/Borg/pkg/fusefs" | ||
| "github.com/Snider/Borg/pkg/tim" | ||
| "github.com/Snider/Borg/pkg/trix" | ||
| "github.com/hanwen/go-fuse/v2/fs" | ||
| "github.com/hanwen/go-fuse/v2/fuse" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| var mountCmd = NewMountCmd() | ||
|
|
||
| func NewMountCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "mount [archive] [mountpoint]", | ||
| Short: "Mount an archive as a read-only filesystem", | ||
| Args: cobra.ExactArgs(2), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| archiveFile := args[0] | ||
| mountpoint := args[1] | ||
| password, _ := cmd.Flags().GetString("password") | ||
|
|
||
| data, err := os.ReadFile(archiveFile) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| var dn *datanode.DataNode | ||
| if strings.HasSuffix(archiveFile, ".stim") || (len(data) >= 4 && string(data[:4]) == "STIM") { | ||
| if password == "" { | ||
| return fmt.Errorf("password required for .stim files") | ||
| } | ||
| m, err := tim.FromSigil(data, password) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| tarball, err := m.ToTar() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| dn, err = datanode.FromTar(tarball) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } else { | ||
| // This handles .dat, .tar, .trix, and .tim files | ||
| dn, err = trix.FromTrix(data, password) | ||
| if err != nil { | ||
| // If FromTrix fails, try FromTar as a fallback for plain tarballs | ||
| if dn, err = datanode.FromTar(data); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| } | ||
|
|
||
| root := fusefs.NewDataNodeFs(dn) | ||
| server, err := fs.Mount(mountpoint, root, &fs.Options{ | ||
| MountOptions: fuse.MountOptions{ | ||
| Debug: true, | ||
| }, | ||
| }) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| c := make(chan os.Signal, 1) | ||
| signal.Notify(c, os.Interrupt, syscall.SIGTERM) | ||
| go func() { | ||
| <-c | ||
| server.Unmount() | ||
| }() | ||
|
|
||
| fmt.Fprintf(cmd.OutOrStdout(), "Archive mounted at %s. Press Ctrl+C to unmount.\n", mountpoint) | ||
| server.Wait() | ||
|
|
||
| return nil | ||
| }, | ||
| } | ||
| cmd.Flags().StringP("password", "p", "", "Password for encrypted archives") | ||
| return cmd | ||
| } | ||
|
|
||
| func GetMountCmd() *cobra.Command { | ||
| return mountCmd | ||
| } | ||
|
|
||
| func init() { | ||
| RootCmd.AddCommand(GetMountCmd()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "runtime" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/Snider/Borg/pkg/datanode" | ||
| "github.com/hanwen/go-fuse/v2/fuse" | ||
| ) | ||
|
|
||
| func TestMountCommand(t *testing.T) { | ||
| if runtime.GOOS != "linux" { | ||
| t.Skip("skipping mount test on non-linux systems") | ||
| } | ||
| if _, err := exec.LookPath("fusermount"); err != nil { | ||
| t.Skip("fusermount not found, skipping mount test") | ||
| } | ||
|
|
||
| // Create a temporary directory for the mount point | ||
| mountDir := t.TempDir() | ||
|
|
||
| // Create a dummy archive | ||
| dn := datanode.New() | ||
| dn.AddData("test.txt", []byte("hello")) | ||
| tarball, err := dn.ToTar() | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
Comment on lines
+29
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test setup for creating a dummy archive is based on the old |
||
|
|
||
| archiveFile := filepath.Join(t.TempDir(), "test.dat") | ||
| if err := os.WriteFile(archiveFile, tarball, 0644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| // Run the mount command in the background | ||
| ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||
| defer cancel() | ||
|
|
||
| cmd := NewMountCmd() | ||
| cmd.SetArgs([]string{archiveFile, mountDir}) | ||
|
|
||
| cmdErrCh := make(chan error, 1) | ||
| go func() { | ||
| cmdErrCh <- cmd.ExecuteContext(ctx) | ||
| }() | ||
|
|
||
| // Give the FUSE server a moment to start up and stabilize | ||
| time.Sleep(3 * time.Second) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // Check that the mount is active. | ||
| mounted, err := isMounted(mountDir) | ||
| if err != nil { | ||
| t.Fatalf("Failed to check if mounted: %v", err) | ||
| } | ||
| if !mounted { | ||
| t.Errorf("Mount directory does not appear to be mounted.") | ||
| } | ||
|
|
||
|
|
||
| // Cancel the context to stop the command | ||
| cancel() | ||
|
|
||
| // Wait for the command to exit | ||
| select { | ||
| case err := <-cmdErrCh: | ||
| if err != nil && err != context.Canceled { | ||
| t.Errorf("Mount command returned an unexpected error: %v", err) | ||
| } | ||
| case <-time.After(2 * time.Second): | ||
| t.Error("Mount command did not exit after context was canceled.") | ||
| } | ||
|
|
||
| // Give the filesystem a moment to unmount. | ||
| time.Sleep(1 * time.Second) | ||
|
|
||
|
|
||
| // Verify that the mount point is now unmounted. | ||
| mounted, err = isMounted(mountDir) | ||
| if err != nil { | ||
| t.Fatalf("Failed to check if unmounted: %v", err) | ||
| } | ||
| if mounted { | ||
| // As a fallback, try to unmount it manually. | ||
| server, _ := fuse.NewServer(nil, mountDir, nil) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| server.Unmount() | ||
| t.Errorf("Mount directory was not unmounted cleanly.") | ||
| } | ||
| } | ||
|
|
||
| // isMounted checks if a directory is a FUSE mount point. | ||
| // This is a simple heuristic and might not be 100% reliable. | ||
| func isMounted(path string) (bool, error) { | ||
| // On Linux, we can check the mountinfo. | ||
| if runtime.GOOS == "linux" { | ||
| data, err := os.ReadFile("/proc/self/mountinfo") | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| return strings.Contains(string(data), path) && strings.Contains(string(data), "fuse"), nil | ||
| } | ||
| return false, nil // Not implemented for other OSes. | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os/exec" | ||
| "runtime" | ||
|
|
||
| "github.com/hanwen/go-fuse/v2/fuse" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| var unmountCmd = NewUnmountCmd() | ||
|
|
||
| func NewUnmountCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "unmount [mountpoint]", | ||
| Short: "Unmount a filesystem", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| mountpoint := args[0] | ||
| server, _ := fuse.NewServer(nil, mountpoint, nil) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| err := server.Unmount() | ||
| if err == nil { | ||
| return nil | ||
| } | ||
|
|
||
| // Fallback to system commands | ||
| var unmountCmd *exec.Cmd | ||
| switch runtime.GOOS { | ||
| case "linux": | ||
| unmountCmd = exec.Command("fusermount", "-u", mountpoint) | ||
| case "darwin": | ||
| unmountCmd = exec.Command("umount", mountpoint) | ||
| default: | ||
| return fmt.Errorf("unmount not supported on %s: %v", runtime.GOOS, err) | ||
| } | ||
|
|
||
| return unmountCmd.Run() | ||
| }, | ||
| } | ||
| return cmd | ||
| } | ||
|
|
||
| func GetUnmountCmd() *cobra.Command { | ||
| return unmountCmd | ||
| } | ||
|
|
||
| func init() { | ||
| RootCmd.AddCommand(GetUnmountCmd()) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block reads the entire archive into memory with
os.ReadFile, which contradicts the goal of supporting large archives via streaming. The logic should be reworked to useos.Openand operate on the resultingio.ReaderAtfile stream. Furthermore, there are compilation errors because functions likedatanode.FromTarare called with[]byteinstead of anio.ReaderAtas required by the new API. The entire file processing logic needs to be refactored for streaming.