서버에 저장된 파일 브라우저로 다운로드하기~~~(많이 쓰는 유용한 기능!)

뿌엑·2022년 7월 16일
0

웹개발을 하다보면 가끔 다운로드 기능을 만들어야 할 때가 있다.

천재 개발자들이 이미 만들어 놓은 api를 갖다 쓰기만 하는 개발-조무사로선 코드를 끄적이면서도 잘하고 있는건가― 싶기도 한데...

몰라!!

이번엔 비밀파일을 다운받아 보려고 한다.

지난번 log 출력할 때 쓴 'logger test' 프로젝트를 구동한다.
이번에 새로 만들 파일은 File IO 관련 컨트롤러와 서비스, jsp 정도이다.

일단 복붙용 전체코드부터 공유~(복사 없으면 개발 못해!)

package com.mycompany.service.impl;

import com.mycompany.vo.FileIOVO;

public interface FileIOService{
	public FileIOVO fileDownload();
	public byte[] encodeBase64(byte[] data);
}
package com.mycompany.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.Base64;

import org.springframework.stereotype.Service;

import com.mycompany.service.impl.FileIOService;
import com.mycompany.vo.FileIOVO;

@Service
public class FileIOServiceImpl implements FileIOService{

	@Override
	public FileIOVO fileDownload() {
		
		FileInputStream input = null;
		FileOutputStream output = null;
		
		File file = null;
		
		String filePath = "C:\\fileDownloadTest\\girl's diary.txt";
//		String filePath = "C:\\fileDownloadTest\\swvy.png";
		file = new File(filePath);
		
		FileIOVO downloadVO = new FileIOVO();
		
		try {
			input = new FileInputStream(filePath);
			
			downloadVO.setFileNm("girl's diary");
			downloadVO.setFileExt("txt");
//			downloadVO.setFileNm("swvy");
//			downloadVO.setFileExt("png");
			downloadVO.setFileAttach(file);
			
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		
		return downloadVO;
	}

	@Override
	public byte[] encodeBase64(byte[] data) {
		java.util.Base64.Encoder encoder = Base64.getEncoder();
		byte[] encodeBytes = encoder.encode(data);
		
		return encodeBytes;
	}
}

서비스단~

package com.mycompany.myapp;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Locale;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.mycompany.service.impl.FileIOService;
import com.mycompany.vo.FileIOVO;

@Controller
public class FileIOController {
	
	@Resource
	FileIOService downloadService;
	
	@RequestMapping(value = "/downTest", method = RequestMethod.GET)
	public String downTest(Locale locale, Model model) {
		return "download";
	}
	
