diff --git a/.gitignore b/.gitignore index 0d7dbff3..84c07d25 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ out/ /tomcat/ /tomcat-e2e/ /e2eTest.log +/files/ diff --git a/src/e2eTest/resources/application.yml b/src/e2eTest/resources/application.yml index 1013de15..330660e2 100644 --- a/src/e2eTest/resources/application.yml +++ b/src/e2eTest/resources/application.yml @@ -8,13 +8,7 @@ hideout: private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ==" public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB" storage: - use-s3: true - endpoint: "http://localhost:8082/test-hideout" - public-url: "http://localhost:8082/test-hideout" - bucket: "test-hideout" - region: "auto" - access-key: "" - secret-key: "" + type: local spring: flyway: diff --git a/src/intTest/resources/application.yml b/src/intTest/resources/application.yml index b40fcd91..c73fc1f3 100644 --- a/src/intTest/resources/application.yml +++ b/src/intTest/resources/application.yml @@ -8,13 +8,7 @@ hideout: private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ==" public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB" storage: - use-s3: true - endpoint: "http://localhost:8082/test-hideout" - public-url: "http://localhost:8082/test-hideout" - bucket: "test-hideout" - region: "auto" - access-key: "" - secret-key: "" + type: local spring: flyway: diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/AwsConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/AwsConfig.kt index 67820efd..ad40a0bd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/AwsConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/AwsConfig.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.application.config +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import software.amazon.awssdk.auth.credentials.AwsBasicCredentials @@ -10,7 +11,8 @@ import java.net.URI @Configuration class AwsConfig { @Bean - fun s3Client(awsConfig: StorageConfig): S3Client { + @ConditionalOnProperty("hideout.storage.type", havingValue = "s3") + fun s3Client(awsConfig: S3StorageConfig): S3Client { return S3Client.builder() .endpointOverride(URI.create(awsConfig.endpoint)) .region(Region.of(awsConfig.region)) diff --git a/src/main/kotlin/dev/usbharu/hideout/application/config/SpringConfig.kt b/src/main/kotlin/dev/usbharu/hideout/application/config/SpringConfig.kt index 89275999..f3e088ab 100644 --- a/src/main/kotlin/dev/usbharu/hideout/application/config/SpringConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/application/config/SpringConfig.kt @@ -1,6 +1,7 @@ package dev.usbharu.hideout.application.config import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -13,9 +14,6 @@ class SpringConfig { @Autowired lateinit var config: ApplicationConfig - @Autowired - lateinit var storageConfig: StorageConfig - @Bean fun requestLoggingFilter(): CommonsRequestLoggingFilter { val loggingFilter = CommonsRequestLoggingFilter() @@ -33,9 +31,9 @@ data class ApplicationConfig( val url: URL ) -@ConfigurationProperties("hideout.storage") -data class StorageConfig( - val useS3: Boolean, +@ConfigurationProperties("hideout.storage.s3") +@ConditionalOnProperty("hideout.storage.type", havingValue = "s3") +data class S3StorageConfig( val endpoint: String, val publicUrl: String, val bucket: String, @@ -44,6 +42,20 @@ data class StorageConfig( val secretKey: String ) + +/** + * メディアの保存にローカルファイルシステムを使用する際のコンフィグ + * + * @property path フォゾンする場所へのパス。 /から始めると絶対パスとなります。 + * @property publicUrl 公開用URL 省略可能 指定するとHideoutがファイルを配信しなくなります。 + */ +@ConfigurationProperties("hideout.storage.local") +@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true) +data class LocalStorageConfig( + val path: String = "files", + val publicUrl: String? +) + @ConfigurationProperties("hideout.character-limit") data class CharacterLimit( val general: General = General(), diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStore.kt new file mode 100644 index 00000000..a7300081 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStore.kt @@ -0,0 +1,109 @@ +package dev.usbharu.hideout.core.service.media + +import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.application.config.LocalStorageConfig +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Service +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import kotlin.io.path.copyTo +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteIfExists +import kotlin.io.path.outputStream + +@Service +@ConditionalOnProperty("hideout.storage.type", havingValue = "local", matchIfMissing = true) +/** + * ローカルファイルシステムにメディアを保存します + * + * @constructor + * ApplicationConfigとLocalStorageConfigをもとに作成 + * + * @param applicationConfig ApplicationConfig + * @param localStorageConfig LocalStorageConfig + */ +class LocalFileSystemMediaDataStore( + applicationConfig: ApplicationConfig, localStorageConfig: LocalStorageConfig +) : MediaDataStore { + + private val savePath: Path = Path.of(localStorageConfig.path).toAbsolutePath() + + private val publicUrl = localStorageConfig.publicUrl ?: "${applicationConfig.url}/files/" + + init { + savePath.createDirectories() + } + + override suspend fun save(dataMediaSave: MediaSave): SavedMedia { + val fileSavePath = buildSavePath(savePath, dataMediaSave.name) + val thumbnailSavePath = buildSavePath(savePath, "thumbnail-" + dataMediaSave.name) + + + dataMediaSave.thumbnailInputStream?.inputStream()?.use { + it.buffered().use { bufferedInputStream -> + thumbnailSavePath.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + .use { outputStream -> + outputStream.buffered().use { + bufferedInputStream.transferTo(it) + } + } + } + } + + + dataMediaSave.fileInputStream.inputStream().use { + it.buffered().use { bufferedInputStream -> + fileSavePath.outputStream(StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE) + .use { outputStream -> outputStream.buffered().use { bufferedInputStream.transferTo(it) } } + } + } + + return SuccessSavedMedia( + dataMediaSave.name, publicUrl + dataMediaSave.name, publicUrl + "thumbnail-" + dataMediaSave.name + ) + } + + override suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia { + logger.info("START Media upload. {}", dataSaveRequest.name) + val fileSavePath = buildSavePath(savePath, dataSaveRequest.name) + val thumbnailSavePath = buildSavePath(savePath, "thumbnail-" + dataSaveRequest.name) + + val fileSavePathString = fileSavePath.toAbsolutePath().toString() + logger.info("MEDIA save. path: {}", fileSavePathString) + + try { + dataSaveRequest.filePath.copyTo(fileSavePath) + dataSaveRequest.thumbnailPath?.copyTo(thumbnailSavePath) + } catch (e: Exception) { + logger.warn("FAILED to Save the media.", e) + return FaildSavedMedia("FAILED to Save the media.", "Failed copy to path: $fileSavePathString", e) + } + + logger.info("SUCCESS Media upload. {}", dataSaveRequest.name) + return SuccessSavedMedia( + dataSaveRequest.name, publicUrl + dataSaveRequest.name, publicUrl + "thumbnail-" + dataSaveRequest.name + ) + } + + /** + * メディアを削除します。サムネイルも削除されます。 + * + * @param id 削除するメディアのid [SuccessSavedMedia.name]を指定します。 + */ + override suspend fun delete(id: String) { + logger.info("START Media delete. id: {}", id) + try { + buildSavePath(savePath, id).deleteIfExists() + buildSavePath(savePath, "thumbnail-$id").deleteIfExists() + } catch (e: Exception) { + logger.warn("FAILED Media delete. id: {}", id, e) + } + } + + private fun buildSavePath(savePath: Path, name: String): Path = savePath.resolve(name) + + companion object { + private val logger = LoggerFactory.getLogger(LocalFileSystemMediaDataStore::class.java) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt index 03ee3e9f..478ddc50 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/MediaDataStore.kt @@ -1,7 +1,31 @@ package dev.usbharu.hideout.core.service.media +/** + * メディアを保存するインタフェース + * + */ interface MediaDataStore { + /** + * InputStreamを使用してメディアを保存します + * + * @param dataMediaSave FileとThumbnailのinputStream + * @return 保存されたメディア + */ suspend fun save(dataMediaSave: MediaSave): SavedMedia + + /** + * 一時ファイルのパスを使用してメディアを保存します + * + * @param dataSaveRequest FileとThumbnailのパス + * @return 保存されたメディア + */ suspend fun save(dataSaveRequest: MediaSaveRequest): SavedMedia + + /** + * メディアを削除します + * 実装はサムネイル、メタデータなども削除するべきです。 + * + * @param id 削除するメディアのid 通常は[SuccessSavedMedia.name]を指定します。 + */ suspend fun delete(id: String) } diff --git a/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt index 4377fdc0..8b0f2235 100644 --- a/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt +++ b/src/main/kotlin/dev/usbharu/hideout/core/service/media/S3MediaDataStore.kt @@ -1,11 +1,12 @@ package dev.usbharu.hideout.core.service.media -import dev.usbharu.hideout.application.config.StorageConfig +import dev.usbharu.hideout.application.config.S3StorageConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.stereotype.Service import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.services.s3.S3Client @@ -14,16 +15,17 @@ import software.amazon.awssdk.services.s3.model.GetUrlRequest import software.amazon.awssdk.services.s3.model.PutObjectRequest @Service -class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig: StorageConfig) : MediaDataStore { +@ConditionalOnProperty("hideout.storage.type", havingValue = "s3") +class S3MediaDataStore(private val s3Client: S3Client, private val s3StorageConfig: S3StorageConfig) : MediaDataStore { override suspend fun save(dataMediaSave: MediaSave): SavedMedia { val fileUploadRequest = PutObjectRequest.builder() - .bucket(storageConfig.bucket) + .bucket(s3StorageConfig.bucket) .key(dataMediaSave.name) .build() val thumbnailKey = "thumbnail-${dataMediaSave.name}" val thumbnailUploadRequest = PutObjectRequest.builder() - .bucket(storageConfig.bucket) + .bucket(s3StorageConfig.bucket) .key(thumbnailKey) .build() @@ -36,7 +38,7 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig RequestBody.fromBytes(dataMediaSave.thumbnailInputStream) ) s3Client.utilities() - .getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(thumbnailKey).build()) + .getUrl(GetUrlRequest.builder().bucket(s3StorageConfig.bucket).key(thumbnailKey).build()) } else { null } @@ -44,14 +46,14 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig async { s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream)) s3Client.utilities() - .getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(dataMediaSave.name).build()) + .getUrl(GetUrlRequest.builder().bucket(s3StorageConfig.bucket).key(dataMediaSave.name).build()) } ) } return SuccessSavedMedia( name = dataMediaSave.name, - url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataMediaSave.name}", - thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey" + url = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/${dataMediaSave.name}", + thumbnailUrl = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/$thumbnailKey" ) } @@ -59,19 +61,19 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig logger.info("MEDIA upload. {}", dataSaveRequest.name) val fileUploadRequest = PutObjectRequest.builder() - .bucket(storageConfig.bucket) + .bucket(s3StorageConfig.bucket) .key(dataSaveRequest.name) .build() - logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, dataSaveRequest.name) + logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, dataSaveRequest.name) val thumbnailKey = "thumbnail-${dataSaveRequest.name}" val thumbnailUploadRequest = PutObjectRequest.builder() - .bucket(storageConfig.bucket) + .bucket(s3StorageConfig.bucket) .key(thumbnailKey) .build() - logger.info("MEDIA upload. bucket: {} key: {}", storageConfig.bucket, thumbnailKey) + logger.info("MEDIA upload. bucket: {} key: {}", s3StorageConfig.bucket, thumbnailKey) withContext(Dispatchers.IO) { awaitAll( @@ -92,8 +94,8 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig } val successSavedMedia = SuccessSavedMedia( name = dataSaveRequest.name, - url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataSaveRequest.name}", - thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey" + url = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/${dataSaveRequest.name}", + thumbnailUrl = "${s3StorageConfig.publicUrl}/${s3StorageConfig.bucket}/$thumbnailKey" ) logger.info("SUCCESS Media upload. {}", dataSaveRequest.name) @@ -108,9 +110,9 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig } override suspend fun delete(id: String) { - val fileDeleteRequest = DeleteObjectRequest.builder().bucket(storageConfig.bucket).key(id).build() + val fileDeleteRequest = DeleteObjectRequest.builder().bucket(s3StorageConfig.bucket).key(id).build() val thumbnailDeleteRequest = - DeleteObjectRequest.builder().bucket(storageConfig.bucket).key("thumbnail-$id").build() + DeleteObjectRequest.builder().bucket(s3StorageConfig.bucket).key("thumbnail-$id").build() s3Client.deleteObject(fileDeleteRequest) s3Client.deleteObject(thumbnailDeleteRequest) } diff --git a/src/test/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStoreTest.kt b/src/test/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStoreTest.kt new file mode 100644 index 00000000..e2854912 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/core/service/media/LocalFileSystemMediaDataStoreTest.kt @@ -0,0 +1,121 @@ +package dev.usbharu.hideout.core.service.media + +import dev.usbharu.hideout.application.config.ApplicationConfig +import dev.usbharu.hideout.application.config.LocalStorageConfig +import kotlinx.coroutines.test.runTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import java.net.URL +import java.nio.file.Path +import java.util.* +import kotlin.io.path.readBytes +import kotlin.io.path.toPath + +class LocalFileSystemMediaDataStoreTest { + + private val path = String.javaClass.classLoader.getResource("400x400.png")?.toURI()?.toPath()!! + + @Test + fun `save inputStreamを使用して正常に保存できる`() = runTest { + val applicationConfig = ApplicationConfig(URL("https://example.com")) + val storageConfig = LocalStorageConfig("files", null) + + val localFileSystemMediaDataStore = LocalFileSystemMediaDataStore(applicationConfig, storageConfig) + + val fileInputStream = path.readBytes() + + assertThat(fileInputStream.size).isNotEqualTo(0) + + val mediaSave = MediaSave( + "test-media-1${UUID.randomUUID()}.png", + "", + fileInputStream, + fileInputStream + ) + + val save = localFileSystemMediaDataStore.save(mediaSave) + + assertThat(save).isInstanceOf(SuccessSavedMedia::class.java) + + save as SuccessSavedMedia + + assertThat(Path.of("files").toAbsolutePath().resolve(save.name)) + .exists() + .hasSize(fileInputStream.size.toLong()) + assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name)) + .exists() + .hasSize(fileInputStream.size.toLong()) + } + + @Test + fun 一時ファイルを使用して正常に保存できる() = runTest { + val applicationConfig = ApplicationConfig(URL("https://example.com")) + val storageConfig = LocalStorageConfig("files", null) + + val localFileSystemMediaDataStore = LocalFileSystemMediaDataStore(applicationConfig, storageConfig) + + val fileInputStream = path.readBytes() + + assertThat(fileInputStream.size).isNotEqualTo(0) + + val saveRequest = MediaSaveRequest( + "test-media-2${UUID.randomUUID()}.png", + "", + path, + path + ) + + val save = localFileSystemMediaDataStore.save(saveRequest) + + assertThat(save).isInstanceOf(SuccessSavedMedia::class.java) + + save as SuccessSavedMedia + + assertThat(Path.of("files").toAbsolutePath().resolve(save.name)) + .exists() + .hasSize(fileInputStream.size.toLong()) + assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name)) + .exists() + .hasSize(fileInputStream.size.toLong()) + } + + @Test + fun idを使用して削除できる() = runTest { + val applicationConfig = ApplicationConfig(URL("https://example.com")) + val storageConfig = LocalStorageConfig("files", null) + + val localFileSystemMediaDataStore = LocalFileSystemMediaDataStore(applicationConfig, storageConfig) + + val fileInputStream = path.readBytes() + + assertThat(fileInputStream.size).isNotEqualTo(0) + + val saveRequest = MediaSaveRequest( + "test-media-2${UUID.randomUUID()}.png", + "", + path, + path + ) + + val save = localFileSystemMediaDataStore.save(saveRequest) + + assertThat(save).isInstanceOf(SuccessSavedMedia::class.java) + + save as SuccessSavedMedia + + assertThat(Path.of("files").toAbsolutePath().resolve(save.name)) + .exists() + .hasSize(fileInputStream.size.toLong()) + assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name)) + .exists() + .hasSize(fileInputStream.size.toLong()) + + + localFileSystemMediaDataStore.delete(save.name) + + assertThat(Path.of("files").toAbsolutePath().resolve(save.name)) + .doesNotExist() + assertThat(Path.of("files").toAbsolutePath().resolve("thumbnail-" + save.name)) + .doesNotExist() + } +}