Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
import com.amazonaws.services.s3.model.CanonicalGrantee;
import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest;
import com.amazonaws.services.s3.model.CompleteMultipartUploadResult;
import com.amazonaws.services.s3.model.CopyObjectRequest;
import com.amazonaws.services.s3.model.CopyObjectResult;
import com.amazonaws.services.s3.model.CreateBucketRequest;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.GetObjectRequest;
Expand Down Expand Up @@ -494,6 +496,105 @@ public void testPutObjectIfMatchMissingKeyFail() {
assertEquals("NoSuchKey", missingKey.getErrorCode());
}

@Test
public void testCopyObject() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(sourceBucketName);
s3Client.createBucket(destBucketName);

InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata());
assertEquals("37b51d194a7513e45b56f6524f2d51f2", putResult.getETag());

CopyObjectResult copyResult = s3Client.copyObject(sourceBucketName, sourceKey, destBucketName, destKey);
assertEquals("37b51d194a7513e45b56f6524f2d51f2", copyResult.getETag());
}

@Test
public void testCopyObjectWithSourceIfMatch() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(sourceBucketName);
s3Client.createBucket(destBucketName);

InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata());
String sourceETag = putResult.getETag();

CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey)
.withMatchingETagConstraint(sourceETag);
CopyObjectResult copyResult = s3Client.copyObject(copyRequest);
assertEquals(sourceETag, copyResult.getETag());
}

@Test
public void testCopyObjectWithSourceIfMatchFail() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(sourceBucketName);
s3Client.createBucket(destBucketName);

InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata());

CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey)
.withMatchingETagConstraint("wrong-etag");

CopyObjectResult copyResult = s3Client.copyObject(copyRequest);
assertNull(copyResult);
}

@Test
public void testCopyObjectWithSourceIfNoneMatch() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(sourceBucketName);
s3Client.createBucket(destBucketName);

InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata());
String sourceETag = putResult.getETag();

CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey)
.withNonmatchingETagConstraint("different-etag");
CopyObjectResult copyResult = s3Client.copyObject(copyRequest);
assertEquals(sourceETag, copyResult.getETag());
}

@Test
public void testCopyObjectWithSourceIfNoneMatchFail() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(sourceBucketName);
s3Client.createBucket(destBucketName);

InputStream is = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
PutObjectResult putResult = s3Client.putObject(sourceBucketName, sourceKey, is, new ObjectMetadata());
String sourceETag = putResult.getETag();

CopyObjectRequest copyRequest = new CopyObjectRequest(sourceBucketName, sourceKey, destBucketName, destKey)
.withNonmatchingETagConstraint(sourceETag);

CopyObjectResult copyResult = s3Client.copyObject(copyRequest);
assertNull(copyResult);
}

@Test
public void testPutObjectWithMD5Header() throws Exception {
final String bucketName = getBucketName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,119 @@ public void testCopyObject() {
assertEquals("\"37b51d194a7513e45b56f6524f2d51f2\"", copyObjectResponse.copyObjectResult().eTag());
}

@Test
public void testCopyObjectWithSourceIfMatch() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(b -> b.bucket(sourceBucketName));
s3Client.createBucket(b -> b.bucket(destBucketName));

PutObjectResponse putObjectResponse = s3Client.putObject(b -> b
.bucket(sourceBucketName)
.key(sourceKey),
RequestBody.fromString(content));

String sourceETag = putObjectResponse.eTag();

CopyObjectRequest copyReq = CopyObjectRequest.builder()
.sourceBucket(sourceBucketName)
.sourceKey(sourceKey)
.destinationBucket(destBucketName)
.destinationKey(destKey)
.copySourceIfMatch(sourceETag)
.build();

CopyObjectResponse copyObjectResponse = s3Client.copyObject(copyReq);
assertEquals(sourceETag, copyObjectResponse.copyObjectResult().eTag());
}

@Test
public void testCopyObjectWithSourceIfMatchFail() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(b -> b.bucket(sourceBucketName));
s3Client.createBucket(b -> b.bucket(destBucketName));

s3Client.putObject(b -> b.bucket(sourceBucketName).key(sourceKey), RequestBody.fromString(content));

CopyObjectRequest copyReq = CopyObjectRequest.builder()
.sourceBucket(sourceBucketName)
.sourceKey(sourceKey)
.destinationBucket(destBucketName)
.destinationKey(destKey)
.copySourceIfMatch("wrong-etag")
.build();

S3Exception exception = assertThrows(S3Exception.class, () -> s3Client.copyObject(copyReq));
assertEquals(412, exception.statusCode());
assertEquals("PreconditionFailed", exception.awsErrorDetails().errorCode());
}

