기록/BACKEND

[Spring] File Download

5월._. 2022. 4. 23.
728x90

.jsp

파일 원본 이름 옆 다운로드 a태그를 만들었다. 이 태그에 속성을 몇 개 추가한다. 이름은 마음대로 해도 된다.

1.  sfolder - 서버 저장 폴더명

2.  sfile - 실제 저장 파일 이름

3.  ofile - 원본 파일 이름

<ul>
  <c:forEach var="file" items="${article.fileInfos}">
    <li>${file.originFile} <a href="#" class="filedown" sfolder="${file.saveFolder}" sfile="${file.saveFile}" ofile="${file.originFile}">[다운로드]</a>
  </c:forEach>
</ul>

a 태그가 클릭되면 실행되는 이벤트 함수를 만들었다. (js)

이 함수는 파일 다운로드 form에 값을 넣고 submit 시킨다.

$('.filedown').click(function() {
    $(document).find('[name="sfolder"]').val($(this).attr('sfolder'));
    $(document).find('[name="ofile"]').val($(this).attr('ofile'));
    $(document).find('[name="sfile"]').val($(this).attr('sfile'));
    $('#downform').attr('action', '주소').attr('method', 'get').submit();
});

파일 다운로드 form은 hidden input으로만 구성되어있다. 주소로 parameter을 붙여 전송해도 되지만, 그 방식은 인코딩이 필요하기 때문에 form 방식을 많이 사용한다.

<form id="downform">
	<input type="hidden" name="sfolder">
	<input type="hidden" name="ofile">
	<input type="hidden" name="sfile">
</form>

 

servlet-context.xml

파일 다운로드를 위해 설정이 필요하다. 다운로드에는 실제 view가 필요하지 않다. 따라서 서블릿 뷰를 만든다. 이 서블릿 뷰는 다른 뷰처럼 뒤에 확장자가 붙을 필요도 없고 앞에 파일 주소가 붙을 필요도 없다.

fileViewResolver가 있으면 order 번호 순서에 맞춰서 빈으로 등록된 자바 파일을 처리한다. order는 Servlet의 Load-on-startup 값과 유사하다.

FileDownLoadView 이름의 클래스를 빈으로 등록했다.

<beans:bean id="fileDownLoadView" class="com.ssafy.guestbook.view.FileDownLoadView"/>
<beans:bean id="fileViewResolver" class="org.springframework.web.servlet.view.BeanNameViewResolver">
	<beans:property name="order" value="0" />
</beans:bean>

 

FileDownLoadView

1.  기본 생성자에서 ContentType을 미리 설정한다. setContentType은 AbstractView에 있는 메서드다. (getContentType포함)

2.  AbstractView의 renderMergedOutputModel을 override 한다.

2-1.  실제 주소를 가져온다. 파일 업로드를 /upload폴더 밑에 했기 때문에 그 주소를 가져왔다.

2-2.  전송받은 모델을 사용 가능하게 타입 변경한다. 여기서는 Map으로 전송했기 때문에 Map으로 변환했다.

2-3.  File객체를 생성해서 실제주소+구분자+저장 폴더에 있는 파일을 가져온다. 실제 저장된 파일 이름으로 접근해야 한다.

2-4.  생성자에서 지정한 ContentType을 response에 적용한다. response의 길이도 file에 맞춘다.

2-5.  브라우저 종류에 따라 인코딩을 다르게 한다. IE의 경우 \\+를 공백(%20)으로 교체하는 작업이 필요하다.

2-6.  response의 header를 설정한 뒤에 outputStream을 받아온다. 

2-7.  fileInputStream으로 2-3의 file을 열어서 사용자 컴퓨터에 저장한다.

2-8.  outputStream을 비운다.

public class FileDownLoadView extends AbstractView {
	public FileDownLoadView() {
		setContentType("apllication/download; charset=UTF-8");//실제로 볼 수 없고 다운로드만 가능한 contenttype
	}
	
	@Override
	protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
		ServletContext ctx = getServletContext();
		String realPath = ctx.getRealPath("/upload");
		
		Map<String, Object> fileInfo = (Map<String, Object>) model.get("downloadFile"); // 전송받은 모델(파일 정보)
        
		String saveFolder = (String) fileInfo.get("sfolder");    // 파일 경로
		String originalFile = (String) fileInfo.get("ofile");    // 원본 파일명(화면에 표시될 파일 이름)
		String saveFile = (String) fileInfo.get("sfile");    // 암호화된 파일명(실제 저장된 파일 이름)
		File file = new File(realPath + File.separator  + saveFolder, saveFile);
		
		response.setContentType(getContentType());//생성자에서 지정한 contenttype을 response에 적용함
		response.setContentLength((int) file.length());
        
		String header = request.getHeader("User-Agent");
		boolean isIE = header.indexOf("MSIE") > -1 || header.indexOf("Trident") > -1;
		String fileName = null;
		// IE는 다르게 처리(공백등..)
		if (isIE) {
		    fileName = URLEncoder.encode(originalFile, "UTF-8").replaceAll("\\+", "%20");
		} else {
		    fileName = new String(originalFile.getBytes("UTF-8"), "ISO-8859-1");
		}
		response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
		response.setHeader("Content-Transfer-Encoding", "binary");
		OutputStream out = response.getOutputStream();
		FileInputStream fis = null;
		try {
		    fis = new FileInputStream(file);
		    FileCopyUtils.copy(fis, out);//서버에 있는걸 컴퓨터로 저장
		} catch (Exception e) {
		    e.printStackTrace();
		} finally {
		    if(fis != null) {
		        try { 
		            fis.close(); 
		        }catch (IOException e) {
		            e.printStackTrace();
		        }
		    }
		}
		out.flush();
	}
}

 

Controller

1.  파라미터 이름과 사용할 이름이 같아서 @RequestParam을 쓰지 않았다.

2.  새 Map을 만들어서 받은 정보를 저장한다.

3.  ModelAndView 객체를 반환한다. 이때, view name은 "fileDownLoadView", model name은 "downloadFile", model Ojbect는 2에서 만든 Map 객체다.

4.  이후에 servlet-context.xml에서 빈으로 설정한 FileDownLoadView로 가서 다운로드를 마무리짓는다.

@GetMapping(value = "/download")
public ModelAndView downloadFile(String sfolder, String ofile, String sfile, HttpSession session) {
	Map<String, Object> fileInfo = new HashMap<String, Object>();
	fileInfo.put("sfolder", sfolder);
	fileInfo.put("ofile", ofile);
	fileInfo.put("sfile", sfile);
	return new ModelAndView("fileDownLoadView", "downloadFile", fileInfo);
}

'기록 > BACKEND' 카테고리의 다른 글

[WEB] MyBatis 설정  (0) 2022.04.25
[Spring] Interceptor  (0) 2022.04.24
[Spring] File Upload  (0) 2022.04.22
[Spring] 예외처리하기 - ControllerAdvice  (0) 2022.04.21
[Spring] DI  (0) 2022.04.20

댓글