Skip to content

Commit cda36a1

Browse files
committed
[fs] Migrate filesystem abstraction to afero
1 parent 4cb71d1 commit cda36a1

File tree

10 files changed

+200
-96
lines changed

10 files changed

+200
-96
lines changed

build.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"encoding/base64"
99
"fmt"
1010
"io"
11-
"io/fs"
1211
"log/slog"
1312
"net/http"
1413
"path"
@@ -21,6 +20,7 @@ import (
2120

2221
"github.com/andybalholm/brotli"
2322
"github.com/klauspost/compress/zstd"
23+
"github.com/spf13/afero"
2424
"github.com/tdewolff/minify/v2"
2525
)
2626

@@ -171,7 +171,7 @@ func catch(description string, fn func()) (err error) {
171171
var routeMatcher *regexp.Regexp = regexp.MustCompile("^(GET|POST|PUT|PATCH|DELETE|SSE) (.*)$")
172172

173173
func (b *builder) addTemplateHandler(path_ string) error {
174-
content, err := fs.ReadFile(b.config.TemplatesFS, path_)
174+
content, err := afero.ReadFile(b.config.TemplatesFS, path_)
175175
if err != nil {
176176
return fmt.Errorf("could not read template file '%s': %v", path_, err)
177177
}

config.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66
"context"
77
"fmt"
88
"html/template"
9-
"io/fs"
109
"log/slog"
10+
11+
"github.com/spf13/afero"
1112
)
1213

1314
func New() (c *Config) {
@@ -17,11 +18,11 @@ func New() (c *Config) {
1718
}
1819

1920
type Config struct {
20-
// The path to the templates directory. Default `templates`.
21+
// The path to the templates directory within the filesystem. Default `templates`.
2122
TemplatesDir string `json:"templates_dir,omitempty" arg:"-t,--template-dir" default:"templates"`
2223

23-
// The FS to load templates from. Overrides TemplatesDir if not nil.
24-
TemplatesFS fs.FS `json:"-" arg:"-"`
24+
// The FS to load templates from. Default: a FS made from the current working directory.
25+
TemplatesFS afero.Fs `json:"-" arg:"-"`
2526

2627
// File extension to search for to find template files. Default `.html`.
2728
TemplateExtension string `json:"template_extension,omitempty" arg:"--template-ext" default:".html"`
@@ -92,7 +93,7 @@ func (c *Config) Options(options ...Option) (*Config, error) {
9293

9394
type Option func(*Config) error
9495

95-
func WithTemplateFS(fs fs.FS) Option {
96+
func WithTemplateFS(fs afero.Fs) Option {
9697
return func(c *Config) error {
9798
if fs == nil {
9899
return fmt.Errorf("nil fs")

dot_fs.go

Lines changed: 37 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,77 +6,68 @@ import (
66
"io"
77
"io/fs"
88
"log/slog"
9-
"path"
10-
)
119

12-
type dotFS struct {
13-
fs fs.FS
14-
log *slog.Logger
15-
opened map[fs.File]struct{}
16-
}
10+
"github.com/spf13/afero"
11+
)
1712

1813
// Dir
1914
type Dir struct {
20-
dot *dotFS
21-
path string
15+
fs afero.Fs
16+
log *slog.Logger
17+
opened map[afero.File]struct{}
2218
}
2319

24-
// Dir returns a
25-
func (d Dir) Dir(name string) (Dir, error) {
26-
name = path.Clean(name)
27-
if st, err := d.Stat(name); err != nil {
28-
return Dir{}, err
29-
} else if !st.IsDir() {
30-
return Dir{}, fmt.Errorf("not a directory: %s", name)
20+
// Chroot returns a copy of the filesystem with root changed to path.
21+
func (d Dir) Chroot(path string) (Dir, error) {
22+
if _, err := d.fs.Stat(path); err != nil {
23+
return Dir{}, fmt.Errorf("failed to chroot to %#v: %w", path, err)
3124
}
32-
return Dir{dot: d.dot, path: path.Join(d.path, name)}, nil
25+
fs := afero.NewBasePathFs(d.fs, path)
26+
return Dir{
27+
fs: fs,
28+
log: d.log,
29+
opened: d.opened,
30+
}, nil
3331
}
3432

35-
// List reads and returns a slice of names from the given directory relative to
36-
// the FS root.
37-
func (d Dir) List(name string) ([]fs.DirEntry, error) {
38-
return fs.ReadDir(d.dot.fs, path.Join(d.path, path.Clean(name)))
33+
// ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename.
34+
func (d Dir) ReadDir(path string) ([]fs.FileInfo, error) {
35+
return afero.ReadDir(d.fs, path)
3936
}
4037

4138
// Exists returns true if filename can be opened successfully.
42-
func (d Dir) Exists(name string) bool {
43-
name = path.Join(d.path, path.Clean(name))
44-
file, err := d.Open(name)
45-
if err == nil {
46-
file.Close()
47-
return true
48-
}
49-
return false
39+
func (d Dir) Exists(filename string) bool {
40+
_, err := d.fs.Stat(filename)
41+
return err == nil
5042
}
5143

5244
// Stat returns Stat of a filename.
5345
//
5446
// Note: if you intend to read the file, afterwards, calling .Open instead may
5547
// be more efficient.
56-
func (d Dir) Stat(name string) (fs.FileInfo, error) {
57-
name = path.Join(d.path, path.Clean(name))
58-
file, err := d.dot.fs.Open(name)
59-
if err != nil {
60-
return nil, err
61-
}
62-
defer file.Close()
63-
return file.Stat()
48+
func (d Dir) Stat(filename string) (fs.FileInfo, error) {
49+
return d.fs.Stat(filename)
6450
}
6551

6652
// Read returns the contents of a filename relative to the FS root as a string.
6753
func (d Dir) Read(name string) (string, error) {
68-
name = path.Join(d.path, path.Clean(name))
69-
7054
buf := bufPool.Get().(*bytes.Buffer)
7155
buf.Reset()
7256
defer bufPool.Put(buf)
57+
defer buf.Reset()
7358

74-
file, err := d.dot.fs.Open(name)
59+
file, err := d.fs.Open(name)
7560
if err != nil {
7661
return "", err
7762
}
7863
defer file.Close()
7964

65+
stat, err := file.Stat()
66+
if err != nil {
67+
return "", err
68+
}
69+
buf.Grow(int(stat.Size()))
70+
8071
_, err = io.Copy(buf, file)
8172
if err != nil {
8273
return "", err
@@ -86,16 +77,14 @@ func (d Dir) Read(name string) (string, error) {
8677
}
8778

8879
// Open opens the file
89-
func (d Dir) Open(name string) (fs.File, error) {
90-
name = path.Join(d.path, path.Clean(name))
91-
92-
file, err := d.dot.fs.Open(name)
80+
func (d Dir) Open(filename string) (afero.File, error) {
81+
file, err := d.fs.Open(filename)
9382
if err != nil {
94-
return nil, fmt.Errorf("failed to open file at path '%s': %w", name, err)
83+
return nil, fmt.Errorf("failed to open file at path %#v: %w", filename, err)
9584
}
9685

97-
d.dot.log.Debug("opened file", slog.String("path", name))
98-
d.dot.opened[file] = struct{}{}
86+
d.log.Debug("opened file", slog.String("filename", filename))
87+
d.opened[file] = struct{}{}
9988

100-
return d.dot.fs.Open(name)
89+
return file, nil
10190
}

dot_fs_config.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import (
66
"fmt"
77
"io/fs"
88
"log/slog"
9-
"os"
9+
10+
"github.com/spf13/afero"
1011
)
1112

1213
// WithDir creates an [xtemplate.Option] that can be used with
1314
// [xtemplate.Config.Server], [xtemplate.Config.Instance], or [xtemplate.Main]
1415
// to add an fs dot provider to the config.
15-
func WithDir(name string, fs fs.FS) Option {
16+
func WithDir(name string, fs afero.Fs) Option {
1617
return func(c *Config) error {
1718
if fs == nil {
1819
return fmt.Errorf("cannot create DotFSProvider with null FS with name %s", name)
@@ -27,9 +28,10 @@ func WithDir(name string, fs fs.FS) Option {
2728
//
2829
// By setting a cli flag: “
2930
type DotDirConfig struct {
30-
Name string `json:"name"`
31-
fs.FS `json:"-"`
32-
Path string `json:"path"`
31+
Name string `json:"name"`
32+
Path string `json:"path"`
33+
Type string `json:"type"`
34+
FS afero.Fs `json:"-"`
3335
}
3436

3537
var _ CleanupDotProvider = &DotDirConfig{}
@@ -39,7 +41,7 @@ func (p *DotDirConfig) Init(ctx context.Context) error {
3941
if p.FS != nil {
4042
return nil
4143
}
42-
newfs := os.DirFS(p.Path)
44+
newfs := afero.NewBasePathFs(afero.NewOsFs(), p.Path)
4345
if _, err := newfs.(interface {
4446
Stat(string) (fs.FileInfo, error)
4547
}).Stat("."); err != nil {
@@ -49,10 +51,10 @@ func (p *DotDirConfig) Init(ctx context.Context) error {
4951
return nil
5052
}
5153
func (p *DotDirConfig) Value(r Request) (any, error) {
52-
return Dir{dot: &dotFS{p.FS, GetLogger(r.R.Context()), make(map[fs.File]struct{})}, path: "."}, nil
54+
return Dir{p.FS, GetLogger(r.R.Context()), make(map[afero.File]struct{})}, nil
5355
}
5456
func (p *DotDirConfig) Cleanup(a any, err error) error {
55-
v := a.(Dir).dot
57+
v := a.(Dir)
5658
errs := []error{}
5759
for file := range v.opened {
5860
if err := file.Close(); err != nil {

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/microcosm-cc/bluemonday v1.0.27
2121
github.com/nats-io/nats-server/v2 v2.10.24
2222
github.com/nats-io/nats.go v1.38.0
23+
github.com/spf13/afero v1.14.0
2324
github.com/tdewolff/minify/v2 v2.21.2
2425
github.com/yuin/goldmark v1.7.8
2526
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
@@ -115,7 +116,6 @@ require (
115116
github.com/spf13/cobra v1.9.1 // indirect
116117
github.com/spf13/pflag v1.0.6 // indirect
117118
github.com/stoewer/go-strcase v1.2.0 // indirect
118-
github.com/stretchr/testify v1.10.0 // indirect
119119
github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
120120
github.com/tdewolff/parse/v2 v2.7.19 // indirect
121121
github.com/urfave/cli v1.22.14 // indirect
@@ -134,7 +134,7 @@ require (
134134
golang.org/x/mod v0.24.0 // indirect
135135
golang.org/x/net v0.38.0 // indirect
136136
golang.org/x/sync v0.12.0 // indirect
137-
golang.org/x/sys v0.31.0 // indirect
137+
golang.org/x/sys v0.34.0 // indirect
138138
golang.org/x/term v0.30.0 // indirect
139139
golang.org/x/text v0.23.0 // indirect
140140
golang.org/x/time v0.11.0 // indirect
@@ -143,6 +143,5 @@ require (
143143
google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
144144
google.golang.org/grpc v1.67.1 // indirect
145145
google.golang.org/protobuf v1.35.1 // indirect
146-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
147146
howett.net/plist v1.0.0 // indirect
148147
)

0 commit comments

Comments
 (0)