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}