Skip to content

Commit 3855d49

Browse files
committed
feat: introduce mountfs
MountFS enables the composition of multiple filesystems by mountpoints, similar to unix mountpoints.
1 parent fb18826 commit 3855d49

File tree

5 files changed

+594
-0
lines changed

5 files changed

+594
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Features
5858
- ReadOnly Wrapper
5959
- DummyFS for quick mocking
6060
- MemFS full in-memory filesystem
61+
- MountFS - support mounts across filesystems
6162

6263
Current state: ALPHA
6364
-----

examples/example_mountfs.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package examples
2+
3+
import (
4+
"github.com/blang/vfs"
5+
"github.com/blang/vfs/memfs"
6+
"github.com/blang/vfs/mountfs"
7+
)
8+
9+
func ExampleMountFS() {
10+
// Create a vfs supporting mounts
11+
// The root fs is accessing the filesystem of the underlying OS
12+
fs := mountfs.Create(vfs.OS())
13+
14+
// Mount a memfs inside
15+
fs.Mount(memfs.Create(), "/memfs")
16+
17+
// This will create /testdir inside the memfs
18+
fs.Mkdir("/memfs/testdir", 0777)
19+
20+
// This will create /tmp/testdir inside your OS fs
21+
fs.Mkdir("/tmp/testdir", 0777)
22+
}

mountfs/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package mountfs defines a filesystem supporting
2+
// the composition of multiple filesystems by mountpoints.
3+
package mountfs