@Test
public void testCopyObjectWithSourceIfNoneMatch() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(b -> b.bucket(sourceBucketName));
s3Client.createBucket(b -> b.bucket(destBucketName));

PutObjectResponse putObjectResponse = s3Client.putObject(b -> b
.bucket(sourceBucketName)
.key(sourceKey),
RequestBody.fromString(content));

String sourceETag = putObjectResponse.eTag();

CopyObjectRequest copyReq = CopyObjectRequest.builder()
.sourceBucket(sourceBucketName)
.sourceKey(sourceKey)
.destinationBucket(destBucketName)
.destinationKey(destKey)
.copySourceIfNoneMatch("different-etag")
.build();

CopyObjectResponse copyObjectResponse = s3Client.copyObject(copyReq);
assertEquals(sourceETag, copyObjectResponse.copyObjectResult().eTag());
}

@Test
public void testCopyObjectWithSourceIfNoneMatchFail() {
final String sourceBucketName = getBucketName("source");
final String destBucketName = getBucketName("dest");
final String sourceKey = getKeyName("source");
final String destKey = getKeyName("dest");
final String content = "bar";
s3Client.createBucket(b -> b.bucket(sourceBucketName));
s3Client.createBucket(b -> b.bucket(destBucketName));

PutObjectResponse putObjectResponse = s3Client.putObject(b -> b
.bucket(sourceBucketName)
.key(sourceKey),
RequestBody.fromString(content));

String sourceETag = putObjectResponse.eTag();

CopyObjectRequest copyReq = CopyObjectRequest.builder()
.sourceBucket(sourceBucketName)
.sourceKey(sourceKey)
.destinationBucket(destBucketName)
.destinationKey(destKey)
.copySourceIfNoneMatch(sourceETag)
.build();

S3Exception exception = assertThrows(S3Exception.class, () -> s3Client.copyObject(copyReq));
assertEquals(412, exception.statusCode());
assertEquals("PreconditionFailed", exception.awsErrorDetails().errorCode());
}

@Test
public void testLowLevelMultipartUpload(@TempDir Path tempDir) throws Exception {
final String bucketName = getBucketName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -976,20 +976,23 @@ void copy(OzoneVolume volume, DigestInputStream src, long srcKeyLen,
ReplicationConfig replication,
Map<String, String> metadata,
PerformanceStringBuilder perf, long startNanos,
Map<String, String> tags)
Map<String, String> tags,
S3ConditionalRequest.WriteConditions writeConditions)
throws IOException {
long copyLength;

if (isDatastreamEnabled() && !(replication != null &&
replication.getReplicationType() == EC) &&
srcKeyLen > getDatastreamMinLength()) {
perf.appendStreamMode();
copyLength = ObjectEndpointStreaming
.copyKeyWithStream(volume.getBucket(destBucket), destKey, srcKeyLen,
getChunkSize(), replication, metadata, src, perf, startNanos, tags);
getChunkSize(), replication, metadata, src, perf, startNanos, tags,
writeConditions);
} else {
try (OzoneOutputStream dest = getClientProtocol()
.createKey(volume.getName(), destBucket, destKey, srcKeyLen,
replication, metadata, tags)) {
try (OzoneOutputStream dest = openKeyForPut(
volume.getName(), destBucket, destKey, srcKeyLen,
replication, metadata, tags, writeConditions)) {
long metadataLatencyNs =
getMetrics().updateCopyKeyMetadataStats(startNanos);
perf.appendMetaLatencyNanos(metadataLatencyNs);
Expand Down Expand Up @@ -1050,6 +1053,14 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
return copyObjectResponse;
}
}

String sourceKeyPath = sourceBucket + "/" + sourceKey;
S3ConditionalRequest.evaluateCopySourcePreconditions(
getHeaders(), sourceKeyPath, sourceKeyDetails);

S3ConditionalRequest.WriteConditions writeConditions =
S3ConditionalRequest.parseWriteConditions(getHeaders(), destkey);

long sourceKeyLen = sourceKeyDetails.getDataSize();

// Object tagging in copyObject with tagging directive
Expand Down Expand Up @@ -1091,7 +1102,7 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
getMetrics().updateCopyKeyMetadataStats(startNanos);
sourceDigestInputStream = new DigestInputStream(src, getMD5DigestInstance());
copy(volume, sourceDigestInputStream, sourceKeyLen, destkey, destBucket, replicationConfig,
customMetadata, perf, startNanos, tags);
customMetadata, perf, startNanos, tags, writeConditions);
}

