interface FileUploadService {
@Multipart
@POST(".")
//원래는 baseURL 뒤에 이 api를 위한 동적인 주소를 적어줘야 하지만,
//baseURL 뒤에 붙어 있는 다른 주소가 없다면 . 을 찍어주면 된다.
suspend fun apiPostFile(
@PartMap partMap: Map<String,@JvmSuppressWildcards RequestBody>,
@Part files: List<MultipartBody.Part>
): retrofit2.Response<ResponseWrapper<FileUploadResponse>>
}
multiPart란?
- 웹 클라이언트가 요청을 보낼 때, http 프로토콜의 바디 부분에 데이터를 여러 부분으로 나눠서 보내는 것.
파일을 한방에 바디에 넣어 보내기엔 너무 무리 이기 때문에, 파일을 잘게 나눠서 보낸다고 이해하면 된다.
Requestbody란?
- multipart 통신을 위해서는 requestBody 라는
okhttp에서 제공하는 타입(아닏듯? http통신 규칙인듯하다.현재 이부분 다시 공부중인데 정확히 공부하고 다시 적어야징)으로 모든 데이터를 보내야합니다.
파일 뿐만 아니라 String, Int 등 primitive 타입 역시 모두 requestBody 타입으로 보내야 합니다.
MultipartBody.Part 역시 body 부분은 requestBody 타입입니다.class Part private constructor( @get:JvmName("headers") val headers: Headers?, @get:JvmName("body") val body: RequestBody )
//string이 아닌 데이터 -> requestBody
//toString()으로 String 타입으로 바꾼 후 toRequestBody()
val num = 3
var requestNum: RequestBody =
num.toString().toRequestBody("text/plain".toMediaTypeOrNull())
//string -> requestBody
val name = "gosoo"
var requestName: RequestBody =
num.toRequestBody("text/plain".toMediaTypeOrNull())
//공식문서에는 .toRequestBody(MultipartBody.FORM) 이라고 되어 있어서 이렇게했는데
// val FORM = "multipart/form-data".toMediaType()
//블로그에는 .toRequestBody("text/plain".toMediaTypeOrNull()) 라고 되어 있었습니다.
//찾아보니, 저 부분은 인코딩을 어떻게 할지 결정하는 부분인데
//아래 사진과 같다고 합니다.
//지금은 파일/이미지를 데이터로 넣는 부분이 아니니 text/plain 을 써도 될 것 같습니다.
//특히나 데이터에 공백이 있다면 무조건 text/plain 타입을 써야 하지 않을까 싶습니다.
위에 방식대로 requestBody로 변환된 primitive 타입 데이터는 두가지 방법으로 데이터를 담아 보낼 수 있습니다.
ㅤ
1. 보내야 할 primitive 타입의 파라미터가 많을 때 -
@PartMap partMap: Map<String, RequestBody>
requestBody를 map 으로 묶어 한번에 보내기// create a map of data to pass along //createPartFromString는 위의 primitive type -> requestBody 부분임. val description = createPartFromString("hello, this is description speaking") val place = createPartFromString("Magdeburg") val time = createPartFromString("2016") ㅤ val partMap : HashMap<String, RequestBody> = hashMapOf() partMap.put("description", description) partMap.put("place", place) partMap.put("time", time)
ㅤ
2. 보내야 할 primitive 타입의 파라미터가 하나 일 때 -
@Part ("description") description : RequestBody
requestBody 하나만 보내기
//description에는 api 에서 요구하는 파라미터 이름을 넣어주면 된다.val description = createPartFromString("hello, this is description speaking")
보통 이미지 하나만 보내는 경우보다 , 여러개의 이미지를 한번에 보내는 경우가 많기 때문에 여러개의 이미지를 리스트로 묶어 보내는 방법을 설명하겠음.
fun getPartList(
uriArray: List<Uri>,
context: Context
): List<MultipartBody.Part> {
return uriArray.mapNotNull {
it.asMultipart("files", context.contentResolver)
// return uriArray.mapNotNull {
// val file = File(it.path)
// MultipartBody.Part
// .createFormData(
// "files",
// file.name,
// file.asRequestBody(context. contentResolver.getType(it)?.toMediaType())
// )
// }
}}
//선택된 파일의 uri -> multipart
//api 29 이전에는 위의 주석된 코드로 작동 가능했으나
//안드로이드 보안 정책 변경으로 인해 아래의 코드로 작동 가능함
// 출처 : https://ohdbjj.tistory.com/49
@SuppressLint("Range")
fun Uri.asMultipart(name: String, contentResolver: ContentResolver): MultipartBody.Part? {
return contentResolver.query(this, null, null, null, null)?.let {
if (it.moveToNext()) {
val displayName = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME));
val requestBody = object : RequestBody() {
override fun contentType(): MediaType? {
return contentResolver.getType(this@asMultipart)?.toMediaType()
}
override fun writeTo(sink: BufferedSink) {
sink.writeAll(contentResolver.openInputStream(this@asMultipart)?.source()!!)
}
}
it.close()
MultipartBody.Part.createFormData(name, displayName, requestBody)
} else {
it.close()
null
}
}
}
궁금한 점
- 회사에서는 s3를 이용하기 때문에 파일 용량에 제한이 없었다.
하지만 만약 서버통신에 파일 용량 제한이 있다면,
파일을 선 압축 후 멀티파트로 변환해서 서버통신을 해야 하는지?- Uri.asMultipart 이 방법은 이미지/pdf 등 파일 종류 상관없이 작동하는지?