diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/IntegrationTestConfiguration.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/IntegrationTestConfiguration.kt index aaf6c4c05..c98f518b0 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/IntegrationTestConfiguration.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/IntegrationTestConfiguration.kt @@ -3,6 +3,7 @@ package fi.hel.haitaton.hanke import fi.hel.haitaton.hanke.application.ApplicationAuthorizer import fi.hel.haitaton.hanke.application.ApplicationService import fi.hel.haitaton.hanke.attachment.application.ApplicationAttachmentService +import fi.hel.haitaton.hanke.attachment.common.AttachmentUploadService import fi.hel.haitaton.hanke.attachment.hanke.HankeAttachmentAuthorizer import fi.hel.haitaton.hanke.attachment.hanke.HankeAttachmentService import fi.hel.haitaton.hanke.configuration.FeatureFlags @@ -73,6 +74,8 @@ class IntegrationTestConfiguration { @Bean fun hankeAttachmentService(): HankeAttachmentService = mockk() + @Bean fun attachmentUploadService(): AttachmentUploadService = mockk() + @Bean fun applicationAttachmentService(): ApplicationAttachmentService = mockk() @Bean fun hankeKayttajaService(): HankeKayttajaService = mockk() diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadServiceITest.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadServiceITest.kt new file mode 100644 index 000000000..af2bffa95 --- /dev/null +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadServiceITest.kt @@ -0,0 +1,98 @@ +package fi.hel.haitaton.hanke.attachment.common + +import assertk.all +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.hasClass +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import assertk.assertions.prop +import fi.hel.haitaton.hanke.DatabaseTest +import fi.hel.haitaton.hanke.attachment.USERNAME +import fi.hel.haitaton.hanke.attachment.azure.Container +import fi.hel.haitaton.hanke.attachment.body +import fi.hel.haitaton.hanke.attachment.failResult +import fi.hel.haitaton.hanke.attachment.response +import fi.hel.haitaton.hanke.attachment.successResult +import fi.hel.haitaton.hanke.attachment.testFile +import fi.hel.haitaton.hanke.factory.HankeFactory +import fi.hel.haitaton.hanke.test.Asserts.isRecent +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.context.ActiveProfiles + +@SpringBootTest +@ActiveProfiles("test") +@WithMockUser(USERNAME) +class AttachmentUploadServiceITest( + @Autowired private val attachmentUploadService: AttachmentUploadService, + @Autowired private val attachmentRepository: HankeAttachmentRepository, + @Autowired private val hankeFactory: HankeFactory, + @Autowired private val fileClient: MockFileClient +) : DatabaseTest() { + + private lateinit var mockClamAv: MockWebServer + + @BeforeEach + fun setup() { + fileClient.recreateContainers() + mockClamAv = MockWebServer() + mockClamAv.start(6789) + } + + @AfterEach + fun tearDown() { + mockClamAv.shutdown() + } + + @Nested + inner class UploadHankeAttachment { + @Test + fun `Should upload blob and return saved metadata`() { + mockClamAv.enqueue(response(body(results = successResult()))) + val hanke = hankeFactory.save() + val file = testFile() + + val result = + attachmentUploadService.uploadHankeAttachment( + hankeTunnus = hanke.hankeTunnus, + attachment = testFile() + ) + + assertThat(result).all { + prop(HankeAttachment::hankeTunnus).isEqualTo(hanke.hankeTunnus) + prop(HankeAttachment::createdAt).isRecent() + prop(HankeAttachment::createdByUserId).isEqualTo(USERNAME) + prop(HankeAttachment::fileName).isEqualTo(file.originalFilename) + } + val attachment = attachmentRepository.findById(result.id).orElseThrow() + val blob = fileClient.download(Container.HANKE_LIITTEET, attachment.blobLocation!!) + assertThat(blob.contentType.toString()).isEqualTo(file.contentType) + } + + @Test + fun `Should throw when infected file is encountered`() { + mockClamAv.enqueue(response(body(results = failResult()))) + val hanke = hankeFactory.save() + + assertFailure { + attachmentUploadService.uploadHankeAttachment( + hankeTunnus = hanke.hankeTunnus, + attachment = testFile() + ) + } + .all { + hasClass(AttachmentInvalidException::class) + hasMessage( + "Attachment upload exception: Infected file detected, see previous logs." + ) + } + } + } +} diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentControllerITests.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentControllerITests.kt index 1d2f3013b..07f9e2330 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentControllerITests.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentControllerITests.kt @@ -11,6 +11,8 @@ import fi.hel.haitaton.hanke.attachment.HANKE_TUNNUS import fi.hel.haitaton.hanke.attachment.USERNAME import fi.hel.haitaton.hanke.attachment.andExpectError import fi.hel.haitaton.hanke.attachment.common.AttachmentContent +import fi.hel.haitaton.hanke.attachment.common.AttachmentInvalidException +import fi.hel.haitaton.hanke.attachment.common.AttachmentUploadService import fi.hel.haitaton.hanke.attachment.testFile import fi.hel.haitaton.hanke.factory.AttachmentFactory import fi.hel.haitaton.hanke.factory.TestHankeIdentifier @@ -56,6 +58,7 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status class HankeAttachmentControllerITests(@Autowired override val mockMvc: MockMvc) : ControllerTest { @Autowired private lateinit var hankeAttachmentService: HankeAttachmentService + @Autowired private lateinit var attachmentUploadService: AttachmentUploadService @Autowired private lateinit var authorizer: HankeAttachmentAuthorizer @BeforeEach @@ -106,17 +109,30 @@ class HankeAttachmentControllerITests(@Autowired override val mockMvc: MockMvc) val file = testFile() val hanke = TestHankeIdentifier(1, HANKE_TUNNUS) every { authorizer.authorizeHankeTunnus(HANKE_TUNNUS, EDIT.name) } returns true - every { hankeAttachmentService.hankeWithRoomForAttachment(HANKE_TUNNUS) } returns hanke - every { - hankeAttachmentService.addAttachment(hanke, FILE_NAME_PDF, APPLICATION_PDF, file.bytes) - } returns AttachmentFactory.hankeAttachment() + every { attachmentUploadService.uploadHankeAttachment(hanke.hankeTunnus, file) } returns + AttachmentFactory.hankeAttachment() postAttachment(file = file).andExpect(status().isOk) verifyOrder { authorizer.authorizeHankeTunnus(HANKE_TUNNUS, EDIT.name) - hankeAttachmentService.hankeWithRoomForAttachment(HANKE_TUNNUS) - hankeAttachmentService.addAttachment(hanke, FILE_NAME_PDF, APPLICATION_PDF, file.bytes) + attachmentUploadService.uploadHankeAttachment(hanke.hankeTunnus, file) + } + } + + @Test + fun `postAttachment when attachment invalid or amount exceeded return bad request`() { + val file = testFile() + val hanke = TestHankeIdentifier(1, HANKE_TUNNUS) + every { authorizer.authorizeHankeTunnus(HANKE_TUNNUS, EDIT.name) } returns true + every { attachmentUploadService.uploadHankeAttachment(hanke.hankeTunnus, file) } throws + AttachmentInvalidException("Something went wrong") + + postAttachment(file = file).andExpect(status().isBadRequest) + + verifyOrder { + authorizer.authorizeHankeTunnus(HANKE_TUNNUS, EDIT.name) + attachmentUploadService.uploadHankeAttachment(hanke.hankeTunnus, file) } } diff --git a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt index 66fc80167..c8d233b86 100644 --- a/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt +++ b/services/hanke-service/src/integrationTest/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentServiceITests.kt @@ -29,15 +29,12 @@ import fi.hel.haitaton.hanke.attachment.common.HankeAttachmentEntity import fi.hel.haitaton.hanke.attachment.common.HankeAttachmentRepository import fi.hel.haitaton.hanke.attachment.common.MockFileClient import fi.hel.haitaton.hanke.attachment.common.MockFileClientExtension -import fi.hel.haitaton.hanke.attachment.failResult import fi.hel.haitaton.hanke.attachment.response import fi.hel.haitaton.hanke.attachment.successResult import fi.hel.haitaton.hanke.factory.AttachmentFactory import fi.hel.haitaton.hanke.factory.HankeAttachmentFactory import fi.hel.haitaton.hanke.factory.HankeFactory import fi.hel.haitaton.hanke.factory.HankeIdentifierFactory -import fi.hel.haitaton.hanke.factory.TestHankeIdentifier -import fi.hel.haitaton.hanke.factory.identifier import java.time.OffsetDateTime import java.util.UUID import okhttp3.mockwebserver.MockWebServer @@ -48,7 +45,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest -import org.springframework.http.MediaType.APPLICATION_PDF import org.springframework.http.MediaType.APPLICATION_PDF_VALUE import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.context.ActiveProfiles @@ -160,24 +156,19 @@ class HankeAttachmentServiceITests( } @Nested - inner class AddAttachment { - - @BeforeEach - fun clear() { - fileClient.recreateContainers() - } - + inner class SaveAttachment { @Test fun `Should return metadata of saved attachment`() { mockClamAv.enqueue(response(body(results = successResult()))) val hanke = hankeFactory.save() + val blobPath = blobPath(hanke.id) val result = - hankeAttachmentService.addAttachment( - hanke = hanke.identifier(), + hankeAttachmentService.saveAttachment( + hankeTunnus = hanke.hankeTunnus, name = FILE_NAME_PDF, - type = APPLICATION_PDF, - content = DEFAULT_DATA + type = APPLICATION_PDF_VALUE, + blobPath = blobPath, ) assertThat(result.id).isNotNull() @@ -192,8 +183,7 @@ class HankeAttachmentServiceITests( assertThat(attachmentInDb.createdByUserId).isEqualTo(USERNAME) assertThat(attachmentInDb.fileName).isEqualTo(FILE_NAME_PDF) assertThat(attachmentInDb.createdAt).isNotNull() - val blob = fileClient.download(HANKE_LIITTEET, attachmentInDb.blobLocation!!) - assertThat(blob.contentType).isEqualTo(APPLICATION_PDF) + assertThat(attachmentInDb.blobLocation).isEqualTo(blobPath) } @Test @@ -205,11 +195,11 @@ class HankeAttachmentServiceITests( .let { hankeAttachmentRepository.saveAll(it) } assertFailure { - hankeAttachmentService.addAttachment( - TestHankeIdentifier(hanke.id, hanke.hankeTunnus), + hankeAttachmentService.saveAttachment( + hanke.hankeTunnus, FILE_NAME_PDF, - APPLICATION_PDF, - ByteArray(0) + APPLICATION_PDF_VALUE, + blobPath(hanke.id), ) } .all { @@ -223,11 +213,11 @@ class HankeAttachmentServiceITests( mockClamAv.enqueue(response(body(results = successResult()))) assertFailure { - hankeAttachmentService.addAttachment( - hanke = TestHankeIdentifier(5, "HAI-123"), + hankeAttachmentService.saveAttachment( + hankeTunnus = "HAI-123", name = FILE_NAME_PDF, - type = APPLICATION_PDF, - content = DEFAULT_DATA + type = APPLICATION_PDF_VALUE, + blobPath = blobPath(123) ) } .hasClass(HankeNotFoundException::class) @@ -235,26 +225,7 @@ class HankeAttachmentServiceITests( assertThat(hankeAttachmentRepository.findAll()).isEmpty() } - @Test - fun `Should throw when infected file is encountered`() { - mockClamAv.enqueue(response(body(results = failResult()))) - val hanke = hankeFactory.save() - - assertFailure { - hankeAttachmentService.addAttachment( - hanke = hanke.identifier(), - name = FILE_NAME_PDF, - type = APPLICATION_PDF, - content = DEFAULT_DATA - ) - } - .all { - hasClass(AttachmentInvalidException::class) - hasMessage( - "Attachment upload exception: Infected file detected, see previous logs." - ) - } - } + private fun blobPath(hankeId: Int) = HankeAttachmentContentService.generateBlobPath(hankeId) } @Nested diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentPersister.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentPersister.kt deleted file mode 100644 index dddcbf9de..000000000 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentPersister.kt +++ /dev/null @@ -1,53 +0,0 @@ -package fi.hel.haitaton.hanke.attachment.common - -import fi.hel.haitaton.hanke.ALLOWED_ATTACHMENT_COUNT -import fi.hel.haitaton.hanke.HankeNotFoundException -import fi.hel.haitaton.hanke.HankeRepository -import fi.hel.haitaton.hanke.currentUserId -import java.time.OffsetDateTime -import mu.KotlinLogging -import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional - -private val logger = KotlinLogging.logger {} - -/** Needed for attachment services transaction management. See usages for reasoning. */ -@Component -class AttachmentPersister( - private val hankeRepository: HankeRepository, - private val hankeAttachmentRepository: HankeAttachmentRepository -) { - @Transactional - fun hankeAttachment( - filename: String, - mediaType: String, - blobPath: String, - hankeTunnus: String, - ): HankeAttachmentEntity = - hankeAttachmentRepository.save( - HankeAttachmentEntity( - id = null, - fileName = filename, - contentType = mediaType, - createdAt = OffsetDateTime.now(), - createdByUserId = currentUserId(), - blobLocation = blobPath, - hanke = getHanke(hankeTunnus), - ) - ) - - fun checkRoomForAttachment(hankeId: Int, hankeTunnus: String) = - verifyCount(hankeAttachmentRepository.countByHankeId(hankeId)) - - private fun getHanke(hankeTunnus: String) = - hankeRepository.findByHankeTunnus(hankeTunnus)?.also { - checkRoomForAttachment(it.id, it.hankeTunnus) - } ?: throw HankeNotFoundException(hankeTunnus) - - private fun verifyCount(count: Int) { - logger.info { "There is $count attachments beforehand." } - if (count >= ALLOWED_ATTACHMENT_COUNT) { - throw AttachmentInvalidException("Attachment limit reached") - } - } -} diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadService.kt new file mode 100644 index 000000000..7d3a0d409 --- /dev/null +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/common/AttachmentUploadService.kt @@ -0,0 +1,51 @@ +package fi.hel.haitaton.hanke.attachment.common + +import fi.hel.haitaton.hanke.attachment.hanke.HankeAttachmentContentService +import fi.hel.haitaton.hanke.attachment.hanke.HankeAttachmentService +import mu.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +private val logger = KotlinLogging.logger {} + +@Service +class AttachmentUploadService( + private val hankeAttachmentService: HankeAttachmentService, + private val hankeAttachmentContentService: HankeAttachmentContentService, + private val scanClient: FileScanClient, +) { + fun uploadHankeAttachment( + hankeTunnus: String, + attachment: MultipartFile, + ): HankeAttachment { + val hanke = hankeAttachmentService.hankeWithRoomForAttachment(hankeTunnus) + + val (filename, mediatype) = attachment.validNameAndType() + + scanAttachment(filename, attachment.bytes) + + val blobPath = + hankeAttachmentContentService.upload( + fileName = filename, + contentType = mediatype, + content = attachment.bytes, + hankeId = hanke.id, + ) + + return hankeAttachmentService + .saveAttachment( + hankeTunnus = hanke.hankeTunnus, + name = filename, + type = mediatype.toString(), + blobPath = blobPath, + ) + .also { logger.info { "Added attachment ${it.id} to hanke ${hanke.hankeTunnus}" } } + } + + private fun scanAttachment(filename: String, content: ByteArray) { + val scanResult = scanClient.scan(listOf(FileScanInput(filename, content))) + if (scanResult.hasInfected()) { + throw AttachmentInvalidException("Infected file detected, see previous logs.") + } + } +} diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentService.kt index aa658887c..68389f8e2 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentContentService.kt @@ -89,6 +89,10 @@ class HankeAttachmentContentService( } companion object { + fun generateBlobPath(hankeId: Int) = + "${hankePrefix(hankeId)}/${UUID.randomUUID()}" + .also { logger.info { "Generated blob path: $it" } } + /** Name (path from container root) the attachment should have in the cloud storage. */ fun HankeAttachmentEntity.cloudPath(): String = hankePrefix(hanke.id) + id!!.toString() @@ -99,9 +103,5 @@ class HankeAttachmentContentService( * to enable deleting all of them at once. */ private fun hankePrefix(hankeId: Int): String = "$hankeId/" - - fun generateBlobPath(hankeId: Int) = - "${hankePrefix(hankeId)}/${UUID.randomUUID()}" - .also { logger.info { "Generated blob path: $it" } } } } diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentController.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentController.kt index f6f20c49a..feef039a8 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentController.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentController.kt @@ -1,9 +1,9 @@ package fi.hel.haitaton.hanke.attachment.hanke import fi.hel.haitaton.hanke.HankeError +import fi.hel.haitaton.hanke.attachment.common.AttachmentUploadService import fi.hel.haitaton.hanke.attachment.common.HankeAttachment import fi.hel.haitaton.hanke.attachment.common.HeadersBuilder.buildHeaders -import fi.hel.haitaton.hanke.attachment.common.validNameAndType import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema @@ -28,7 +28,10 @@ private val logger = KotlinLogging.logger {} @RestController @RequestMapping("/hankkeet/{hankeTunnus}/liitteet") @SecurityRequirement(name = "bearerAuth") -class HankeAttachmentController(private val hankeAttachmentService: HankeAttachmentService) { +class HankeAttachmentController( + private val hankeAttachmentService: HankeAttachmentService, + private val attachmentUploadService: AttachmentUploadService, +) { @GetMapping @Operation(summary = "Get metadata from hanke attachments") @@ -107,16 +110,7 @@ class HankeAttachmentController(private val hankeAttachmentService: HankeAttachm "content type = ${attachment.contentType}" } - val hanke = hankeAttachmentService.hankeWithRoomForAttachment(hankeTunnus) - - attachment.validNameAndType().let { (name, type) -> - return hankeAttachmentService.addAttachment( - hanke = hanke, - name = name, - type = type, - content = attachment.bytes - ) - } + return attachmentUploadService.uploadHankeAttachment(hankeTunnus, attachment) } @DeleteMapping("/{attachmentId}") diff --git a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentService.kt b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentService.kt index 527a7f048..9878e0b4f 100644 --- a/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentService.kt +++ b/services/hanke-service/src/main/kotlin/fi/hel/haitaton/hanke/attachment/hanke/HankeAttachmentService.kt @@ -1,21 +1,20 @@ package fi.hel.haitaton.hanke.attachment.hanke +import fi.hel.haitaton.hanke.ALLOWED_ATTACHMENT_COUNT import fi.hel.haitaton.hanke.HankeIdentifier import fi.hel.haitaton.hanke.HankeNotFoundException import fi.hel.haitaton.hanke.HankeRepository import fi.hel.haitaton.hanke.attachment.common.AttachmentContent import fi.hel.haitaton.hanke.attachment.common.AttachmentInvalidException import fi.hel.haitaton.hanke.attachment.common.AttachmentNotFoundException -import fi.hel.haitaton.hanke.attachment.common.AttachmentPersister -import fi.hel.haitaton.hanke.attachment.common.FileScanClient -import fi.hel.haitaton.hanke.attachment.common.FileScanInput import fi.hel.haitaton.hanke.attachment.common.HankeAttachment +import fi.hel.haitaton.hanke.attachment.common.HankeAttachmentEntity import fi.hel.haitaton.hanke.attachment.common.HankeAttachmentRepository -import fi.hel.haitaton.hanke.attachment.common.hasInfected +import fi.hel.haitaton.hanke.currentUserId +import java.time.OffsetDateTime import java.util.UUID import mu.KotlinLogging import org.springframework.data.repository.findByIdOrNull -import org.springframework.http.MediaType import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -26,8 +25,6 @@ class HankeAttachmentService( private val hankeRepository: HankeRepository, private val attachmentRepository: HankeAttachmentRepository, private val attachmentContentService: HankeAttachmentContentService, - private val persister: AttachmentPersister, - private val scanClient: FileScanClient, ) { @Transactional(readOnly = true) @@ -42,33 +39,28 @@ class HankeAttachmentService( return AttachmentContent(attachment.fileName, attachment.contentType, content) } - fun addAttachment( - hanke: HankeIdentifier, + @Transactional + fun saveAttachment( + hankeTunnus: String, name: String, - type: MediaType, - content: ByteArray + type: String, + blobPath: String, ): HankeAttachment { - scanAttachment(name, content) - - val blobPath = - attachmentContentService.upload( - fileName = name, - contentType = type, - content = content, - hankeId = hanke.id - ) - - val entity = // transaction only after http calls. - persister.hankeAttachment( - filename = name, - mediaType = type.toString(), - blobPath = blobPath, - hankeTunnus = hanke.hankeTunnus, + val hanke = findHanke(hankeTunnus).also { checkRoomForAttachment(it.id) } + + return attachmentRepository + .save( + HankeAttachmentEntity( + id = null, + fileName = name, + contentType = type, + createdAt = OffsetDateTime.now(), + createdByUserId = currentUserId(), + blobLocation = blobPath, + hanke = hanke, + ) ) - - return entity.toDomain().also { - logger.info { "Added attachment ${it.id} to hanke ${hanke.hankeTunnus}" } - } + .toDomain() } /** Move the attachment content to cloud. In test-data use for now, can be used for HAI-1964. */ @@ -97,9 +89,17 @@ class HankeAttachmentService( @Transactional(readOnly = true) fun hankeWithRoomForAttachment(hankeTunnus: String): HankeIdentifier = - findHankeIdentifiers(hankeTunnus).also { - persister.checkRoomForAttachment(it.id, it.hankeTunnus) + findHankeIdentifiers(hankeTunnus).also { checkRoomForAttachment(it.id) } + + private fun checkRoomForAttachment(hankeId: Int) = + verifyCount(attachmentRepository.countByHankeId(hankeId)) + + private fun verifyCount(count: Int) { + logger.info { "There is $count attachments beforehand." } + if (count >= ALLOWED_ATTACHMENT_COUNT) { + throw AttachmentInvalidException("Attachment limit reached") } + } private fun findAttachment(attachmentId: UUID) = attachmentRepository.findByIdOrNull(attachmentId) @@ -111,11 +111,4 @@ class HankeAttachmentService( private fun findHankeIdentifiers(hankeTunnus: String) = hankeRepository.findOneByHankeTunnus(hankeTunnus) ?: throw HankeNotFoundException(hankeTunnus) - - private fun scanAttachment(filename: String, content: ByteArray) { - val scanResult = scanClient.scan(listOf(FileScanInput(filename, content))) - if (scanResult.hasInfected()) { - throw AttachmentInvalidException("Infected file detected, see previous logs.") - } - } }