volley로 이미지를 업로드하기 위해서는 mime-type이 multipart인 리퀘스트를 보내야한다.
그러려면 volley를 업그레이드하기 위해 돈을내던가, 직접구현해야한다.
* 구현한 코드는 맨 밑에 첨부합니다.
A HTTP multipart request is a HTTP request that HTTP clients
construct to send files and data over to a HTTP Server.
It is commonly used by browsers and HTTP clients to upload files to the server.
As the official specification says,
"one or more different sets of data are combined in a single body".
So when photos and music are handled as multipart messages as
mentioned in the question, probably there is some plain text metadata
associated as well, thus making the request containing different types
of data (binary, text), which implies the usage of multipart.
대충 해석해보면 멀티파트 리퀘스트란 http 서버로 파일과 데이터를 전송하기위한 http 리퀘스트다.
이미지와 함께 이미지를 설명하는 텍스트 등을 함께 보낼 수 있다.
실제 사용예를 보자.
private fun testMultipart(url: String)
{
val volleyMultipartRequest: VolleyMultipartRequest = object : VolleyMultipartRequest(
Method.POST, url,
Response.Listener {
//응답
},
Response.ErrorListener {
//에러
})
//VolleyMultipartRequest의 구현부
{
override fun getByteData(): java.util.ArrayList<Pair<String, DataPart>> {
//여기서 이미지파일을 넣은 컬렉션을 리턴하면 된다.
val params = ArrayList<Pair<String, DataPart>>()
params.add(Pair(/*이미지를 구분할 태그*/,
DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ )))
return params
}
override fun getParams(): MutableMap<String, String> {
//여기서 이미지와 함께할 정보를 컬렉션에 넣어 리턴하면 된다.
val params: MutableMap<String, String> = HashMap()
return params
}
}
VolleyHelper.getInstance(this).addRequestQueue(volleyMultipartRequest)
}
여기서 이 부분에 주목해보자.
override fun getByteData(): java.util.ArrayList<Pair<String, DataPart>> {
//여기서 이미지파일을 넣은 컬렉션을 리턴하면 된다.
val params = ArrayList<Pair<String, DataPart>>()
params.add(Pair(/*이미지를 구분할 태그*/,
DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ )))
return params
}
여기서 DataPart는 실제 데이터 즉, payload를 의미한다.
payload가 아닌것에는 위에 써놓은 것처럼 이미지를 구분할 태그가 있는데, 이는 html의 form에서 name에 해당하는 부분이다.
<form id="form1" action="http://localhost:8001/writePost" method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="images/*">
<input type="submit">
</form>
이렇게 서버로 전송된 이미지는 node express에서
.post('/updatePost', upload.array(/*이미지 태그*/, 5), (req, res) => {
//req.files 에 이미지파일들이 들어가있다.
}
이렇게 받을 수 있다.
upload.array의 두번째인자는 최대로 받을 이미지 갯수이다.
예를들어
params.add(Pair("image", DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ )))
이렇게 보냈으면
.post('/updatePost', upload.array("image", 5), (req, res) => {
//req.files 에 이미지파일들이 들어가있다.
}
이렇게 받는다.
내가 프로젝트를 진행할때는 5개의 파일을 모두 image라는 태그에 묶어서 보냈다.
<form id="form1" action="http://localhost:8001/writePost" method="POST" enctype="multipart/form-data">
<input type="file" name="image" accept="images/*">
<input type="file" name="image" accept="images/*">
<input type="file" name="image" accept="images/*">
<input type="file" name="image" accept="images/*">
<input type="file" name="image" accept="images/*">
<input type="submit">
</form>
요렇게해서 form으로 전송한 것과 같다.
하지만 문제가 발생했었다.
사실 처음에는 getByteData의 리턴타입이 ArrayList<ArrayList<Pair<String, DataPart>>이 아니라 원래는 map이였다.
그러다보니
override fun getByteData(): Map<String, DataPart> {
//여기서 이미지파일을 넣은 컬렉션을 리턴하면 된다.
val params = HashMap<String, DataPart>()
params.put("image", DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ ))
params.put("image", DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ ))
params.put("image", DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ ))
params.put("image", DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ ))
params.put("image", DataPart(/*이미지파일 이름*/,/*이미지를 바이트어레이로 변환한 것*/ ))
return params
}
이런식으로 코드를 짰었는데, 결과적으로 params 에는 하나의 데이터만 들어갔다.
hashMap이 중복을 허용하지 않는다는 것을 간과했던 것이다.
따라서 MultipartRequest의 getByteData() 함수의 리턴타입을 ArrayList<ArrayList<Pair<String, DataPart>>로 바꾼것이다.
아무튼 이점은 유의해야한다.
마지막으로 내가 쓴 MultipartRequest 전체를 올린다.
package com.manta.firstapp;
import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Map;
import kotlin.Pair;
public class VolleyMultipartRequest extends Request<NetworkResponse> {
private final String twoHyphens = "--";
private final String lineEnd = "\r\n";
private final String boundary = "apiclient-" + System.currentTimeMillis();
private Response.Listener<NetworkResponse> mListener;
private Response.ErrorListener mErrorListener;
private Map<String, String> mHeaders;
public VolleyMultipartRequest(int method, String url,
Response.Listener<NetworkResponse> listener,
Response.ErrorListener errorListener) {
super(method, url, errorListener);
this.mListener = listener;
this.mErrorListener = errorListener;
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return (mHeaders != null) ? mHeaders : super.getHeaders();
}
@Override
public String getBodyContentType() {
return "multipart/form-data;boundary=" + boundary;
}
@Override
public byte[] getBody() throws AuthFailureError {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
try {
// populate text payload
Map<String, String> params = getParams();
if (params != null && params.size() > 0) {
textParse(dos, params, getParamsEncoding());
}
// populate data byte payload
ArrayList<Pair<String, DataPart>> data = getByteData();
if (data != null && data.size() > 0) {
dataParse(dos, data);
}
// close multipart form data after text and file data
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
//Log.d("volley", bos.toString());
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* Custom method handle data payload.
*
* @return Map data part label with data byte
* @throws AuthFailureError
*/
protected ArrayList<Pair<String, DataPart>> getByteData() throws AuthFailureError {
return null;
}
@Override
protected Response<NetworkResponse> parseNetworkResponse(NetworkResponse response) {
try {
return Response.success(
response,
HttpHeaderParser.parseCacheHeaders(response));
} catch (Exception e) {
return Response.error(new ParseError(e));
}
}
@Override
protected void deliverResponse(NetworkResponse response) {
mListener.onResponse(response);
}
@Override
public void deliverError(VolleyError error) {
mErrorListener.onErrorResponse(error);
}
/**
* Parse string map into data output stream by key and value.
*
* @param dataOutputStream data output stream handle string parsing
* @param params string inputs collection
* @param encoding encode the inputs, default UTF-8
* @throws IOException
*/
private void textParse(DataOutputStream dataOutputStream, Map<String, String> params, String encoding) throws IOException {
try {
for (Map.Entry<String, String> entry : params.entrySet()) {
buildTextPart(dataOutputStream, entry.getKey(), entry.getValue());
}
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("Encoding not supported: " + encoding, uee);
}
}
/**
* Parse data into data output stream.
*
* @param dataOutputStream data output stream handle file attachment
* @param data loop through data
* @throws IOException
*/
private void dataParse(DataOutputStream dataOutputStream,ArrayList<Pair<String, DataPart>> data) throws IOException {
for (Pair<String, DataPart> entry : data) {
buildDataPart(dataOutputStream, entry.getSecond(), entry.getFirst());
}
}
/**
* Write string data into header and data output stream.
*
* @param dataOutputStream data output stream handle string parsing
* @param parameterName name of input
* @param parameterValue value of input
* @throws IOException
*/
private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + parameterName + "\"" + lineEnd);
dataOutputStream.writeBytes(lineEnd);
dataOutputStream.write(parameterValue.getBytes("UTF-8"));
dataOutputStream.writeBytes(lineEnd);
}
/**
* Write data file into header and data output stream.
*
* @param dataOutputStream data output stream handle data parsing
* @param dataFile data byte as DataPart from collection
* @param inputName name of data input
* @throws IOException
*/
//https://lng1982.tistory.com/209 참고
private void buildDataPart(DataOutputStream dataOutputStream, DataPart dataFile, String inputName) throws IOException {
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
//inputName은 파라미터가 되는듯?
dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" +
inputName + "\"; filename=\"" + dataFile.getFileName() + "\"" + lineEnd);
if (dataFile.getType() != null && !dataFile.getType().trim().isEmpty()) {
dataOutputStream.writeBytes("Content-Type: " + dataFile.getType() + lineEnd);
}
dataOutputStream.writeBytes(lineEnd);
ByteArrayInputStream fileInputStream = new ByteArrayInputStream(dataFile.getContent());
int bytesAvailable = fileInputStream.available();
int maxBufferSize = 1024 * 1024;
int bufferSize = Math.min(bytesAvailable, maxBufferSize);
byte[] buffer = new byte[bufferSize];
int bytesRead = fileInputStream.read(buffer, 0, bufferSize);
while (bytesRead > 0) {
dataOutputStream.write(buffer, 0, bufferSize);
bytesAvailable = fileInputStream.available();
bufferSize = Math.min(bytesAvailable, maxBufferSize);
bytesRead = fileInputStream.read(buffer, 0, bufferSize);
}
dataOutputStream.writeBytes(lineEnd);
}
public class DataPart {
private String fileName;
private byte[] content;
private String type;
public DataPart() {
}
public DataPart(String name, byte[] data, String type) {
fileName = name;
content = data;
this.type = type;
}
String getFileName() {
return fileName;
}
byte[] getContent() {
return content;
}
String getType() {
return type;
}
}
}
'안드로이드 앱개발' 카테고리의 다른 글
파일입출력2 : 저장소 (Android Studio Internal/External Storage) (0) | 2020.10.27 |
---|---|
파일입출력 1 : 쓰고 읽기 (File I/O in Kotlin) (0) | 2020.10.27 |
AdMob nativeAd 를 RecyclerView를 활용해 표시하기 (nativeAd with RecyclerView) (0) | 2020.10.27 |
라이브러리는 이해하고 사용하자. (0) | 2020.10.27 |
액티비티(Activity)와 인텐트(Intent) (0) | 2020.10.27 |