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/fs"
11 "os"
12 "strings"
13 "time"
14
15 "gitroot.dev/server/logger"
16)
17
18var ErrFsNotFound = errors.New("fs not found")
19
20type multipleFs struct {
21 logger *logger.Logger
22 subFs map[string]fs.FS
23}
24
25func NewMultiple(ctx context.Context, subFs map[string]fs.FS) *multipleFs {
26 return &multipleFs{
27 logger: logger.NewLoggerCtx(logger.FS_PLUGIN, ctx).NewSubLogger("multiple"),
28 subFs: subFs,
29 }
30}
31
32func (m *multipleFs) UpdateSubFs(path string, subFs fs.FS) {
33 m.subFs[path] = subFs
34}
35
36func (m *multipleFs) splitFsFile(name string) (fs.FS, string, error) {
37 parts := strings.SplitN(name, string(os.PathSeparator), 2)
38 subFs, ok := m.subFs[parts[0]]
39 if !ok || len(parts) < 2 {
40 return nil, "", ErrFsNotFound
41 }
42 return subFs, parts[1], nil
43}
44
45func (m *multipleFs) ReadFile(name string) ([]byte, error) {
46 m.logger.Debug("read file", logger.NewLoggerPair("name", name))
47 subFs, filepath, err := m.splitFsFile(name)
48 if err != nil {
49 return nil, err
50 }
51 return fs.ReadFile(subFs, filepath)
52}
53
54func (m *multipleFs) ReadDir(name string) ([]fs.DirEntry, error) {
55 m.logger.Debug("ReadDir", logger.NewLoggerPair("filepath", name))
56 subFs, filepath, err := m.splitFsFile(name)
57 if err != nil {
58 return nil, err
59 }
60 return fs.ReadDir(subFs, filepath)
61}
62
63func (m *multipleFs) Stat(name string) (fs.FileInfo, error) {
64 m.logger.Debug("Stat")
65 subFs, filepath, err := m.splitFsFile(name)
66 if err != nil {
67 return nil, err
68 }
69 return fs.Stat(subFs, filepath)
70}
71
72func (m *multipleFs) Sub(dir string) (fs.FS, error) {
73 m.logger.Debug("Sub")
74 subFs, filepath, err := m.splitFsFile(dir)
75 if err != nil {
76 return nil, err
77 }
78 return fs.Sub(subFs, filepath)
79}
80
81func (m *multipleFs) Open(name string) (fs.File, error) {
82 m.logger.Debug("open file", logger.NewLoggerPair("name", name))
83 if name == "." {
84 return &fakeFile{
85 name: "/",
86 subFs: m.subFs,
87 }, nil
88 }
89 subFs, filepath, err := m.splitFsFile(name)
90 if err != nil {
91 return nil, err
92 }
93 return subFs.Open(filepath)
94}
95
96type fakeFile struct {
97 name string
98 subFs map[string]fs.FS
99}
100type fakeFileInfo struct {
101 name string
102}
103
104func (m *fakeFile) Close() error {
105 return nil
106}
107
108func (m *fakeFile) Read(p []byte) (int, error) {
109 return 0, errors.New("can't read a dir")
110}
111
112func (m *fakeFile) Stat() (fs.FileInfo, error) {
113 return &fakeFileInfo{}, nil
114}
115
116func (m *fakeFile) ReadDir(n int) ([]fs.DirEntry, error) {
117 subDirs := []fs.FileInfo{}
118 for key := range m.subFs {
119 subDirs = append(subDirs, &fakeFileInfo{name: key})
120 }
121 return toDirEntry(subDirs), nil
122}
123
124func (f *fakeFileInfo) Name() string {
125 return f.name
126}
127func (f *fakeFileInfo) Size() int64 {
128 return 0
129}
130func (f *fakeFileInfo) Mode() fs.FileMode {
131 return fs.ModeDir
132}
133func (f *fakeFileInfo) ModTime() time.Time {
134 return time.Now()
135}
136func (f *fakeFileInfo) IsDir() bool {
137 return true
138}
139func (f *fakeFileInfo) Sys() any {
140 return nil
141}