grafana/pkg/infra/filestorage/wrapper.go
Artur Wierzbicki 9f0b6a5754
Storage: refactor filtering, improve performance (#47403)
* Storage: refactor filtering, improve performance

* added a comment to `newpathfilter`

* after merge fixes
2022-04-21 23:27:43 +04:00

384 lines
8.3 KiB
Go

package filestorage
import (
"context"
"fmt"
"mime"
"path/filepath"
"regexp"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
)
var (
directoryMarker = ".___gf_dir_marker___"
pathRegex = regexp.MustCompile(`(^/$)|(^(/[A-Za-z0-9!\-_.*'() ]+)+$)`)
)
type wrapper struct {
log log.Logger
wrapped FileStorage
filter PathFilter
rootFolder string
}
func wrapPathFilter(filter PathFilter, rootFolder string) PathFilter {
return &wrappedPathFilter{filter: filter, rootFolder: rootFolder}
}
type wrappedPathFilter struct {
rootFolder string
filter PathFilter
}
func (w wrappedPathFilter) IsAllowed(path string) bool {
pathWithReplacedRoot := Delimiter + strings.TrimPrefix(path, w.rootFolder)
return w.filter.IsAllowed(pathWithReplacedRoot)
}
func (w wrappedPathFilter) ToString() string {
return w.filter.ToString()
}
func (w wrappedPathFilter) asSQLFilter() accesscontrol.SQLFilter {
sqlFilter := w.filter.asSQLFilter()
for i := range sqlFilter.Args {
if path, ok := sqlFilter.Args[i].(string); ok {
sqlFilter.Args[i] = w.rootFolder + strings.TrimPrefix(path, Delimiter)
}
}
return sqlFilter
}
func newWrapper(log log.Logger, wrapped FileStorage, pathFilter PathFilter, rootFolder string) FileStorage {
var wrappedPathFilter PathFilter
if pathFilter != nil {
wrappedPathFilter = wrapPathFilter(pathFilter, rootFolder)
} else {
wrappedPathFilter = wrapPathFilter(NewAllowAllPathFilter(), rootFolder)
}
return &wrapper{
log: log,
wrapped: wrapped,
filter: wrappedPathFilter,
rootFolder: rootFolder,
}
}
var (
_ FileStorage = (*wrapper)(nil) // wrapper implements FileStorage
)
func getParentFolderPath(path string) string {
if path == Delimiter || path == "" {
return path
}
if !strings.Contains(path, Delimiter) {
return ""
}
path = strings.TrimSuffix(path, Delimiter)
split := strings.Split(path, Delimiter)
splitWithoutLastPart := split[:len(split)-1]
if len(splitWithoutLastPart) == 1 && split[0] == "" {
return Delimiter
}
return strings.Join(splitWithoutLastPart, Delimiter)
}
func getName(path string) string {
if path == Delimiter || path == "" {
return ""
}
split := strings.Split(path, Delimiter)
return split[len(split)-1]
}
func validatePath(path string) error {
if !filepath.IsAbs(path) {
return ErrRelativePath
}
if path == Delimiter {
return nil
}
if filepath.Clean(path) != path {
return ErrNonCanonicalPath
}
if strings.HasSuffix(path, Delimiter) {
return ErrPathEndsWithDelimiter
}
if len(path) > 1000 {
return ErrPathTooLong
}
matches := pathRegex.MatchString(path)
if !matches {
return ErrPathInvalid
}
return nil
}
func (b wrapper) validatePath(path string) error {
if err := validatePath(path); err != nil {
b.log.Error("Path failed validation", "path", path, "error", err)
return err
}
return nil
}
func (b wrapper) addRoot(path string) string {
return b.rootFolder + strings.TrimPrefix(path, Delimiter)
}
func (b wrapper) removeRoot(path string) string {
return Join(Delimiter, strings.TrimPrefix(path, b.rootFolder))
}
func (b wrapper) Get(ctx context.Context, path string) (*File, error) {
if err := b.validatePath(path); err != nil {
return nil, err
}
rootedPath := b.addRoot(path)
if !b.filter.IsAllowed(rootedPath) {
return nil, nil
}
if b.rootFolder == rootedPath {
return nil, nil
}
file, err := b.wrapped.Get(ctx, rootedPath)
if file != nil {
file.FullPath = b.removeRoot(file.FullPath)
}
return file, err
}
func (b wrapper) Delete(ctx context.Context, path string) error {
if err := b.validatePath(path); err != nil {
return err
}
rootedPath := b.addRoot(path)
if !b.filter.IsAllowed(rootedPath) {
return nil
}
return b.wrapped.Delete(ctx, rootedPath)
}
func detectContentType(path string, originalGuess string) string {
if originalGuess == "application/octet-stream" || originalGuess == "" {
mimeTypeBasedOnExt := mime.TypeByExtension(filepath.Ext(path))
if mimeTypeBasedOnExt == "" {
return "application/octet-stream"
}
return mimeTypeBasedOnExt
}
return originalGuess
}
func (b wrapper) Upsert(ctx context.Context, file *UpsertFileCommand) error {
if err := b.validatePath(file.Path); err != nil {
return err
}
rootedPath := b.addRoot(file.Path)
if !b.filter.IsAllowed(rootedPath) {
return nil
}
path := getParentFolderPath(file.Path)
b.log.Info("Creating folder before upserting file", "file", file.Path, "folder", path)
if err := b.CreateFolder(ctx, path); err != nil {
return err
}
if file.Contents != nil && file.MimeType == "" {
file.MimeType = detectContentType(file.Path, "")
}
return b.wrapped.Upsert(ctx, &UpsertFileCommand{
Path: rootedPath,
MimeType: file.MimeType,
Contents: file.Contents,
Properties: file.Properties,
})
}
func (b wrapper) pagingOptionsWithDefaults(paging *Paging) *Paging {
if paging == nil {
return &Paging{
First: 100,
}
}
if paging.First <= 0 {
paging.First = 100
}
if paging.After != "" {
paging.After = b.addRoot(paging.After)
}
return paging
}
func (b wrapper) listOptionsWithDefaults(options *ListOptions) *ListOptions {
if options == nil {
return &ListOptions{
Recursive: false,
Filter: b.filter,
WithFiles: true,
WithFolders: false,
WithContents: false,
}
}
withFiles := options.WithFiles
if !options.WithFiles && !options.WithFolders {
withFiles = true
}
if b.filter == nil {
return &ListOptions{
Recursive: options.Recursive,
Filter: b.filter,
WithFiles: withFiles,
WithFolders: options.WithFolders,
WithContents: options.WithContents,
}
}
var filter PathFilter
if options.Filter != nil {
filter = newAndPathFilter(b.filter, wrapPathFilter(options.Filter, b.rootFolder))
} else {
filter = b.filter
}
return &ListOptions{
Recursive: options.Recursive,
Filter: filter,
WithFiles: withFiles,
WithFolders: options.WithFolders,
WithContents: options.WithContents,
}
}
func (b wrapper) CreateFolder(ctx context.Context, path string) error {
if err := b.validatePath(path); err != nil {
return err
}
rootedPath := b.addRoot(path)
if !b.filter.IsAllowed(rootedPath) {
return nil
}
return b.wrapped.CreateFolder(ctx, rootedPath)
}
func (b wrapper) DeleteFolder(ctx context.Context, path string) error {
if err := b.validatePath(path); err != nil {
return err
}
rootedPath := b.addRoot(path)
if !b.filter.IsAllowed(rootedPath) {
return nil
}
isEmpty, err := b.isFolderEmpty(ctx, path)
if err != nil {
return err
}
if !isEmpty {
return fmt.Errorf("folder %s is not empty - cant remove it", path)
}
return b.wrapped.DeleteFolder(ctx, rootedPath)
}
func (b wrapper) List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error) {
if err := b.validatePath(folderPath); err != nil {
return nil, err
}
options = b.listOptionsWithDefaults(options)
var fileChan = make(chan *File)
fileRetrievalCtx, cancelFileGet := context.WithCancel(ctx)
defer cancelFileGet()
go func() {
if options.WithFiles {
if f, err := b.Get(fileRetrievalCtx, folderPath); err == nil {
fileChan <- f
return
}
}
fileChan <- nil
}()
pathWithRoot := b.addRoot(folderPath)
resp, err := b.wrapped.List(ctx, pathWithRoot, b.pagingOptionsWithDefaults(paging), options)
if err != nil {
return nil, err
}
if resp != nil && resp.Files != nil && len(resp.Files) > 0 {
if resp.LastPath != "" {
resp.LastPath = b.removeRoot(resp.LastPath)
}
for i := 0; i < len(resp.Files); i++ {
resp.Files[i].FullPath = b.removeRoot(resp.Files[i].FullPath)
}
return resp, err
}
file := <-fileChan
if file != nil {
var contents []byte
if options.WithContents {
contents = file.Contents
} else {
contents = []byte{}
}
return &ListResponse{
Files: []*File{{Contents: contents, FileMetadata: file.FileMetadata}},
HasMore: false,
LastPath: file.FileMetadata.FullPath,
}, nil
}
return resp, err
}
func (b wrapper) isFolderEmpty(ctx context.Context, path string) (bool, error) {
resp, err := b.List(ctx, path, &Paging{First: 1}, &ListOptions{Recursive: true, WithFolders: true, WithFiles: true})
if err != nil {
return false, err
}
if len(resp.Files) > 0 {
return false, nil
}
return true, nil
}
func (b wrapper) close() error {
return b.wrapped.close()
}