diff --git a/simples3.go b/simples3.go index 072534c..11ff045 100644 --- a/simples3.go +++ b/simples3.go @@ -7,6 +7,7 @@ import ( "bytes" "crypto/hmac" "crypto/sha256" + "encoding/hex" "encoding/json" "encoding/xml" "errors" @@ -15,8 +16,10 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "regexp" "strings" "time" + "unicode/utf8" ) const ( @@ -159,17 +162,23 @@ func (s3 *S3) getClient() *http.Client { return s3.Client } -func (s3 *S3) getURL(bucket string, args ...string) (uri string) { +// getURL constructs a URL for a given path, with multiple optional +// arguments as individual subfolders, based on the endpoint +// specified in s3 struct. +func (s3 *S3) getURL(path string, args ...string) (uri string) { + if len(args) > 0 { + path += "/" + strings.Join(args, "/") + } + // need to encode special characters in the path part of the URL + encodedPath := encodePath(path) + if len(s3.Endpoint) > 0 { - uri = s3.Endpoint + "/" + bucket + uri = s3.Endpoint + "/" + encodedPath } else { - uri = fmt.Sprintf(s3.URIFormat, s3.Region, bucket) + uri = fmt.Sprintf(s3.URIFormat, s3.Region, encodedPath) } - if len(args) > 0 { - uri = uri + "/" + strings.Join(args, "/") - } - return + return uri } // SetEndpoint can be used to the set a custom endpoint for @@ -391,3 +400,45 @@ func (s3 *S3) FileDelete(u DeleteInput) error { return nil } + +// if object matches reserved string, no need to encode them +var reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") + +// encodePath encode the strings from UTF-8 byte representations to HTML hex escape sequences +// +// This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8 +// non english characters cannot be parsed due to the nature in which url.Encode() is written +// +// This function on the other hand is a direct replacement for url.Encode() technique to support +// pretty much every UTF-8 character. +// adapted from https://github.com/minio/minio-go/blob/fe1f3855b146c1b6ce4199740d317e44cf9e85c2/pkg/s3utils/utils.go#L285 +func encodePath(pathName string) string { + if reservedObjectNames.MatchString(pathName) { + return pathName + } + var encodedPathname strings.Builder + for _, s := range pathName { + if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark) + encodedPathname.WriteRune(s) + continue + } + switch s { + case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) + encodedPathname.WriteRune(s) + continue + default: + len := utf8.RuneLen(s) + if len < 0 { + // if utf8 cannot convert, return the same string as is + return pathName + } + u := make([]byte, len) + utf8.EncodeRune(u, s) + for _, r := range u { + hex := hex.EncodeToString([]byte{r}) + encodedPathname.WriteString("%" + strings.ToUpper(hex)) + } + } + } + return encodedPathname.String() +}