diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java index 330c80eb3392aaecd9ced6732c41e1fa4d65a7dd..310181716fca2e482bb5e0157804812824634d0f 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/FileChunkInfo.java @@ -1,14 +1,30 @@ package de.ozgcloud.nachrichten.postfach.osiv2.model; +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*; + +import java.io.InputStream; + +import org.springframework.core.io.AbstractResource; + +import de.ozgcloud.nachrichten.postfach.osiv2.storage.LimitedInputStream; import lombok.Builder; @Builder public record FileChunkInfo( - String guid, - String fileName, - String contentType, - Integer chunkIndex, - Integer totalChunks, - Long totalFileSize + Osi2FileUpload upload, + long chunkIndex ) { + public AbstractResource createUploadResource(InputStream fileInputStream) { + return new AbstractResource() { + @Override + public String getDescription() { + return FileChunkInfo.this.toString(); + } + + @Override + public InputStream getInputStream() { + return new LimitedInputStream(fileInputStream, CHUNK_SIZE); + } + }; + } } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java index 16e1ec1a3ec8e817861355abc7320cb7168b05d9..5c3ddb497dfa91c0bb1e468414d6428b8c5cb6a5 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java @@ -1,5 +1,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.model; +import java.util.UUID; + import de.ozgcloud.apilib.file.OzgCloudFile; import lombok.Builder; @@ -8,4 +10,17 @@ public record Osi2FileUpload( String guid, OzgCloudFile file ) { + public static final long CHUNK_SIZE = 100L * (2L << 10); + + public static Osi2FileUpload from(OzgCloudFile file) { + return Osi2FileUpload.builder() + .guid(UUID.randomUUID().toString()) + .file(file) + .build(); + } + + public long numberOfChunks() { + return (file.getSize() + CHUNK_SIZE - 1) / CHUNK_SIZE; + } + } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/LimitedInputStream.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/LimitedInputStream.java index c4c8ddfcf21552cc5a6b05ce6b74d9ab92b229d3..027bdaf47545ce3ab1f7a77250f91725207ee80c 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/LimitedInputStream.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/storage/LimitedInputStream.java @@ -8,8 +8,8 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public class LimitedInputStream extends InputStream { private final InputStream parentStream; - private final int limit; - private int readOffset = 0; + private final long limit; + private long readOffset = 0; @Override public int read() throws IOException { @@ -18,12 +18,11 @@ public class LimitedInputStream extends InputStream { return result < 0 ? result : buffer[0]; } - @Override public int read(byte[] buffer, int offset, int length) throws IOException { var remainingLength = limit - readOffset; if (remainingLength > 0) { - var readLength = parentStream.read(buffer, offset, Math.min(remainingLength, length)); + var readLength = parentStream.read(buffer, offset, (int) Math.min(remainingLength, length)); if (readLength > 0) { readOffset += readLength; return readLength; diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java index 7938fa52c7d6b5715e34dc65c1574b9dd290cbac..df620a81491b55fd3a1d1f2ebca4e9f5fe976dc6 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java @@ -11,6 +11,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.BooleanSupplier; +import java.util.stream.LongStream; import java.util.stream.Stream; import de.ozgcloud.apilib.file.OzgCloudFile; @@ -31,7 +32,6 @@ public class Osi2QuarantineService { static final Duration POLLING_INTERVAL = Duration.ofSeconds(1); static final Duration POLLING_TIMEOUT = Duration.ofMinutes(5); - static final int CHUNK_SIZE = 100 * (2 << 10); public List<Osi2FileUpload> uploadAttachments(List<String> attachmentIds) { var ozgCloudFiles = binaryFileService.getFileMetadataOfIds(attachmentIds); @@ -47,23 +47,36 @@ public class Osi2QuarantineService { .toList(); } - Osi2FileUpload uploadFileToQuarantine(OzgCloudFile file) { try (var fileInputStream = binaryFileService.streamFileContent(file.getId().toString())) { return uploadInputStreamToQuarantine(file, fileInputStream); } catch (IOException e) { - throw new OsiPostfachException("Failed to upload file to quarantine!", e); + throw new OsiPostfachException("Failed to close input stream!", e); } } Osi2FileUpload uploadInputStreamToQuarantine(OzgCloudFile file, InputStream fileInputStream) { - // TODO - return null; + var fileUpload = Osi2FileUpload.from(file); + streamFileChunkInfos(fileUpload) + .forEachOrdered(chunkInfo -> + postfachApiFacadeService.uploadChunk( + chunkInfo, + chunkInfo.createUploadResource(fileInputStream) + ) + ); + return fileUpload; } - Stream<FileChunkInfo> streamFileChunkInfos(OzgCloudFile file) { - // TODO - return null; + Stream<FileChunkInfo> streamFileChunkInfos(Osi2FileUpload upload) { + return LongStream.range(0, upload.numberOfChunks()) + .mapToObj(chunkIndex -> buildFileChunkInfo(upload, chunkIndex)); + } + + private FileChunkInfo buildFileChunkInfo(Osi2FileUpload upload, long chunkIndex) { + return FileChunkInfo.builder() + .upload(upload) + .chunkIndex(chunkIndex) + .build(); } @@ -81,7 +94,7 @@ public class Osi2QuarantineService { } catch (TimeoutException e) { throw new OsiPostfachException("Expect the scan to complete after %d seconds!".formatted(POLLING_TIMEOUT.getSeconds()), e); } catch (InterruptedException e) { - LOG.error("[waitForVirusScan] Interrupt"); + LOG.debug("[waitForVirusScan] Interrupt"); Thread.currentThread().interrupt(); } } @@ -99,7 +112,6 @@ public class Osi2QuarantineService { }).get(timeout.getSeconds(), TimeUnit.SECONDS); } - public void deleteAttachments(List<Osi2FileUpload> osi2FileMetadata) { // TODO delete on exception } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java index 9ed7845cdf8e1530428a290b2b943048f148182d..e00189c87cf08e02ba0a9766f3673e9523c6d142 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapper.java @@ -77,7 +77,11 @@ public interface Osi2RequestMapper { } @Mapping(target = "target", ignore = true) - @Mapping(target = "uploadUid", source = "guid") + @Mapping(target = "uploadUid", source = "upload.guid") + @Mapping(target = "fileName", source = "upload.file.name") + @Mapping(target = "contentType", source = "upload.file.contentType") + @Mapping(target = "totalChunks", expression = "java( (int) fileChunkInfo.upload().numberOfChunks() )") + @Mapping(target = "totalFileSize", source = "upload.file.size") DomainChunkMetaData mapDomainChunkMetaData(FileChunkInfo fileChunkInfo); default String mapMailboxId(PostfachNachricht nachricht) { diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/FileChunkInfoTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/FileChunkInfoTestFactory.java index 846c2d825f146cfff5ddcc94cafc00a5c628442c..56d8f10addabdaf746ac3df4bfacf048b7d61de0 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/FileChunkInfoTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/factory/FileChunkInfoTestFactory.java @@ -1,18 +1,11 @@ package de.ozgcloud.nachrichten.postfach.osiv2.factory; -import java.util.UUID; - import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; +import de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2FileUploadTestFactory; public class FileChunkInfoTestFactory { - public static final String FILE_UPLOAD_GUID = UUID.randomUUID().toString(); - public static final String FILE_NAME = "test-file-name"; - public static final String CONTENT_TYPE = "test-content-type"; public static final Integer CHUNK_INDEX = 1; - public static final Integer TOTAL_CHUNKS = 10; - public static final Long TOTAL_FILE_SIZE = 1000L; - public static FileChunkInfo create() { return createBuilder().build(); @@ -20,11 +13,7 @@ public class FileChunkInfoTestFactory { public static FileChunkInfo.FileChunkInfoBuilder createBuilder() { return FileChunkInfo.builder() - .guid(FILE_UPLOAD_GUID) - .fileName(FILE_NAME) - .contentType(CONTENT_TYPE) - .chunkIndex(CHUNK_INDEX) - .totalChunks(TOTAL_CHUNKS) - .totalFileSize(TOTAL_FILE_SIZE); + .upload(Osi2FileUploadTestFactory.create()) + .chunkIndex(CHUNK_INDEX); } } diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUploadTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUploadTest.java new file mode 100644 index 0000000000000000000000000000000000000000..90dc14cb0807f8abfed6c2699541542e619e3a94 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUploadTest.java @@ -0,0 +1,60 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.model; + +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import de.ozgcloud.apilib.file.OzgCloudFileTestFactory; + +public class Osi2FileUploadTest { + + @DisplayName("calculate number of chunks") + @Nested + class TestCalculateNumberOfChunks { + + @DisplayName("should return zero") + @Test + void shouldReturnZero() { + var upload = createWithSize(0L); + + var result = upload.numberOfChunks(); + + assertThat(result).isZero(); + } + + @DisplayName("should return one") + @ParameterizedTest + @ValueSource(longs = { 1, CHUNK_SIZE - 1, CHUNK_SIZE }) + void shouldReturnOne(long fileLength) { + var upload = createWithSize(fileLength); + + var result = upload.numberOfChunks(); + + assertThat(result).isEqualTo(1L); + } + + @DisplayName("should return two") + @ParameterizedTest + @ValueSource(longs = { CHUNK_SIZE + 1, CHUNK_SIZE * 2 - 1, CHUNK_SIZE * 2 }) + void shouldReturnTwo(long fileLength) { + var upload = createWithSize(fileLength); + + var result = upload.numberOfChunks(); + + assertThat(result).isEqualTo(2L); + } + + private Osi2FileUpload createWithSize(long size) { + return Osi2FileUpload.builder() + .file(OzgCloudFileTestFactory.createBuilder() + .size(size) + .build()) + .build(); + } + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2FileUploadTestFactory.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2FileUploadTestFactory.java index 9a79726e6e085478515751817de3f612f0e243d3..b9cf5d26efd8c35da40219aeb089068087b838e7 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2FileUploadTestFactory.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2FileUploadTestFactory.java @@ -1,5 +1,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*; + import java.util.UUID; import de.ozgcloud.apilib.file.OzgCloudFile; @@ -13,7 +15,8 @@ public class Osi2FileUploadTestFactory { public static final String UPLOAD_GUID2 = UUID.randomUUID().toString(); public static final String UPLOAD_NAME = "upload.txt"; public static final String UPLOAD_CONTENT_TYPE = "text/plain"; - public static final Long UPLOAD_SIZE = 1001L; + public static final long NUMBER_OF_CHUNKS = 5; + public static final long UPLOAD_SIZE = CHUNK_SIZE * NUMBER_OF_CHUNKS; public static Osi2FileUpload create() { return createBuilder().build(); diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java index 6e26bd2f501d0391a5fbf741d238e53638c74e1f..a0784519b193ca1b972c03bbcae0a9ca18db279b 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2PostfachServiceTest.java @@ -31,8 +31,8 @@ class Osi2PostfachServiceTest { @DisplayName("send message") @Nested class TestSendMessage { - private PostfachNachricht nachricht = PostfachNachrichtTestFactory.create(); - private List<Osi2FileUpload> uploadFiles = List.of(Osi2FileUploadTestFactory.create()); + private final PostfachNachricht nachricht = PostfachNachrichtTestFactory.create(); + private final List<Osi2FileUpload> uploadFiles = List.of(Osi2FileUploadTestFactory.create()); @BeforeEach void mock() { diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java index f0c34e89abd63e6a577c80d4ef30c774528f2df0..811d5e8d1d80a7cbb6a41e5ac107bb5edb2d8a62 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java @@ -1,10 +1,11 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; +import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*; import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2FileUploadTestFactory.*; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -19,6 +20,8 @@ import org.mockito.Spy; import de.ozgcloud.apilib.file.OzgCloudFile; import de.ozgcloud.apilib.file.OzgCloudFileId; import de.ozgcloud.apilib.file.OzgCloudFileTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.OsiPostfachException; +import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; import de.ozgcloud.nachrichten.postfach.osiv2.storage.Osi2BinaryFileRemoteService; import lombok.SneakyThrows; @@ -159,9 +162,56 @@ class Osi2QuarantineServiceTest { assertThat(result).isEqualTo(uploadFile); } + @DisplayName("should close inputStream") + @Test + @SneakyThrows + void shouldCloseInputStream() { + uploadFileToQuarantine(); + + verify(fileInputStream).close(); + } + + @DisplayName("should throw OsiPostfachException if close fails with IOException") + @Test + @SneakyThrows + void shouldThrowOsiPostfachException() { + doThrow(new IOException("test")).when(fileInputStream).close(); + + assertThatThrownBy(this::uploadFileToQuarantine) + .isInstanceOf(OsiPostfachException.class); + } + Osi2FileUpload uploadFileToQuarantine() { return service.uploadFileToQuarantine(file); } } + @DisplayName("stream file chunk infos") + @Nested + class TestStreamFileChunkInfos { + + private final Osi2FileUpload upload = Osi2FileUploadTestFactory.createBuilder() + .file(OzgCloudFileTestFactory.createBuilder() + .size(CHUNK_SIZE * 2) + .build()) + .build(); + + private final FileChunkInfo chunkInfo1 = FileChunkInfo.builder() + .upload(upload) + .chunkIndex(0) + .build(); + private final FileChunkInfo chunkInfo2 = FileChunkInfo.builder() + .upload(upload) + .chunkIndex(1) + .build(); + + @DisplayName("should stream chunk infos") + @Test + void shouldStreamChunkInfos() { + var result = service.streamFileChunkInfos(upload).toList(); + + assertThat(result).containsExactly(chunkInfo1, chunkInfo2); + } + } + } \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java index 765df69534bcf8429c5c5997fc377f886d62e66a..eef22fd05e33f867fbdbabe63d6bfe4668e1e5de 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2RequestMapperTest.java @@ -271,7 +271,7 @@ class Osi2RequestMapperTest { void shouldMapUploadGuid() { var result = doMapping(); - assertThat(result.getUploadUid()).isEqualTo(UUID.fromString(FileChunkInfoTestFactory.FILE_UPLOAD_GUID)); + assertThat(result.getUploadUid()).isEqualTo(UUID.fromString(UPLOAD_GUID)); } @DisplayName("should map file name") @@ -279,7 +279,7 @@ class Osi2RequestMapperTest { void shouldMapFileName() { var result = doMapping(); - assertThat(result.getFileName()).isEqualTo(FileChunkInfoTestFactory.FILE_NAME); + assertThat(result.getFileName()).isEqualTo(UPLOAD_NAME); } @DisplayName("should map content type") @@ -287,7 +287,7 @@ class Osi2RequestMapperTest { void shouldMapContentType() { var result = doMapping(); - assertThat(result.getContentType()).isEqualTo(FileChunkInfoTestFactory.CONTENT_TYPE); + assertThat(result.getContentType()).isEqualTo(UPLOAD_CONTENT_TYPE); } @DisplayName("should map chunk index") @@ -303,7 +303,7 @@ class Osi2RequestMapperTest { void shouldMapTotalChunks() { var result = doMapping(); - assertThat(result.getTotalChunks()).isEqualTo(FileChunkInfoTestFactory.TOTAL_CHUNKS); + assertThat(result.getTotalChunks()).isEqualTo(NUMBER_OF_CHUNKS); } @DisplayName("should map total file size") @@ -311,7 +311,7 @@ class Osi2RequestMapperTest { void shouldMapTotalFileSize() { var result = doMapping(); - assertThat(result.getTotalFileSize()).isEqualTo(FileChunkInfoTestFactory.TOTAL_FILE_SIZE); + assertThat(result.getTotalFileSize()).isEqualTo(UPLOAD_SIZE); } private DomainChunkMetaData doMapping() { diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java index 0f9b57d824813196948f83a8117147c43ebd81aa..a89ecc780e801000b07fed65195eca6127e15b4a 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java @@ -65,7 +65,7 @@ class PostfachApiFacadeServiceTest { @Mock MessageExchangeSendMessageResponse messageExchangeSendMessageResponse; - List<Osi2FileUpload> files = List.of(Osi2FileUploadTestFactory.create()); + private final List<Osi2FileUpload> files = List.of(Osi2FileUploadTestFactory.create()); private final PostfachNachricht nachricht = PostfachNachrichtTestFactory.create();