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/v6"
 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 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		m.orig.Close()
105	}
106	return nil
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
129	if n <= 0 {
130		m.currentReadDir = 0
131		return infos, nil
132	}
133
134	if m.currentReadDir >= len(infos) {
135		m.currentReadDir = 0
136		return nil, io.EOF
137	}
138
139	from := m.currentReadDir
140	to := from + n
141	if to > len(infos) {
142		to = len(infos)
143	}
144
145	result := infos[from:to]
146	m.currentReadDir = to
147
148	if len(result) < n {
149		m.currentReadDir = 0
150	}
151	return result, nil
152}
153
154func (m *mineFile) Seek(offset int64, whence int) (int64, error) {
155	if m.orig == nil {
156		return 0, errors.New("can't seek a dir")
157	}
158	return m.orig.Seek(offset, whence)
159}