Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -392,10 +392,12 @@ public Response putObject(@PathParam("bucket") String bucket,
byte[] data = decodeAwsChunked(body, contentEncoding, contentSha256);
validateChecksumHeaders(httpHeaders, data);
String persistedEncoding = toPersistedContentEncoding(contentEncoding);
String cacheControl = httpHeaders.getHeaderString("Cache-Control");
S3Object obj = s3Service.putObject(bucket, key, data, contentType, extractUserMetadata(httpHeaders),
httpHeaders.getHeaderString("x-amz-storage-class"),
persistedEncoding,
lockMode, retainUntil, legalHold);
lockMode, retainUntil, legalHold,
cacheControl);
var resp = Response.ok().header("ETag", obj.getETag());
if (obj.getVersionId() != null) {
resp.header("x-amz-version-id", obj.getVersionId());
Expand Down Expand Up @@ -1287,6 +1289,9 @@ private void appendObjectHeaders(Response.ResponseBuilder resp, S3Object obj) {
if (obj.getContentEncoding() != null) {
resp.header("Content-Encoding", obj.getContentEncoding());
}
if (obj.getCacheControl() != null) {
resp.header("Cache-Control", obj.getCacheControl());
}
if (obj.getMetadata() != null) {
for (Map.Entry<String, String> entry : obj.getMetadata().entrySet()) {
resp.header("x-amz-meta-" + entry.getKey(), entry.getValue());
Expand Down Expand Up @@ -1331,12 +1336,14 @@ private Response handleCopyObject(String copySource, String destBucket, String d
String sourceKey = decodedSource.substring(slashIndex + 1);

String copyContentEncoding = toPersistedContentEncoding(httpHeaders.getHeaderString("Content-Encoding"));
String copyCacheControl = httpHeaders.getHeaderString("Cache-Control");
S3Object copy = s3Service.copyObject(sourceBucket, sourceKey, destBucket, destKey,
httpHeaders.getHeaderString("x-amz-metadata-directive"),
extractUserMetadata(httpHeaders),
httpHeaders.getHeaderString("x-amz-storage-class"),
contentType,
copyContentEncoding);
copyContentEncoding,
copyCacheControl);
String xml = new XmlBuilder()
.raw("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
.start("CopyObjectResult", AwsNamespaces.S3)
Expand Down
29 changes: 17 additions & 12 deletions src/main/java/io/github/hectorvent/floci/services/s3/S3Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -184,29 +184,30 @@ public List<Bucket> listBuckets() {

public S3Object putObject(String bucketName, String key, byte[] data,
String contentType, Map<String, String> metadata) {
return putObject(bucketName, key, data, contentType, metadata, null, null, null, null);
return putObject(bucketName, key, data, contentType, metadata, null, null, null, null, null, null);
}

public S3Object putObject(String bucketName, String key, byte[] data,
String contentType, Map<String, String> metadata,
String objectLockMode, Instant retainUntilDate, String legalHoldStatus) {
return putObject(bucketName, key, data, contentType, metadata, null,
objectLockMode, retainUntilDate, legalHoldStatus);
return putObject(bucketName, key, data, contentType, metadata, null, null,
objectLockMode, retainUntilDate, legalHoldStatus, null);
}

public S3Object putObject(String bucketName, String key, byte[] data,
String contentType, Map<String, String> metadata, String storageClass,
String objectLockMode, Instant retainUntilDate, String legalHoldStatus) {
return putObject(bucketName, key, data, contentType, metadata, storageClass, null,
objectLockMode, retainUntilDate, legalHoldStatus);
objectLockMode, retainUntilDate, legalHoldStatus, null);
}

public S3Object putObject(String bucketName, String key, byte[] data,
String contentType, Map<String, String> metadata, String storageClass,
String contentEncoding,
String objectLockMode, Instant retainUntilDate, String legalHoldStatus) {
String objectLockMode, Instant retainUntilDate, String legalHoldStatus,
String cacheControl) {
S3Object object = storeObject(bucketName, key, data, contentType, metadata, storageClass, null, null,
objectLockMode, retainUntilDate, legalHoldStatus, contentEncoding);
objectLockMode, retainUntilDate, legalHoldStatus, contentEncoding, cacheControl);
fireNotifications(bucketName, key, "ObjectCreated:Put", object);
return object;
}
Expand All @@ -217,22 +218,22 @@ public S3Object putObject(String bucketName, String key, byte[] data,
private S3Object storeObject(String bucketName, String key, byte[] data,
String contentType, Map<String, String> metadata) {
return storeObject(bucketName, key, data, contentType, metadata, null, null, null,
null, null, null, null);
null, null, null, null, null);
}

private S3Object storeObject(String bucketName, String key, byte[] data,
String contentType, Map<String, String> metadata, String storageClass,
S3Checksum checksum, List<Part> parts,
String objectLockMode, Instant retainUntilDate, String legalHoldStatus) {
return storeObject(bucketName, key, data, contentType, metadata, storageClass, checksum, parts,
objectLockMode, retainUntilDate, legalHoldStatus, null);
objectLockMode, retainUntilDate, legalHoldStatus, null, null);
}

private S3Object storeObject(String bucketName, String key, byte[] data,
String contentType, Map<String, String> metadata, String storageClass,
S3Checksum checksum, List<Part> parts,
String objectLockMode, Instant retainUntilDate, String legalHoldStatus,
String contentEncoding) {
String contentEncoding, String cacheControl) {
Bucket bucket = bucketStore.get(bucketName)
.orElseThrow(() -> new AwsException("NoSuchBucket",
"The specified bucket does not exist.", 404));
Expand All @@ -245,6 +246,7 @@ private S3Object storeObject(String bucketName, String key, byte[] data,
object.setChecksum(checksum != null ? copyChecksum(checksum) : buildChecksum(data, parts, false));
object.setParts(copyParts(parts));
object.setContentEncoding(contentEncoding);
object.setCacheControl(cacheControl);

if (bucket.isVersioningEnabled()) {
String versionId = UUID.randomUUID().toString();
Expand Down Expand Up @@ -597,13 +599,14 @@ public S3Object copyObject(String sourceBucket, String sourceKey,
String metadataDirective, Map<String, String> replacementMetadata,
String storageClass, String contentType) {
return copyObject(sourceBucket, sourceKey, destBucket, destKey, metadataDirective,
replacementMetadata, storageClass, contentType, null);
replacementMetadata, storageClass, contentType, null, null);
}

public S3Object copyObject(String sourceBucket, String sourceKey,
String destBucket, String destKey,
String metadataDirective, Map<String, String> replacementMetadata,
String storageClass, String contentType, String contentEncoding) {
String storageClass, String contentType, String contentEncoding,
String cacheControl) {
S3Object source = getObject(sourceBucket, sourceKey);
ensureBucketExists(destBucket);

Expand All @@ -616,9 +619,10 @@ public S3Object copyObject(String sourceBucket, String sourceKey,
String effectiveContentType = replaceMetadata && contentType != null ? contentType : source.getContentType();
String effectiveStorageClass = storageClass != null ? storageClass : source.getStorageClass();
String effectiveContentEncoding = replaceMetadata && contentEncoding != null ? contentEncoding : source.getContentEncoding();
String effectiveCacheControl = replaceMetadata && cacheControl != null ? cacheControl : source.getCacheControl();
S3Object copy = storeObject(destBucket, destKey, source.getData(), effectiveContentType, metadata,
effectiveStorageClass, source.getChecksum(), source.getParts(), null, null, null,
effectiveContentEncoding);
effectiveContentEncoding, effectiveCacheControl);
copy.setETag(source.getETag());
LOG.debugv("Copied object: {0}/{1} -> {2}/{3}", sourceBucket, sourceKey, destBucket, destKey);
fireNotifications(destBucket, destKey, "ObjectCreated:Copy", copy);
Expand Down Expand Up @@ -1560,6 +1564,7 @@ private static S3Object copyObject(S3Object source) {
copy.setMetadata(new HashMap<>(source.getMetadata()));
copy.setContentType(source.getContentType());
copy.setContentEncoding(source.getContentEncoding());
copy.setCacheControl(source.getCacheControl());
copy.setSize(source.getSize());
copy.setLastModified(source.getLastModified());
copy.setETag(source.getETag());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class S3Object {
private Map<String, String> metadata;
private String contentType;
private String contentEncoding;
private String cacheControl;
private long size;
private Instant lastModified;
private String eTag;
Expand Down Expand Up @@ -81,6 +82,9 @@ public S3Object(String bucketName, String key, byte[] data, String contentType)
public String getContentEncoding() { return contentEncoding; }
public void setContentEncoding(String contentEncoding) { this.contentEncoding = contentEncoding; }

public String getCacheControl() { return cacheControl; }
public void setCacheControl(String cacheControl) { this.cacheControl = cacheControl; }

public long getSize() { return size; }
public void setSize(long size) { this.size = size; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,98 @@ void cleanupContentEncodingBucket() {
given().delete("/encoding-test-bucket");
}

// --- Cache-Control header preservation ---

@Test
@Order(89)
void createCacheControlBucketAndPutObject() {
given()
.put("/cache-control-bucket")
.then()
.statusCode(200);

given()
.contentType("text/plain")
.header("Cache-Control", "public, max-age=31536000")
.body("cached-content")
.when()
.put("/cache-control-bucket/cached.txt")
.then()
.statusCode(200)
.header("ETag", notNullValue());
}

@Test
@Order(90)
void getObjectReturnsCacheControl() {
given()
.when()
.get("/cache-control-bucket/cached.txt")
.then()
.statusCode(200)
.header("Cache-Control", equalTo("public, max-age=31536000"));
}

@Test
@Order(90)
void headObjectReturnsCacheControl() {
given()
.when()
.head("/cache-control-bucket/cached.txt")
.then()
.statusCode(200)
.header("Cache-Control", equalTo("public, max-age=31536000"));
}

@Test
@Order(91)
void copyObjectPreservesCacheControl() {
given()
.header("x-amz-copy-source", "/cache-control-bucket/cached.txt")
.when()
.put("/cache-control-bucket/cached-copy.txt")
.then()
.statusCode(200)
.body(containsString("CopyObjectResult"));

given()
.when()
.head("/cache-control-bucket/cached-copy.txt")
.then()
.statusCode(200)
.header("Cache-Control", equalTo("public, max-age=31536000"));
}

@Test
@Order(91)
void copyObjectReplaceCacheControl() {
given()
.header("x-amz-copy-source", "/cache-control-bucket/cached.txt")
.header("x-amz-metadata-directive", "REPLACE")
.header("Cache-Control", "no-cache")
.when()
.put("/cache-control-bucket/cached-nocache.txt")
.then()
.statusCode(200)
.body(containsString("CopyObjectResult"));

given()
.when()
.head("/cache-control-bucket/cached-nocache.txt")
.then()
.statusCode(200)
.header("Cache-Control", equalTo("no-cache"));
}

@Test
@Order(92)
void cleanupCacheControlBucket() {
given().delete("/cache-control-bucket/cached.txt");
given().delete("/cache-control-bucket/cached-copy.txt");
given().delete("/cache-control-bucket/cached-nocache.txt");
given().delete("/cache-control-bucket");
}

// --- S3 Notification Configuration with Filter ---

@Test
Expand Down