와챠의 우당탕탕 코딩 일기장

[Android/Kotlin] S3 image upload/순서대로 여러장 업로드하기/RxKotlin/MultiUploaderS3Client 본문

코딩 일기장/Android(Kotlin)

[Android/Kotlin] S3 image upload/순서대로 여러장 업로드하기/RxKotlin/MultiUploaderS3Client

minWachya 2022. 9. 17. 16:18
반응형

오늘 해볼 것:

1. 사용자가 갤러리에서 선택한 이미지를(uri)

2. S3에 순서대로!! 여러장을 한꺼번에!! 업로드하고

3. 업로드한 이미지 링크(url)를 순서대로!! 한꺼번에!! 받아오기

 

플젝할 때 이미지 url을 DB에 저장하기로 했는데

순서가 중요하단 말임..??

순서대로 업로드하고, 그 링크를 순서대로 저장하기 위해 RxKotlin을 사용해 보았습니다..

 

S3와 Burket은 이미 생성되어있다고 가정한 상태에서 설명을 해보겠습니다.


1.S3 Access Key와 Secret access key를 github에 보이지 않도록 저장

아래 글 참고!!

https://min-wachya.tistory.com/220

 

[Android/Kotlin] hide KEY/KEY 숨기기

ACCESS KEY나 SECRET ACCESS KEY 같이 중요한 정보는 github에서 보여지면 위험하기 때문에 꼬옥 숨겨주어야 한다. 프로젝트 할 때 S3와 통신할 일이 있어서 ACCESS KEY랑 SECRET ACCESS KEY를 안드 내에 저장하고..

min-wachya.tistory.com

 

2. app단위 build.gradle에 라이브러리 추가(S3와 RxKotlin)

// S3
implementation 'com.amazonaws:aws-android-sdk-mobile-client:2.13.5'
implementation 'com.amazonaws:aws-android-sdk-cognito:2.13.5'
implementation 'com.amazonaws:aws-android-sdk-s3:2.13.5'

//RxAndroid, RxJava, RxKotlin
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.17'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
implementation 'com.uniquext.android:rxlifecycle:2.0.0'

 

3. Manifest에 권한 추가 + S3 Service 추가

<!--Glide, S3 사용 위한 권한-->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <application ...>
        <activity
            ...
        </activity>
        
        <service android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService" android:enabled="true"/>
    </application>

 

 

4. MultiUploaderS3Client class 생성

이 클래스의 목적은 아래와 같다.

1. S3에 이미지를 순서대로 업로드

2. 업로드 성공한 이미지의 URL을 받아와 저장

 

나는 모든 이미지가 성공한 뒤 처리할 이벤트가 있었기 때문에

클래스의 매개변수로 viewModel을 넣었고, 그 viewModel의 LiveData 변수를 listImageUrl을 넣어 초기화함으로써

완료 시점에 다른 이벤트를 실행할 수 있도록 구현하였다.

 

순서가 중요하지 않은 경우에는 이 링크를 참고하여 구현하면 된다.

나는 순서가 굉장히!! 중요했기 때문에

 

 

순서를 지켜주기 위해 Uri와 File이 들어간 Map도 LinkedHashMap을 사용하였으며,

flatMapCompletable로 하면 Map 안의 아이템들 순서를 유지하지 않기 때문에 concatMapEager를 사용했다.

// 이거 대신에
Observable.fromIterable(fileToKeyUploads.entries)
        .flatMapCompletable { entry ->
            uploadSingle(~)
        }
       
// 이거 사용
Observable.fromIterable(fileToKeyUploads.entries)
        .concatMapEager {
            uploadSingle(~~).toObservable()
        }.toList()

하지만 concatMapEager를 사용한다 해서 순서가 꼭 지켜지는 건 아니었다!!!

이미지1, 이미지2, 이미지3 이런 순서로 uploadSingle함수가 실행되는 건 맞았지만

이미지 크기나 용량 등에 따라 업로드가 완료되는 타이밍이 각각 달랐기 때문에

count 변수를 index로 사용하여 이미지 업로드 완료 후 listImageUrl에 저장되는 순서도 지켜주었다!!

import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.util.Log
import com.amazonaws.auth.BasicAWSCredentials
import com.amazonaws.mobileconnectors.s3.transferutility.TransferListener
import com.amazonaws.mobileconnectors.s3.transferutility.TransferNetworkLossHandler
import com.amazonaws.mobileconnectors.s3.transferutility.TransferState
import com.amazonaws.mobileconnectors.s3.transferutility.TransferUtility
import com.amazonaws.regions.Region
import com.amazonaws.regions.Regions
import com.amazonaws.services.s3.AmazonS3Client
import com.example.safetymanagement2022.common.TAG
import com.example.safetymanagement2022.ui.list.buildingcreate.BuildingCreateViewModel
import io.reactivex.Observable
import io.reactivex.Single
import java.io.File
import java.net.URL

