// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package fs import ( "context" "errors" "io/fs" "os" "strings" "time" "gitroot.dev/server/logger" ) var ErrFsNotFound = errors.New("fs not found") type multipleFs struct { logger *logger.Logger subFs map[string]fs.FS } func NewMultiple(ctx context.Context, subFs map[string]fs.FS) *multipleFs { return &multipleFs{ logger: logger.NewLoggerCtx(logger.FS_PLUGIN, ctx).NewSubLogger("multiple"), subFs: subFs, } } func (m *multipleFs) UpdateSubFs(path string, subFs fs.FS) { m.subFs[path] = subFs } func (m *multipleFs) splitFsFile(name string) (fs.FS, string, error) { parts := strings.SplitN(name, string(os.PathSeparator), 2) subFs, ok := m.subFs[parts[0]] if !ok || len(parts) < 2 { return nil, "", ErrFsNotFound } return subFs, parts[1], nil } func (m *multipleFs) ReadFile(name string) ([]byte, error) { m.logger.Debug("read file", logger.NewLoggerPair("name", name)) subFs, filepath, err := m.splitFsFile(name) if err != nil { return nil, err } return fs.ReadFile(subFs, filepath) } func (m *multipleFs) ReadDir(name string) ([]fs.DirEntry, error) { m.logger.Debug("ReadDir", logger.NewLoggerPair("filepath", name)) subFs, filepath, err := m.splitFsFile(name) if err != nil { return nil, err } return fs.ReadDir(subFs, filepath) } func (m *multipleFs) Stat(name string) (fs.FileInfo, error) { m.logger.Debug("Stat") subFs, filepath, err := m.splitFsFile(name) if err != nil { return nil, err } return fs.Stat(subFs, filepath) } func (m *multipleFs) Sub(dir string) (fs.FS, error) { m.logger.Debug("Sub") subFs, filepath, err := m.splitFsFile(dir) if err != nil { return nil, err } return fs.Sub(subFs, filepath) } func (m *multipleFs) Open(name string) (fs.File, error) { m.logger.Debug("open file", logger.NewLoggerPair("name", name)) if name == "." { return &fakeFile{ name: "/", subFs: m.subFs, }, nil } subFs, filepath, err := m.splitFsFile(name) if err != nil { return nil, err } return subFs.Open(filepath) } type fakeFile struct { name string subFs map[string]fs.FS } type fakeFileInfo struct { name string } func (m *fakeFile) Close() error { return nil } func (m *fakeFile) Read(p []byte) (int, error) { return 0, errors.New("can't read a dir") } func (m *fakeFile) Stat() (fs.FileInfo, error) { return &fakeFileInfo{}, nil } func (m *fakeFile) ReadDir(n int) ([]fs.DirEntry, error) { subDirs := []fs.DirEntry{} for key := range m.subFs { subDirs = append(subDirs, newFakeDirEntry(key)) } return subDirs, nil } type fakeDirEntry struct { name string fakeFileInfo *fakeFileInfo } func newFakeDirEntry(name string) *fakeDirEntry { return &fakeDirEntry{ name: name, fakeFileInfo: &fakeFileInfo{name: name}, } } func (f *fakeDirEntry) Name() string { return f.name } func (f *fakeDirEntry) IsDir() bool { return true } func (f *fakeDirEntry) Type() fs.FileMode { return fs.ModeDir } func (f *fakeDirEntry) Info() (fs.FileInfo, error) { return f.fakeFileInfo, nil } func (f *fakeFileInfo) Name() string { return f.name } func (f *fakeFileInfo) Size() int64 { return 0 } func (f *fakeFileInfo) Mode() fs.FileMode { return fs.ModeDir } func (f *fakeFileInfo) ModTime() time.Time { return time.Now() } func (f *fakeFileInfo) IsDir() bool { return true } func (f *fakeFileInfo) Sys() any { return nil }