Git backed by object storage because you can't stop me
git
object-storage
kefka
1// Package mountfs composes several billy filesystems into one, dispatching by
2// the first path component. objgit uses it to give a hook sandbox a read-only
3// /src (a git tree) alongside a writable /tmp (an in-memory scratch fs) under a
4// single filesystem, since the kefka shell mounts exactly one.
5//
6// The root directory is virtual: it only lists the configured mount points and
7// cannot itself be written to.
8package mountfs
9
10import (
11 "io/fs"
12 "os"
13 "path"
14 "sort"
15 "strings"
16 "time"
17
18 "github.com/go-git/go-billy/v6"
19)
20
21// FS routes operations to a mounted filesystem chosen by the leading path
22// component.
23type FS struct {
24 mounts map[string]billy.Filesystem
25 names []string // sorted mount names, for a stable root listing
26}
27
28// New builds a composite filesystem. Keys are top-level directory names (e.g.
29// "src", "tmp") without slashes.
30func New(mounts map[string]billy.Filesystem) *FS {
31 names := make([]string, 0, len(mounts))
32 for name := range mounts {
33 names = append(names, name)
34 }
35 sort.Strings(names)
36 return &FS{mounts: mounts, names: names}
37}
38
39// route resolves p to a mount and a path relative to it. isRoot is true when p
40// addresses the virtual root itself. isMount is true when p addresses a mount
41// point exactly (rel == ".").
42func (f *FS) route(p string) (sub billy.Filesystem, rel string, isRoot, isMount bool, err error) {
43 clean := path.Clean("/" + p)[1:] // strip leading slash; root -> ""
44 if clean == "" {
45 return nil, "", true, false, nil
46 }
47 name, rest, _ := strings.Cut(clean, "/")
48 sub, ok := f.mounts[name]
49 if !ok {
50 return nil, "", false, false, fs.ErrNotExist
51 }
52 if rest == "" {
53 return sub, ".", false, true, nil
54 }
55 return sub, rest, false, false, nil
56}
57
58func pathErr(op, name string, err error) error {
59 return &os.PathError{Op: op, Path: name, Err: err}
60}
61
62func (f *FS) Open(filename string) (billy.File, error) {
63 return f.OpenFile(filename, os.O_RDONLY, 0)
64}
65
66func (f *FS) OpenFile(filename string, flag int, perm fs.FileMode) (billy.File, error) {
67 sub, rel, isRoot, isMount, err := f.route(filename)
68 if err != nil {
69 return nil, pathErr("open", filename, err)
70 }
71 if isRoot || isMount {
72 return nil, pathErr("open", filename, billy.ErrNotSupported)
73 }
74 return sub.OpenFile(rel, flag, perm)
75}
76
77func (f *FS) Stat(filename string) (fs.FileInfo, error) {
78 sub, rel, isRoot, isMount, err := f.route(filename)
79 if err != nil {
80 return nil, pathErr("stat", filename, err)
81 }
82 if isRoot {
83 return dirInfo{name: "/"}, nil
84 }
85 if isMount {
86 return dirInfo{name: path.Base(path.Clean("/" + filename))}, nil
87 }
88 return sub.Stat(rel)
89}
90
91func (f *FS) Lstat(filename string) (fs.FileInfo, error) {
92 sub, rel, isRoot, isMount, err := f.route(filename)
93 if err != nil {
94 return nil, pathErr("lstat", filename, err)
95 }
96 if isRoot || isMount {
97 return f.Stat(filename)
98 }
99 if sym, ok := sub.(billy.Symlink); ok {
100 return sym.Lstat(rel)
101 }
102 return sub.Stat(rel)
103}
104
105func (f *FS) ReadDir(p string) ([]fs.DirEntry, error) {
106 sub, rel, isRoot, _, err := f.route(p)
107 if err != nil {
108 return nil, pathErr("readdir", p, err)
109 }
110 if isRoot {
111 out := make([]fs.DirEntry, 0, len(f.names))
112 for _, name := range f.names {
113 out = append(out, dirInfo{name: name})
114 }
115 return out, nil
116 }
117 return sub.ReadDir(rel)
118}
119
120func (f *FS) Readlink(link string) (string, error) {
121 sub, rel, isRoot, isMount, err := f.route(link)
122 if err != nil {
123 return "", pathErr("readlink", link, err)
124 }
125 if isRoot || isMount {
126 return "", pathErr("readlink", link, billy.ErrNotSupported)
127 }
128 if sym, ok := sub.(billy.Symlink); ok {
129 return sym.Readlink(rel)
130 }
131 return "", pathErr("readlink", link, billy.ErrNotSupported)
132}
133
134func (f *FS) Symlink(target, link string) error {
135 sub, rel, isRoot, isMount, err := f.route(link)
136 if err != nil {
137 return pathErr("symlink", link, err)
138 }
139 if isRoot || isMount {
140 return pathErr("symlink", link, billy.ErrReadOnly)
141 }
142 if sym, ok := sub.(billy.Symlink); ok {
143 return sym.Symlink(target, rel)
144 }
145 return pathErr("symlink", link, billy.ErrNotSupported)
146}
147
148func (f *FS) Create(filename string) (billy.File, error) {
149 return f.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
150}
151
152func (f *FS) Remove(filename string) error {
153 sub, rel, isRoot, isMount, err := f.route(filename)
154 if err != nil {
155 return pathErr("remove", filename, err)
156 }
157 if isRoot || isMount {
158 return pathErr("remove", filename, billy.ErrReadOnly)
159 }
160 return sub.Remove(rel)
161}
162
163func (f *FS) MkdirAll(filename string, perm fs.FileMode) error {
164 sub, rel, isRoot, isMount, err := f.route(filename)
165 if err != nil {
166 return pathErr("mkdir", filename, err)
167 }
168 if isRoot || isMount {
169 return pathErr("mkdir", filename, billy.ErrReadOnly)
170 }
171 return sub.MkdirAll(rel, perm)
172}
173
174// Rename only works within a single mount.
175func (f *FS) Rename(oldpath, newpath string) error {
176 oldSub, oldRel, oldRoot, oldMount, err := f.route(oldpath)
177 if err != nil {
178 return pathErr("rename", oldpath, err)
179 }
180 newSub, newRel, newRoot, newMount, err := f.route(newpath)
181 if err != nil {
182 return pathErr("rename", newpath, err)
183 }
184 if oldRoot || oldMount || newRoot || newMount || oldSub != newSub {
185 return pathErr("rename", oldpath, billy.ErrNotSupported)
186 }
187 return oldSub.Rename(oldRel, newRel)
188}
189
190func (f *FS) TempFile(dir, prefix string) (billy.File, error) {
191 sub, rel, isRoot, _, err := f.route(dir)
192 if err != nil {
193 return nil, pathErr("tempfile", dir, err)
194 }
195 if isRoot {
196 return nil, pathErr("tempfile", dir, billy.ErrReadOnly)
197 }
198 return sub.TempFile(rel, prefix)
199}
200
201// Chroot returns the mounted filesystem (optionally further chrooted) so callers
202// that chroot into /src or /tmp keep working.
203func (f *FS) Chroot(p string) (billy.Filesystem, error) {
204 sub, rel, isRoot, isMount, err := f.route(p)
205 if err != nil {
206 return nil, pathErr("chroot", p, err)
207 }
208 if isRoot {
209 return f, nil
210 }
211 if isMount {
212 return sub, nil
213 }
214 return sub.Chroot(rel)
215}
216
217func (f *FS) Root() string { return "/" }
218
219func (f *FS) Join(elem ...string) string { return path.Join(elem...) }
220
221// dirInfo describes the virtual root and the mount-point directories.
222type dirInfo struct{ name string }
223
224func (d dirInfo) Name() string { return d.name }
225func (d dirInfo) Size() int64 { return 0 }
226func (d dirInfo) Mode() fs.FileMode { return fs.ModeDir | 0o755 }
227func (d dirInfo) ModTime() time.Time { return time.Time{} }
228func (d dirInfo) IsDir() bool { return true }
229func (d dirInfo) Sys() any { return nil }
230func (d dirInfo) Type() fs.FileMode { return fs.ModeDir }
231func (d dirInfo) Info() (fs.FileInfo, error) { return d, nil }
232
233var (
234 _ billy.Filesystem = (*FS)(nil)
235 _ fs.FileInfo = dirInfo{}
236 _ fs.DirEntry = dirInfo{}
237)