diff options
Diffstat (limited to 'test/s3/copying/s3_copying_test.go')
| -rw-r--r-- | test/s3/copying/s3_copying_test.go | 1014 |
1 files changed, 1014 insertions, 0 deletions
diff --git a/test/s3/copying/s3_copying_test.go b/test/s3/copying/s3_copying_test.go new file mode 100644 index 000000000..4bad01de4 --- /dev/null +++ b/test/s3/copying/s3_copying_test.go @@ -0,0 +1,1014 @@ +package copying_test + +import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "io" + mathrand "math/rand" + "net/url" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// S3TestConfig holds configuration for S3 tests +type S3TestConfig struct { + Endpoint string + AccessKey string + SecretKey string + Region string + BucketPrefix string + UseSSL bool + SkipVerifySSL bool +} + +// Default test configuration - should match test_config.json +var defaultConfig = &S3TestConfig{ + Endpoint: "http://127.0.0.1:8000", // Use explicit IPv4 address + AccessKey: "some_access_key1", + SecretKey: "some_secret_key1", + Region: "us-east-1", + BucketPrefix: "test-copying-", + UseSSL: false, + SkipVerifySSL: true, +} + +// Initialize math/rand with current time to ensure randomness +func init() { + mathrand.Seed(time.Now().UnixNano()) +} + +// getS3Client creates an AWS S3 client for testing +func getS3Client(t *testing.T) *s3.Client { + cfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithRegion(defaultConfig.Region), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + defaultConfig.AccessKey, + defaultConfig.SecretKey, + "", + )), + config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc( + func(service, region string, options ...interface{}) (aws.Endpoint, error) { + return aws.Endpoint{ + URL: defaultConfig.Endpoint, + SigningRegion: defaultConfig.Region, + HostnameImmutable: true, + }, nil + })), + ) + require.NoError(t, err) + + return s3.NewFromConfig(cfg, func(o *s3.Options) { + o.UsePathStyle = true // Important for SeaweedFS + }) +} + +// waitForS3Service waits for the S3 service to be ready +func waitForS3Service(t *testing.T, client *s3.Client, timeout time.Duration) { + start := time.Now() + for time.Since(start) < timeout { + _, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{}) + if err == nil { + return + } + t.Logf("Waiting for S3 service to be ready... (error: %v)", err) + time.Sleep(time.Second) + } + t.Fatalf("S3 service not ready after %v", timeout) +} + +// getNewBucketName generates a unique bucket name +func getNewBucketName() string { + timestamp := time.Now().UnixNano() + // Add random suffix to prevent collisions when tests run quickly + randomSuffix := mathrand.Intn(100000) + return fmt.Sprintf("%s%d-%d", defaultConfig.BucketPrefix, timestamp, randomSuffix) +} + +// cleanupTestBuckets removes any leftover test buckets from previous runs +func cleanupTestBuckets(t *testing.T, client *s3.Client) { + resp, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{}) + if err != nil { + t.Logf("Warning: failed to list buckets for cleanup: %v", err) + return + } + + for _, bucket := range resp.Buckets { + bucketName := *bucket.Name + // Only delete buckets that match our test prefix + if strings.HasPrefix(bucketName, defaultConfig.BucketPrefix) { + t.Logf("Cleaning up leftover test bucket: %s", bucketName) + deleteBucket(t, client, bucketName) + } + } +} + +// createBucket creates a new bucket for testing +func createBucket(t *testing.T, client *s3.Client, bucketName string) { + // First, try to delete the bucket if it exists (cleanup from previous failed tests) + deleteBucket(t, client, bucketName) + + // Create the bucket + _, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{ + Bucket: aws.String(bucketName), + }) + require.NoError(t, err) +} + +// deleteBucket deletes a bucket and all its contents +func deleteBucket(t *testing.T, client *s3.Client, bucketName string) { + // First, delete all objects + deleteAllObjects(t, client, bucketName) + + // Then delete the bucket + _, err := client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{ + Bucket: aws.String(bucketName), + }) + if err != nil { + // Only log warnings for actual errors, not "bucket doesn't exist" + if !strings.Contains(err.Error(), "NoSuchBucket") { + t.Logf("Warning: failed to delete bucket %s: %v", bucketName, err) + } + } +} + +// deleteAllObjects deletes all objects in a bucket +func deleteAllObjects(t *testing.T, client *s3.Client, bucketName string) { + // List all objects + paginator := s3.NewListObjectsV2Paginator(client, &s3.ListObjectsV2Input{ + Bucket: aws.String(bucketName), + }) + + for paginator.HasMorePages() { + page, err := paginator.NextPage(context.TODO()) + if err != nil { + // Only log warnings for actual errors, not "bucket doesn't exist" + if !strings.Contains(err.Error(), "NoSuchBucket") { + t.Logf("Warning: failed to list objects in bucket %s: %v", bucketName, err) + } + return + } + + if len(page.Contents) == 0 { + break + } + + var objectsToDelete []types.ObjectIdentifier + for _, obj := range page.Contents { + objectsToDelete = append(objectsToDelete, types.ObjectIdentifier{ + Key: obj.Key, + }) + } + + // Delete objects in batches + if len(objectsToDelete) > 0 { + _, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{ + Bucket: aws.String(bucketName), + Delete: &types.Delete{ + Objects: objectsToDelete, + Quiet: aws.Bool(true), + }, + }) + if err != nil { + t.Logf("Warning: failed to delete objects in bucket %s: %v", bucketName, err) + } + } + } +} + +// putObject puts an object into a bucket +func putObject(t *testing.T, client *s3.Client, bucketName, key, content string) *s3.PutObjectOutput { + resp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + Body: strings.NewReader(content), + }) + require.NoError(t, err) + return resp +} + +// putObjectWithMetadata puts an object with metadata into a bucket +func putObjectWithMetadata(t *testing.T, client *s3.Client, bucketName, key, content string, metadata map[string]string, contentType string) *s3.PutObjectOutput { + input := &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + Body: strings.NewReader(content), + } + + if metadata != nil { + input.Metadata = metadata + } + + if contentType != "" { + input.ContentType = aws.String(contentType) + } + + resp, err := client.PutObject(context.TODO(), input) + require.NoError(t, err) + return resp +} + +// getObject gets an object from a bucket +func getObject(t *testing.T, client *s3.Client, bucketName, key string) *s3.GetObjectOutput { + resp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(key), + }) + require.NoError(t, err) + return resp +} + +// getObjectBody gets the body content of an object +func getObjectBody(t *testing.T, resp *s3.GetObjectOutput) string { + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + resp.Body.Close() + return string(body) +} + +// generateRandomData generates random data of specified size +func generateRandomData(size int) []byte { + data := make([]byte, size) + _, err := rand.Read(data) + if err != nil { + panic(err) + } + return data +} + +// createCopySource creates a properly URL-encoded copy source string +func createCopySource(bucketName, key string) string { + // URL encode the key to handle special characters like spaces + encodedKey := url.PathEscape(key) + return fmt.Sprintf("%s/%s", bucketName, encodedKey) +} + +// TestBasicPutGet tests basic S3 put and get operations +func TestBasicPutGet(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Test 1: Put and get a simple text object + t.Run("Simple text object", func(t *testing.T) { + key := "test-simple.txt" + content := "Hello, SeaweedFS S3!" + + // Put object + putResp := putObject(t, client, bucketName, key, content) + assert.NotNil(t, putResp.ETag) + + // Get object + getResp := getObject(t, client, bucketName, key) + body := getObjectBody(t, getResp) + assert.Equal(t, content, body) + assert.Equal(t, putResp.ETag, getResp.ETag) + }) + + // Test 2: Put and get an empty object + t.Run("Empty object", func(t *testing.T) { + key := "test-empty.txt" + content := "" + + putResp := putObject(t, client, bucketName, key, content) + assert.NotNil(t, putResp.ETag) + + getResp := getObject(t, client, bucketName, key) + body := getObjectBody(t, getResp) + assert.Equal(t, content, body) + assert.Equal(t, putResp.ETag, getResp.ETag) + }) + + // Test 3: Put and get a binary object + t.Run("Binary object", func(t *testing.T) { + key := "test-binary.bin" + content := string(generateRandomData(1024)) // 1KB of random data + + putResp := putObject(t, client, bucketName, key, content) + assert.NotNil(t, putResp.ETag) + + getResp := getObject(t, client, bucketName, key) + body := getObjectBody(t, getResp) + assert.Equal(t, content, body) + assert.Equal(t, putResp.ETag, getResp.ETag) + }) + + // Test 4: Put and get object with metadata + t.Run("Object with metadata", func(t *testing.T) { + key := "test-metadata.txt" + content := "Content with metadata" + metadata := map[string]string{ + "author": "test", + "description": "test object with metadata", + } + contentType := "text/plain" + + putResp := putObjectWithMetadata(t, client, bucketName, key, content, metadata, contentType) + assert.NotNil(t, putResp.ETag) + + getResp := getObject(t, client, bucketName, key) + body := getObjectBody(t, getResp) + assert.Equal(t, content, body) + assert.Equal(t, putResp.ETag, getResp.ETag) + assert.Equal(t, contentType, *getResp.ContentType) + assert.Equal(t, metadata["author"], getResp.Metadata["author"]) + assert.Equal(t, metadata["description"], getResp.Metadata["description"]) + }) +} + +// TestBasicBucketOperations tests basic bucket operations +func TestBasicBucketOperations(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Test 1: Create bucket + t.Run("Create bucket", func(t *testing.T) { + createBucket(t, client, bucketName) + + // Verify bucket exists by listing buckets + resp, err := client.ListBuckets(context.TODO(), &s3.ListBucketsInput{}) + require.NoError(t, err) + + found := false + for _, bucket := range resp.Buckets { + if *bucket.Name == bucketName { + found = true + break + } + } + assert.True(t, found, "Bucket should exist after creation") + }) + + // Test 2: Put objects and list them + t.Run("List objects", func(t *testing.T) { + // Put multiple objects + objects := []string{"test1.txt", "test2.txt", "dir/test3.txt"} + for _, key := range objects { + putObject(t, client, bucketName, key, fmt.Sprintf("content of %s", key)) + } + + // List objects + resp, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ + Bucket: aws.String(bucketName), + }) + require.NoError(t, err) + + assert.Equal(t, len(objects), len(resp.Contents)) + + // Verify each object exists + for _, obj := range resp.Contents { + found := false + for _, expected := range objects { + if *obj.Key == expected { + found = true + break + } + } + assert.True(t, found, "Object %s should be in list", *obj.Key) + } + }) + + // Test 3: Delete bucket (cleanup) + t.Run("Delete bucket", func(t *testing.T) { + deleteBucket(t, client, bucketName) + + // Verify bucket is deleted by trying to list its contents + _, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ + Bucket: aws.String(bucketName), + }) + assert.Error(t, err, "Bucket should not exist after deletion") + }) +} + +// TestBasicLargeObject tests handling of larger objects (up to volume limit) +func TestBasicLargeObject(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Test with progressively larger objects + sizes := []int{ + 1024, // 1KB + 1024 * 10, // 10KB + 1024 * 100, // 100KB + 1024 * 1024, // 1MB + 1024 * 1024 * 5, // 5MB + 1024 * 1024 * 10, // 10MB + } + + for _, size := range sizes { + t.Run(fmt.Sprintf("Size_%dMB", size/(1024*1024)), func(t *testing.T) { + key := fmt.Sprintf("large-object-%d.bin", size) + content := string(generateRandomData(size)) + + putResp := putObject(t, client, bucketName, key, content) + assert.NotNil(t, putResp.ETag) + + getResp := getObject(t, client, bucketName, key) + body := getObjectBody(t, getResp) + assert.Equal(t, len(content), len(body)) + assert.Equal(t, content, body) + assert.Equal(t, putResp.ETag, getResp.ETag) + }) + } +} + +// TestObjectCopySameBucket tests copying an object within the same bucket +func TestObjectCopySameBucket(t *testing.T) { + client := getS3Client(t) + + // Wait for S3 service to be ready + waitForS3Service(t, client, 30*time.Second) + + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Put source object + sourceKey := "foo123bar" + sourceContent := "foo" + putObject(t, client, bucketName, sourceKey, sourceContent) + + // Copy object within the same bucket + destKey := "bar321foo" + copySource := createCopySource(bucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + }) + require.NoError(t, err, "Failed to copy object within same bucket") + + // Verify the copied object + resp := getObject(t, client, bucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) +} + +// TestObjectCopyDiffBucket tests copying an object to a different bucket +func TestObjectCopyDiffBucket(t *testing.T) { + client := getS3Client(t) + sourceBucketName := getNewBucketName() + destBucketName := getNewBucketName() + + // Create buckets + createBucket(t, client, sourceBucketName) + defer deleteBucket(t, client, sourceBucketName) + createBucket(t, client, destBucketName) + defer deleteBucket(t, client, destBucketName) + + // Put source object + sourceKey := "foo123bar" + sourceContent := "foo" + putObject(t, client, sourceBucketName, sourceKey, sourceContent) + + // Copy object to different bucket + destKey := "bar321foo" + copySource := createCopySource(sourceBucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, destBucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) +} + +// TestObjectCopyCannedAcl tests copying with ACL settings +func TestObjectCopyCannedAcl(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Put source object + sourceKey := "foo123bar" + sourceContent := "foo" + putObject(t, client, bucketName, sourceKey, sourceContent) + + // Copy object with public-read ACL + destKey := "bar321foo" + copySource := createCopySource(bucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + ACL: types.ObjectCannedACLPublicRead, + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, bucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) + + // Test metadata replacement with ACL + metadata := map[string]string{"abc": "def"} + destKey2 := "foo123bar2" + copySource2 := createCopySource(bucketName, destKey) + _, err = client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey2), + CopySource: aws.String(copySource2), + ACL: types.ObjectCannedACLPublicRead, + Metadata: metadata, + MetadataDirective: types.MetadataDirectiveReplace, + }) + require.NoError(t, err) + + // Verify the copied object with metadata + resp2 := getObject(t, client, bucketName, destKey2) + body2 := getObjectBody(t, resp2) + assert.Equal(t, sourceContent, body2) + assert.Equal(t, metadata, resp2.Metadata) +} + +// TestObjectCopyRetainingMetadata tests copying while retaining metadata +func TestObjectCopyRetainingMetadata(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Test with different sizes + sizes := []int{3, 1024 * 1024} // 3 bytes and 1MB + for _, size := range sizes { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + sourceKey := fmt.Sprintf("foo123bar_%d", size) + sourceContent := string(generateRandomData(size)) + contentType := "audio/ogg" + metadata := map[string]string{"key1": "value1", "key2": "value2"} + + // Put source object with metadata + putObjectWithMetadata(t, client, bucketName, sourceKey, sourceContent, metadata, contentType) + + // Copy object (should retain metadata) + destKey := fmt.Sprintf("bar321foo_%d", size) + copySource := createCopySource(bucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, bucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) + assert.Equal(t, contentType, *resp.ContentType) + assert.Equal(t, metadata, resp.Metadata) + require.NotNil(t, resp.ContentLength) + assert.Equal(t, int64(size), *resp.ContentLength) + }) + } +} + +// TestMultipartCopySmall tests multipart copying of small files +func TestMultipartCopySmall(t *testing.T) { + client := getS3Client(t) + + // Clean up any leftover buckets from previous test runs + cleanupTestBuckets(t, client) + + sourceBucketName := getNewBucketName() + destBucketName := getNewBucketName() + + // Create buckets + createBucket(t, client, sourceBucketName) + defer deleteBucket(t, client, sourceBucketName) + createBucket(t, client, destBucketName) + defer deleteBucket(t, client, destBucketName) + + // Put source object + sourceKey := "foo" + sourceContent := "x" // 1 byte + putObject(t, client, sourceBucketName, sourceKey, sourceContent) + + // Create multipart upload + destKey := "mymultipart" + createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + }) + require.NoError(t, err) + uploadID := *createResp.UploadId + + // Upload part copy + copySource := createCopySource(sourceBucketName, sourceKey) + copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + PartNumber: aws.Int32(1), + CopySource: aws.String(copySource), + CopySourceRange: aws.String("bytes=0-0"), + }) + require.NoError(t, err) + + // Complete multipart upload + _, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: []types.CompletedPart{ + { + ETag: copyResp.CopyPartResult.ETag, + PartNumber: aws.Int32(1), + }, + }, + }, + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, destBucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) + require.NotNil(t, resp.ContentLength) + assert.Equal(t, int64(1), *resp.ContentLength) +} + +// TestMultipartCopyWithoutRange tests multipart copying without range specification +func TestMultipartCopyWithoutRange(t *testing.T) { + client := getS3Client(t) + + // Clean up any leftover buckets from previous test runs + cleanupTestBuckets(t, client) + + sourceBucketName := getNewBucketName() + destBucketName := getNewBucketName() + + // Create buckets + createBucket(t, client, sourceBucketName) + defer deleteBucket(t, client, sourceBucketName) + createBucket(t, client, destBucketName) + defer deleteBucket(t, client, destBucketName) + + // Put source object + sourceKey := "source" + sourceContent := string(generateRandomData(10)) + putObject(t, client, sourceBucketName, sourceKey, sourceContent) + + // Create multipart upload + destKey := "mymultipartcopy" + createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + }) + require.NoError(t, err) + uploadID := *createResp.UploadId + + // Upload part copy without range (should copy entire object) + copySource := createCopySource(sourceBucketName, sourceKey) + copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + PartNumber: aws.Int32(1), + CopySource: aws.String(copySource), + }) + require.NoError(t, err) + + // Complete multipart upload + _, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: []types.CompletedPart{ + { + ETag: copyResp.CopyPartResult.ETag, + PartNumber: aws.Int32(1), + }, + }, + }, + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, destBucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) + require.NotNil(t, resp.ContentLength) + assert.Equal(t, int64(10), *resp.ContentLength) +} + +// TestMultipartCopySpecialNames tests multipart copying with special character names +func TestMultipartCopySpecialNames(t *testing.T) { + client := getS3Client(t) + + // Clean up any leftover buckets from previous test runs + cleanupTestBuckets(t, client) + + sourceBucketName := getNewBucketName() + destBucketName := getNewBucketName() + + // Create buckets + createBucket(t, client, sourceBucketName) + defer deleteBucket(t, client, sourceBucketName) + createBucket(t, client, destBucketName) + defer deleteBucket(t, client, destBucketName) + + // Test with special key names + specialKeys := []string{" ", "_", "__", "?versionId"} + sourceContent := "x" // 1 byte + destKey := "mymultipart" + + for i, sourceKey := range specialKeys { + t.Run(fmt.Sprintf("special_key_%d", i), func(t *testing.T) { + // Put source object + putObject(t, client, sourceBucketName, sourceKey, sourceContent) + + // Create multipart upload + createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + }) + require.NoError(t, err) + uploadID := *createResp.UploadId + + // Upload part copy + copySource := createCopySource(sourceBucketName, sourceKey) + copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + PartNumber: aws.Int32(1), + CopySource: aws.String(copySource), + CopySourceRange: aws.String("bytes=0-0"), + }) + require.NoError(t, err) + + // Complete multipart upload + _, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: []types.CompletedPart{ + { + ETag: copyResp.CopyPartResult.ETag, + PartNumber: aws.Int32(1), + }, + }, + }, + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, destBucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) + require.NotNil(t, resp.ContentLength) + assert.Equal(t, int64(1), *resp.ContentLength) + }) + } +} + +// TestMultipartCopyMultipleSizes tests multipart copying with various file sizes +func TestMultipartCopyMultipleSizes(t *testing.T) { + client := getS3Client(t) + + // Clean up any leftover buckets from previous test runs + cleanupTestBuckets(t, client) + + sourceBucketName := getNewBucketName() + destBucketName := getNewBucketName() + + // Create buckets + createBucket(t, client, sourceBucketName) + defer deleteBucket(t, client, sourceBucketName) + createBucket(t, client, destBucketName) + defer deleteBucket(t, client, destBucketName) + + // Put source object (12MB) + sourceKey := "foo" + sourceSize := 12 * 1024 * 1024 + sourceContent := generateRandomData(sourceSize) + _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(sourceBucketName), + Key: aws.String(sourceKey), + Body: bytes.NewReader(sourceContent), + }) + require.NoError(t, err) + + destKey := "mymultipart" + partSize := 5 * 1024 * 1024 // 5MB parts + + // Test different copy sizes + testSizes := []int{ + 5 * 1024 * 1024, // 5MB + 5*1024*1024 + 100*1024, // 5MB + 100KB + 5*1024*1024 + 600*1024, // 5MB + 600KB + 10*1024*1024 + 100*1024, // 10MB + 100KB + 10*1024*1024 + 600*1024, // 10MB + 600KB + 10 * 1024 * 1024, // 10MB + } + + for _, size := range testSizes { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + // Create multipart upload + createResp, err := client.CreateMultipartUpload(context.TODO(), &s3.CreateMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + }) + require.NoError(t, err) + uploadID := *createResp.UploadId + + // Upload parts + var parts []types.CompletedPart + copySource := createCopySource(sourceBucketName, sourceKey) + + for i := 0; i < size; i += partSize { + partNum := int32(len(parts) + 1) + endOffset := i + partSize - 1 + if endOffset >= size { + endOffset = size - 1 + } + + copyRange := fmt.Sprintf("bytes=%d-%d", i, endOffset) + copyResp, err := client.UploadPartCopy(context.TODO(), &s3.UploadPartCopyInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + PartNumber: aws.Int32(partNum), + CopySource: aws.String(copySource), + CopySourceRange: aws.String(copyRange), + }) + require.NoError(t, err) + + parts = append(parts, types.CompletedPart{ + ETag: copyResp.CopyPartResult.ETag, + PartNumber: aws.Int32(partNum), + }) + } + + // Complete multipart upload + _, err = client.CompleteMultipartUpload(context.TODO(), &s3.CompleteMultipartUploadInput{ + Bucket: aws.String(destBucketName), + Key: aws.String(destKey), + UploadId: aws.String(uploadID), + MultipartUpload: &types.CompletedMultipartUpload{ + Parts: parts, + }, + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, destBucketName, destKey) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + resp.Body.Close() + + require.NotNil(t, resp.ContentLength) + assert.Equal(t, int64(size), *resp.ContentLength) + assert.Equal(t, sourceContent[:size], body) + }) + } +} + +// TestCopyObjectIfMatchGood tests copying with matching ETag condition +func TestCopyObjectIfMatchGood(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Put source object + sourceKey := "foo" + sourceContent := "bar" + putResp := putObject(t, client, bucketName, sourceKey, sourceContent) + + // Copy object with matching ETag + destKey := "bar" + copySource := createCopySource(bucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + CopySourceIfMatch: putResp.ETag, + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, bucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) +} + +// TestCopyObjectIfNoneMatchFailed tests copying with non-matching ETag condition +func TestCopyObjectIfNoneMatchFailed(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Put source object + sourceKey := "foo" + sourceContent := "bar" + putObject(t, client, bucketName, sourceKey, sourceContent) + + // Copy object with non-matching ETag (should succeed) + destKey := "bar" + copySource := createCopySource(bucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + CopySourceIfNoneMatch: aws.String("ABCORZ"), + }) + require.NoError(t, err) + + // Verify the copied object + resp := getObject(t, client, bucketName, destKey) + body := getObjectBody(t, resp) + assert.Equal(t, sourceContent, body) +} + +// TestCopyObjectIfMatchFailed tests copying with non-matching ETag condition (should fail) +func TestCopyObjectIfMatchFailed(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Put source object + sourceKey := "foo" + sourceContent := "bar" + putObject(t, client, bucketName, sourceKey, sourceContent) + + // Copy object with non-matching ETag (should fail) + destKey := "bar" + copySource := createCopySource(bucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + CopySourceIfMatch: aws.String("ABCORZ"), + }) + + // Should fail with precondition failed + require.Error(t, err) + // Note: We could check for specific error types, but SeaweedFS might return different error codes +} + +// TestCopyObjectIfNoneMatchGood tests copying with matching ETag condition (should fail) +func TestCopyObjectIfNoneMatchGood(t *testing.T) { + client := getS3Client(t) + bucketName := getNewBucketName() + + // Create bucket + createBucket(t, client, bucketName) + defer deleteBucket(t, client, bucketName) + + // Put source object + sourceKey := "foo" + sourceContent := "bar" + putResp := putObject(t, client, bucketName, sourceKey, sourceContent) + + // Copy object with matching ETag for IfNoneMatch (should fail) + destKey := "bar" + copySource := createCopySource(bucketName, sourceKey) + _, err := client.CopyObject(context.TODO(), &s3.CopyObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(destKey), + CopySource: aws.String(copySource), + CopySourceIfNoneMatch: putResp.ETag, + }) + + // Should fail with precondition failed + require.Error(t, err) +} |
