gofakes3/prefix.go
2024-04-12 16:40:42 +01:00

171 lines
4.1 KiB
Go

package gofakes3
import (
"fmt"
"net/url"
"strings"
)
type Prefix struct {
HasPrefix bool
Prefix string
HasDelimiter bool
Delimiter string
}
func prefixFromQuery(query url.Values) Prefix {
prefix := Prefix{
Prefix: query.Get("prefix"),
Delimiter: query.Get("delimiter"),
}
_, prefix.HasPrefix = query["prefix"]
_, prefix.HasDelimiter = query["delimiter"]
return prefix
}
func NewPrefix(prefix, delim *string) (p Prefix) {
if prefix != nil {
p.HasPrefix, p.Prefix = true, *prefix
}
if delim != nil {
p.HasDelimiter, p.Delimiter = true, *delim
}
return p
}
func NewFolderPrefix(prefix string) (p Prefix) {
p.HasPrefix, p.Prefix = true, prefix
p.HasDelimiter, p.Delimiter = true, "/"
return p
}
// FilePrefix returns the path portion, then the remaining portion of the
// Prefix if the Delimiter is "/". If the Delimiter is not set, or not "/",
// ok will be false.
//
// For example:
//
// /foo/bar/ : path: /foo/bar remaining: ""
// /foo/bar/b : path: /foo/bar remaining: "b"
// /foo/bar : path: /foo remaining: "bar"
func (p Prefix) FilePrefix() (path, remaining string, ok bool) {
if !p.HasPrefix || !p.HasDelimiter || p.Delimiter != "/" {
return "", "", ok
}
idx := strings.LastIndexByte(p.Prefix, '/')
if idx < 0 {
return "", p.Prefix, true
} else {
return p.Prefix[:idx], p.Prefix[idx+1:], true
}
}
// PrefixMatch checks whether key starts with prefix. If the prefix does not
// match, nil is returned.
//
// It is a best-effort attempt to implement the prefix/delimiter matching found
// in S3.
//
// To check whether the key belongs in Contents or CommonPrefixes, compare the
// result to key.
func (p Prefix) Match(key string, match *PrefixMatch) (ok bool) {
if !p.HasPrefix && !p.HasDelimiter {
// If there is no prefix in the search, the match is the prefix:
if match != nil {
*match = PrefixMatch{Key: key, MatchedPart: key}
}
return true
}
if !p.HasDelimiter {
// If the request does not contain a delimiter, prefix matching is a
// simple string prefix:
if strings.HasPrefix(key, p.Prefix) {
if match != nil {
*match = PrefixMatch{Key: key, MatchedPart: p.Prefix}
}
return true
}
return false
}
// Delimited + Prefix matches, for example:
// $ aws s3 ls s3://my-bucket/
// PRE AWSLogs/
// $ aws s3 ls s3://my-bucket/AWSLogs
// PRE AWSLogs/
// $ aws s3 ls s3://my-bucket/AWSLogs/
// PRE 260839334643/
// $ aws s3 ls s3://my-bucket/AWSLogs/2608
// PRE 260839334643/
keyParts := strings.Split(strings.TrimLeft(key, p.Delimiter), p.Delimiter)
preParts := strings.Split(strings.TrimLeft(p.Prefix, p.Delimiter), p.Delimiter)
if len(keyParts) < len(preParts) {
return false
}
// If the key exactly matches the prefix, but only up to a delimiter,
// AWS appends the delimiter to the result:
// $ aws s3 ls s3://my-bucket/AWSLogs
// PRE AWSLogs/
appendDelim := len(keyParts) != len(preParts)
matched := 0
last := len(preParts) - 1
for i := 0; i < len(preParts); i++ {
if i == last {
if !strings.HasPrefix(keyParts[i], preParts[i]) {
return false
}
} else {
if keyParts[i] != preParts[i] {
return false
}
}
matched++
}
if matched == 0 {
return false
}
out := strings.Join(keyParts[:matched], p.Delimiter)
if appendDelim {
out += p.Delimiter
}
if match != nil {
*match = PrefixMatch{Key: key, CommonPrefix: out != key, MatchedPart: out}
}
return true
}
func (p Prefix) String() string {
if p.HasDelimiter {
return fmt.Sprintf("prefix:%q, delim:%q", p.Prefix, p.Delimiter)
} else {
return fmt.Sprintf("prefix:%q", p.Prefix)
}
}
type PrefixMatch struct {
// Input key passed to PrefixMatch.
Key string
// CommonPrefix indicates whether this key should be returned in the bucket
// contents or the common prefixes part of the "list bucket" response.
CommonPrefix bool
// The longest matched part of the key.
MatchedPart string
}
func (match *PrefixMatch) AsCommonPrefix() CommonPrefix {
return CommonPrefix{Prefix: match.MatchedPart}
}