Java EE 정리 15
19 Mar 2019
Reading time ~7 minutes
Java EE 정리 15 - File Upload(1), File Download
File Upload
- RFC 1867 Form Based File Upload in HTML
- form태그로 웹에서 파일을 업로드하기 위한 규약
- Web Browser를 사용하여 File을 업로드
- 대용량 파일(600MB 이상) 업로드 시 효율이 많이 떨어진다.
- 전송시 에러가 발생하면 처음부터 재전송해야 한다.
- 별도의 프로그램을 개발하지 않고도 편하게 파일을 전송할 수 있다.
- File Upload Component(외부 jar 파일, 외부 library)는 Apache Commons File Upload, Multipart Request(cos.jar) 등이 존재
- 주의사항
- encType을 변경해야 한다.
- parameter 전송방식에서 file upload 방식으로 변경해야 함
- parameter 전송방식 - “application/x-www-form-urlencoded”
- 기본전송방식
- submit 발생 시 request.getParameter로 값을 받을 수 있음
- file upload 전송방식- “multipart/form-data”
- request.getParameter로 받을 수 없다.
- 파일은 GET방식(문자열 데이터 255자)으로 절대 전달할 수 없다.
- 파일 전송방식은 POST!
- encType을 변경해야 한다.
<!-- 변경 전, 보통 생략 -->
<form ... enctype="application/x-www-form-urlencoded">
...
<!-- 변경 후 -->
<form ... enctype="multipart/form-data">
<input type="file" ... />
...
</form>
파일 크기 계산법
- 파일의 저장 단위
- byte -> kb -> mb -> gb -> tb -> …
- 5 MB의 크기는?
- 1024(byte)*1024(kb)*5(mb)
- 10 MB의 크기는?
- 1024(byte)*1024(kb)*10(mb)
Apache Commons File Upload 설치
- Apache 재단 사이트 - Commons - FileUpload 선택
- Commons IO에 의존성 존재, Commons IO도 같이 설치해야 함
- Apache Commons IO 2.6(JDK 1.7이상 필요), Apache Commons FileUpload 1.4(JDK 1.6이상 필요) Binaries 집 다운로드 후 사용할 jar파일들을 작업하는 프로젝트 WEB-INF/lib에 설치
commons-fileupload-1.4.jar
,commons-io-2.6.jar
- Apache Commons FileUpload 사용하기 위한 사전준비 페이지
<!-- upload_form.jsp -->
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
...
<script type="text/javascript">
$(function() {
$('#btnUpload').click(function() {
var uploader = $("#uploader").val();
if(uploader == "") {
$("#uploader").focus();
return;
}
var file = $("#upfile").val();
if (file == "") {
alert("업로드할 파일을 선택해주세요.");
return;
}
$('#uploadFrm').submit();
});
});
</script>
...
<!-- HTML 폼 기반 파일 업로드
1. enctype="multipart/form-data"
2. method="post"
-->
<form id="uploadFrm" action="upload_process.jsp" method="post" enctype="multipart/form-data">
<label>이름</label>
<input type="text" name="uploader" id="uploader" class="inputBox"/><br/>
<label>나이</label>
<select name="age" id="age">
<c:forEach var="i" begin="10" end="80" step="10">
<option value="${ i }"><c:out value="${ i }"/></option>
</c:forEach>
</select>
<br/>
<label>파일</label>
<input type="file" name="upfile" id="upfile" class="inputBox" style="width:200px;"/><br/>
<input type="button" value="upload" class="btn" id="btnUpload" style="width:200px;"/>
</form>
- 업로드 버튼을 누르면 이동하는 페이지
<!-- upload_process.jsp -->
<!-- enctype="multipart/form-data"인 경우 parameter를 받을 수 없다 -->
<% request.setCharacterEncoding("UTF-8"); // 한글처리 %>
이름 : <%= request.getParameter("uploader") %><br/>
나이 : <%= request.getParameter("age") %><br/>
- Apache Commons FileUpload는 Doc을 참고해서 사용하면 됨
- Apache Commons FileUpload 사용
- 매뉴얼대로 진행
- 우선 파일 업로드 요청인지 확인하는 작업이 필요
- 파일업로드가 아닌 부적합한 요청은 리다이렉트
- 그리고 업로드 되는 파일의 위치, 크기제한 등 설정
<%
request.setCharacterEncoding("UTF-8"); // POST방식 한글처리
// 파일업로드에 적합한 요청인지 확인
boolean isMultipart = ServletFileUpload.isMultipartContent(request);
i f(isMultipart) { // 파일 업로드에 적합한 요청
File repository = new File("C:/Users/owner/youngRepositories/SSangYoung/dev/workspace/jsp_prj/WebContent/upload");
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
// 업로드 파일이 메모리에 저장되는 최대 크기
factory.setSizeThreshold(1024*1024*1); // 1mb씩 끊어 저장
// 저장될 위치
factory.setRepository(repository);
// Create a new file upload handler
ServletFileUpload upload = new ServletFileUpload(factory);
// 업로드 파일의 최대 크기 설정
upload.setSizeMax(1024*1024*5); // 5mb까지 저장
// Parse the request, 전송된 파일 요청저장
List<FileItem> items = upload.parseRequest(request);
// Process the uploaded items, for보다 Iterator가 빠름
Iterator<FileItem> iter = items.iterator();
while (iter.hasNext()) {
FileItem item = iter.next();
if (item.isFormField()) {
// 일반 HTML Form 컨트롤인지? <input type="file"이 아닌 것
out.println("일반 폼 컨트롤<br/>");
} else {
// File Upload Control인지? <input type="file"
out.println("파일 폼 컨트롤<br/>");
}
}
} else {
// 파일 업로드에 부적합한 요청(GET방식 요청 시)
response.sendRedirect("upload_form.jsp");
}
%>
- 파일 요청이 맞고 전송된 파일이 있을 때 파일필드명과 값을 가져올 수 있다. * request.getParameter로 가져올 순 없음 * 파일명은 직접 한글처리클래스를 이용해서 한글처리
// Process the uploaded items
Iterator<FileItem> iter = items.iterator();
while (iter.hasNext()) {
FileItem item = iter.next();
if (item.isFormField()) { // 일반 HTML Form 컨트롤인지? <input type="file"이 아닌 것
String controlName = item.getFieldName();
String controlValue = item.getString();
out.println("컨트롤 명 : " + controlName);
// 선택된 파일이 한글일 때 한글처리를 해줘야 안깨진다.
out.println(" 컨트롤 값 : " + HangulConv.toUTF(controlValue) + "<br/>");
} else { // File Upload Control인지? <input type="file"
out.println("파일 폼 컨트롤<br/>");
}
}
- 전송된 파일이 있는 요청일 때 파일필드명과 값을 가져올 수 있다.
- request.getParameter를 사용할 수 없다.
// Process the uploaded items
Iterator<FileItem> iter = items.iterator();
String uploader = "", age="";
while (iter.hasNext()) {
FileItem item = iter.next();
if (item.isFormField()) { // 일반 HTML Form 컨트롤인지? <input type="file"이 아닌 것
String controlName = item.getFieldName();
String controlValue = item.getString();
// request.getParameter보다 처리가 까다롭다
if ("uploader".equals(controlName)) {
uploader = HangulConv.toUTF(controlValue);
}
if ("age".equals(controlName)) {
age = controlValue;
}
} else { // File Upload Control인지? <input type="file"
out.println("파일 폼 컨트롤<br/>");
}
}
%>
업로더명 : <%=uploader %><br/>
연령대 : <%=age %><br/>
<%
- 업로드요청한 파일 정보 가져오기
...
} else { // File Upload Control인지? <input type="file"
// fieldName == File Control명, 여러 type="file" 존재 시 구분하기 위해 사용
fieldName = item.getFieldName(); // upfile
fileName = item.getName(); // File Control 값
contentType = item.getContentType(); // 업로드 파일의 종류
sizeInBytes = item.getSize(); // 크기
}
}
%>
업로더명 : <%=uploader %><br/>
연령대 : <%=age %><br/>
file control명 : <%=fieldName %><br/>
file control값 : <%=fileName %><br/>
업로드 형식 : <%=contentType %><br/>
크기 : <%=sizeInBytes %><br/>
<%
- 업로드요청한 파일 저장 하기
...
} else { // File Upload Control인지? <input type="file"
// File Control명, 여러 type="file" 존재 시 구분하기 위해 사용
fieldName = item.getFieldName(); // upfile
fileName = item.getName(); // File Control 값
contentType = item.getContentType(); // 업로드 파일의 종류
sizeInBytes = item.getSize(); // 크기
// 업로드파일의 저장경로와 파일명 설정
File uploadedFile = new File(repository.getAbsoluteFile()+"/"+fileName);
item.write(uploadedFile); // 설정된 파일을 업로드
}
}
%>
파일업로드 성공<br/>
업로더명 : <%=uploader %><br/>
연령대 : <%=age %><br/>
업로드 파일명 : <%=fileName %><br/>
크기 : <%=sizeInBytes %><br/>
<a href="upload_form.jsp">업로드</a><!-- 업로드 처리 jsp -->
<%
} else {
// 파일 업로드에 부적합한 요청(GET방식 요청 시)
response.sendRedirect("upload_form.jsp");
}
%>
- 제한한 사이즈보다 큰 파일을 업로드하면 SizeLimitExceededException 예외발생
- SizeLimitExceededException을 try~catch로 잡아 처리했을 때 콘솔에 출력은 되지만 브라우저에 출력은 안된다!
- 파일업로드 기능에선 특정 확장자 파일의 업로드를 막는다
- 서버에서 실행가능한 파일을 업로드 후 uri를 예측해서 악의적인 목적으로 실행가능기 때문!
<%@ page contentType="text/html;charset=UTF-8"
pageEncoding="UTF-8"%>
악의적인 목적을 심어둔 코드들
<!-- 서버 정보 탈취 -->
<%= System.getenv() %>
<%= System.getProperties() %>
<!-- 서버를 종료시킬 수도 있다 -->
<% System.exit(0); %>
- 이런 이유로 특정 확장자 파일의 업로드를 막는다. * 프론트앤드에서 막음
...
// 확장자가 .jsp, .java, .class, .xml은 업로드 하지 못하도록 막는다
var bannedExt = [ "jsp", "java", "class", "xml" ];
var extFlag = false;
var fileExt = file.substring(file.lastIndexOf(".")+1).toLowerCase();
for(var i=0; i<bannedExt.length; i++) {
if(bannedExt[i] == fileExt) {
extFlag = true;
}
}
if (extFlag) {
alert("확장자가 jsp, java, class, xml인 파일은 업로드하실 수 없습니다.");
return;
}
$('#uploadFrm').submit();
});
});
- 저장된 파일들을 보는 페이지
<!-- upload_form.jsp, upload_process.jsp -->
...
<a href="file_list.jsp">파일리스트</a><br/>
...
<%
File uploadDir = new File("C:/Users/owner/youngRepositories/SSangYoung/dev/workspace/jsp_prj/WebContent/upload");
File[] uploadedFiles = uploadDir.listFiles(); // 업로드한 파일들의 정보를 가져옴
%>
<a href="upload_form.jsp">업로드</a><br/>
<table border='1'>
<tr>
<th width="60">번호</th>
<th width="350">파일명</th>
<th width="150">업로드 일시</th>
<th width="120">크기</th>
</tr>
<%
if (uploadedFiles.length != 0) {
File temp = null;
for(int i=0; i<uploadedFiles.length; i++) {
temp = uploadedFiles[i];
%>
<tr>
<td><%=i+1 %></td>
<td><%=temp.getName() %></td>
<td><%=new SimpleDateFormat("yyyy-MM-dd a HH:mm").format(temp.lastModified())%></td>
<td><%=new DecimalFormat("#,###,###,###,###").format(temp.length())%> byte</td>
</tr>
<% }
} else {
out.println("<tr><td colspan='4' align='center'>업로드된 파일이 존재하지 않습니다.</td></tr>");
}
%>
</table>
- 파일명 클릭해서 다운로드 하기 * 브라우저가 해석할 수 없는 확장자는 다운로드처리 * 브라우저가 해석하는 확장자는 브라우저에서 연다
<td><a href="../upload/<%=temp.getName()%>"><%=temp.getName() %></a></td>
File Download
- 응답 방식을 변경(html -> 8bit Stream)
- 응답 헤더를 설정 - 다운로드하게 될 파일명 설정
- FileStream을 사용하여 읽어들일 파일과 연결
- 파일 내용을 읽어들임
- 출력 스트림 초기화
- 현재페이지의 내용으로 출력스트림 변경
- 출력 스트림을 8bit Stream으로 재설정
- 출력 스트림에 파일 내용을 기록
- 분출처리
<!-- file_list.jsp -->
...
<!-- 파일명을 param으로 전달-->
<td><a href="download.jsp?file_name=<%=temp.getName()%>"><%=temp.getName() %></a></td>
...
- param으로 전달받은 파일명의 파일을 다운로드
<!-- download.jsp -->
<%@page import="java.io.OutputStream"%>
<%@page import="java.io.File"%>
<%@page import="java.io.FileInputStream"%>
<%@page import="java.net.URLEncoder"%>
<%@page import="java.net.URLDecoder"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
// 1. 응답 방식 설정
response.setContentType("application/octet-stream"); // 8bit Stream
String data = request.getParameter("file_name");
if (data == null || "".equals(data)) { // 전달된 file_name이 없으면
response.sendRedirect("file_list.jsp"); // 파일 리스트로 리다이렉트
return;
}
// 2. 응답 헤더 설정, 저장할 파일명 설정
response.setHeader("Content-Disposition", "attachment;fileName="+
URLEncoder.encode(data,"UTF-8")); // 한글파일명 처리
// 3. FileStream을 연결
File file = new File("C:/Users/owner/youngRepositories/SSangYoung/dev/workspace/jsp_prj/WebContent/upload/"+data);
FileInputStream fis = null;
OutputStream os = null;
try {
if (file.exists()) { // 파일이 존재하는지
fis = new FileInputStream(file);
// 4. 파일 내용을 읽어들여 byte[]에 저장
// 파일의 내용을 저장하기 위한 배열
byte[] fileArr = new byte[(int)file.length()];
int read = 0, idx = 0;
while((read = fis.read()) != -1) { // 읽어들일 내용이 있을 때까지
fileArr[idx] = (byte)read; // 읽은 바이트를 배열에 기록
idx++;
}
// 5. 출력 스트림 초기화
out.clear();
// 6. 현재 페이지의 내용으로 출력스트림을 변경
out = pageContext.pushBody();
// 7. 출력 스트림을 8bit Stream으로 재설정
os = response.getOutputStream();
// 8. 출력 스트림에 파일 내용을 기록
os.write(fileArr);
// 9. 분출
os.flush();
}
} finally { // 연결 끊기
if (fis != null) { fis.close(); }
if (os != null) { os.close(); }
}
%>