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
14 "github.com/go-git/go-billy/v5"
15 "gitroot.dev/server/logger"
16)
17
18type StandardFs struct {
19 ctx context.Context
20 logger *logger.Logger
21 orig billy.Filesystem
22}
23
24type mineFile struct {
25 logger *logger.Logger
26 fs *StandardFs
27 name string
28 orig billy.File
29 currentReadDir int
30}
31
32func ToFs(ctx context.Context, filesystem billy.Filesystem) *StandardFs {
33 return &StandardFs{
34 ctx: ctx,
35 logger: logger.NewLoggerCtx(logger.FS_PLUGIN, ctx),
36 orig: filesystem,
37 }
38}
39
40func (m *StandardFs) ReadFile(name string) ([]byte, error) {
41 m.logger.Debug("read file", logger.NewLoggerPair("name", name))
42 file, err := m.orig.Open(name)
43 if err != nil {
44 return nil, err
45 }
46 return io.ReadAll(file)
47}
48
49func (m *StandardFs) ReadDir(name string) ([]fs.DirEntry, error) {
50 m.logger.Debug("ReadDir")
51 entries, err := m.orig.ReadDir(name)
52 if err != nil {
53 return nil, err
54 }
55 return toDirEntry(entries), nil
56}
57
58func (m *StandardFs) Stat(name string) (fs.FileInfo, error) {
59 m.logger.Debug("Stat")
60 return m.orig.Stat(name)
61}
62
63func (m *StandardFs) Sub(dir string) (fs.FS, error) {
64 m.logger.Debug("Sub")
65 f, err := m.orig.Chroot(dir)
66 return ToFs(m.ctx, f), err
67}
68
69func (m *StandardFs) Open(name string) (fs.File, error) {
70 m.logger.Debug("open file", logger.NewLoggerPair("name", name))
71 info, err := m.orig.Stat(name)
72 if err != nil {
73 if !os.IsNotExist(err) {
74 m.logger.Error("open file stat", err, logger.NewLoggerPair("name", name))
75 }
76 return nil, err
77 }
78 if info.IsDir() {
79 return &mineFile{
80 logger: m.logger.NewSubLogger("file").With("file", name),
81 fs: m,
82 name: name,
83 currentReadDir: 0,
84 }, nil
85 }
86 file, err := m.orig.Open(name)
87 if err != nil {
88 m.logger.Error("open file err", err, logger.NewLoggerPair("name", name))
89 return nil, err
90 }
91 return &mineFile{
92 logger: m.logger.NewSubLogger("file").With("file", name),
93 fs: m,
94 name: name,
95 orig: file,
96 currentReadDir: 0,
97 }, nil
98}
99
100func (m *mineFile) Close() error {
101 m.logger.Debug("close")
102 if m.orig == nil {
103 return errors.New("can't close a dir")
104 }
105 return m.orig.Close()
106}
107
108func (m *mineFile) Read(p []byte) (int, error) {
109 m.logger.Debug("Read")
110 if m.orig == nil {
111 return 0, errors.New("can't read a dir")
112 }
113 return m.orig.Read(p)
114}
115
116func (m *mineFile) Stat() (fs.FileInfo, error) {
117 m.logger.Debug("Stat")
118 return m.fs.orig.Stat(m.name)
119}
120
121func (m *mineFile) ReadDir(n int) ([]fs.DirEntry, error) {
122 m.logger.Debug("ReadDir")
123 infos, err := m.fs.orig.ReadDir(m.name)
124 if err != nil {
125 return nil, err
126 }
127 if n > 0 {
128 from := m.currentReadDir
129 to := m.currentReadDir + n
130 if to > len(infos) {
131 to = len(infos) - 1
132 }
133 if to < 0 {
134 return []fs.DirEntry{}, io.EOF
135 }
136 infos = infos[from:to]
137 m.currentReadDir = to
138 } else {
139 m.currentReadDir = 0
140 }
141 return toDirEntry(infos), nil
142}
143
144func (m *mineFile) Seek(offset int64, whence int) (int64, error) {
145 if m.orig == nil {
146 return 0, errors.New("can't seek a dir")
147 }
148 return m.orig.Seek(offset, whence)
149}
150
151func toDirEntry(infos []fs.FileInfo) []fs.DirEntry {
152 res := make([]fs.DirEntry, len(infos))
153 for i, info := range infos {
154 res[i] = fs.FileInfoToDirEntry(info)
155 }
156 return res
157}