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).NewSubLogger("standard"),
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 defer file.Close()
47 return io.ReadAll(file)
48}
49
50func (m *StandardFs) ReadDir(name string) ([]fs.DirEntry, error) {
51 m.logger.Debug("ReadDir", logger.NewLoggerPair("filepath", name))
52 entries, err := m.orig.ReadDir(name)
53 if err != nil {
54 return nil, err
55 }
56 return toDirEntry(entries), nil
57}
58
59func (m *StandardFs) Stat(name string) (fs.FileInfo, error) {
60 m.logger.Debug("Stat")
61 return m.orig.Stat(name)
62}
63
64func (m *StandardFs) Sub(dir string) (fs.FS, error) {
65 m.logger.Debug("Sub")
66 f, err := m.orig.Chroot(dir)
67 return ToFs(m.ctx, f), err
68}
69
70func (m *StandardFs) Open(name string) (fs.File, error) {
71 m.logger.Debug("open file", logger.NewLoggerPair("name", name))
72 info, err := m.orig.Stat(name)
73 if err != nil {
74 if !os.IsNotExist(err) {
75 m.logger.Error("open file stat", err, logger.NewLoggerPair("name", name))
76 }
77 return nil, err
78 }
79 if info.IsDir() {
80 return &mineFile{
81 logger: m.logger.NewSubLogger("file").With("file", name),
82 fs: m,
83 name: name,
84 currentReadDir: 0,
85 }, nil
86 }
87 file, err := m.orig.Open(name)
88 if err != nil {
89 m.logger.Error("open file err", err, logger.NewLoggerPair("name", name))
90 return nil, err
91 }
92 return &mineFile{
93 logger: m.logger.NewSubLogger("file").With("file", name),
94 fs: m,
95 name: name,
96 orig: file,
97 currentReadDir: 0,
98 }, nil
99}
100
101func (m *mineFile) Close() error {
102 m.logger.Debug("close")
103 if m.orig == nil {
104 return errors.New("can't close a dir")
105 }
106 return m.orig.Close()
107}
108
109func (m *mineFile) Read(p []byte) (int, error) {
110 m.logger.Debug("Read")
111 if m.orig == nil {
112 return 0, errors.New("can't read a dir")
113 }
114 return m.orig.Read(p)
115}
116
117func (m *mineFile) Stat() (fs.FileInfo, error) {
118 m.logger.Debug("Stat")
119 return m.fs.orig.Stat(m.name)
120}
121
122func (m *mineFile) ReadDir(n int) ([]fs.DirEntry, error) {
123 m.logger.Debug("ReadDir")
124 infos, err := m.fs.orig.ReadDir(m.name)
125 if err != nil {
126 return nil, err
127 }
128 if n > 0 {
129 from := m.currentReadDir
130 to := m.currentReadDir + n
131 if to > len(infos) {
132 to = len(infos) - 1
133 }
134 if to < 0 {
135 return []fs.DirEntry{}, io.EOF
136 }
137 infos = infos[from:to]
138 m.currentReadDir = to
139 } else {
140 m.currentReadDir = 0
141 }
142 return toDirEntry(infos), nil
143}
144
145func (m *mineFile) Seek(offset int64, whence int) (int64, error) {
146 if m.orig == nil {
147 return 0, errors.New("can't seek a dir")
148 }
149 return m.orig.Seek(offset, whence)
150}
151
152func toDirEntry(infos []fs.FileInfo) []fs.DirEntry {
153 res := make([]fs.DirEntry, len(infos))
154 for i, info := range infos {
155 res[i] = fs.FileInfoToDirEntry(info)
156 }
157 return res
158}