Skip to content

Commit ca4c96a

Browse files
authored
blobstore: Enable timestamp data in BlobInfo in List api (#201)
1 parent 7a7efd0 commit ca4c96a

File tree

9 files changed

+368
-3
lines changed

9 files changed

+368
-3
lines changed

blob/blob-aws/src/main/java/com/salesforce/multicloudj/blob/aws/AwsTransformer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ public BlobInfo toInfo(S3Object s3) {
112112
return new BlobInfo.Builder()
113113
.withKey(s3.key())
114114
.withObjectSize(s3.size())
115+
.withLastModified(s3.lastModified())
115116
.build();
116117
}
117118

blob/blob-aws/src/main/java/com/salesforce/multicloudj/blob/aws/BlobInfoIterator.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private List<BlobInfo> nextBatch() {
6060
.map(s3Obj -> new BlobInfo.Builder()
6161
.withKey(s3Obj.key())
6262
.withObjectSize(s3Obj.size())
63+
.withLastModified(s3Obj.lastModified())
6364
.build())
6465
.collect(toList());
6566
}

blob/blob-aws/src/test/java/com/salesforce/multicloudj/blob/aws/AwsTransformerTest.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,16 @@ void testListBlobsBatch() {
185185

186186
@Test
187187
void testToInfo() {
188-
var s3 = S3Object.builder().key("some/key/path.file").size(1024L).build();
188+
Instant lastModified = Instant.now();
189+
var s3 = S3Object.builder()
190+
.key("some/key/path.file")
191+
.size(1024L)
192+
.lastModified(lastModified)
193+
.build();
189194
var info = transformer.toInfo(s3);
190195
assertEquals(s3.key(), info.getKey());
191196
assertEquals(s3.size(), info.getObjectSize());
197+
assertEquals(lastModified, info.getLastModified());
192198
}
193199

194200
@Test
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.salesforce.multicloudj.blob.aws;
2+
3+
import com.salesforce.multicloudj.blob.driver.BlobInfo;
4+
import com.salesforce.multicloudj.blob.driver.ListBlobsRequest;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
import software.amazon.awssdk.services.s3.S3Client;
8+
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
9+
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
10+
import software.amazon.awssdk.services.s3.model.S3Object;
11+
12+
import java.time.Instant;
13+
import java.util.Iterator;
14+
import java.util.List;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertNotNull;
18+
import static org.junit.jupiter.api.Assertions.assertTrue;
19+
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
22+
23+
/**
24+
* Unit tests for BlobInfoIterator
25+
*/
26+
public class BlobInfoIteratorTest {
27+
28+
private S3Client mockS3Client;
29+
private static final String TEST_BUCKET = "test-bucket";
30+
31+
@BeforeEach
32+
void setUp() {
33+
mockS3Client = mock(S3Client.class);
34+
}
35+
36+
@Test
37+
void testBlobInfoIteratorIncludesTimestamp() {
38+
// Given
39+
Instant timestamp1 = Instant.now();
40+
Instant timestamp2 = timestamp1.plusSeconds(100);
41+
42+
S3Object s3Object1 = S3Object.builder()
43+
.key("test-key-1")
44+
.size(1024L)
45+
.lastModified(timestamp1)
46+
.build();
47+
48+
S3Object s3Object2 = S3Object.builder()
49+
.key("test-key-2")
50+
.size(2048L)
51+
.lastModified(timestamp2)
52+
.build();
53+
54+
ListObjectsV2Response response = ListObjectsV2Response.builder()
55+
.contents(s3Object1, s3Object2)
56+
.isTruncated(false)
57+
.build();
58+
59+
when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(response);
60+
61+
ListBlobsRequest listRequest = ListBlobsRequest.builder().build();
62+
BlobInfoIterator iterator = new BlobInfoIterator(mockS3Client, TEST_BUCKET, listRequest);
63+
64+
// When
65+
assertTrue(iterator.hasNext());
66+
BlobInfo blobInfo1 = iterator.next();
67+
assertTrue(iterator.hasNext());
68+
BlobInfo blobInfo2 = iterator.next();
69+
70+
// Then
71+
assertNotNull(blobInfo1);
72+
assertEquals("test-key-1", blobInfo1.getKey());
73+
assertEquals(1024L, blobInfo1.getObjectSize());
74+
assertEquals(timestamp1, blobInfo1.getLastModified());
75+
76+
assertNotNull(blobInfo2);
77+
assertEquals("test-key-2", blobInfo2.getKey());
78+
assertEquals(2048L, blobInfo2.getObjectSize());
79+
assertEquals(timestamp2, blobInfo2.getLastModified());
80+
}
81+
82+
@Test
83+
void testBlobInfoIteratorWithPrefix() {
84+
// Given
85+
Instant timestamp = Instant.now();
86+
S3Object s3Object = S3Object.builder()
87+
.key("prefix/test-key")
88+
.size(1024L)
89+
.lastModified(timestamp)
90+
.build();
91+
92+
ListObjectsV2Response response = ListObjectsV2Response.builder()
93+
.contents(s3Object)
94+
.isTruncated(false)
95+
.build();
96+
97+
when(mockS3Client.listObjectsV2(any(ListObjectsV2Request.class))).thenReturn(response);
98+
99+
ListBlobsRequest listRequest = ListBlobsRequest.builder()
100+
.withPrefix("prefix/")
101+
.build();
102+
BlobInfoIterator iterator = new BlobInfoIterator(mockS3Client, TEST_BUCKET, listRequest);
103+
104+
// When
105+
assertTrue(iterator.hasNext());
106+
BlobInfo blobInfo = iterator.next();
107+
108+
// Then
109+
assertNotNull(blobInfo);
110+
assertEquals("prefix/test-key", blobInfo.getKey());
111+
assertEquals(1024L, blobInfo.getObjectSize());
112+
assertEquals(timestamp, blobInfo.getLastModified());
113+
}
114+
}
115+

blob/blob-client/src/main/java/com/salesforce/multicloudj/blob/driver/BlobInfo.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.salesforce.multicloudj.blob.driver;
22

3+
import java.time.Instant;
34
import java.util.Objects;
45

56
/**
@@ -9,10 +10,12 @@ public class BlobInfo {
910

1011
private String key;
1112
private long objectSize;
13+
private Instant lastModified;
1214

1315
private BlobInfo(Builder builder) {
1416
this.key = builder.key;
1517
this.objectSize = builder.objectSize;
18+
this.lastModified = builder.lastModified;
1619
}
1720

1821
@Override
@@ -25,12 +28,12 @@ public boolean equals(Object obj) {
2528
}
2629

2730
BlobInfo blobInfo = (BlobInfo) obj;
28-
return objectSize == blobInfo.objectSize && Objects.equals(key, blobInfo.key);
31+
return objectSize == blobInfo.objectSize && Objects.equals(key, blobInfo.key) && Objects.equals(lastModified, blobInfo.lastModified);
2932
}
3033

3134
@Override
3235
public int hashCode() {
33-
return Objects.hash(key, objectSize);
36+
return Objects.hash(key, objectSize, lastModified);
3437
}
3538

3639
public String getKey() {
@@ -41,13 +44,18 @@ public long getObjectSize() {
4144
return objectSize;
4245
}
4346

47+
public Instant getLastModified() {
48+
return lastModified;
49+
}
50+
4451
public static Builder builder() {
4552
return new Builder();
4653
}
4754

4855
public static class Builder {
4956
private String key;
5057
private long objectSize;
58+
private Instant lastModified;
5159

5260
public Builder withKey(String key) {
5361
this.key = key;
@@ -59,6 +67,11 @@ public Builder withObjectSize(long objectSize) {
5967
return this;
6068
}
6169

70+
public Builder withLastModified(Instant lastModified) {
71+
this.lastModified = lastModified;
72+
return this;
73+
}
74+
6275
public BlobInfo build() {
6376
return new BlobInfo(this);
6477
}

blob/blob-client/src/test/java/com/salesforce/multicloudj/blob/client/AbstractBlobStoreIT.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.nio.file.Files;
5151
import java.nio.file.Path;
5252
import java.time.Duration;
53+
import java.time.Instant;
5354
import java.util.ArrayList;
5455
import java.util.Arrays;
5556
import java.util.Collection;
@@ -1656,6 +1657,58 @@ public void testListPage() throws IOException {
16561657
}
16571658
}
16581659

1660+
//@Test
1661+
public void testListPage_withTimeStamp() throws IOException {
1662+
1663+
// Create the BucketClient
1664+
AbstractBlobStore blobStore = harness.createBlobStore(true, true, false);
1665+
BucketClient bucketClient = new BucketClient(blobStore);
1666+
1667+
// Upload a blob to test timestamp
1668+
String baseKey = "conformance-tests/blob-for-list-page-timestamp";
1669+
String[] keys = new String[]{baseKey};
1670+
byte[] blobBytes = "Default content for this blob".getBytes(StandardCharsets.UTF_8);
1671+
1672+
try {
1673+
// Upload the blob
1674+
try (InputStream inputStream = new ByteArrayInputStream(blobBytes)) {
1675+
UploadRequest request = new UploadRequest.Builder()
1676+
.withKey(baseKey)
1677+
.withContentLength(blobBytes.length)
1678+
.build();
1679+
bucketClient.upload(request, inputStream);
1680+
}
1681+
1682+
Instant now = Instant.now();
1683+
Instant minTimestamp = Instant.parse("2000-01-01T00:00:00Z");
1684+
Instant maxTimestamp = now.plusSeconds(300);
1685+
1686+
// Test listPage and verify timestamp
1687+
ListBlobsPageRequest request = ListBlobsPageRequest.builder()
1688+
.withPrefix(baseKey)
1689+
.build();
1690+
1691+
ListBlobsPageResponse page = bucketClient.listPage(request);
1692+
Assertions.assertNotNull(page);
1693+
Assertions.assertNotNull(page.getBlobs());
1694+
Assertions.assertFalse(page.getBlobs().isEmpty(),
1695+
"testListPage_withTimeStamp: Should return at least one blob");
1696+
1697+
// Verify timestamp is present and reasonable
1698+
BlobInfo blobInfo = page.getBlobs().get(0);
1699+
Assertions.assertNotNull(blobInfo.getLastModified(),
1700+
"testListPage_withTimeStamp: BlobInfo should have a lastModified timestamp");
1701+
Assertions.assertFalse(blobInfo.getLastModified().isAfter(maxTimestamp),
1702+
"testListPage_withTimeStamp: lastModified timestamp should not be too far in the future (allowing for clock skew)");
1703+
Assertions.assertFalse(blobInfo.getLastModified().isBefore(minTimestamp),
1704+
"testListPage_withTimeStamp: lastModified timestamp should be reasonable (not before 2000)");
1705+
}
1706+
// Clean up
1707+
finally {
1708+
safeDeleteBlobs(bucketClient, keys);
1709+
}
1710+
}
1711+
16591712
@Test
16601713
public void testGetMetadata() throws IOException {
16611714

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.salesforce.multicloudj.blob.driver;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.time.Instant;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
9+
import static org.junit.jupiter.api.Assertions.assertNull;
10+
11+
/**
12+
* Unit tests for BlobInfo class
13+
*/
14+
public class BlobInfoTest {
15+
16+
@Test
17+
void testBuilderWithTimestamp() {
18+
Instant timestamp = Instant.now();
19+
BlobInfo blobInfo = BlobInfo.builder()
20+
.withKey("test-key")
21+
.withObjectSize(1024L)
22+
.withLastModified(timestamp)
23+
.build();
24+
25+
assertEquals("test-key", blobInfo.getKey());
26+
assertEquals(1024L, blobInfo.getObjectSize());
27+
assertEquals(timestamp, blobInfo.getLastModified());
28+
}
29+
30+
@Test
31+
void testBuilderWithoutTimestamp() {
32+
BlobInfo blobInfo = BlobInfo.builder()
33+
.withKey("test-key")
34+
.withObjectSize(1024L)
35+
.build();
36+
37+
assertEquals("test-key", blobInfo.getKey());
38+
assertEquals(1024L, blobInfo.getObjectSize());
39+
assertNull(blobInfo.getLastModified());
40+
}
41+
42+
@Test
43+
void testEqualsWithTimestamp() {
44+
Instant timestamp = Instant.now();
45+
BlobInfo blobInfo1 = BlobInfo.builder()
46+
.withKey("test-key")
47+
.withObjectSize(1024L)
48+
.withLastModified(timestamp)
49+
.build();
50+
51+
BlobInfo blobInfo2 = BlobInfo.builder()
52+
.withKey("test-key")
53+
.withObjectSize(1024L)
54+
.withLastModified(timestamp)
55+
.build();
56+
57+
assertEquals(blobInfo1, blobInfo2);
58+
assertEquals(blobInfo1.hashCode(), blobInfo2.hashCode());
59+
}
60+
61+
@Test
62+
void testEqualsWithDifferentTimestamps() {
63+
Instant timestamp1 = Instant.now();
64+
Instant timestamp2 = timestamp1.plusSeconds(100);
65+
66+
BlobInfo blobInfo1 = BlobInfo.builder()
67+
.withKey("test-key")
68+
.withObjectSize(1024L)
69+
.withLastModified(timestamp1)
70+
.build();
71+
72+
BlobInfo blobInfo2 = BlobInfo.builder()
73+
.withKey("test-key")
74+
.withObjectSize(1024L)
75+
.withLastModified(timestamp2)
76+
.build();
77+
78+
assertNotEquals(blobInfo1, blobInfo2);
79+
}
80+
81+
@Test
82+
void testEqualsWithNullTimestamp() {
83+
BlobInfo blobInfo1 = BlobInfo.builder()
84+
.withKey("test-key")
85+
.withObjectSize(1024L)
86+
.build();
87+
88+
BlobInfo blobInfo2 = BlobInfo.builder()
89+
.withKey("test-key")
90+
.withObjectSize(1024L)
91+
.build();
92+
93+
assertEquals(blobInfo1, blobInfo2);
94+
assertEquals(blobInfo1.hashCode(), blobInfo2.hashCode());
95+
}
96+
97+
@Test
98+
void testEqualsWithOneNullTimestamp() {
99+
Instant timestamp = Instant.now();
100+
BlobInfo blobInfo1 = BlobInfo.builder()
101+
.withKey("test-key")
102+
.withObjectSize(1024L)
103+
.withLastModified(timestamp)
104+
.build();
105+
106+
BlobInfo blobInfo2 = BlobInfo.builder()
107+
.withKey("test-key")
108+
.withObjectSize(1024L)
109+
.build();
110+
111+
assertNotEquals(blobInfo1, blobInfo2);
112+
}
113+
}
114+

0 commit comments

Comments
 (0)