diff --git a/src/main/java/com/epam/aidial/core/storage/BlobStorage.java b/src/main/java/com/epam/aidial/core/storage/BlobStorage.java index 925e28045..8cf5c8198 100644 --- a/src/main/java/com/epam/aidial/core/storage/BlobStorage.java +++ b/src/main/java/com/epam/aidial/core/storage/BlobStorage.java @@ -23,6 +23,7 @@ import org.jclouds.io.ContentMetadata; import org.jclouds.io.ContentMetadataBuilder; import org.jclouds.io.payloads.BaseMutableContentMetadata; +import org.jclouds.s3.domain.ObjectMetadataBuilder; import java.io.Closeable; import java.util.List; @@ -32,6 +33,11 @@ @Slf4j public class BlobStorage implements Closeable { + // S3 implementation do not return a blob content type without additional head request. + // To avoid additional request for each blob in the listing we try to recognize blob content type by its extension. + // Default value is binary/octet-stream, see org.jclouds.s3.domain.ObjectMetadataBuilder + private static final String DEFAULT_CONTENT_TYPE = ObjectMetadataBuilder.create().build().getContentMetadata().getContentType(); + private final BlobStoreContext storeContext; private final BlobStore blobStore; private final String bucketName; @@ -171,8 +177,14 @@ private static FileMetadataBase buildFileMetadata(ResourceDescription resource, resource.getBucketLocation(), metadata.getName()); return switch (metadata.getType()) { - case BLOB -> new FileMetadata(resultResource, metadata.getSize(), - ((BlobMetadata) metadata).getContentMetadata().getContentType()); + case BLOB -> { + String blobContentType = ((BlobMetadata) metadata).getContentMetadata().getContentType(); + if (blobContentType != null && blobContentType.equals(DEFAULT_CONTENT_TYPE)) { + blobContentType = BlobStorageUtil.getContentType(metadata.getName()); + } + + yield new FileMetadata(resultResource, metadata.getSize(), blobContentType); + } case FOLDER, RELATIVE_PATH -> new FolderMetadata(resultResource); case CONTAINER -> throw new IllegalArgumentException("Can't list container"); }; diff --git a/src/test/java/com/epam/aidial/core/FileApiTest.java b/src/test/java/com/epam/aidial/core/FileApiTest.java index b117afbfe..eccca67bf 100644 --- a/src/test/java/com/epam/aidial/core/FileApiTest.java +++ b/src/test/java/com/epam/aidial/core/FileApiTest.java @@ -389,6 +389,72 @@ public void testListFileWithFolder(Vertx vertx, VertxTestContext context) { }); } + @Test + public void testListFileWithDefaultContentType(Vertx vertx, VertxTestContext context) { + Checkpoint checkpoint = context.checkpoint(3); + WebClient client = WebClient.create(vertx); + + FolderMetadata emptyFolderResponse = new FolderMetadata("7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt", + null, null, "7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/", List.of()); + + FileMetadata expectedFileMetadata1 = new FileMetadata("7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt", + "image.png", null, "7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/image.png", 17, "binary/octet-stream"); + + FileMetadata expectedImageMetadata = new FileMetadata("7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt", + "image.png", null, "7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/image.png", 17, "image/png"); + FolderMetadata expectedRootFolderMetadata = new FolderMetadata("7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt", + null, null, "7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/", + List.of(expectedImageMetadata)); + + Future.succeededFuture().compose((mapper) -> { + Promise promise = Promise.promise(); + // verify no files + client.get(serverPort, "localhost", "/v1/files/metadata/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/") + .putHeader("Api-key", "proxyKey2") + .as(BodyCodec.json(FolderMetadata.class)) + .send(context.succeeding(response -> { + context.verify(() -> { + assertEquals(200, response.statusCode()); + assertEquals(emptyFolderResponse, response.body()); + checkpoint.flag(); + promise.complete(); + }); + })); + + return promise.future(); + }).compose((mapper) -> { + Promise promise = Promise.promise(); + // upload test file1 + client.put(serverPort, "localhost", "/v1/files/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/image.png") + .putHeader("Api-key", "proxyKey2") + .as(BodyCodec.json(FileMetadata.class)) + .sendMultipartForm(generateMultipartForm("filename", TEST_FILE_CONTENT, "binary/octet-stream"), + context.succeeding(response -> { + context.verify(() -> { + assertEquals(200, response.statusCode()); + assertEquals(expectedFileMetadata1, response.body()); + checkpoint.flag(); + promise.complete(); + }); + }) + ); + + return promise.future(); + }).andThen((result) -> { + // verify uploaded files can be listed + client.get(serverPort, "localhost", "/v1/files/metadata/7G9WZNcoY26Vy9D7bEgbv6zqbJGfyDp9KZyEbJR4XMZt/") + .putHeader("Api-key", "proxyKey2") + .as(BodyCodec.string()) + .send(context.succeeding(response -> { + context.verify(() -> { + assertEquals(200, response.statusCode()); + assertEquals(ProxyUtil.MAPPER.writeValueAsString(expectedRootFolderMetadata), response.body()); + checkpoint.flag(); + }); + })); + }); + } + @Test public void testFileDelete(Vertx vertx, VertxTestContext context) { Checkpoint checkpoint = context.checkpoint(3);