2022-03-03 00:53:26 -06:00
|
|
|
package filestorage
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
2022-05-16 12:26:40 -05:00
|
|
|
"fmt"
|
2022-06-09 12:09:06 -05:00
|
|
|
"path/filepath"
|
2022-03-11 12:08:19 -06:00
|
|
|
"regexp"
|
2022-03-03 00:53:26 -06:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
ErrRelativePath = errors.New("path cant be relative")
|
|
|
|
ErrNonCanonicalPath = errors.New("path must be canonical")
|
|
|
|
ErrPathTooLong = errors.New("path is too long")
|
2022-06-09 12:09:06 -05:00
|
|
|
ErrInvalidCharacters = errors.New("path contains unsupported characters")
|
2022-03-03 00:53:26 -06:00
|
|
|
ErrPathEndsWithDelimiter = errors.New("path can not end with delimiter")
|
2022-06-09 12:09:06 -05:00
|
|
|
ErrPathPartTooLong = errors.New("path part is too long")
|
|
|
|
ErrEmptyPathPart = errors.New("path can not have empty parts")
|
2022-03-03 00:53:26 -06:00
|
|
|
Delimiter = "/"
|
2022-03-15 12:21:22 -05:00
|
|
|
DirectoryMimeType = "directory"
|
2022-03-11 12:08:19 -06:00
|
|
|
multipleDelimiters = regexp.MustCompile(`/+`)
|
2022-06-09 12:09:06 -05:00
|
|
|
pathRegex = regexp.MustCompile(`(^/$)|(^(/[A-Za-z\d!\-_.*'() ]+)+$)`)
|
|
|
|
maxPathLength = 1024
|
|
|
|
maxPathPartLength = 256
|
2022-03-03 00:53:26 -06:00
|
|
|
)
|
|
|
|
|
2022-06-09 12:09:06 -05:00
|
|
|
func ValidatePath(path string) error {
|
2022-07-07 06:32:18 -05:00
|
|
|
if !strings.HasPrefix(path, Delimiter) {
|
2022-06-09 12:09:06 -05:00
|
|
|
return ErrRelativePath
|
|
|
|
}
|
|
|
|
|
|
|
|
if path == Delimiter {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if strings.HasSuffix(path, Delimiter) {
|
|
|
|
return ErrPathEndsWithDelimiter
|
|
|
|
}
|
|
|
|
|
2022-07-07 06:32:18 -05:00
|
|
|
// apply `ToSlash` to replace OS-specific separators introduced by the Clean() function
|
|
|
|
if filepath.ToSlash(filepath.Clean(path)) != path {
|
2022-06-09 12:09:06 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-03 00:53:26 -06:00
|
|
|
func Join(parts ...string) string {
|
2022-03-11 12:08:19 -06:00
|
|
|
joinedPath := Delimiter + strings.Join(parts, Delimiter)
|
2022-03-03 00:53:26 -06:00
|
|
|
|
2022-03-11 12:08:19 -06:00
|
|
|
// makes the API more forgiving for clients without compromising safety
|
|
|
|
return multipleDelimiters.ReplaceAllString(joinedPath, Delimiter)
|
2022-03-03 00:53:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
type File struct {
|
|
|
|
Contents []byte
|
|
|
|
FileMetadata
|
|
|
|
}
|
|
|
|
|
2022-03-15 12:21:22 -05:00
|
|
|
func (f *File) IsFolder() bool {
|
|
|
|
return f.MimeType == DirectoryMimeType
|
|
|
|
}
|
|
|
|
|
2022-03-03 00:53:26 -06:00
|
|
|
type FileMetadata struct {
|
|
|
|
Name string
|
|
|
|
FullPath string
|
|
|
|
MimeType string
|
|
|
|
Modified time.Time
|
|
|
|
Created time.Time
|
|
|
|
Size int64
|
|
|
|
Properties map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Paging struct {
|
|
|
|
After string
|
|
|
|
First int
|
|
|
|
}
|
|
|
|
|
|
|
|
type UpsertFileCommand struct {
|
2022-05-16 12:26:40 -05:00
|
|
|
Path string
|
|
|
|
MimeType string
|
|
|
|
CacheControl string
|
|
|
|
ContentDisposition string
|
2022-04-12 09:58:09 -05:00
|
|
|
|
|
|
|
// 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
|
2022-03-03 00:53:26 -06:00
|
|
|
Properties map[string]string
|
|
|
|
}
|
|
|
|
|
2022-03-11 12:08:19 -06:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-03-15 12:21:22 -05:00
|
|
|
type ListResponse struct {
|
|
|
|
Files []*File
|
|
|
|
HasMore bool
|
|
|
|
LastPath string
|
|
|
|
}
|
|
|
|
|
2022-05-16 12:26:40 -05:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2022-03-03 00:53:26 -06:00
|
|
|
type ListOptions struct {
|
2022-03-15 12:21:22 -05:00
|
|
|
Recursive bool
|
|
|
|
WithFiles bool
|
|
|
|
WithFolders bool
|
|
|
|
WithContents bool
|
2022-04-21 14:27:43 -05:00
|
|
|
Filter PathFilter
|
2022-03-03 00:53:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
type FileStorage interface {
|
|
|
|
Get(ctx context.Context, path string) (*File, error)
|
|
|
|
Delete(ctx context.Context, path string) error
|
|
|
|
Upsert(ctx context.Context, command *UpsertFileCommand) error
|
|
|
|
|
2022-03-15 12:21:22 -05:00
|
|
|
// List lists only files without content by default
|
|
|
|
List(ctx context.Context, folderPath string, paging *Paging, options *ListOptions) (*ListResponse, error)
|
2022-03-03 00:53:26 -06:00
|
|
|
|
|
|
|
CreateFolder(ctx context.Context, path string) error
|
|
|
|
DeleteFolder(ctx context.Context, path string) error
|
|
|
|
|
|
|
|
close() error
|
|
|
|
}
|