티스토리 뷰

매번 서버로 요청해서 데이타를 받거나 파일을 받아 저장만 했었는데,

이번에는 로컬의 파일을 서버로 보내는 일을 하게 되었네요.

 

기존 volley를 이용해 보냈는데 파일이 없다고 해서 왜 없나 한참을 고민하다,

iOS에서 AlamoFire 라이브러리로 통신을 하는걸 보니 param 구조가 다르다는걸 알게 되었어요.

 

기존 VolleyFileUploadRequest class에 몇가지 추가를 해주면 된다.

getBody()에 getByteData에 파일 정보를 가져오는 코드를 추가해준다.

 

    override fun getBody(): ByteArray {
        val byteArrayOutputStream = ByteArrayOutputStream()
        val dataOutputStream = DataOutputStream(byteArrayOutputStream)
        try {
            if (params != null && params.isNotEmpty()) {
                processParams(dataOutputStream, params, paramsEncoding)
            }

            // 파일 내용을 가져오는 코드 추가 [[
            val data = getByteData() as? Map<String, FileDataPart>?
            if (data != null && data.isNotEmpty()) {
                processData(dataOutputStream, data)
            }
            // ]]

            dataOutputStream.writeBytes(divider + boundary + divider + ending)
            return byteArrayOutputStream.toByteArray()

        } catch (e: IOException) {
            e.printStackTrace()
        }
        return super.getBody()
    }

 

그리고 추가한 코드에서 필요한 함수도 추가

    @Throws(AuthFailureError::class)
    open fun getByteData(): Map<String, Any>? {
        return null
    }
    
    @Throws(IOException::class)
    private fun processData(dataOutputStream: DataOutputStream, data: Map<String, FileDataPart>) {
        data.forEach {
            val dataFile = it.value
            dataOutputStream.writeBytes("$divider$boundary$ending")
            dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"${it.key}\"; filename=\"${dataFile.fileName}\"$ending")
            if (dataFile.type.trim().isNotEmpty()) {
                dataOutputStream.writeBytes("Content-Type: ${dataFile.type}$ending")
            }
            dataOutputStream.writeBytes(ending)
            val fileInputStream = ByteArrayInputStream(dataFile.data)
            var bytesAvailable = fileInputStream.available()
            val maxBufferSize = 1024 * 1024
            var bufferSize = min(bytesAvailable, maxBufferSize)
            val buffer = ByteArray(bufferSize)
            var bytesRead = fileInputStream.read(buffer, 0, bufferSize)
            while (bytesRead > 0) {
                dataOutputStream.write(buffer, 0, bufferSize)
                bytesAvailable = fileInputStream.available()
                bufferSize = min(bytesAvailable, maxBufferSize)
                bytesRead = fileInputStream.read(buffer, 0, bufferSize)
            }
            dataOutputStream.writeBytes(ending)
        }
    }
}

class FileDataPart(var fileName: String?, var data: ByteArray, var type: String)

 

파일 데이터는 multipart/form-data 형식으로 보낼때 일반 parameter와 다른걸 알 수 있었다.

일반 parameter는 key(name), value 구조로 전송(실제 전송 데이타는 이것저것 구분되는것이 추가 됨) 하지만,

파일 데이터는 key(name), filename, Content-Type(옵션) 그리고 파일 데이터는 "Content-Type"에 추가되어 전송이 이뤄지는걸 알 수 있다.

이 부분이 없어 파일이 전송이 안되는 것이였다.

 

그리고 여기서 중요한 것은 파일을 받아 저장하는 서버에서 지정된 paramter 이름으로 해야 파일 데이터를 전달받아 저장할 수 있다.

예를들어 parameter 이름이 fileCtnt인데 이 부분에 다른 이름인 파일 이름을 넣어줬더니 서버에서 수신을 못해 문제가 발생하였다.

 

그리고 VolleyFileUploadRequest를 호출하는 곳에서 getByteData()를 override 해줘야한다.

            override fun getByteData(): Map<String, Any> {
                val params = HashMap<String, FileDataPart>()

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    val file = File(경로, 파일명)  // 업로드할 파일 정보
                    params[fileUpload.fileKeyName] = FileDataPart("fileCtnt", Files.readAllBytes(file.toPath()), "jpg")
                } else {
                    // TODO android version O 미만에 대한 처리 필요
                }

                return params
            }

파일의 byteArray를 가져오기 위해 Files.readAllBytes()를 사용하다보니 android O 이상에서만 동작이 되어, 하위 버전에 대한 처리는 추가로 작업이 필요하네요. ㅠㅠ

 