final OzoneKeyDetails destKeyDetails = getClientProtocol().getKeyDetails(
Expand All @@ -1104,9 +1115,17 @@ private CopyObjectResponse copyObject(OzoneVolume volume,
return copyObjectResponse;
} catch (OMException ex) {
if (ex.getResult() == ResultCodes.KEY_NOT_FOUND) {
if (getHeaders().getHeaderString(S3Consts.IF_MATCH_HEADER) != null) {
throw newError(PRECOND_FAILED, destkey, ex);
}
throw newError(S3ErrorTable.NO_SUCH_KEY, sourceKey, ex);
} else if (ex.getResult() == ResultCodes.BUCKET_NOT_FOUND) {
throw newError(S3ErrorTable.NO_SUCH_BUCKET, sourceBucket, ex);
} else if (ex.getResult() == ResultCodes.ATOMIC_WRITE_CONFLICT
|| ex.getResult() == ResultCodes.KEY_ALREADY_EXISTS
|| ex.getResult() == ResultCodes.ETAG_MISMATCH
|| ex.getResult() == ResultCodes.ETAG_NOT_AVAILABLE) {
throw newError(PRECOND_FAILED, destkey, ex);
}
throw newError(destBucket + "/" + destkey, ex);
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,13 @@ public static long copyKeyWithStream(
ReplicationConfig replicationConfig,
Map<String, String> keyMetadata,
DigestInputStream body, PerformanceStringBuilder perf, long startNanos,
Map<String, String> tags)
Map<String, String> tags,
S3ConditionalRequest.WriteConditions writeConditions)
throws IOException {
long writeLen;
try (OzoneDataStreamOutput streamOutput = bucket.createStreamKey(keyPath,
length, replicationConfig, keyMetadata, tags)) {
try (OzoneDataStreamOutput streamOutput = openStreamKeyForPut(bucket,
keyPath, length, replicationConfig, keyMetadata, tags,
writeConditions)) {
long metadataLatencyNs =
METRICS.updateCopyKeyMetadataStats(startNanos);
writeLen = writeToStreamOutput(streamOutput, body, bufferSize, length);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,46 @@ static Response evaluateReadPreconditions(HttpHeaders headers,
return null;
}

/**
* Evaluates copy source preconditions for CopyObject operation.
* Checks x-amz-copy-source-if-match, x-amz-copy-source-if-none-match,
* x-amz-copy-source-if-modified-since, x-amz-copy-source-if-unmodified-since.
*
* @param headers HTTP headers containing the conditional headers
* @param sourceKeyPath path of the source key for error messages
* @param sourceKey the source key metadata
* @throws OS3Exception with 412 Precondition Failed if conditions are not met
*/
static void evaluateCopySourcePreconditions(HttpHeaders headers,
Comment thread
YutaLin marked this conversation as resolved.
Outdated
String sourceKeyPath, OzoneKey sourceKey) throws OS3Exception {
String currentETag = sourceKey.getMetadata().get(OzoneConsts.ETAG);

String ifMatch = headers.getHeaderString(S3Consts.COPY_SOURCE_IF_MATCH);
if (ifMatch != null && !eTagMatches(ifMatch, currentETag)) {
throw newError(PRECOND_FAILED, sourceKeyPath);
}

String ifUnmodifiedSince = headers.getHeaderString(
S3Consts.COPY_SOURCE_IF_UNMODIFIED_SINCE);
if (ifMatch == null && ifUnmodifiedSince != null
&& !matchesIfUnmodifiedSince(sourceKey, ifUnmodifiedSince)) {
throw newError(PRECOND_FAILED, sourceKeyPath);
}

String ifNoneMatch = headers.getHeaderString(
S3Consts.COPY_SOURCE_IF_NONE_MATCH);
if (ifNoneMatch != null && eTagMatches(ifNoneMatch, currentETag)) {
throw newError(PRECOND_FAILED, sourceKeyPath);
}

String ifModifiedSince = headers.getHeaderString(
S3Consts.COPY_SOURCE_IF_MODIFIED_SINCE);
if (ifNoneMatch == null && ifModifiedSince != null
&& !matchesIfModifiedSince(sourceKey, ifModifiedSince)) {
throw newError(PRECOND_FAILED, sourceKeyPath);
}
}

static boolean checkCopySourceModificationTime(Long lastModificationTime,
String copySourceIfModifiedSinceStr,
String copySourceIfUnmodifiedSinceStr) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public final class S3Consts {

// Constants related to Range Header
public static final String COPY_SOURCE_IF_PREFIX = "x-amz-copy-source-if-";
public static final String COPY_SOURCE_IF_MATCH =
COPY_SOURCE_IF_PREFIX + "match";
public static final String COPY_SOURCE_IF_NONE_MATCH =
COPY_SOURCE_IF_PREFIX + "none-match";
public static final String COPY_SOURCE_IF_MODIFIED_SINCE =
COPY_SOURCE_IF_PREFIX + "modified-since";
public static final String COPY_SOURCE_IF_UNMODIFIED_SINCE =
Expand Down
Loading