The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/infra/filestorage/api.go

189 lines
4.7 KiB

package filestorage
import (
"context"
"errors"
"fmt"
"path/filepath"
"regexp"
"strings"
"time"
)
var (
ErrRelativePath = errors.New("path cant be relative")
ErrNonCanonicalPath = errors.New("path must be canonical")
ErrPathTooLong = errors.New("path is too long")
ErrInvalidCharacters = errors.New("path contains unsupported characters")
ErrPathEndsWithDelimiter = errors.New("path can not end with delimiter")
ErrPathPartTooLong = errors.New("path part is too long")
ErrEmptyPathPart = errors.New("path can not have empty parts")
Delimiter = "/"
DirectoryMimeType = "directory"
multipleDelimiters = regexp.MustCompile(`/+`)
pathRegex = regexp.MustCompile(`(^/$)|(^(/[A-Za-z\d!\-_.*'() ]+)+$)`)
maxPathLength = 1024
maxPathPartLength = 256
)
func ValidatePath(path string) error {
if !strings.HasPrefix(path, Delimiter) {
return ErrRelativePath
}
if path == Delimiter {
return nil
}
if strings.HasSuffix(path, Delimiter) {
return ErrPathEndsWithDelimiter
}
// apply `ToSlash` to replace OS-specific separators introduced by the Clean() function
if filepath.ToSlash(filepath.Clean(path)) != path {
return ErrNonCanonicalPath
}
if len(path) > maxPathLength {
return ErrPathTooLong
}
for _, part := range strings.Split(strings.TrimPrefix(path, Delimiter), Delimiter) {
if strings.TrimSpace(part) == "" {
return ErrEmptyPathPart
}
if len(part) > maxPathPartLength {
return ErrPathPartTooLong
}
}
matches := pathRegex.MatchString(path)
if !matches {
return ErrInvalidCharacters
}
return nil
}
func Join(parts ...string) string {
joinedPath := Delimiter + strings.Join(parts, Delimiter)
// makes the API more forgiving for clients without compromising safety
return multipleDelimiters.ReplaceAllString(joinedPath, Delimiter)
}
type File struct {
Contents []byte
FileMetadata
}
func (f *File) IsFolder() bool {
return f.MimeType == DirectoryMimeType
}
type FileMetadata struct {
Name string
FullPath string
MimeType string
Modified time.Time
Created time.Time
Size int64
Properties map[string]string
}
type Paging struct {
// The number of items to return
Limit int
// Starting after the key
After string
}
type UpsertFileCommand struct {
Path string
MimeType string
CacheControl string
ContentDisposition string
// Contents of an existing file won't be modified if cmd.Contents is nil
Contents []byte
// Properties of an existing file won't be modified if cmd.Properties is nil
Properties map[string]string
}
func toLower(list []string) []string {
if list == nil {
return nil
}
lower := make([]string, 0)
for _, el := range list {
lower = append(lower, strings.ToLower(el))
}
return lower
}
type ListResponse struct {
Files []*File
HasMore bool
LastPath string
}
func (r *ListResponse) String() string {
if r == nil {
return "Nil ListResponse"
}
if r.Files == nil {
return "ListResponse with Nil files slice"
}
if len(r.Files) == 0 {
return "Empty ListResponse"
}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("ListResponse with %d files\n", len(r.Files)))
for i := range r.Files {
sb.WriteString(fmt.Sprintf(" - %s, contentsLength: %d\n", r.Files[i].FullPath, len(r.Files[i].Contents)))
}
sb.WriteString(fmt.Sprintf("Last path: %s, has more: %t\n", r.LastPath, r.HasMore))
return sb.String()
}
type ListOptions struct {
Recursive bool
WithFiles bool
WithFolders bool
WithContents bool
Filter PathFilter
}
type DeleteFolderOptions struct {
// Force if set to true, the `deleteFolder` operation will delete the selected folder together with all the nested files & folders
Force bool
// AccessFilter must match all the nested files & folders in order for the `deleteFolder` operation to succeed
// The access check is not performed if `AccessFilter` is nil
AccessFilter PathFilter
}
type GetFileOptions struct {
// WithContents if set to false, the `Get` operation will return just the file metadata. Default is `true`
WithContents bool
}
//go:generate mockery --name FileStorage --structname MockFileStorage --inpackage --filename file_storage_mock.go
type FileStorage interface {
Get(ctx context.Context, path string, options *GetFileOptions) (*File, bool, error)
Delete(ctx context.Context, path string) error
Upsert(ctx context.Context, command *UpsertFileCommand) error
// List lists only files without content by default
List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error)
CreateFolder(ctx context.Context, path string) error
DeleteFolder(ctx context.Context, path string, options *DeleteFolderOptions) error
close() error
}