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}