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}