mirror of
https://github.com/rclone/rclone.git
synced 2026-05-18 13:52:30 -04:00
Add support for S3 Object Lock with the following new options: - --s3-object-lock-mode: set retention mode (GOVERNANCE/COMPLIANCE/copy) - --s3-object-lock-retain-until-date: set retention date (RFC3339/duration/copy) - --s3-object-lock-legal-hold-status: set legal hold (ON/OFF/copy) - --s3-bypass-governance-retention: bypass GOVERNANCE lock on delete - --s3-bucket-object-lock-enabled: enable Object Lock on bucket creation - --s3-object-lock-set-after-upload: apply lock via separate API calls The special value "copy" preserves the source object's setting when used with --metadata flag, enabling scenarios like cloning objects from COMPLIANCE to GOVERNANCE mode while preserving the original retention date. Includes integration tests that create a temporary Object Lock bucket covering: - Retention Mode and Date - Legal Hold - Apply settings after upload - Override protections using bypass-governance flag The tests are gracefully skipped on providers that do not support Object Lock. Fixes #4683 Closes #7894 #7893 #8866
180 lines
4.5 KiB
Go
180 lines
4.5 KiB
Go
// Test S3 filesystem interface
|
|
package s3
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fstest"
|
|
"github.com/rclone/rclone/fstest/fstests"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func SetupS3Test(t *testing.T) (context.Context, *Options, *http.Client) {
|
|
ctx, opt := context.Background(), new(Options)
|
|
opt.Provider = "AWS"
|
|
client := getClient(ctx, opt)
|
|
return ctx, opt, client
|
|
}
|
|
|
|
// TestIntegration runs integration tests against the remote
|
|
func TestIntegration(t *testing.T) {
|
|
opt := &fstests.Opt{
|
|
RemoteName: "TestS3:",
|
|
NilObject: (*Object)(nil),
|
|
TiersToTest: []string{"STANDARD"},
|
|
ChunkedUpload: fstests.ChunkedUploadConfig{
|
|
MinChunkSize: minChunkSize,
|
|
},
|
|
}
|
|
// Test wider range of tiers on AWS
|
|
if *fstest.RemoteName == "" || *fstest.RemoteName == "TestS3:" {
|
|
opt.TiersToTest = []string{"STANDARD", "STANDARD_IA"}
|
|
}
|
|
fstests.Run(t, opt)
|
|
|
|
}
|
|
|
|
func TestIntegration2(t *testing.T) {
|
|
if *fstest.RemoteName != "" {
|
|
t.Skip("skipping as -remote is set")
|
|
}
|
|
name := "TestS3"
|
|
fstests.Run(t, &fstests.Opt{
|
|
RemoteName: name + ":",
|
|
NilObject: (*Object)(nil),
|
|
TiersToTest: []string{"STANDARD", "STANDARD_IA"},
|
|
ChunkedUpload: fstests.ChunkedUploadConfig{
|
|
MinChunkSize: minChunkSize,
|
|
},
|
|
ExtraConfig: []fstests.ExtraConfigItem{
|
|
{Name: name, Key: "directory_markers", Value: "true"},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestAWSDualStackOption(t *testing.T) {
|
|
{
|
|
// test enabled
|
|
ctx, opt, client := SetupS3Test(t)
|
|
opt.UseDualStack = true
|
|
s3Conn, _, err := s3Connection(ctx, opt, client)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, aws.DualStackEndpointStateEnabled, s3Conn.Options().EndpointOptions.UseDualStackEndpoint)
|
|
}
|
|
{
|
|
// test default case
|
|
ctx, opt, client := SetupS3Test(t)
|
|
s3Conn, _, err := s3Connection(ctx, opt, client)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, aws.DualStackEndpointStateDisabled, s3Conn.Options().EndpointOptions.UseDualStackEndpoint)
|
|
}
|
|
}
|
|
|
|
func (f *Fs) SetUploadChunkSize(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
|
|
return f.setUploadChunkSize(cs)
|
|
}
|
|
|
|
func (f *Fs) SetUploadCutoff(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
|
|
return f.setUploadCutoff(cs)
|
|
}
|
|
|
|
func (f *Fs) SetCopyCutoff(cs fs.SizeSuffix) (fs.SizeSuffix, error) {
|
|
return f.setCopyCutoff(cs)
|
|
}
|
|
|
|
var (
|
|
_ fstests.SetUploadChunkSizer = (*Fs)(nil)
|
|
_ fstests.SetUploadCutoffer = (*Fs)(nil)
|
|
_ fstests.SetCopyCutoffer = (*Fs)(nil)
|
|
)
|
|
|
|
func TestParseRetainUntilDate(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantErr bool
|
|
checkFunc func(t *testing.T, result time.Time)
|
|
}{
|
|
{
|
|
name: "RFC3339 date",
|
|
input: "2030-01-15T10:30:00Z",
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, result time.Time) {
|
|
expected, _ := time.Parse(time.RFC3339, "2030-01-15T10:30:00Z")
|
|
assert.Equal(t, expected, result)
|
|
},
|
|
},
|
|
{
|
|
name: "RFC3339 date with timezone",
|
|
input: "2030-06-15T10:30:00+02:00",
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, result time.Time) {
|
|
expected, _ := time.Parse(time.RFC3339, "2030-06-15T10:30:00+02:00")
|
|
assert.Equal(t, expected, result)
|
|
},
|
|
},
|
|
{
|
|
name: "duration days",
|
|
input: "365d",
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, result time.Time) {
|
|
expected := now.Add(365 * 24 * time.Hour)
|
|
diff := result.Sub(expected)
|
|
assert.Less(t, diff.Abs(), 2*time.Second, "result should be ~365 days from now")
|
|
},
|
|
},
|
|
{
|
|
name: "duration hours",
|
|
input: "24h",
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, result time.Time) {
|
|
expected := now.Add(24 * time.Hour)
|
|
diff := result.Sub(expected)
|
|
assert.Less(t, diff.Abs(), 2*time.Second, "result should be ~24 hours from now")
|
|
},
|
|
},
|
|
{
|
|
name: "duration minutes",
|
|
input: "30m",
|
|
wantErr: false,
|
|
checkFunc: func(t *testing.T, result time.Time) {
|
|
expected := now.Add(30 * time.Minute)
|
|
diff := result.Sub(expected)
|
|
assert.Less(t, diff.Abs(), 2*time.Second, "result should be ~30 minutes from now")
|
|
},
|
|
},
|
|
{
|
|
name: "invalid input",
|
|
input: "not-a-date",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty input",
|
|
input: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := parseRetainUntilDate(tt.input)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
if tt.checkFunc != nil {
|
|
tt.checkFunc(t, result)
|
|
}
|
|
})
|
|
}
|
|
}
|