forked from grafana/grafana
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathapi.go
189 lines (155 loc) · 4.72 KB
/
api.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
}