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}