grafana/pkg/plugins/localfiles.go
Giuseppe Guerra 1c3ad81826
Plugins: Fix module.js file not being closed when loading plugins (#66288)
* Plugins: Loader: Fix module.js file not being closed

* Plugins: LocalFS: Add comments, ensure same Read() behaviour as os.File's

* Changed comment for Close()

* Add tests for LocalFS

* "Fix" linter error

* "Fix" linter error again
2023-04-13 10:48:15 +02:00

140 lines
3.6 KiB
Go

package plugins
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/grafana/grafana/pkg/util"
)
var _ fs.FS = (*LocalFS)(nil)
// LocalFS is a plugins.FS that allows accessing files on the local file system.
type LocalFS struct {
// m is a map of relative file paths that can be accessed on the local filesystem.
// The path separator must be os-specific.
m map[string]*LocalFile
// basePath is the basePath that will be prepended to all the files (in m map) before accessing them.
basePath string
}
// NewLocalFS returns a new LocalFS that can access the specified files in the specified base path.
// Both the map keys and basePath should use the os-specific path separator for Open() to work properly.
func NewLocalFS(m map[string]struct{}, basePath string) LocalFS {
pfs := make(map[string]*LocalFile, len(m))
for k := range m {
pfs[k] = &LocalFile{
path: k,
}
}
return LocalFS{
m: pfs,
basePath: basePath,
}
}
// Open opens the specified file on the local filesystem, and returns the corresponding fs.File.
// If a nil error is returned, the caller should take care of closing the returned file.
func (f LocalFS) Open(name string) (fs.File, error) {
cleanPath, err := util.CleanRelativePath(name)
if err != nil {
return nil, err
}
if kv, exists := f.m[filepath.Join(f.basePath, cleanPath)]; exists {
if kv.f != nil {
return kv.f, nil
}
file, err := os.Open(kv.path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrFileNotExist
}
return nil, ErrPluginFileRead
}
return file, nil
}
return nil, ErrFileNotExist
}
// Base returns the base path for the LocalFS.
func (f LocalFS) Base() string {
return f.basePath
}
// Files returns a slice of all the file paths in the LocalFS relative to the base path.
// The returned strings use the same path separator as the
func (f LocalFS) Files() []string {
var files []string
for p := range f.m {
r, err := filepath.Rel(f.basePath, p)
if strings.Contains(r, "..") || err != nil {
continue
}
files = append(files, r)
}
return files
}
var _ fs.File = (*LocalFile)(nil)
// LocalFile implements a fs.File for accessing the local filesystem.
type LocalFile struct {
f *os.File
path string
}
// Stat returns a FileInfo describing the named file.
// It returns ErrFileNotExist if the file does not exist, or ErrPluginFileRead if another error occurs.
func (p *LocalFile) Stat() (fs.FileInfo, error) {
fi, err := os.Stat(p.path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrFileNotExist
}
return nil, ErrPluginFileRead
}
return fi, nil
}
// Read reads up to len(b) bytes from the File and stores them in b.
// It returns the number of bytes read and any error encountered.
// At end of file, Read returns 0, io.EOF.
// If the file is already open, it is opened again, without closing it first.
// The file is not closed at the end of the read operation. If a non-nil error is returned, it
// must be manually closed by the caller by calling Close().
func (p *LocalFile) Read(b []byte) (int, error) {
if p.f != nil {
// File is already open, Read() can be called more than once.
// io.EOF is returned if the file has been read entirely.
return p.f.Read(b)
}
var err error
p.f, err = os.Open(p.path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return 0, ErrFileNotExist
}
return 0, ErrPluginFileRead
}
return p.f.Read(b)
}
// Close closes the file.
// If the file was never open, nil is returned.
// If the file is already closed, an error is returned.
func (p *LocalFile) Close() error {
if p.f != nil {
return p.f.Close()
}
p.f = nil
return nil
}