// 이미지를 S3에 여러장 + 순서대로 업로드하는 클래스
class MultiUploaderS3Client(private val bucketName: String, context: Context, val vm: BuildingCreateViewModel) {
    private var maxSize = 0			// 선택한 이미지의 최대 갯수
    lateinit var listImageUrl: Array<String>	// URL을 순서대로 저장해둘 배열
    private var count = 0			// 순서대로 저장하기 위해 필요한 변수(index로 사용됨)

    private val ai: ApplicationInfo = context.packageManager
        .getApplicationInfo(context.packageName, PackageManager.GET_META_DATA)
    private val ak: String = ai.metaData["accessKey"].toString()
    private val sak: String = ai.metaData["secretAccessKey"].toString()
    private val wsCredentials: BasicAWSCredentials = BasicAWSCredentials(ak, sak)
    private val s3Client: AmazonS3Client =
        AmazonS3Client(wsCredentials, Region.getRegion(Regions.AP_NORTHEAST_2))
    private val transferUtility: TransferUtility = TransferUtility.builder()
        .s3Client(s3Client)
        .context(context)
        .build()

    init {
        TransferNetworkLossHandler.getInstance(context)
    }

	// 여러장을 한꺼번에 업로드
    // fileToKeyUploads: <Uri를 String화한 것, File>로 된 Map
    fun uploadMultiple(fileToKeyUploads: LinkedHashMap<String, File>): Single<MutableList<Unit>>? {
        maxSize = fileToKeyUploads.size	// 이미지 최대 갯수 초기화
        listImageUrl = Array(maxSize) { "" }	// url 배열 초기화
        // 사진 하나씩 순서대로 업로드(uploadSingle)
        return Observable.fromIterable(fileToKeyUploads.entries)
            .concatMapEager {
                uploadSingle(
                    transferUtility,
                    it.value,	// File
                    it.key,		// Uri to String
                    count++		// index
                ).toObservable()
            }.toList()
    }

	// 사진 하나씩 업로드하고 url 받아와서 저장하기
    private fun uploadSingle(
        transferUtility: TransferUtility?,
        aLocalFile: File?,
        toRemoteKey: String?,
        index: Int
    ): Single<Unit> {
        return Single.create {
            transferUtility?.upload(bucketName, toRemoteKey, aLocalFile)
                ?.setTransferListener(object : TransferListener {
                    override fun onStateChanged(id: Int, state: TransferState?) {
                        if (state == TransferState.COMPLETED) {
                        	// 업로드 성공 시 URL 저장하기
                            val s3Url: URL = s3Client.getUrl(bucketName, toRemoteKey)
                            listImageUrl[index] = s3Url.toString()
                            // 모든 이미지 업로드 성공 시 viewModel의 변수를 listImageUrl로 초기화
                            if(listImageUrl.size == maxSize)  vm.setArrS3Url(listImageUrl)
                        }
                    }

                    override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
                        val done = bytesCurrent / bytesTotal * 100.0
                        Log.d(TAG, "UPLOAD - - ID: $id, percent done = $done")
                    }

                    override fun onError(id: Int, ex: Exception?) {
                        Log.d(TAG, "UPLOAD ERROR - - ID: $id - - EX:" + ex.toString())
                    }
                })
        }
    }
}

 

5. Fragment나 Activity에 아래 함수를 정의해놓고 S3에 업로드할 때 사용

 private fun uploadImageToS3(map: LinkedHashMap<String, File>) {
        val buildingId = requireArguments().getInt(KEY_BUILDING_ID)
        MultiUploaderS3Client(
            "burket이름/저장되길 원하는 폴더명/$buildingId",
            requireContext(),
            viewModel
        ).uploadMultiple(map)
            ?.subscribeOn(Schedulers.io())
            ?.observeOn(Schedulers.io())
            ?.subscribe()
    }

 

끝입니다^_^


이 기능을 몇시간 내에 완성해내야 했기 때문에... 평소 써보고 싶었던 RxKotlin을 대충 다룬 거 같아서 아쉽다.

RxKotlin에 대한 공부를 더 해보고 이 기능을 다시 수정해봐야겟다


참고

https://stackoverflow.com/questions/46059515/amazon-s3-upload-multiple-files-android

 

Amazon s3 upload multiple files android

Incase anyone still looking for a solution i ended up using a loop on the code blew i did not find an official api to do multiple files upload. ------- I have an ArrayList of ImageFiles, that I w...

stackoverflow.com

https://velog.io/@cmplxn/%EC%88%9C%EC%84%9C%EB%A5%BC-%EC%9C%A0%EC%A7%80%ED%95%98%EB%A9%B0-%EC%9A%94%EC%B2%AD-%EB%B3%B4%EB%82%B4%EA%B8%B0

 

순서를 유지하며 요청 보내기

요즘 세상에 어느정도 기능이 있는 앱이라면, 서버와의 통신이 없을 수 없다.클라이언트에 수많은 데이터를 저장해둔다던가, 고성능이 필요한 연산을 실행할 수는 없기 때문이다.그래서 안드로

velog.io

 

반응형
Comments