Skip to content

Commit 0227d87

Browse files
authored
Make all file operations through the filesystem interface (#283)
1 parent d2e3d77 commit 0227d87

18 files changed

+210
-129
lines changed

code/go/internal/fspath/fspath.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package fspath
6+
7+
import (
8+
"io/fs"
9+
"os"
10+
"path/filepath"
11+
)
12+
13+
// FS implements the fs interface and can also show a path where the fs is located.
14+
// This is useful to report error messages relative to the location of the file system.
15+
type FS interface {
16+
fs.FS
17+
18+
Path(name ...string) string
19+
}
20+
21+
type fsDir struct {
22+
fs.FS
23+
24+
path string
25+
}
26+
27+
// Path returns a path for the given names, based on the location of the file system.
28+
func (fs *fsDir) Path(names ...string) string {
29+
return filepath.Join(append([]string{fs.path}, names...)...)
30+
}
31+
32+
// DirFS returns a file system for a directory, it keeps the path to implement the FS interface.
33+
func DirFS(path string) FS {
34+
return &fsDir{
35+
FS: os.DirFS(path),
36+
path: path,
37+
}
38+
}

code/go/internal/pkgpath/files.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ package pkgpath
77
import (
88
"encoding/json"
99
"fmt"
10-
"io/ioutil"
10+
"io/fs"
1111
"os"
1212
"path/filepath"
1313
"strings"
@@ -16,31 +16,34 @@ import (
1616
"github.com/joeshaw/multierror"
1717
"github.com/pkg/errors"
1818
"gopkg.in/yaml.v3"
19+
20+
"github.com/elastic/package-spec/code/go/internal/fspath"
1921
)
2022

2123
// File represents a file in the package.
2224
type File struct {
25+
fsys fspath.FS
2326
path string
2427
os.FileInfo
2528
}
2629

2730
// Files finds files for the given glob
28-
func Files(glob string) ([]File, error) {
29-
paths, err := filepath.Glob(glob)
31+
func Files(fsys fspath.FS, glob string) ([]File, error) {
32+
paths, err := fs.Glob(fsys, glob)
3033
if err != nil {
3134
return nil, err
3235
}
3336

3437
var errs multierror.Errors
3538
var files = make([]File, 0)
3639
for _, path := range paths {
37-
info, err := os.Stat(path)
40+
info, err := fs.Stat(fsys, path)
3841
if err != nil {
3942
errs = append(errs, err)
4043
continue
4144
}
4245

43-
file := File{path, info}
46+
file := File{fsys, path, info}
4447
files = append(files, file)
4548
}
4649

@@ -58,19 +61,19 @@ func (f File) Values(path string) (interface{}, error) {
5861
return nil, fmt.Errorf("cannot extract values from file type = %s", fileExt)
5962
}
6063

61-
contents, err := ioutil.ReadFile(f.path)
64+
contents, err := fs.ReadFile(f.fsys, f.path)
6265
if err != nil {
6366
return nil, errors.Wrap(err, "reading file content failed")
6467
}
6568

6669
var v interface{}
6770
if fileExt == "yaml" || fileExt == "yml" {
6871
if err := yaml.Unmarshal(contents, &v); err != nil {
69-
return nil, errors.Wrapf(err, "unmarshalling YAML file failed (path: %s)", fileName)
72+
return nil, errors.Wrapf(err, "unmarshalling YAML file failed (path: %s)", f.fsys.Path(fileName))
7073
}
7174
} else if fileExt == "json" {
7275
if err := json.Unmarshal(contents, &v); err != nil {
73-
return nil, errors.Wrapf(err, "unmarshalling JSON file failed (path: %s)", fileName)
76+
return nil, errors.Wrapf(err, "unmarshalling JSON file failed (path: %s)", f.fsys.Path(fileName))
7477
}
7578
}
7679

code/go/internal/validator/folder_item_content.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import (
88
"bytes"
99
"encoding/json"
1010
"fmt"
11-
"io/ioutil"
11+
"io/fs"
1212
"mime"
1313

1414
"github.com/pkg/errors"
1515
"gopkg.in/yaml.v3"
1616
)
1717

18-
func loadItemContent(itemPath, mediaType string) ([]byte, error) {
19-
itemData, err := ioutil.ReadFile(itemPath)
18+
func loadItemContent(fsys fs.FS, itemPath, mediaType string) ([]byte, error) {
19+
itemData, err := fs.ReadFile(fsys, itemPath)
2020
if err != nil {
2121
return nil, errors.Wrap(err, "reading item file failed")
2222
}

code/go/internal/validator/folder_item_spec.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package validator
77
import (
88
"fmt"
99
"io/fs"
10-
"os"
1110
"path/filepath"
1211
"regexp"
1312
"sync"
@@ -35,7 +34,7 @@ type folderItemSpec struct {
3534

3635
var formatCheckersMutex sync.Mutex
3736

38-
func (s *folderItemSpec) matchingFileExists(files []os.FileInfo) (bool, error) {
37+
func (s *folderItemSpec) matchingFileExists(files []fs.DirEntry) (bool, error) {
3938
if s.Name != "" {
4039
for _, file := range files {
4140
if file.Name() == s.Name {
@@ -57,7 +56,13 @@ func (s *folderItemSpec) matchingFileExists(files []os.FileInfo) (bool, error) {
5756
return false, nil
5857
}
5958

60-
func (s *folderItemSpec) isSameType(file os.FileInfo) bool {
59+
// sameFileChecker is the interface that parameters of isSameType should implement,
60+
// this is intended to accept both fs.DirEntry and fs.FileInfo.
61+
type sameFileChecker interface {
62+
IsDir() bool
63+
}
64+
65+
func (s *folderItemSpec) isSameType(file sameFileChecker) bool {
6166
switch s.ItemType {
6267
case itemTypeFile:
6368
return !file.IsDir()
@@ -68,17 +73,17 @@ func (s *folderItemSpec) isSameType(file os.FileInfo) bool {
6873
return false
6974
}
7075

71-
func (s *folderItemSpec) validate(fs fs.FS, folderSpecPath string, itemPath string) ve.ValidationErrors {
76+
func (s *folderItemSpec) validate(schemaFS fs.FS, fsys fs.FS, folderSpecPath string, itemPath string) ve.ValidationErrors {
7277
// loading item content
73-
itemData, err := loadItemContent(itemPath, s.ContentMediaType)
78+
itemData, err := loadItemContent(fsys, itemPath, s.ContentMediaType)
7479
if err != nil {
7580
return ve.ValidationErrors{err}
7681
}
7782

7883
var schemaLoader gojsonschema.JSONLoader
7984
if s.Ref != "" {
8085
schemaPath := filepath.Join(filepath.Dir(folderSpecPath), s.Ref)
81-
schemaLoader = yamlschema.NewReferenceLoaderFileSystem("file:///"+schemaPath, fs)
86+
schemaLoader = yamlschema.NewReferenceLoaderFileSystem("file:///"+schemaPath, schemaFS)
8287
} else {
8388
return nil // item's schema is not defined
8489
}
@@ -93,8 +98,8 @@ func (s *folderItemSpec) validate(fs fs.FS, folderSpecPath string, itemPath stri
9398
formatCheckersMutex.Unlock()
9499
}()
95100

96-
semantic.LoadRelativePathFormatChecker(filepath.Dir(itemPath))
97-
semantic.LoadDataStreamNameFormatChecker(filepath.Dir(itemPath))
101+
semantic.LoadRelativePathFormatChecker(fsys, filepath.Dir(itemPath))
102+
semantic.LoadDataStreamNameFormatChecker(fsys, filepath.Dir(itemPath))
98103
result, err := gojsonschema.Validate(schemaLoader, documentLoader)
99104
if err != nil {
100105
return ve.ValidationErrors{err}

code/go/internal/validator/folder_item_spec_format.go

Lines changed: 0 additions & 30 deletions
This file was deleted.

code/go/internal/validator/folder_spec.go

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ import (
88
"fmt"
99
"io/fs"
1010
"io/ioutil"
11-
"path"
1211
"path/filepath"
1312
"regexp"
1413
"strings"
1514

16-
ve "github.com/elastic/package-spec/code/go/internal/errors"
17-
1815
"github.com/pkg/errors"
1916
"gopkg.in/yaml.v3"
17+
18+
ve "github.com/elastic/package-spec/code/go/internal/errors"
19+
"github.com/elastic/package-spec/code/go/internal/fspath"
2020
)
2121

2222
const (
@@ -66,11 +66,11 @@ func newFolderSpec(fs fs.FS, specPath string) (*folderSpec, error) {
6666
return &spec, nil
6767
}
6868

69-
func (s *folderSpec) validate(packageName string, folderPath string) ve.ValidationErrors {
69+
func (s *folderSpec) validate(packageName string, fsys fspath.FS, path string) ve.ValidationErrors {
7070
var errs ve.ValidationErrors
71-
files, err := ioutil.ReadDir(folderPath)
71+
files, err := fs.ReadDir(fsys, path)
7272
if err != nil {
73-
errs = append(errs, errors.Wrapf(err, "could not read folder [%s]", folderPath))
73+
errs = append(errs, errors.Wrapf(err, "could not read folder [%s]", fsys.Path(path)))
7474
return errs
7575
}
7676

@@ -87,16 +87,16 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati
8787
if file.IsDir() {
8888
if !s.DevelopmentFolder && strings.Contains(fileName, "-") {
8989
errs = append(errs,
90-
fmt.Errorf(`file "%s/%s" is invalid: directory name inside package %s contains -: %s`,
91-
folderPath, fileName, packageName, fileName))
90+
fmt.Errorf(`file "%s" is invalid: directory name inside package %s contains -: %s`,
91+
fsys.Path(path, fileName), packageName, fileName))
9292
}
9393
}
9494
continue
9595
}
9696

9797
if itemSpec == nil && !s.AdditionalContents {
9898
// No spec found for current folder item and we do not allow additional contents in folder.
99-
errs = append(errs, fmt.Errorf("item [%s] is not allowed in folder [%s]", fileName, folderPath))
99+
errs = append(errs, fmt.Errorf("item [%s] is not allowed in folder [%s]", fileName, fsys.Path(path)))
100100
continue
101101
}
102102

@@ -118,7 +118,7 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati
118118

119119
var subFolderSpec *folderSpec
120120
if itemSpec.Ref != "" {
121-
subFolderSpecPath := path.Join(filepath.Dir(s.specPath), itemSpec.Ref)
121+
subFolderSpecPath := filepath.Join(filepath.Dir(s.specPath), itemSpec.Ref)
122122
subFolderSpec, err = newFolderSpec(s.fs, subFolderSpecPath)
123123
if err != nil {
124124
errs = append(errs, err)
@@ -140,23 +140,23 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati
140140
subFolderSpec.DevelopmentFolder = true
141141
}
142142

143-
subFolderPath := path.Join(folderPath, fileName)
144-
subErrs := subFolderSpec.validate(packageName, subFolderPath)
143+
subFolderPath := filepath.Join(path, fileName)
144+
subErrs := subFolderSpec.validate(packageName, fsys, subFolderPath)
145145
if len(subErrs) > 0 {
146146
errs = append(errs, subErrs...)
147147
}
148148

149149
} else {
150150
if !itemSpec.isSameType(file) {
151-
errs = append(errs, fmt.Errorf("[%s] is a file but is expected to be a folder", fileName))
151+
errs = append(errs, fmt.Errorf("[%s] is a file but is expected to be a folder", fsys.Path(fileName)))
152152
continue
153153
}
154154

155-
itemPath := filepath.Join(folderPath, file.Name())
156-
itemValidationErrs := itemSpec.validate(s.fs, s.specPath, itemPath)
155+
itemPath := filepath.Join(path, file.Name())
156+
itemValidationErrs := itemSpec.validate(s.fs, fsys, s.specPath, itemPath)
157157
if itemValidationErrs != nil {
158158
for _, ive := range itemValidationErrs {
159-
errs = append(errs, errors.Wrapf(ive, "file \"%s\" is invalid", itemPath))
159+
errs = append(errs, errors.Wrapf(ive, "file \"%s\" is invalid", fsys.Path(itemPath)))
160160
}
161161
}
162162
}
@@ -177,9 +177,9 @@ func (s *folderSpec) validate(packageName string, folderPath string) ve.Validati
177177
if !fileFound {
178178
var err error
179179
if itemSpec.Name != "" {
180-
err = fmt.Errorf("expecting to find [%s] %s in folder [%s]", itemSpec.Name, itemSpec.ItemType, folderPath)
180+
err = fmt.Errorf("expecting to find [%s] %s in folder [%s]", itemSpec.Name, itemSpec.ItemType, fsys.Path(path))
181181
} else if itemSpec.Pattern != "" {
182-
err = fmt.Errorf("expecting to find %s matching pattern [%s] in folder [%s]", itemSpec.ItemType, itemSpec.Pattern, folderPath)
182+
err = fmt.Errorf("expecting to find %s matching pattern [%s] in folder [%s]", itemSpec.ItemType, itemSpec.Pattern, fsys.Path(path))
183183
}
184184
errs = append(errs, err)
185185
}

code/go/internal/validator/package.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ package validator
66

77
import (
88
"fmt"
9-
"io/ioutil"
9+
"io/fs"
1010
"os"
11-
"path"
11+
"path/filepath"
1212

1313
"github.com/Masterminds/semver/v3"
1414
"github.com/pkg/errors"
@@ -19,7 +19,19 @@ import (
1919
type Package struct {
2020
Name string
2121
SpecVersion *semver.Version
22-
RootPath string
22+
23+
fs fs.FS
24+
location string
25+
}
26+
27+
// Open opens a file in the package filesystem.
28+
func (p *Package) Open(name string) (fs.File, error) {
29+
return p.fs.Open(name)
30+
}
31+
32+
// Path returns a path meaningful for the user.
33+
func (p *Package) Path(names ...string) string {
34+
return filepath.Join(append([]string{p.location}, names...)...)
2335
}
2436

2537
// NewPackage creates a new Package from a path to the package's root folder
@@ -33,13 +45,19 @@ func NewPackage(pkgRootPath string) (*Package, error) {
3345
return nil, fmt.Errorf("no package folder found at path [%v]", pkgRootPath)
3446
}
3547

36-
pkgManifestPath := path.Join(pkgRootPath, "manifest.yml")
37-
info, err = os.Stat(pkgManifestPath)
48+
return NewPackageFromFS(pkgRootPath, os.DirFS(pkgRootPath))
49+
}
50+
51+
// NewPackageFromFS creates a new package from a given filesystem. A root path can be indicated
52+
// to help building paths meaningful for the users.
53+
func NewPackageFromFS(location string, fsys fs.FS) (*Package, error) {
54+
pkgManifestPath := "manifest.yml"
55+
_, err := fs.Stat(fsys, pkgManifestPath)
3856
if os.IsNotExist(err) {
3957
return nil, errors.Wrapf(err, "no package manifest file found at path [%v]", pkgManifestPath)
4058
}
4159

42-
data, err := ioutil.ReadFile(pkgManifestPath)
60+
data, err := fs.ReadFile(fsys, pkgManifestPath)
4361
if err != nil {
4462
return nil, fmt.Errorf("could not read package manifest file [%v]", pkgManifestPath)
4563
}
@@ -60,8 +78,10 @@ func NewPackage(pkgRootPath string) (*Package, error) {
6078
// Instantiate Package object and return it
6179
p := Package{
6280
Name: manifest.Name,
63-
RootPath: pkgRootPath,
6481
SpecVersion: specVersion,
82+
fs: fsys,
83+
84+
location: location,
6585
}
6686

6787
return &p, nil

0 commit comments

Comments
 (0)