	@ResponseBody
	@RequestMapping(value = "/downTest/fileDownload", method = RequestMethod.POST)
	public void fileDownload(HttpServletResponse res) {
		FileIOVO fileVO = new FileIOVO();
		File file = null;
		String fileNm = null;
		String fileExt = null;
		String fileSize = null;
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		ByteArrayOutputStream out2 = new ByteArrayOutputStream();
		FileInputStream fis = null; 
		byte[] byteArr = null;
		byte[] byteRst = null;
		byte[] buffer = new byte[1024];
		int readBuffer = 0;
		
		
		fileVO = downloadService.fileDownload();

		file = fileVO.getFileAttach();
		fileNm = fileVO.getFileNm();
		fileExt = fileVO.getFileExt();

		
		try {
			if(file.isFile()) {
				fis = new FileInputStream(file);
				
				while((readBuffer = fis.read(buffer)) != -1) {
					out.write(buffer, 0, readBuffer);
				}
				
				out.flush();
			}
			
			out.close();
			fis.close();
			
			byteArr = out.toByteArray();
			byteRst = downloadService.encodeBase64(byteArr);
			out2.write(downloadService.encodeBase64(byteArr));
			out2.writeTo(res.getOutputStream());
			fileSize = Integer.toString(byteRst.length);
			
			res.setHeader("Content-Type", "multipart/octet/stream");
			res.setHeader("Content-Disposition", "attachment; filename=" + fileNm + "." + fileExt + ";");
			res.setHeader("Content-Transfer-Encoding", "binary");
			res.setHeader("Content-Length", fileSize);
			
			if(out2 != null) {
				out2.close();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if(out != null) {
					out.close();
				}
				if(out2 != null) {
					out2.close();
				}
				if(fis != null) {
					fis.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

응~ 컨트롤러~

package com.mycompany.vo;
import java.io.File;

import javax.annotation.Resource;

@Resource
public class FileIOVO {
	private String fileNm;
	private String fileExt;
	private Long fileSize;
	private File fileAttach;
	
	
	public String getFileNm() {
		return fileNm;
	}
	public void setFileNm(String fileNm) {
		this.fileNm = fileNm;
	}
	public String getFileExt() {
		return fileExt;
	}
	public void setFileExt(String fileExt) {
		this.fileExt = fileExt;
	}
	public Long getFileSize() {
		return fileSize;
	}
	public void setFileSize(Long fileSize) {
		this.fileSize = fileSize;
	}
	public File getFileAttach() {
		return fileAttach;
	}
	public void setFileAttach(File fileAttach) {
		this.fileAttach = fileAttach;
	}
}

무쓸모한 vo~~

<%@ page language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="EUC-KR">
<title>파일 다운로드 합니다!</title>
<script type="text/javascript" src="/resources/plugin/jquery-3.6.0.min.js"></script>
</head>
<script>

window.onload = function(){
	document.getElementById("secretFile").addEventListener("click", downloadSecret);
}
	
	
// 비밀파일 다운로드 해보리기~
function downloadSecret(){
	$.ajax({
		url: "/downTest/fileDownload",
// 		data: form,
		dataType: "text",
		processData: false,
		contentType: false,
		type: "POST",
		success: function(result, status, jqXHR){
			var blob;
			
			var header_disposition = jqXHR.getResponseHeader('Content-Disposition');
			var fileName;
			var ext;
			
			if(header_disposition != null){
				fileName = header_disposition.substring(header_disposition.indexOf('filename=') + 9, header_disposition.length - 1);
			}else{
				fileName = 'swvy.png';
			}
			
			
			if(jqXHR.readyState == XMLHttpRequest.DONE && jqXHR.status == 200){
				const link = document.createElement('a');
				link.style.display = 'none';
				document.body.appendChild(link);
				
				blob = b64toBlob(result, 'application/pdf');
				
				link.href = URL.createObjectURL(blob);
				link.download = fileName;
				link.click();
			}
		},
		error: function(request, status, error){
			console.log(request, status, error);
		}
	});
}

// base64 디코딩 해보리기~
function b64toBlob(b64Data, contentType, sliceSize=512){
	const byteCharacters = atob(b64Data);
	const byteArrays = [];
	
	for(let offset = 0; offset < byteCharacters.length; offset+=sliceSize){
		const slice = byteCharacters.slice(offset, offset + sliceSize);
		
		const byteNumbers = new Array(slice.length);
		for(let i = 0; i < slice.length; i++){
			byteNumbers[i] = slice.charCodeAt(i);
		}
		
		const byteArray = new Uint8Array(byteNumbers);
		byteArrays.push(byteArray);
	}
	
	const blob = new Blob(byteArrays, {type: contentType});
	return blob;	
}

</script>
<body>
	<button id="secretFile">비밀파일 다운로드하기~~!</button>
</body>
</html>

'비밀' 파일 다운로드 해보리기~~

짠! 하고 나타난 화면은~~
뿌엑의 머릿속처럼 새하얀 백지화면~~(큿소!!)

무지성 다운로드 연타 해보리기~~ 겟또다제~!

14번 파일을 열어보니 나폴리탄 서사의 걸작으로 꼽히는 '소녀의 일기장'이 나타났다..!

과연 산타는 누구였을까? 12월 65일은 언제인걸까...? 소녀는 무엇을 깨달은 것일까..!
나폴리탄은 해석하는 글이 아니지만 웹상엔 유괴 등의 범죄란 의견이..ㅋㅋㅋㅋ

하지만 비밀 파일은 이게 끝이 아니다..

String filePath = "C:\\fileDownloadTest\\swvy.png";
서비스단의 주석을 보면 숨겨진 파일경로가!
swvy는 무엇일까... 얼마나 대단한 파일이길래 철저히 숨겨놓기까지!

// String filePath = "C:\\fileDownloadTest\\girl's diary.txt";
String filePath = "C:\\fileDownloadTest\\swvy.png";
file = new File(filePath);

FileIOVO downloadVO = new FileIOVO();

try {
	input = new FileInputStream(filePath);
	
//	downloadVO.setFileNm("girl's diary");
//	downloadVO.setFileExt("txt");
	downloadVO.setFileNm("swvy");
	downloadVO.setFileExt("png");
	downloadVO.setFileAttach(file);

비밀스럽게 주석을 풀어 보았다.

그렇게 나타난 파일은 차세대 국힙 원탑 스월비!

난 원해, 많이, 많이, 많이, 빛이 나는 outfit, 내 money way는 시상식이 따로 없지, get it
Walk it like I talk it, too fast and almost riding, 니들 전부 잡으려면 뛰어야지, 매일

찢었다!!


MVC 패턴이야 별거 없는거고,

String filePath = "C:\\fileDownloadTest\\swvy.png";
file = new File(filePath);

하드의 파일을 가져오는 부분.
File.isFile()로 경로가 파일인지를 구분할 수 있다.
디렉토리를 가져오면 오류(FileNotFoundException)가 난다.
java.io.FileNotFoundException: C:\fileDownloadTest (액세스가 거부되었습니다)

Base64

Base64는 8비트 이진 데이터(이미지나 ZIP 파일 등)을 브라우저로 데이터를 전송할 때 포맷이 손상되지 않도록 공통 ASCII 영역의 문자로만 이뤄진 일련의 문자열로 변경하는 인코딩 방식이다.
Base64는 용어만으로 봤을 땐 64진법이란 의미이며 기본적으론 ASCII 코드를 64진법으로 바꾸고 Base64 색인표에 따라 인코딩한다.

그를 브라우저에서 다시 디코딩하여 데이터의 손실 및 왜곡없이 안전한 데이터 전송이 이뤄진다.

@Override
public byte[] encodeBase64(byte[] data) {
	java.util.Base64.Encoder encoder = Base64.getEncoder();
	byte[] encodeBytes = encoder.encode(data);
	
	return encodeBytes;
}

자바단에서 인코딩

function b64toBlob(b64Data, contentType, sliceSize=512){
	const byteCharacters = atob(b64Data);
	const byteArrays = [];
	
	for(let offset = 0; offset < byteCharacters.length; offset+=sliceSize){
		const slice = byteCharacters.slice(offset, offset + sliceSize);
		
		const byteNumbers = new Array(slice.length);
		for(let i = 0; i < slice.length; i++){
			byteNumbers[i] = slice.charCodeAt(i);
		}
		
		const byteArray = new Uint8Array(byteNumbers);
		byteArrays.push(byteArray);
	}
	
	const blob = new Blob(byteArrays, {type: contentType});
	return blob;	
}

js단에서 디코딩한다.

js에서 atob()는 base64 디코더 함수이다.
그건 ASCII to Binary data를 의미하는 것으로 btoa()는 그 반대란 걸 알 수 있을 것이다.

단적으로 말해 base64는 이진 데이터를 ASCII 문자의 부분집합으로 구성된 텍스트로 변환하는 인코딩이다.

ByteArrayOutputStream는 바이트 배열을 출력(write)하기 위한 클래스이다. 내부적인 저장 공간을 지니며 해당 클래스로 데이터를 출력하면(write(byte[] b, int off, int len)) 그 내용이 버퍼에 저장된다.
이후 toByteArray() 메서드를 사용해 바이트 배열을 반환할 수 있다.

HttpServletResponse 객체로 응답을 처리할 수 있다.

res.setHeader("Content-Type", "multipart/octet/stream");
res.setHeader("Content-Disposition", "attachment; filename=" + fileNm + "." + fileExt + ";");
res.setHeader("Content-Transfer-Encoding", "binary");
res.setHeader("Content-Length", fileSize);

상세한 응답의 컨텍스트를 제공하기 위해 응답 헤더에 몇몇 정보를 제공한다.
base64로 인코딩하면 인코딩 방식의 이유로 원본보다 4/3 가량 크기가 늘어난다. 이때 fileSize는 늘어난 크기에 맞춰줘야 한다.

Content-type: 전송하는 자원의 형식
Content-Disposition: HTTP Response Body에 들어가는 컨텐츠의 종류
Content-Transfer-Encoding: 전송 데이터 body의 인코딩 방식
Content-Length: 수신자에게 전송되는 바이트 단위의 body 크기

코드에 사용된 HTTP Header 간단 정리~

var header_disposition = jqXHR.getResponseHeader('Content-Disposition');

ajax의 jqXHR(jQuery XMLHttpRequest object)는 getResponseHeader() 등의 메서드로 헤더의 텍스트를 가져올 수 있다.
여기선 Content-Disposition에 저장된 파일명을 가져오기 위해 Content-Disposition를 호출했는데 파일이 텍스트가 아닐 경우엔 HTTP Header에 설정한 Content-type, Content-Disposition 등의 속성이 없어지는 문제점이 있었다.
이유는 찾지 못했고, 만약 이게 실무라면...

그땐 미래의 내가 어떻게든 하겠지~~

그게 아니고, 세부적인 디테일은 다음 기회에~(절대 귀찮아서 도망치는거 아님!ㅋㅋ)

1개의 댓글

comment-user-thumbnail
2024년 3월 7일

스월비 선곡에 개추박고갑니다

답글 달기