GitRoot
Craft your forge, Build your project, Grow your community freely
1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5package fs
6
7import (
8 "context"
9 "errors"
10 "io"
11 "io/fs"
12 "os"
13 "strings"
14 "time"
15
16 "gitroot.dev/server/logger"
17)
18
19var ErrFsNotFound = errors.New("fs not found")
20
21type multipleFs struct {
22 logger *logger.Logger
23 subFs map[string]fs.FS
24}
25
26func NewMultiple(ctx context.Context, subFs map[string]fs.FS) *multipleFs {
27 return &multipleFs{
28 logger: logger.NewLoggerCtx(logger.FS_PLUGIN, ctx).NewSubLogger("multiple"),
29 subFs: subFs,
30 }
31}
32
33func (m *multipleFs) UpdateSubFs(path string, subFs fs.FS) {
34 m.subFs[path] = subFs
35}
36
37func (m *multipleFs) splitFsFile(name string) (fs.FS, string, error) {
38 name = strings.Trim(name, string(os.PathSeparator))
39
40 if name == "" {
41 return nil, "", ErrFsNotFound
42 }
43
44 parts := strings.SplitN(name, string(os.PathSeparator), 2)
45 subFs, ok := m.subFs[parts[0]]
46 if !ok {
47 return nil, "", ErrFsNotFound
48 }
49
50 if len(parts) < 2 {
51 return subFs, ".", nil
52 }
53
54 return subFs, parts[1], nil
55}
56
57func (m *multipleFs) ReadFile(name string) ([]byte, error) {
58 m.logger.Debug("read file", logger.NewLoggerPair("name", name))
59 subFs, filepath, err := m.splitFsFile(name)
60 if err != nil {
61 return nil, err
62 }
63 return fs.ReadFile(subFs, filepath)
64}
65
66func (m *multipleFs) ReadDir(name string) ([]fs.DirEntry, error) {
67 m.logger.Debug("ReadDir", logger.NewLoggerPair("filepath", name))
68 subFs, filepath, err := m.splitFsFile(name)
69 if err != nil {
70 return nil, err
71 }
72 return fs.ReadDir(subFs, filepath)
73}
74
75func (m *multipleFs) Stat(name string) (fs.FileInfo, error) {
76 m.logger.Debug("Stat")
77 subFs, filepath, err := m.splitFsFile(name)
78 if err != nil {
79 return nil, err
80 }
81 return fs.Stat(subFs, filepath)
82}
83
84func (m *multipleFs) Sub(dir string) (fs.FS, error) {
85 m.logger.Debug("Sub")
86 subFs, filepath, err := m.splitFsFile(dir)
87 if err != nil {
88 return nil, err
89 }
90 return fs.Sub(subFs, filepath)
91}
92
93func (m *multipleFs) Open(name string) (fs.File, error) {
94 m.logger.Debug("open file", logger.NewLoggerPair("name", name))
95
96 name = strings.Trim(name, "/")
97
98 if name == "." {
99 return &fakeFile{
100 name: "/",
101 subFs: m.subFs,
102 logger: m.logger.NewSubLogger("/"),
103 }, nil
104 }
105
106 if subFs, ok := m.subFs[name]; ok {
107 return subFs.Open(".")
108 }
109
110 subFs, filepath, err := m.splitFsFile(name)
111 if err != nil {
112 return nil, err
113 }
114 return subFs.Open(filepath)
115}
116
117type fakeFile struct {
118 name string
119 subFs map[string]fs.FS
120 currentReadDir int
121 logger *logger.Logger
122}
123type fakeFileInfo struct {
124 name string
125}
126
127func (m *fakeFile) Close() error {
128 return nil
129}
130
131func (m *fakeFile) Read(p []byte) (int, error) {
132 return 0, errors.New("can't read a dir")
133}
134
135func (m *fakeFile) Stat() (fs.FileInfo, error) {
136 return &fakeFileInfo{}, nil
137}
138
139func (m *fakeFile) ReadDir(n int) ([]fs.DirEntry, error) {
140 m.logger.Debug("ReadDir", logger.NewLoggerPair("n", n))
141
142 subDirs := []fs.DirEntry{}
143 for key := range m.subFs {
144 subDirs = append(subDirs, newFakeDirEntry(key))
145 }
146
147 if n <= 0 {
148 m.currentReadDir = 0
149 return subDirs, nil
150 }
151
152 if m.currentReadDir >= len(subDirs) {
153 m.currentReadDir = 0
154 return nil, io.EOF
155 }
156
157 from := m.currentReadDir
158 to := from + n
159 if to > len(subDirs) {
160 to = len(subDirs)
161 }
162
163 result := subDirs[from:to]
164 m.currentReadDir = to
165
166 if len(result) < n {
167 m.currentReadDir = 0
168 }
169 return result, nil
170}
171
172type fakeDirEntry struct {
173 name string
174 fakeFileInfo *fakeFileInfo
175}
176
177func newFakeDirEntry(name string) *fakeDirEntry {
178 return &fakeDirEntry{
179 name: name,
180 fakeFileInfo: &fakeFileInfo{name: name},
181 }
182}
183
184func (f *fakeDirEntry) Name() string {
185 return f.name
186}
187func (f *fakeDirEntry) IsDir() bool {
188 return true
189}
190func (f *fakeDirEntry) Type() fs.FileMode {
191 return fs.ModeDir
192}
193
194func (f *fakeDirEntry) Info() (fs.FileInfo, error) {
195 return f.fakeFileInfo, nil
196}
197
198func (f *fakeFileInfo) Name() string {
199 return f.name
200}
201func (f *fakeFileInfo) Size() int64 {
202 return 0
203}
204func (f *fakeFileInfo) Mode() fs.FileMode {
205 return fs.ModeDir
206}
207func (f *fakeFileInfo) ModTime() time.Time {
208 return time.Now()
209}
210func (f *fakeFileInfo) IsDir() bool {
211 return true
212}
213func (f *fakeFileInfo) Sys() any {
214 return nil
215}