Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
1082 lines
34 KiB
Go
1082 lines
34 KiB
Go
package copying_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
mathrand "math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"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
|
|
MasterEndpoint 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
|
|
MasterEndpoint: "http://127.0.0.1:9333", // Default SeaweedFS master HTTP port
|
|
AccessKey: "some_access_key1",
|
|
SecretKey: "some_secret_key1",
|
|
Region: "us-east-1",
|
|
BucketPrefix: "test-copying-",
|
|
UseSSL: false,
|
|
SkipVerifySSL: true,
|
|
}
|
|
|
|
func init() {
|
|
mathrand.Seed(time.Now().UnixNano())
|
|
if endpoint := os.Getenv("S3_ENDPOINT"); endpoint != "" {
|
|
defaultConfig.Endpoint = endpoint
|
|
}
|
|
if masterEndpoint := os.Getenv("MASTER_ENDPOINT"); masterEndpoint != "" {
|
|
defaultConfig.MasterEndpoint = masterEndpoint
|
|
}
|
|
}
|
|
|
|
// 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("%sr%s-%d-%d", defaultConfig.BucketPrefix, testRunID, timestamp, randomSuffix)
|
|
}
|
|
|
|
// testRunID uniquely identifies this `go test` invocation. It's embedded into
|
|
// every bucket created by getNewBucketName (after defaultConfig.BucketPrefix), so
|
|
// the cleanup sweep can scope deletions to buckets that belong to this run only —
|
|
// letting concurrent runs against the same endpoints coexist without tearing each
|
|
// other's buckets down. Stale buckets left behind by a crashed prior run share the
|
|
// family prefix but a different runID, so they are not swept here; `make clean`
|
|
// (or wiping the data dir) is the right cleanup for that case.
|
|
var testRunID = strconv.FormatInt(time.Now().UnixNano(), 36)
|
|
|
|
// cleanupTestBuckets removes any leftover test buckets from this run.
|
|
// Buckets from concurrent runs carry a different runID marker and are skipped.
|
|
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
|
|
}
|
|
|
|
runPrefix := defaultConfig.BucketPrefix + "r" + testRunID + "-"
|
|
for _, bucket := range resp.Buckets {
|
|
bucketName := *bucket.Name
|
|
// Only sweep buckets owned by this run (prefix + runID marker).
|
|
if strings.HasPrefix(bucketName, runPrefix) {
|
|
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) {
|
|
// Sweep stale buckets from prior tests/runs so each new bucket starts on a
|
|
// fresh slate. Without this, leaked collection volumes from a panicked or
|
|
// interrupted earlier run accumulate on a single `weed mini` data node and
|
|
// the suite eventually exhausts its volume slots.
|
|
cleanupTestBuckets(t, client)
|
|
// 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.
|
|
// Always force-drops the underlying collection at the master afterwards: the S3
|
|
// DeleteBucket can race with concurrent `volume_grow` requests (the warm-create
|
|
// batch keeps registering volumes after the master's collection-delete sweep has
|
|
// already snapshotted the layout), so 1-3 volumes per bucket can leak. Without
|
|
// this, running enough tests on a single `weed mini` server exhausts the data
|
|
// node's volume slots and every subsequent PutObject 500s with "Not enough data
|
|
// nodes found".
|
|
func deleteBucket(t *testing.T, client *s3.Client, bucketName string) {
|
|
defer forceDeleteCollection(t, bucketName)
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// forceDeleteCollection drops the SeaweedFS collection backing a test bucket via the master's
|
|
// /col/delete admin endpoint. The S3 layer normally drops the collection on DeleteBucket, but
|
|
// in-flight `volume_grow` requests can register volumes after the master's first sweep, leaking
|
|
// them. Best-effort: a 400 from the master means the collection was already gone, which is the
|
|
// success path and not an error.
|
|
func forceDeleteCollection(t *testing.T, bucketName string) {
|
|
if defaultConfig.MasterEndpoint == "" {
|
|
return
|
|
}
|
|
endpoint := strings.TrimRight(defaultConfig.MasterEndpoint, "/") + "/col/delete?collection=" + url.QueryEscape(bucketName)
|
|
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
t.Logf("Note: building collection delete request for %s failed: %v", bucketName, err)
|
|
return
|
|
}
|
|
httpClient := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
t.Logf("Note: force-delete collection %s failed: %v", bucketName, err)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
switch resp.StatusCode {
|
|
case http.StatusNoContent:
|
|
t.Logf("Force-deleted collection %s", bucketName)
|
|
case http.StatusBadRequest:
|
|
// Collection already gone - normal path when DeleteBucket succeeded.
|
|
default:
|
|
t.Logf("Note: force-delete collection %s returned HTTP %d", bucketName, resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|