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