이렇게 해서 파일이 서버에 올라는것은 확인 했습니다.

하지만 파일 사이즈가 큰 것은 분할해서 보내야한다고 하네요.

이 부분에 대한 숙제가 남아있어 다시 찾아보러 갑니다.

 

아래는 VolleyFileUploadRequest class 전체 내용입니다.

import com.android.volley.*
import com.android.volley.toolbox.HttpHeaderParser
import java.io.*
import java.lang.Integer.min

open class VolleyFileUploadRequest(
    method: Int,
    url: String,
    listener: Response.Listener<NetworkResponse>,
    errorListener: Response.ErrorListener) : Request<NetworkResponse>(method, url, errorListener) {

    private var responseListener: Response.Listener<NetworkResponse>? = null

    init {
        this.responseListener = listener
    }

    private var headers: Map<String, String>? = null
    private val divider: String = "--"
    private val ending = "\r\n"
    private val boundary = "imageRequest${System.currentTimeMillis()}"

    override fun getHeaders(): MutableMap<String, String> =
        when(headers) {
            null -> super.getHeaders()
            else -> headers!!.toMutableMap()
        }

    override fun getBodyContentType() = "multipart/form-data;boundary=$boundary"

    @Throws(AuthFailureError::class)
    override fun getBody(): ByteArray {
        val byteArrayOutputStream = ByteArrayOutputStream()
        val dataOutputStream = DataOutputStream(byteArrayOutputStream)
        try {
            if (params != null && params.isNotEmpty()) {
                processParams(dataOutputStream, params, paramsEncoding)
            }

            val data = getByteData() as? Map<String, FileDataPart>?
            if (data != null && data.isNotEmpty()) {
                processData(dataOutputStream, data)
            }

            dataOutputStream.writeBytes(divider + boundary + divider + ending)
            return byteArrayOutputStream.toByteArray()

        } catch (e: IOException) {
            e.printStackTrace()
        }
        return super.getBody()
    }

    @Throws(AuthFailureError::class)
    open fun getByteData(): Map<String, Any>? {
        return null
    }

    override fun parseNetworkResponse(response: NetworkResponse): Response<NetworkResponse> {
        return try {
            Response.success(response, HttpHeaderParser.parseCacheHeaders(response))
        } catch (e: Exception) {
            Response.error(ParseError(e))
        }
    }

    override fun deliverResponse(response: NetworkResponse) {
        responseListener?.onResponse(response)
    }

    override fun deliverError(error: VolleyError) {
        errorListener?.onErrorResponse(error)
    }

    @Throws(IOException::class)
    private fun processParams(dataOutputStream: DataOutputStream, params: Map<String, String>, encoding: String) {
        try {
            params.forEach {
                dataOutputStream.writeBytes(divider + boundary + ending)
                dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"${it.key}\"$ending")
                dataOutputStream.writeBytes(ending)
                dataOutputStream.writeBytes(it.value + ending)
            }
        } catch (e: UnsupportedEncodingException) {
            throw RuntimeException("Unsupported encoding not supported: $encoding with error: ${e.message}", e)
        }
    }

    @Throws(IOException::class)
    private fun processData(dataOutputStream: DataOutputStream, data: Map<String, FileDataPart>) {
        data.forEach {
            val dataFile = it.value
            dataOutputStream.writeBytes("$divider$boundary$ending")
            dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"${it.key}\"; filename=\"${dataFile.fileName}\"$ending")
            if (dataFile.type.trim().isNotEmpty()) {
                dataOutputStream.writeBytes("Content-Type: ${dataFile.type}$ending")
            }
            dataOutputStream.writeBytes(ending)
            val fileInputStream = ByteArrayInputStream(dataFile.data)
            var bytesAvailable = fileInputStream.available()
            val maxBufferSize = 1024 * 1024
            var bufferSize = min(bytesAvailable, maxBufferSize)
            val buffer = ByteArray(bufferSize)
            var bytesRead = fileInputStream.read(buffer, 0, bufferSize)
            while (bytesRead > 0) {
                dataOutputStream.write(buffer, 0, bufferSize)
                bytesAvailable = fileInputStream.available()
                bufferSize = min(bytesAvailable, maxBufferSize)
                bytesRead = fileInputStream.read(buffer, 0, bufferSize)
            }
            dataOutputStream.writeBytes(ending)
        }
    }
}

class FileDataPart(var fileName: String?, var data: ByteArray, var type: String)

 

 

 

출처 : https://blog.naver.com/rain3k/222036923043