1
+package cli
2
+
3
+import (
4
+ "os"
5
+ "os/exec"
6
+ "path/filepath"
7
+ "runtime"
8
+ "strings"
9
+ "testing"
10
+
11
+ "github.com/ipfs/kubo/test/cli/harness"
12
+ "github.com/ipfs/kubo/test/cli/testutils"
13
+ "github.com/stretchr/testify/require"
14
+)
15
+
16
+func TestFUSE(t *testing.T) {
17
+ testutils.RequiresFUSE(t)
18
+ t.Parallel()
19
+
20
+ t.Run("mount and unmount work correctly", func(t *testing.T) {
21
+ t.Parallel()
22
+
23
+ // Create a node and start daemon
24
+ node := harness.NewT(t).NewNode().Init()
25
+ node.StartDaemon()
26
+
27
+ // Create mount directories in the node's working directory
28
+ nodeDir := node.Dir
29
+ ipfsMount := filepath.Join(nodeDir, "ipfs")
30
+ ipnsMount := filepath.Join(nodeDir, "ipns")
31
+ mfsMount := filepath.Join(nodeDir, "mfs")
32
+
33
+ err := os.MkdirAll(ipfsMount, 0755)
34
+ require.NoError(t, err)
35
+ err = os.MkdirAll(ipnsMount, 0755)
36
+ require.NoError(t, err)
37
+ err = os.MkdirAll(mfsMount, 0755)
38
+ require.NoError(t, err)
39
+
40
+ // Ensure any existing mounts are cleaned up first
41
+ failOnError := false // mount points might not exist from previous runs
42
+ doUnmount(t, ipfsMount, failOnError)
43
+ doUnmount(t, ipnsMount, failOnError)
44
+ doUnmount(t, mfsMount, failOnError)
45
+
46
+ // Test mount operation
47
+ result := node.IPFS("mount", "-f", ipfsMount, "-n", ipnsMount, "-m", mfsMount)
48
+
49
+ // Verify mount output
50
+ expectedOutput := "IPFS mounted at: " + ipfsMount + "\n" +
51
+ "IPNS mounted at: " + ipnsMount + "\n" +
52
+ "MFS mounted at: " + mfsMount + "\n"
53
+ require.Equal(t, expectedOutput, result.Stdout.String())
54
+
55
+ // Test basic MFS functionality via FUSE mount
56
+ testFile := filepath.Join(mfsMount, "testfile")
57
+ testContent := "hello fuse world"
58
+
59
+ // Create file via FUSE mount
60
+ err = os.WriteFile(testFile, []byte(testContent), 0644)
61
+ require.NoError(t, err)
62
+
63
+ // Verify file appears in MFS via IPFS commands
64
+ result = node.IPFS("files", "ls", "/")
65
+ require.Contains(t, result.Stdout.String(), "testfile")
66
+
67
+ // Read content back via MFS FUSE mount
68
+ readContent, err := os.ReadFile(testFile)
69
+ require.NoError(t, err)
70
+ require.Equal(t, testContent, string(readContent))
71
+
72
+ // Get the CID of the MFS file
73
+ result = node.IPFS("files", "stat", "/testfile", "--format=<hash>")
74
+ fileCID := strings.TrimSpace(result.Stdout.String())
75
+ require.NotEmpty(t, fileCID, "should have a CID for the MFS file")
76
+
77
+ // Read the same content via IPFS FUSE mount using the CID
78
+ ipfsFile := filepath.Join(ipfsMount, fileCID)
79
+ ipfsContent, err := os.ReadFile(ipfsFile)
80
+ require.NoError(t, err)
81
+ require.Equal(t, testContent, string(ipfsContent), "content should match between MFS and IPFS mounts")
82
+
83
+ // Verify both FUSE mounts return identical data
84
+ require.Equal(t, readContent, ipfsContent, "MFS and IPFS FUSE mounts should return identical data")
85
+
86
+ // Test that mount directories cannot be removed while mounted
87
+ err = os.Remove(ipfsMount)
88
+ require.Error(t, err, "should not be able to remove mounted directory")
89
+
90
+ // Stop daemon - this should trigger automatic unmount via context cancellation
91
+ node.StopDaemon()
92
+
93
+ // Daemon shutdown should handle unmount synchronously via context.AfterFunc
94
+
95
+ // Verify directories can now be removed (indicating successful unmount)
96
+ err = os.Remove(ipfsMount)
97
+ require.NoError(t, err, "should be able to remove directory after unmount")
98
+ err = os.Remove(ipnsMount)
99
+ require.NoError(t, err, "should be able to remove directory after unmount")
100
+ err = os.Remove(mfsMount)
101
+ require.NoError(t, err, "should be able to remove directory after unmount")
102
+ })
103
+
104
+ t.Run("explicit unmount works", func(t *testing.T) {
105
+ t.Parallel()
106
+
107
+ node := harness.NewT(t).NewNode().Init()
108
+ node.StartDaemon()
109
+
110
+ // Create mount directories
111
+ nodeDir := node.Dir
112
+ ipfsMount := filepath.Join(nodeDir, "ipfs")
113
+ ipnsMount := filepath.Join(nodeDir, "ipns")
114
+ mfsMount := filepath.Join(nodeDir, "mfs")
115
+
116
+ err := os.MkdirAll(ipfsMount, 0755)
117
+ require.NoError(t, err)
118
+ err = os.MkdirAll(ipnsMount, 0755)
119
+ require.NoError(t, err)
120
+ err = os.MkdirAll(mfsMount, 0755)
121
+ require.NoError(t, err)
122
+
123
+ // Clean up any existing mounts
124
+ failOnError := false // mount points might not exist from previous runs
125
+ doUnmount(t, ipfsMount, failOnError)
126
+ doUnmount(t, ipnsMount, failOnError)
127
+ doUnmount(t, mfsMount, failOnError)
128
+
129
+ // Mount
130
+ node.IPFS("mount", "-f", ipfsMount, "-n", ipnsMount, "-m", mfsMount)
131
+
132
+ // Explicit unmount via platform-specific command
133
+ failOnError = true // test that explicit unmount works correctly
134
+ doUnmount(t, ipfsMount, failOnError)
135
+ doUnmount(t, ipnsMount, failOnError)
136
+ doUnmount(t, mfsMount, failOnError)
137
+
138
+ // Verify directories can be removed after explicit unmount
139
+ err = os.Remove(ipfsMount)
140
+ require.NoError(t, err)
141
+ err = os.Remove(ipnsMount)
142
+ require.NoError(t, err)
143
+ err = os.Remove(mfsMount)
144
+ require.NoError(t, err)
145
+
146
+ node.StopDaemon()
147
+ })
148
+}
149
+
150
+// doUnmount performs platform-specific unmount, similar to sharness do_umount
151
+// failOnError: if true, unmount errors cause test failure; if false, errors are ignored (useful for cleanup)
152
+func doUnmount(t *testing.T, mountPoint string, failOnError bool) {
153
+ t.Helper()
154
+ var cmd *exec.Cmd
155
+ if runtime.GOOS == "linux" {
156
+ // fusermount -u: unmount filesystem (strict - fails if busy)
157
+ cmd = exec.Command("fusermount", "-u", mountPoint)
158
+ } else {
159
+ cmd = exec.Command("umount", mountPoint)
160
+ }
161
+
162
+ err := cmd.Run()
163
+ if err != nil && failOnError {
164
+ t.Fatalf("failed to unmount %s: %v", mountPoint, err)
165
+ }
166
+}