mountfs/mountfs.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package mountfs
2+
3+
import (
4+
"errors"
5+
"github.com/blang/vfs"
6+
"os"
7+
filepath "path"
8+
"strings"
9+
)
10+
11+
// ErrBoundary is returned if an operation
12+
// can not act across filesystem boundaries.
13+
var ErrBoundary = errors.New("Crossing boundary")
14+
15+
func Create(rootFS vfs.Filesystem) *MountFS {
16+
return &MountFS{
17+
rootFS: rootFS,
18+
mounts: make(map[string]vfs.Filesystem),
19+
parents: make(map[string][]string),
20+
}
21+
}
22+
23+
// MountFS represents a filesystem build upon a root filesystem
24+
// and multiple filesystems can be mounted inside it.
25+
// In contrast to unix filesystems, the mount path may
26+
// not be a directory or event exist.
27+
//
28+
// Only filesystems with the same path separator are compatible.
29+
// It's not possible to mount a specific source directory, only the
30+
// root of the filesystem can be mounted, use a chroot in this case.
31+
// The resulting filesystem is case-sensitive.
32+
type MountFS struct {
33+
rootFS vfs.Filesystem
34+
mounts map[string]vfs.Filesystem
35+
parents map[string][]string
36+
}
37+
38+
// Mount mounts a filesystem on the given path.
39+
// Mounts inside mounts are supported, the longest path match will be taken.
40+
// Mount paths may be overwritten if set on the same path.
41+
// Path `/` can be used to change rootfs.
42+
// Only absolute paths are allowed.
43+
func (fs *MountFS) Mount(mount vfs.Filesystem, path string) error {
44+
pathSeparator := string(fs.rootFS.PathSeparator())
45+
46+
// Clean path and make absolute
47+
path = filepath.Clean(path)
48+
segm := vfs.SplitPath(path, pathSeparator)
49+
segm[0] = "" // make absolute
50+
path = strings.Join(segm, pathSeparator)
51+
52+
// Change rootfs disabled
53+
if path == "" {
54+
fs.rootFS = mount
55+
return nil
56+
}
57+
58+
parent := strings.Join(segm[0:len(segm)-1], pathSeparator)
59+
if parent == "" {
60+
parent = "/"
61+
}
62+
fs.parents[parent] = append(fs.parents[parent], path)
63+
fs.mounts[path] = mount
64+
return nil
65+
}
66+
67+
// PathSeparator returns the path separator
68+
func (fs MountFS) PathSeparator() uint8 {
69+
return fs.rootFS.PathSeparator()
70+
}
71+
72+
// findMount finds a valid mountpoint for the given path.
73+
// It returns the corresponding filesystem and the path inside of this filesystem.
74+
func findMount(path string, mounts map[string]vfs.Filesystem, fallback vfs.Filesystem, pathSeparator string) (vfs.Filesystem, string) {
75+
path = filepath.Clean(path)
76+
segs := vfs.SplitPath(path, pathSeparator)
77+
l := len(segs)
78+
for i := l; i > 0; i-- {
79+
mountPath := strings.Join(segs[0:i], pathSeparator)
80+
if fs, ok := mounts[mountPath]; ok {
81+
return fs, "/" + strings.Join(segs[i:l], pathSeparator)
82+
}
83+
}
84+
return fallback, path
85+
}
86+
87+
type innerFile struct {
88+
vfs.File
89+
name string
90+
}
91+
92+
// Name returns the full path inside mountfs
93+
func (f innerFile) Name() string {
94+
return f.name
95+
}
96+
97+
// OpenFile find the mount of the given path and executes OpenFile
98+
// on the corresponding filesystem.
99+
// It wraps the resulting file to return the path inside mountfs on Name()
100+
func (fs MountFS) OpenFile(name string, flag int, perm os.FileMode) (vfs.File, error) {
101+
mount, innerPath := findMount(name, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
102+
file, err := mount.OpenFile(innerPath, flag, perm)
103+
return innerFile{File: file, name: name}, err
104+
}
105+
106+
// Remove removes a file or directory
107+
func (fs MountFS) Remove(name string) error {
108+
mount, innerPath := findMount(name, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
109+
return mount.Remove(innerPath)
110+
}
111+
112+
// Rename renames a file.
113+
// Renames across filesystems are not allowed.
114+
func (fs MountFS) Rename(oldpath, newpath string) error {
115+
oldMount, oldInnerPath := findMount(oldpath, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
116+
newMount, newInnerPath := findMount(newpath, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
117+
if oldMount != newMount {
118+
return ErrBoundary
119+
}
120+
return oldMount.Rename(oldInnerPath, newInnerPath)
121+
}
122+
123+
// Mkdir creates a directory
124+
func (fs MountFS) Mkdir(name string, perm os.FileMode) error {
125+
mount, innerPath := findMount(name, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
126+
return mount.Mkdir(innerPath, perm)
127+
}
128+
129+
type innerFileInfo struct {
130+
os.FileInfo
131+
name string
132+
}
133+
134+
func (fi innerFileInfo) Name() string {
135+
return fi.name
136+
}
137+
138+
// Stat returns the fileinfo of a file
139+
func (fs MountFS) Stat(name string) (os.FileInfo, error) {
140+
mount, innerPath := findMount(name, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
141+
fi, err := mount.Stat(innerPath)
142+
if innerPath == "/" {
143+
return innerFileInfo{FileInfo: fi, name: filepath.Base(name)}, err
144+
}
145+
return fi, err
146+
}
147+
148+
// Lstat returns the fileinfo of a file or link.
149+
func (fs MountFS) Lstat(name string) (os.FileInfo, error) {
150+
mount, innerPath := findMount(name, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
151+
fi, err := mount.Lstat(innerPath)
152+
if innerPath == "/" {
153+
return innerFileInfo{FileInfo: fi, name: filepath.Base(name)}, err
154+
}
155+
return fi, err
156+
}
157+
158+
// ReadDir reads the directory named by path and returns a list of sorted directory entries.
159+
func (fs MountFS) ReadDir(path string) ([]os.FileInfo, error) {
160+
path = filepath.Clean(path)
161+
mount, innerPath := findMount(path, fs.mounts, fs.rootFS, string(fs.PathSeparator()))
162+
163+
fis, err := mount.ReadDir(innerPath)
164+
if err != nil {
165+
return fis, err
166+
}
167+
168+
// Add mountpoints
169+
if childs, ok := fs.parents[path]; ok {
170+
for _, c := range childs {
171+
mfi, err := fs.Stat(c)
172+
if err == nil {
173+
fis = append(fis, mfi)
174+
}
175+
}
176+
}
177+
return fis, err
178+
}

0 commit comments

Comments
 (0)