AWS S3 requires Content-MD5 for PutObject with Object Lock parameters. Since rclone passes a non-seekable io.Reader, the SDK cannot compute checksums automatically. Buffer the body and compute MD5 manually for singlepart PutObject and presigned request uploads when Object Lock parameters are set. Multipart uploads are unaffected as Object Lock headers go on CreateMultipartUpload which has no body. Add object_lock_supported provider quirk (default true) to allow skipping Object Lock integration tests on providers with incomplete S3 API support. Set to false for GCS which uses non-standard x-goog-bypass-governance-retention header and doesn't implement PutObjectLegalHold/GetObjectLegalHold. Add Multipart and Presigned subtests to Object Lock integration tests to cover all three upload paths. Fixes #9199
243 lines
8.1 KiB
Go
243 lines
8.1 KiB
Go
package s3
|
|
|
|
import (
|
|
"embed"
|
|
stdfs "io/fs"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/rclone/rclone/fs"
|
|
orderedmap "github.com/wk8/go-ordered-map/v2"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// YamlMap is converted to YAML in the correct order
|
|
type YamlMap = *orderedmap.OrderedMap[string, string]
|
|
|
|
// NewYamlMap creates a new ordered map
|
|
var NewYamlMap = orderedmap.New[string, string]
|
|
|
|
// Quirks defines all the S3 provider quirks
|
|
type Quirks struct {
|
|
ListVersion *int `yaml:"list_version,omitempty"` // 1 or 2
|
|
ForcePathStyle *bool `yaml:"force_path_style,omitempty"` // true = path-style
|
|
ListURLEncode *bool `yaml:"list_url_encode,omitempty"`
|
|
UseMultipartEtag *bool `yaml:"use_multipart_etag,omitempty"`
|
|
UseAlreadyExists *bool `yaml:"use_already_exists,omitempty"`
|
|
UseAcceptEncodingGzip *bool `yaml:"use_accept_encoding_gzip,omitempty"`
|
|
UseDataIntegrityProtections *bool `yaml:"use_data_integrity_protections,omitempty"`
|
|
MightGzip *bool `yaml:"might_gzip,omitempty"`
|
|
UseMultipartUploads *bool `yaml:"use_multipart_uploads,omitempty"`
|
|
UseUnsignedPayload *bool `yaml:"use_unsigned_payload,omitempty"`
|
|
UseXID *bool `yaml:"use_x_id,omitempty"`
|
|
SignAcceptEncoding *bool `yaml:"sign_accept_encoding,omitempty"`
|
|
EtagIsNotMD5 *bool `yaml:"etag_is_not_md5,omitempty"`
|
|
ObjectLockSupported *bool `yaml:"object_lock_supported,omitempty"`
|
|
CopyCutoff *int64 `yaml:"copy_cutoff,omitempty"`
|
|
MaxUploadParts *int `yaml:"max_upload_parts,omitempty"`
|
|
MinChunkSize *int64 `yaml:"min_chunk_size,omitempty"`
|
|
}
|
|
|
|
// Provider defines the configurable data in each provider.yaml
|
|
type Provider struct {
|
|
Name string `yaml:"name,omitempty"`
|
|
Description string `yaml:"description,omitempty"`
|
|
Region YamlMap `yaml:"region,omitempty"`
|
|
Endpoint YamlMap `yaml:"endpoint,omitempty"`
|
|
LocationConstraint YamlMap `yaml:"location_constraint,omitempty"`
|
|
ACL YamlMap `yaml:"acl,omitempty"`
|
|
StorageClass YamlMap `yaml:"storage_class,omitempty"`
|
|
ServerSideEncryption YamlMap `yaml:"server_side_encryption,omitempty"`
|
|
|
|
// other
|
|
IBMApiKey bool `yaml:"ibm_api_key,omitempty"`
|
|
IBMResourceInstanceID bool `yaml:"ibm_resource_instance_id,omitempty"`
|
|
IBMIAMEndpoint bool `yaml:"ibm_iam_endpoint,omitempty"`
|
|
|
|
// advanced
|
|
BucketACL bool `yaml:"bucket_acl,omitempty"`
|
|
DirectoryBucket bool `yaml:"directory_bucket,omitempty"`
|
|
LeavePartsOnError bool `yaml:"leave_parts_on_error,omitempty"`
|
|
RequesterPays bool `yaml:"requester_pays,omitempty"`
|
|
SSECustomerAlgorithm bool `yaml:"sse_customer_algorithm,omitempty"`
|
|
SSECustomerKey bool `yaml:"sse_customer_key,omitempty"`
|
|
SSECustomerKeyBase64 bool `yaml:"sse_customer_key_base64,omitempty"`
|
|
SSECustomerKeyMd5 bool `yaml:"sse_customer_key_md5,omitempty"`
|
|
SSEKmsKeyID bool `yaml:"sse_kms_key_id,omitempty"`
|
|
STSEndpoint bool `yaml:"sts_endpoint,omitempty"`
|
|
UseAccelerateEndpoint bool `yaml:"use_accelerate_endpoint,omitempty"`
|
|
|
|
Quirks Quirks `yaml:"quirks,omitempty"`
|
|
}
|
|
|
|
//go:embed provider/*.yaml
|
|
var providerFS embed.FS
|
|
|
|
// addProvidersToInfo adds provider information to the fs.RegInfo
|
|
func addProvidersToInfo(info *fs.RegInfo) *fs.RegInfo {
|
|
providerMap := loadProviders()
|
|
providerList := constructProviders(info.Options, providerMap)
|
|
info.Description += strings.TrimSuffix(providerList, ", ")
|
|
return info
|
|
}
|
|
|
|
// loadProvider loads a single provider
|
|
//
|
|
// It returns nil if it could not be found except if "Other" which is a fatal error.
|
|
func loadProvider(name string) *Provider {
|
|
data, err := stdfs.ReadFile(providerFS, "provider/"+name+".yaml")
|
|
if err != nil {
|
|
if os.IsNotExist(err) && name != "Other" {
|
|
return nil
|
|
}
|
|
fs.Fatalf(nil, "internal error: failed to load provider %q: %v", name, err)
|
|
}
|
|
var p Provider
|
|
err = yaml.Unmarshal(data, &p)
|
|
if err != nil {
|
|
fs.Fatalf(nil, "internal error: failed to unmarshal provider %q: %v", name, err)
|
|
}
|
|
return &p
|
|
}
|
|
|
|
// loadProviders loads provider definitions from embedded YAML files
|
|
func loadProviders() map[string]*Provider {
|
|
providers, err := stdfs.ReadDir(providerFS, "provider")
|
|
if err != nil {
|
|
fs.Fatalf(nil, "internal error: failed to read embedded providers: %v", err)
|
|
}
|
|
providerMap := make(map[string]*Provider, len(providers))
|
|
|
|
for _, provider := range providers {
|
|
name, _ := strings.CutSuffix(provider.Name(), ".yaml")
|
|
p := loadProvider(name)
|
|
providerMap[p.Name] = p
|
|
}
|
|
return providerMap
|
|
}
|
|
|
|
// constructProviders populates fs.Options with provider-specific examples and information
|
|
func constructProviders(options fs.Options, providerMap map[string]*Provider) string {
|
|
// Defaults for map options set to {}
|
|
defaults := providerMap["Other"]
|
|
|
|
// sort providers: AWS first, Other last, rest alphabetically
|
|
providers := make([]*Provider, 0, len(providerMap))
|
|
for _, p := range providerMap {
|
|
providers = append(providers, p)
|
|
}
|
|
sort.Slice(providers, func(i, j int) bool {
|
|
if providers[i].Name == "AWS" {
|
|
return true
|
|
}
|
|
if providers[j].Name == "AWS" {
|
|
return false
|
|
}
|
|
if providers[i].Name == "Other" {
|
|
return false
|
|
}
|
|
if providers[j].Name == "Other" {
|
|
return true
|
|
}
|
|
return strings.ToLower(providers[i].Name) < strings.ToLower(providers[j].Name)
|
|
})
|
|
|
|
addProvider := func(sp *string, name string) {
|
|
if *sp != "" {
|
|
*sp += ","
|
|
}
|
|
*sp += name
|
|
}
|
|
|
|
addBool := func(opt *fs.Option, p *Provider, flag bool) {
|
|
if flag {
|
|
addProvider(&opt.Provider, p.Name)
|
|
}
|
|
}
|
|
|
|
addExample := func(opt *fs.Option, p *Provider, examples, defaultExamples YamlMap) {
|
|
if examples == nil {
|
|
return
|
|
}
|
|
if examples.Len() == 0 {
|
|
examples = defaultExamples
|
|
}
|
|
addProvider(&opt.Provider, p.Name)
|
|
OUTER:
|
|
for pair := examples.Oldest(); pair != nil; pair = pair.Next() {
|
|
// Find an existing example to add to if possible
|
|
for i, example := range opt.Examples {
|
|
if example.Value == pair.Key && example.Help == pair.Value {
|
|
addProvider(&opt.Examples[i].Provider, p.Name)
|
|
continue OUTER
|
|
}
|
|
}
|
|
// Otherwise add a new one
|
|
opt.Examples = append(opt.Examples, fs.OptionExample{
|
|
Value: pair.Key,
|
|
Help: pair.Value,
|
|
Provider: p.Name,
|
|
})
|
|
}
|
|
}
|
|
|
|
var providerList strings.Builder
|
|
|
|
for _, p := range providers {
|
|
for i := range options {
|
|
opt := &options[i]
|
|
switch opt.Name {
|
|
case "provider":
|
|
opt.Examples = append(opt.Examples, fs.OptionExample{
|
|
Value: p.Name,
|
|
Help: p.Description,
|
|
})
|
|
providerList.WriteString(p.Name + ", ")
|
|
case "region":
|
|
addExample(opt, p, p.Region, defaults.Region)
|
|
case "endpoint":
|
|
addExample(opt, p, p.Endpoint, defaults.Endpoint)
|
|
case "location_constraint":
|
|
addExample(opt, p, p.LocationConstraint, defaults.LocationConstraint)
|
|
case "acl":
|
|
addExample(opt, p, p.ACL, defaults.ACL)
|
|
case "storage_class":
|
|
addExample(opt, p, p.StorageClass, defaults.StorageClass)
|
|
case "server_side_encryption":
|
|
addExample(opt, p, p.ServerSideEncryption, defaults.ServerSideEncryption)
|
|
case "bucket_acl":
|
|
addBool(opt, p, p.BucketACL)
|
|
case "requester_pays":
|
|
addBool(opt, p, p.RequesterPays)
|
|
case "sse_customer_algorithm":
|
|
addBool(opt, p, p.SSECustomerAlgorithm)
|
|
case "sse_kms_key_id":
|
|
addBool(opt, p, p.SSEKmsKeyID)
|
|
case "sse_customer_key":
|
|
addBool(opt, p, p.SSECustomerKey)
|
|
case "sse_customer_key_base64":
|
|
addBool(opt, p, p.SSECustomerKeyBase64)
|
|
case "sse_customer_key_md5":
|
|
addBool(opt, p, p.SSECustomerKeyMd5)
|
|
case "directory_bucket":
|
|
addBool(opt, p, p.DirectoryBucket)
|
|
case "ibm_api_key":
|
|
addBool(opt, p, p.IBMApiKey)
|
|
case "ibm_resource_instance_id":
|
|
addBool(opt, p, p.IBMResourceInstanceID)
|
|
case "ibm_iam_endpoint":
|
|
addBool(opt, p, p.IBMIAMEndpoint)
|
|
case "leave_parts_on_error":
|
|
addBool(opt, p, p.LeavePartsOnError)
|
|
case "sts_endpoint":
|
|
addBool(opt, p, p.STSEndpoint)
|
|
case "use_accelerate_endpoint":
|
|
addBool(opt, p, p.UseAccelerateEndpoint)
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings.TrimSuffix(providerList.String(), ", ")
|
|
}
|