From fb5cfdaa2fa9ff38c7bbf3569f9376f7cdd6b86f Mon Sep 17 00:00:00 2001 From: Ray Jang <48707913+ajou4095@users.noreply.github.com> Date: Sun, 4 Feb 2024 00:48:21 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20PreSigned=20=EA=B4=80=EB=A0=A8=20AP?= =?UTF-8?q?I=20/=20Repository=20=EC=97=B0=EA=B2=B0=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/data/di/RepositoryModule.kt | 8 ++ .../data/remote/network/api/FileApi.kt | 73 +++++++++++++++++++ .../network/model/file/GetPreSignedUrlRes.kt | 21 ++++++ .../repository/file/MockFileRepository.kt | 36 +++++++++ .../repository/file/RealFileRepository.kt | 29 ++++++++ .../android/domain/model/file/PreSignedUrl.kt | 6 ++ .../domain/repository/FileRepository.kt | 14 ++++ 7 files changed, 187 insertions(+) create mode 100644 data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/FileApi.kt create mode 100644 data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/file/GetPreSignedUrlRes.kt create mode 100644 data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/MockFileRepository.kt create mode 100644 data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/RealFileRepository.kt create mode 100644 domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/file/PreSignedUrl.kt create mode 100644 domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/FileRepository.kt diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt index b9ac914f..2bde022e 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt @@ -1,9 +1,11 @@ package ac.dnd.bookkeeping.android.data.di import ac.dnd.bookkeeping.android.data.repository.authentication.MockAuthenticationRepository +import ac.dnd.bookkeeping.android.data.repository.file.MockFileRepository import ac.dnd.bookkeeping.android.data.repository.member.MockMemberRepository import ac.dnd.bookkeeping.android.data.repository.sociallogin.KakaoLoginRepositoryImpl import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository +import ac.dnd.bookkeeping.android.domain.repository.FileRepository import ac.dnd.bookkeeping.android.domain.repository.KakaoLoginRepository import ac.dnd.bookkeeping.android.domain.repository.MemberRepository import dagger.Binds @@ -28,6 +30,12 @@ internal abstract class RepositoryModule { memberRepository: MockMemberRepository ): MemberRepository + @Binds + @Singleton + abstract fun bindsFileRepository( + fileRepository: MockFileRepository + ): FileRepository + @Binds @Singleton abstract fun bindsKakaoLoginRepository( diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/FileApi.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/FileApi.kt new file mode 100644 index 00000000..66b66f3a --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/FileApi.kt @@ -0,0 +1,73 @@ +package ac.dnd.bookkeeping.android.data.remote.network.api + +import ac.dnd.bookkeeping.android.data.remote.network.di.AuthHttpClient +import ac.dnd.bookkeeping.android.data.remote.network.di.NoAuthHttpClient +import ac.dnd.bookkeeping.android.data.remote.network.environment.BaseUrlProvider +import ac.dnd.bookkeeping.android.data.remote.network.environment.ErrorMessageMapper +import ac.dnd.bookkeeping.android.data.remote.network.model.file.GetPreSignedUrlRes +import ac.dnd.bookkeeping.android.data.remote.network.util.convert +import android.net.Uri +import android.webkit.MimeTypeMap +import io.ktor.client.HttpClient +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitFormWithBinaryData +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import java.io.File +import javax.inject.Inject + + +class FileApi @Inject constructor( + @NoAuthHttpClient private val noAuthClient: HttpClient, + @AuthHttpClient private val client: HttpClient, + private val baseUrlProvider: BaseUrlProvider, + private val errorMessageMapper: ErrorMessageMapper +) { + private val baseUrl: String + get() = baseUrlProvider.get() + + suspend fun getPreSignedUrl( + fileName: String + ): Result { + return client.get("$baseUrl/api/v1/files/presigned") { + parameter("fileName", fileName) + }.convert(errorMessageMapper::map) + } + + suspend fun upload( + preSignedUrl: String, + imageUri: String, + fileName: String? = null + ): Result { + val image = Uri.parse(imageUri)?.path ?: let { + return Result.failure(IllegalArgumentException("Invalid imageUri")) + } + val file = File(image) + val name = fileName ?: file.name + val contentType = getContentType(file.path) + + return noAuthClient.submitFormWithBinaryData( + url = preSignedUrl, + formData = formData { + append( + "image", + file.readBytes(), + Headers.build { + contentType?.let { append(HttpHeaders.ContentType, it) } + append(HttpHeaders.ContentDisposition, "filename=$name") + } + ) + } + ).convert(errorMessageMapper::map) + } + + private fun getContentType( + url: String + ): String? { + return MimeTypeMap.getFileExtensionFromUrl(url)?.let { fileExtension -> + MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension) + } + } +} diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/file/GetPreSignedUrlRes.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/file/GetPreSignedUrlRes.kt new file mode 100644 index 00000000..deaa6bcc --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/file/GetPreSignedUrlRes.kt @@ -0,0 +1,21 @@ +package ac.dnd.bookkeeping.android.data.remote.network.model.file + +import ac.dnd.bookkeeping.android.data.remote.mapper.DataMapper +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetPreSignedUrlRes( + @SerialName("preSignedUrl") + val preSignedUrl: String, + @SerialName("uploadFileUrl") + val uploadFileUrl: String +) : DataMapper { + override fun toDomain(): PreSignedUrl { + return PreSignedUrl( + preSignedUrl = preSignedUrl, + uploadFileUrl = uploadFileUrl + ) + } +} diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/MockFileRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/MockFileRepository.kt new file mode 100644 index 00000000..1429a110 --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/MockFileRepository.kt @@ -0,0 +1,36 @@ +package ac.dnd.bookkeeping.android.data.repository.file + +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl +import ac.dnd.bookkeeping.android.domain.repository.FileRepository +import kotlinx.coroutines.delay +import javax.inject.Inject + +class MockFileRepository @Inject constructor() : FileRepository { + override suspend fun getPreSignedUrl( + fileName: String + ): Result { + randomShortDelay() + return Result.success( + PreSignedUrl( + preSignedUrl = "https://example.com", + uploadFileUrl = "https://example.com" + ) + ) + } + + override suspend fun upload( + preSignedUrl: String, + imageUri: String + ): Result { + randomLongDelay() + return Result.success(Unit) + } + + private suspend fun randomShortDelay() { + delay(LongRange(100, 500).random()) + } + + private suspend fun randomLongDelay() { + delay(LongRange(500, 2000).random()) + } +} diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/RealFileRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/RealFileRepository.kt new file mode 100644 index 00000000..3d7b473c --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/RealFileRepository.kt @@ -0,0 +1,29 @@ +package ac.dnd.bookkeeping.android.data.repository.file + +import ac.dnd.bookkeeping.android.data.remote.local.SharedPreferencesManager +import ac.dnd.bookkeeping.android.data.remote.network.api.FileApi +import ac.dnd.bookkeeping.android.data.remote.network.util.toDomain +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl +import ac.dnd.bookkeeping.android.domain.repository.FileRepository +import javax.inject.Inject + +class RealFileRepository @Inject constructor( + private val fileApi: FileApi, + private val sharedPreferencesManager: SharedPreferencesManager +) : FileRepository { + override suspend fun getPreSignedUrl( + fileName: String + ): Result { + return fileApi.getPreSignedUrl(fileName).toDomain() + } + + override suspend fun upload( + preSignedUrl: String, + imageUri: String + ): Result { + return fileApi.upload( + preSignedUrl = preSignedUrl, + imageUri = imageUri + ) + } +} diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/file/PreSignedUrl.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/file/PreSignedUrl.kt new file mode 100644 index 00000000..043aad03 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/file/PreSignedUrl.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.domain.model.file + +data class PreSignedUrl( + val preSignedUrl: String, + val uploadFileUrl: String +) diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/FileRepository.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/FileRepository.kt new file mode 100644 index 00000000..af80f801 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/FileRepository.kt @@ -0,0 +1,14 @@ +package ac.dnd.bookkeeping.android.domain.repository + +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl + +interface FileRepository { + suspend fun getPreSignedUrl( + fileName: String + ): Result + + suspend fun upload( + preSignedUrl: String, + imageUri: String + ): Result +}