mirror of https://github.com/grafana/grafana
Storage: list filtering and root folder support (#46453)
* git the things: FS api internal changes * remove filestorage/service.go * remove filestore flag * remove dummy fs * readd fileblob importpull/46469/head
parent
a29159f362
commit
ed924b3d0c
@ -1,51 +0,0 @@ |
|||||||
package filestorage |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
|
|
||||||
_ "gocloud.dev/blob/fileblob" |
|
||||||
_ "gocloud.dev/blob/memblob" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
_ FileStorage = (*dummyFileStorage)(nil) // dummyFileStorage implements FileStorage
|
|
||||||
) |
|
||||||
|
|
||||||
type dummyFileStorage struct { |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) Get(ctx context.Context, path string) (*File, error) { |
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) Delete(ctx context.Context, path string) error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) Upsert(ctx context.Context, file *UpsertFileCommand) error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) ListFiles(ctx context.Context, path string, cursor *Paging, options *ListOptions) (*ListFilesResponse, error) { |
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) ListFolders(ctx context.Context, path string, options *ListOptions) ([]FileMetadata, error) { |
|
||||||
return nil, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) CreateFolder(ctx context.Context, path string) error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) DeleteFolder(ctx context.Context, path string) error { |
|
||||||
return nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) IsFolderEmpty(ctx context.Context, path string) (bool, error) { |
|
||||||
return true, nil |
|
||||||
} |
|
||||||
|
|
||||||
func (d dummyFileStorage) close() error { |
|
||||||
return nil |
|
||||||
} |
|
@ -1,160 +0,0 @@ |
|||||||
package filestorage |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"fmt" |
|
||||||
"os" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log" |
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
|
||||||
"github.com/grafana/grafana/pkg/setting" |
|
||||||
"gocloud.dev/blob" |
|
||||||
|
|
||||||
_ "gocloud.dev/blob/fileblob" |
|
||||||
_ "gocloud.dev/blob/memblob" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
ServiceName = "FileStorage" |
|
||||||
) |
|
||||||
|
|
||||||
func ProvideService(features featuremgmt.FeatureToggles, cfg *setting.Cfg) (FileStorage, error) { |
|
||||||
grafanaDsStorageLogger := log.New("grafanaDsStorage") |
|
||||||
|
|
||||||
path := fmt.Sprintf("file://%s", cfg.StaticRootPath) |
|
||||||
grafanaDsStorageLogger.Info("Initializing grafana ds storage", "path", path) |
|
||||||
bucket, err := blob.OpenBucket(context.Background(), path) |
|
||||||
if err != nil { |
|
||||||
currentDir, _ := os.Getwd() |
|
||||||
grafanaDsStorageLogger.Error("Failed to initialize grafana ds storage", "path", path, "error", err, "cwd", currentDir) |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
prefixes := []string{ |
|
||||||
"testdata/", |
|
||||||
"img/icons/", |
|
||||||
"img/bg/", |
|
||||||
"gazetteer/", |
|
||||||
"maps/", |
|
||||||
"upload/", |
|
||||||
} |
|
||||||
|
|
||||||
var grafanaDsStorage FileStorage |
|
||||||
if features.IsEnabled(featuremgmt.FlagFileStoreApi) { |
|
||||||
grafanaDsStorage = &wrapper{ |
|
||||||
log: grafanaDsStorageLogger, |
|
||||||
wrapped: cdkBlobStorage{ |
|
||||||
log: grafanaDsStorageLogger, |
|
||||||
bucket: bucket, |
|
||||||
rootFolder: "", |
|
||||||
}, |
|
||||||
pathFilters: &PathFilters{allowedPrefixes: prefixes}, |
|
||||||
} |
|
||||||
} else { |
|
||||||
grafanaDsStorage = &dummyFileStorage{} |
|
||||||
} |
|
||||||
|
|
||||||
return &service{ |
|
||||||
grafanaDsStorage: grafanaDsStorage, |
|
||||||
log: log.New("fileStorageService"), |
|
||||||
}, nil |
|
||||||
} |
|
||||||
|
|
||||||
type service struct { |
|
||||||
log log.Logger |
|
||||||
grafanaDsStorage FileStorage |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) Get(ctx context.Context, path string) (*File, error) { |
|
||||||
var filestorage FileStorage |
|
||||||
if belongsToStorage(path, StorageNamePublic) { |
|
||||||
filestorage = b.grafanaDsStorage |
|
||||||
path = removeStoragePrefix(path) |
|
||||||
} |
|
||||||
|
|
||||||
if err := validatePath(path); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return filestorage.Get(ctx, path) |
|
||||||
} |
|
||||||
|
|
||||||
func removeStoragePrefix(path string) string { |
|
||||||
path = strings.TrimPrefix(path, Delimiter) |
|
||||||
if path == Delimiter || path == "" { |
|
||||||
return Delimiter |
|
||||||
} |
|
||||||
|
|
||||||
if !strings.Contains(path, Delimiter) { |
|
||||||
return Delimiter |
|
||||||
} |
|
||||||
|
|
||||||
split := strings.Split(path, Delimiter) |
|
||||||
|
|
||||||
// root of storage
|
|
||||||
if len(split) == 2 && split[1] == "" { |
|
||||||
return Delimiter |
|
||||||
} |
|
||||||
|
|
||||||
// replace storage
|
|
||||||
split[0] = "" |
|
||||||
return strings.Join(split, Delimiter) |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) Delete(ctx context.Context, path string) error { |
|
||||||
return errors.New("not implemented") |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) Upsert(ctx context.Context, file *UpsertFileCommand) error { |
|
||||||
return errors.New("not implemented") |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) ListFiles(ctx context.Context, path string, cursor *Paging, options *ListOptions) (*ListFilesResponse, error) { |
|
||||||
var filestorage FileStorage |
|
||||||
if belongsToStorage(path, StorageNamePublic) { |
|
||||||
filestorage = b.grafanaDsStorage |
|
||||||
path = removeStoragePrefix(path) |
|
||||||
} else { |
|
||||||
return nil, errors.New("not implemented") |
|
||||||
} |
|
||||||
|
|
||||||
if err := validatePath(path); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return filestorage.ListFiles(ctx, path, cursor, options) |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) ListFolders(ctx context.Context, path string, options *ListOptions) ([]FileMetadata, error) { |
|
||||||
var filestorage FileStorage |
|
||||||
if belongsToStorage(path, StorageNamePublic) { |
|
||||||
filestorage = b.grafanaDsStorage |
|
||||||
path = removeStoragePrefix(path) |
|
||||||
} else { |
|
||||||
return nil, errors.New("not implemented") |
|
||||||
} |
|
||||||
|
|
||||||
if err := validatePath(path); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
|
|
||||||
return filestorage.ListFolders(ctx, path, options) |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) CreateFolder(ctx context.Context, path string) error { |
|
||||||
return errors.New("not implemented") |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) DeleteFolder(ctx context.Context, path string) error { |
|
||||||
return errors.New("not implemented") |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) IsFolderEmpty(ctx context.Context, path string) (bool, error) { |
|
||||||
return true, errors.New("not implemented") |
|
||||||
} |
|
||||||
|
|
||||||
func (b service) close() error { |
|
||||||
return b.grafanaDsStorage.close() |
|
||||||
} |
|
@ -1,46 +0,0 @@ |
|||||||
package filestorage |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"testing" |
|
||||||
|
|
||||||
"github.com/stretchr/testify/require" |
|
||||||
) |
|
||||||
|
|
||||||
func TestFilestorage_removeStoragePrefix(t *testing.T) { |
|
||||||
var tests = []struct { |
|
||||||
name string |
|
||||||
path string |
|
||||||
expected string |
|
||||||
}{ |
|
||||||
{ |
|
||||||
name: "should return root if path is empty", |
|
||||||
path: "", |
|
||||||
expected: Delimiter, |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "should remove prefix folder from path with multiple parts", |
|
||||||
path: "public/abc/d", |
|
||||||
expected: "/abc/d", |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "should return root path if path is just the storage name", |
|
||||||
path: "public", |
|
||||||
expected: Delimiter, |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: "should return root path if path is the prefix of storage", |
|
||||||
path: "public/", |
|
||||||
expected: Delimiter, |
|
||||||
}, |
|
||||||
} |
|
||||||
for _, tt := range tests { |
|
||||||
t.Run(fmt.Sprintf("%s%s", "absolute: ", tt.name), func(t *testing.T) { |
|
||||||
require.Equal(t, tt.expected, removeStoragePrefix(Delimiter+tt.path)) |
|
||||||
}) |
|
||||||
|
|
||||||
t.Run(fmt.Sprintf("%s%s", "relative: ", tt.name), func(t *testing.T) { |
|
||||||
require.Equal(t, tt.expected, removeStoragePrefix(tt.path)) |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,193 @@ |
|||||||
|
package filestorage |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
) |
||||||
|
|
||||||
|
func TestWrapper_addRootFolderToFilters(t *testing.T) { |
||||||
|
t.Run("should return null if passed filters are null", func(t *testing.T) { |
||||||
|
require.Nil(t, addRootFolderToFilters(nil, "root")) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should not allocate empty arrays in place of nil arrays", func(t *testing.T) { |
||||||
|
filters := NewPathFilters(nil, nil, nil, nil) |
||||||
|
rootedFilters := addRootFolderToFilters(filters, "root") |
||||||
|
require.NotNil(t, rootedFilters) |
||||||
|
require.Nil(t, rootedFilters.disallowedPrefixes) |
||||||
|
require.Nil(t, rootedFilters.disallowedPaths) |
||||||
|
require.Nil(t, rootedFilters.allowedPrefixes) |
||||||
|
require.Nil(t, rootedFilters.allowedPaths) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should preserve empty arrays", func(t *testing.T) { |
||||||
|
filters := NewPathFilters([]string{}, []string{}, nil, nil) |
||||||
|
rootedFilters := addRootFolderToFilters(filters, "root") |
||||||
|
require.NotNil(t, rootedFilters) |
||||||
|
require.Nil(t, rootedFilters.disallowedPrefixes) |
||||||
|
require.Nil(t, rootedFilters.disallowedPaths) |
||||||
|
require.NotNil(t, rootedFilters.allowedPrefixes) |
||||||
|
require.Equal(t, []string{}, rootedFilters.allowedPrefixes) |
||||||
|
|
||||||
|
require.NotNil(t, rootedFilters.allowedPaths) |
||||||
|
require.Equal(t, []string{}, rootedFilters.allowedPaths) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should mutate arrays rather than reallocate", func(t *testing.T) { |
||||||
|
filters := NewPathFilters([]string{"/abc", "/abc2"}, nil, nil, []string{"/abc/", "/abc2/"}) |
||||||
|
originalAllowedPrefixes := filters.allowedPrefixes |
||||||
|
originalDisallowedPaths := filters.disallowedPaths |
||||||
|
|
||||||
|
rootedFilters := addRootFolderToFilters(filters, "root/") |
||||||
|
require.NotNil(t, rootedFilters) |
||||||
|
require.Nil(t, rootedFilters.allowedPaths) |
||||||
|
require.Nil(t, rootedFilters.disallowedPrefixes) |
||||||
|
|
||||||
|
expectedAllowedPrefixes := []string{"root/abc", "root/abc2"} |
||||||
|
expectedDisallowedPaths := []string{"root/abc/", "root/abc2/"} |
||||||
|
require.Equal(t, expectedAllowedPrefixes, rootedFilters.allowedPrefixes) |
||||||
|
require.Equal(t, expectedDisallowedPaths, rootedFilters.disallowedPaths) |
||||||
|
|
||||||
|
require.Equal(t, expectedAllowedPrefixes, originalAllowedPrefixes) |
||||||
|
require.Equal(t, expectedDisallowedPaths, originalDisallowedPaths) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWrapper_copyPathFilters(t *testing.T) { |
||||||
|
t.Run("should return null if passed pathFilters are null", func(t *testing.T) { |
||||||
|
require.Nil(t, copyPathFilters(nil)) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should not allocate empty arrays in place of nil arrays", func(t *testing.T) { |
||||||
|
copiedFilters := copyPathFilters(NewPathFilters(nil, nil, nil, nil)) |
||||||
|
require.NotNil(t, copiedFilters) |
||||||
|
require.Nil(t, copiedFilters.disallowedPrefixes) |
||||||
|
require.Nil(t, copiedFilters.disallowedPaths) |
||||||
|
require.Nil(t, copiedFilters.allowedPrefixes) |
||||||
|
require.Nil(t, copiedFilters.allowedPaths) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should preserve empty arrays", func(t *testing.T) { |
||||||
|
copiedFilters := copyPathFilters(NewPathFilters([]string{}, []string{}, nil, nil)) |
||||||
|
require.NotNil(t, copiedFilters) |
||||||
|
require.Nil(t, copiedFilters.disallowedPrefixes) |
||||||
|
require.Nil(t, copiedFilters.disallowedPaths) |
||||||
|
require.NotNil(t, copiedFilters.allowedPrefixes) |
||||||
|
require.Equal(t, []string{}, copiedFilters.allowedPrefixes) |
||||||
|
|
||||||
|
require.NotNil(t, copiedFilters.allowedPaths) |
||||||
|
require.Equal(t, []string{}, copiedFilters.allowedPaths) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("should new pointer with new slices", func(t *testing.T) { |
||||||
|
filters := NewPathFilters([]string{"/abc", "/abc2"}, nil, nil, []string{"/abc/", "/abc2/"}) |
||||||
|
|
||||||
|
copiedFilters := copyPathFilters(filters) |
||||||
|
|
||||||
|
require.NotSame(t, filters, copiedFilters) |
||||||
|
|
||||||
|
require.Equal(t, filters.allowedPrefixes, copiedFilters.allowedPrefixes) |
||||||
|
require.Equal(t, filters.allowedPaths, copiedFilters.allowedPaths) |
||||||
|
require.Equal(t, filters.disallowedPrefixes, copiedFilters.disallowedPrefixes) |
||||||
|
require.Equal(t, filters.disallowedPaths, copiedFilters.disallowedPaths) |
||||||
|
|
||||||
|
copiedFilters.disallowedPaths[0] = "changed" |
||||||
|
require.Equal(t, []string{"/abc/", "/abc2/"}, filters.disallowedPaths) |
||||||
|
require.Equal(t, []string{"changed", "/abc2/"}, copiedFilters.disallowedPaths) |
||||||
|
require.NotEqual(t, filters.disallowedPaths, copiedFilters.disallowedPaths) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestWrapper_addPathFilters(t *testing.T) { |
||||||
|
t.Run("should return pointer to the first argument", func(t *testing.T) { |
||||||
|
base := NewPathFilters(nil, nil, nil, nil) |
||||||
|
toAdd := NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}) |
||||||
|
require.Same(t, base, addPathFilters(base, toAdd)) |
||||||
|
}) |
||||||
|
|
||||||
|
testcases := []struct { |
||||||
|
base *PathFilters |
||||||
|
toAdd *PathFilters |
||||||
|
expected *PathFilters |
||||||
|
}{ |
||||||
|
{ |
||||||
|
base: NewPathFilters(nil, nil, nil, nil), |
||||||
|
toAdd: NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}), |
||||||
|
expected: NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}), |
||||||
|
}, |
||||||
|
{ |
||||||
|
base: NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}), |
||||||
|
toAdd: NewPathFilters(nil, nil, nil, nil), |
||||||
|
expected: NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}), |
||||||
|
}, |
||||||
|
{ |
||||||
|
base: NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}), |
||||||
|
toAdd: NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}), |
||||||
|
expected: NewPathFilters([]string{"abc", "abc"}, []string{"abc2", "abc2"}, []string{"abc3", "abc3"}, []string{"abc4", "abc4"}), |
||||||
|
}, |
||||||
|
{ |
||||||
|
base: NewPathFilters([]string{"abc"}, []string{}, nil, []string{"abc4"}), |
||||||
|
toAdd: NewPathFilters([]string{"abc"}, []string{"abc2", "abc22", "abc222"}, []string{"abc3"}, []string{"abc4"}), |
||||||
|
expected: NewPathFilters([]string{"abc", "abc"}, []string{"abc2", "abc22", "abc222"}, []string{"abc3"}, []string{"abc4", "abc4"}), |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, tt := range testcases { |
||||||
|
require.Equal(t, tt.expected, addPathFilters(tt.base, tt.toAdd)) |
||||||
|
} |
||||||
|
|
||||||
|
t.Run("should not reuse arrays allocations from the second arg", func(t *testing.T) { |
||||||
|
base := NewPathFilters(nil, []string{}, nil, nil) |
||||||
|
toAdd := NewPathFilters([]string{"abc"}, []string{"abc2"}, []string{"abc3"}, []string{"abc4"}) |
||||||
|
|
||||||
|
_ = addPathFilters(base, toAdd) |
||||||
|
|
||||||
|
require.Equal(t, toAdd.allowedPaths, base.allowedPaths) |
||||||
|
base.allowedPaths[0] = "mutated" |
||||||
|
require.Equal(t, []string{"mutated"}, base.allowedPaths) |
||||||
|
require.Equal(t, []string{"abc2"}, toAdd.allowedPaths) |
||||||
|
require.NotEqual(t, toAdd.allowedPaths, base.allowedPaths) |
||||||
|
|
||||||
|
require.Equal(t, toAdd.allowedPrefixes, base.allowedPrefixes) |
||||||
|
base.allowedPrefixes[0] = "mutated2" |
||||||
|
require.Equal(t, []string{"mutated2"}, base.allowedPrefixes) |
||||||
|
require.Equal(t, []string{"abc"}, toAdd.allowedPrefixes) |
||||||
|
require.NotEqual(t, toAdd.allowedPrefixes, base.allowedPrefixes) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestFilestorage_getParentFolderPath(t *testing.T) { |
||||||
|
var tests = []struct { |
||||||
|
name string |
||||||
|
path string |
||||||
|
expected string |
||||||
|
}{ |
||||||
|
{ |
||||||
|
name: "should return empty path if path has a single part - relative, suffix", |
||||||
|
path: "ab/", |
||||||
|
expected: "", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "should return empty path if path has a single part - relative, no suffix", |
||||||
|
path: "ab", |
||||||
|
expected: "", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "should return root if path has a single part - abs, no suffix", |
||||||
|
path: "/public", |
||||||
|
expected: Delimiter, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "should return root if path has a single part - abs, suffix", |
||||||
|
path: "/public/", |
||||||
|
expected: Delimiter, |
||||||
|
}, |
||||||
|
} |
||||||
|
for _, tt := range tests { |
||||||
|
t.Run(fmt.Sprintf(tt.name), func(t *testing.T) { |
||||||
|
require.Equal(t, tt.expected, getParentFolderPath(tt.path)) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue