스프링

 

서버에서는 파일을 저장할 때 반드시 고려해야 하는 사항은 다음과 같다.

- 파일 이름의 중복 문제

- 파일의 저장 경로에 대한 문제

- 원본 파일을 그대로 보여주는 경우 브라우저에서 보여지는 파일의 크기 문제

 

- FAT32 : 단일 파일 4GB, 최대 볼륨32GB, 볼륨당 파일 수 4,117,920개, 폴더당 파일수 65,534 개

- NTFS : 단일 파일 16TB,  최대 볼륨 256TB, 볼륨당 파일 수 4,294,967,295 개,  폴더당 파일 수 4,294,967,295 개

 

대부분 프로젝트에서는 사용자가 업로드한 이미지 파일은 썸네일(thumbnail) 이미지라는 원본 이미지 파일의 축소본(썸네일 이미지)

을 만들어서 전송한다. 물론, 이러한 작업을 하는 가장 중요한 이유는 서버에서 최소한의 데이터를 전송하기 위해서이다.

 

● 파일 업로드용 클래스 설계하기

1) 자동적인 폴더의 생성이 가능하며, 

2) 파일 저장은 UUID를 이용해서 처리하고,

3) 썸네일 이미지를 생성하는 기능이 처리

 

 

● 썸네일 생성하기

업로드 되는 파일은 크게 브라우저에서 보여지는 이미지 종류의 파일과 사용자가 다운로드 하는 형태의 파일로 구분될 수 있다.

이미지 타입의 파일 : JPG, GIF ,PNG, JPEG (BMP 는 흔히 사용되지 않으므로 일반 파일로 간주)

다운로드 타입의 파일 : 이미지 타입의 파일을 제외한 모든 종류

 

 

 파일 저장 하기 

 

프로젝트 밖에 

년월일 별로 폴더가 생성된 것을 볼 수 있다.

서버내 디렉토리 주소로 설정 하면 된다.

servlet-context.xml

	<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
	  <beans:property name="maxUploadSize"   value="10485760" />
	</beans:bean>
	
	<!-- 서버의 파일 저장 경로 설정 -->
	<beans:bean id="uploadPath" class="java.lang.String">
	 <beans:constructor-arg  value="D:\dev\spring_simple_blog\.metadata\.plugins\org.eclipse.wst.server.core\tmp1\wtpwebapps\upload"  />
	</beans:bean>
	

 

 

 

 

 

 

* 라이브러리 설정

http://mvnrepository.com/artifact/commons-fileupload/commons-fileupload

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>

 

Imgscalr A Java Image Scaling Library

<!-- https://mvnrepository.com/artifact/org.imgscalr/imgscalr-lib -->
<dependency>
    <groupId>org.imgscalr</groupId>
    <artifactId>imgscalr-lib</artifactId>
    <version>4.2</version>
</dependency>

 

UploadFileUtils

package config.uploadfile;

import java.awt.image.BufferedImage;
import java.io.File;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.UUID;

import javax.imageio.ImageIO;

import org.imgscalr.Scalr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;


public class UploadFileUtils {
	
	private static final Logger logger=LoggerFactory.getLogger(UploadFileUtils.class);

	private static Integer WIDTH_SIZE=100;
	
	public static Integer getWIDTH_SIZE() {
		return WIDTH_SIZE;
	}

	public static void setWIDTH_SIZE(Integer wIDTH_SIZE) {
		WIDTH_SIZE = wIDTH_SIZE;
	}

	//1.파일의 저장 경로(uploadPath), 2.원본 파일의 이름(originalName), 3.파일 데이터(byte[])
	public static String uploadFile(String uploadPath, String originalName, byte[] fileData) throws Exception{
		
		
	  //★ 1. 고유값 생성
	  UUID uid =UUID.randomUUID();
	  String savedName=uid.toString()+"_"+originalName;
	  		
	  //★ 2. 년/월/일' 정보 생성
	  String savedPath=calcPath(uploadPath);
		
	  //★ 3. 원본파일 저장
	  File target =new File(uploadPath+savedPath, savedName);
	  FileCopyUtils.copy(fileData, target);
		
		
	  //★ 4. 이미지 일경우 썸네일 이미지 생성 후 url 주소로 반환
	  String formatName=originalName.substring(originalName.lastIndexOf(".")+1);
	  String uploadedFileName=null;
	  
	  if(MediaUtils.getMediaType(formatName)!=null){
		  //이미지일 경우 썸네일 생성 후 이미지 이름 반환 ( 경로+년월일+s_이름)
		  uploadedFileName=makeThumbnail(uploadPath, savedPath, savedName);
	  }else{
		  uploadedFileName=makeIcon(uploadPath, savedPath, savedName);
	  } 
	  
	  // 파일 경로를  -> url 경로로 변경해서 반환
	  return uploadedFileName;	
	}
	
	// 이미지가 아닐경우  단지 파일 경로를 -> url 경로로 변경해서 반환
	private static String makeIcon(String uploadPath, String savedPath, String savedName) {
		String iconName=uploadPath+savedPath+File.separator+savedName;
		return iconName.substring(uploadPath.length()).replace(File.separatorChar, '/');
	}


	//파일이 저장될  '년/월/일' 정보 생성
	private static String calcPath(String uploadPath){	
		Calendar cal =Calendar.getInstance();
		// 역슬래시 + 2017
		String yearPath=File.separator + cal.get(Calendar.YEAR);
		
		//  /2017 +/+ 10  한자리 월 일경우 01, 02 형식으로 포멧
		String monthPath=yearPath + File.separator + new DecimalFormat("00").format(cal.get(Calendar.MONTH)+1);
		
		// /2017/10 +/ + 22 
		String datePath = monthPath+ File.separator+ new DecimalFormat("00").format(cal.get(Calendar.DATE));
		
		//년월일 폴더 생성하기
		makeDir(uploadPath, yearPath, monthPath, datePath);
		
		logger.info(" datePath - {}", datePath);
		
		return datePath;
	}

	
	// 실질 적인 날짜 폴더 생성
	private static void makeDir(String uploadPath, String... paths){	
		if(new File(paths[paths.length-1]).exists()){
			//년 월 일 에서 일 배열 paths 에서 paths -1 은 일  즉 해당일의 폴더가 존재하면 return
			return ;
		}
		
		for(String path :paths){
			File dirPath =new File(uploadPath+path);
			if(!dirPath.exists()){
				//년 월일에 대한 해당 폴더가 존재하지 않으면 폴더 생성
				dirPath.mkdir();
			}
		}
		
	}
	
	//썸네일 이미지 생성하기
	// 1.파일 경로 2. 년월일 경로, 3. 파일 이름 
	private static String makeThumbnail(String uploadPath, String path, String fileName) throws Exception{
		// 파일 존재하는 이미지를 메모리상 이미지에 올려 붙인다. 즉 메모리에 로딩시킨다.
		BufferedImage sourceImg= ImageIO.read(new File(uploadPath+path, fileName));
		
		//메모리상 이미지를 정해진 크기에 맞게 복사한다.
		BufferedImage destImage=Scalr.resize(sourceImg, Scalr.Method.AUTOMATIC,
				Scalr.Mode.FIT_TO_HEIGHT, WIDTH_SIZE);
		
		//썸네일 이미지 이름을 정한다. 썸네일 이미지를 앞에 s_ 붙인다.
		String thumbnailName=uploadPath+path+File.separator+"s_"+fileName;
		
		// 파일 경로의 객체를 생성한다.
		File newFile=new File(thumbnailName);
		
		// 경로의 마지막 에 . 으로 중심으로 분리해서 .jpg, png, jpeg gif 의 문자를 추출한다.  
		String formatName=fileName.substring(fileName.lastIndexOf(".")+1);
		
		//실질적인 썸네일 이미지를 복사한다.  
		ImageIO.write(destImage, formatName.toUpperCase(), newFile);
	
		// 파일 경로인 역슬러시를 url 의 경로인 슬러시로 변경해서 해서 반환시킨다.
		return thumbnailName.substring(uploadPath.length()).replace(File.separatorChar, '/');
	}
	
	
	
	
	
}









 

MediaUtils

package config.uploadfile;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.MediaType;

public class MediaUtils {

	private static Map<String, MediaType> mediaMap;
	
	static {
		mediaMap=new HashMap<String, MediaType>();
		mediaMap.put("JPEG", MediaType.IMAGE_JPEG);
		mediaMap.put("JPG", MediaType.IMAGE_JPEG);
		mediaMap.put("GIF", MediaType.IMAGE_GIF);
		mediaMap.put("PNG", MediaType.IMAGE_PNG);
	}
	
	public static MediaType getMediaType(String type){
		return mediaMap.get(type.toUpperCase());
	}
	
}

 

 

 

UploadController

	    
    //년월일 폴더로 분리후 이미지일 경우 썸네일 이미지 생성
    @ResponseBody
    @RequestMapping(value="/test/uploadAjax2", method=RequestMethod.POST, produces="text/plain;charset=UTF-8")
    public ResponseEntity<String> uploadAjax2(MultipartFile file) throws Exception{
        logger.info("originalName ,  {} ",  file.getOriginalFilename());    
        
        //사이즈 가로 설정
        UploadFileUtils.setWIDTH_SIZE(200);
        
        String uploadFile=UploadFileUtils.uploadFile(uploadPath, file.getOriginalFilename(), file.getBytes());
        logger.info("파일 저장후 반환 값 - ,  {} ",  uploadFile);
        return new ResponseEntity<String>(uploadFile, HttpStatus.CREATED);
    }
	
	

 

uploadAjax.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<style type="text/css">
.fileDrop{

	width:100%;
	height:200px;
	border:1px dotted blue;

}

small{
	margin-left:3px;
	font-weight:bold;
	color:gray;
}

</style>
</head>
<body>



<h3>Ajax File Upload</h3>
<div class="fileDrop"></div>
<div class="uploadedList"></div>

<script src="//code.jquery.com/jquery-1.11.3.min.js"></script>



</body>
</html>

 

<script>
$(".fileDrop").on("dragenter dragover", function(event){
	event.preventDefault();
});

$(".fileDrop").on("drop", function(event){
	event.preventDefault();
	
	var files=event.originalEvent.dataTransfer.files;
	
	var file=files[0];
	
	console.log(file);
	var formData=new FormData();
	formData.append("file", file);

	$.ajax({
		
		url:'/test/uploadAjax2',
		data:formData,
		dataType:'text',
		processData:false,
		contentType:false,
		type:'POST',
		success:function(data){
			alert(data);	
		}
	});
	
		
});

</script>

 

 

 

 

 

 

만일 저장하는 파일이 이미지 타입이 아닌 파일이라면, 결과 메시지에  's_' 로 시작되지 않고, 저장 경로에도 파일은 하나만 생성된다.

 

 

콘솔에 출력되는 값은 다음과 같다. 

이미지인경우

INFO : test.controller.UploadController - originalName ,  1.jpg 
INFO : config.uploadfile.UploadFileUtils -  datePath - \2017\10\22
INFO : test.controller.UploadController - 파일 저장후 반환 값 - ,  /2017/10/22/s_550eb43a-0369-4972-be98-9a9e093897dd_1.jpg 

 

일반 파일인 경우

INFO : test.controller.UploadController - originalName ,  welcome.jsp 
INFO : config.uploadfile.UploadFileUtils -  datePath - \2017\10\22
INFO : test.controller.UploadController - 파일 저장후 반환 값 - ,  /2017/10/22/26a7c77b-f7f7-4c6b-b559-58b0ed156bc7_welcome.jsp 

 

 

보안

 보안을 위해 이미지만 허용하거나 특정 파일만 허용가능하도록 해야 한다.

그래서 다음과 같이 클래스를 만들고 변경하였다.

 

추가

UploadSecurity

package test.controller;

import org.springframework.web.multipart.MultipartFile;

public class UploadSecurity {

	public static boolean check(MultipartFile uploadedFile) {

		if (!uploadedFile.isEmpty()) {

			String fileName = uploadedFile.getOriginalFilename();

			// jpg, jpeg, png, gif, 만 업로드 되도록 수정
			if (fileName.toLowerCase().endsWith(".jpg") || fileName.toLowerCase().endsWith(".jpeg")
					|| fileName.toLowerCase().endsWith(".png") || fileName.toLowerCase().endsWith(".gif")) {
				return true;
			}
		}

		return false;
	}

}

 

controller

	//년월일 폴더로 분리후 이미지일 경우 썸네일 이미지 생성
	@ResponseBody
	@RequestMapping(value="/test/uploadAjax2", method=RequestMethod.POST, produces="text/plain;charset=UTF-8")
	public ResponseEntity<String> uploadAjax2(MultipartFile file) throws Exception{
		logger.info("originalName ,  {} ",  file.getOriginalFilename());	
		
		 String uploadFile="";
		// 보안상 , jpg, jpeg, png, gif, 만 업로드 되도록 수정 
		if(UploadSecurity.check(file)){
			//사이즈 가로 설정
			UploadFileUtils.setWIDTH_SIZE(200);
				
			uploadFile=UploadFileUtils.uploadFile(uploadPath, file.getOriginalFilename(), file.getBytes());
		}else{
			uploadFile="허용되지 않은 파일 입니다.";
		}
		
		logger.info("파일 저장후 반환 값 - ,  {} ",  uploadFile);
		return new ResponseEntity<String>(uploadFile, HttpStatus.CREATED);
	}
	

 

 

 

 파일 화면에 표시하기 

utils 또는 cofig 패키지에 다음 클래스를 만든다.

UploadFileDisplay

package config.uploadfile;

import java.io.FileInputStream;
import java.io.InputStream;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

//전송된 파일을 화면에 표시하는 클래스

public class UploadFileDisplay {
	
	private static final Logger logger = LoggerFactory.getLogger(UploadFileDisplay.class);
	
	private static UploadFileDisplay instance;
	
	//싱글톤
	private UploadFileDisplay (){
		
	}

	public static UploadFileDisplay getInstance(){
		if(instance==null){
			instance=new UploadFileDisplay();
		}
		return instance;
	}
	
	
	public ResponseEntity<byte[]> disPlay(String fileName, String uploadPath) throws Exception{
		//fileName 은 /년/월/일/파일명의 형태로 입력을 받는다.
		
		InputStream in=null;
		//ResponseEntity<byte[]> 로 결과는 실제로 파일의 데이터가 된다.
		//컨트롤에서 @ResponseBody 를 이용해야 하며 
		//byte[] 데이터가 그대로 전송될 것임을 명시한다.
		ResponseEntity<byte[]> entity=null;
				
		logger.info("File NAME : "+ fileName);
		
		try{
			String formatName =fileName.substring(fileName.lastIndexOf(".")+1);
			
			MediaType mType =MediaUtils.getMediaType(formatName);
			
			HttpHeaders headers =new HttpHeaders();
			//   경로 +/년/월/일 /파일이름
			in =new FileInputStream(uploadPath+fileName);
			if(mType!=null){
				//이미지인 경우 
				headers.setContentType(mType);
			}else{
				//이미지가 아닌 경우
				//MIME 타입을 다운로드 용으로 사용되는 'application/octet-stream 으로 지정한다.
				//브라우저는 이 MIME 타입을 보고 사용자에게 자동으로 다운로드 창을 열어 준다.
				
				fileName=fileName.substring(fileName.indexOf("_")+1);
				//한글 깨짐 방지 설정

				//다운로드 할 때 사용자에게 보이는 파일의 이름이므로 한글 처리를 해서
				// 전송합니다. 한글 파일의 경우 다운로드 하면 파일의 이름이 깨져서
				// 나오기 반드시 인코딩 처리를 할 필요가 있다.
				String attach="\""+new String(fileName.getBytes("UTF-8"), "ISO-8859-1")+"\"";
				headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
				headers.add("Content-Disposition", "attachment; filename="+attach);				
			}
			
			//실제로 데이터를 읽는 부분은 commons 라이브러리의 기능을 활용해서 대상
			// 파일에서 데이터를 읽어내는 IOUtils.toByteArray() 이다.
			entity=new ResponseEntity<byte[]>(IOUtils.toByteArray(in), headers, HttpStatus.CREATED);
			
		}catch(Exception e){
			e.printStackTrace();
			entity=new ResponseEntity<byte[]>(HttpStatus.BAD_REQUEST);
		}finally{
			in.close();
		}
		
		return entity;
	}
	
	
}

 

controller

	@ResponseBody
	@RequestMapping("/displayFile")
	public ResponseEntity<byte[]> displayFile(String fileName) throws Exception{
		UploadFileDisplay display=UploadFileDisplay.getInstance();
		
		return display.disPlay(fileName, uploadPath);
	}
	
	

 

 

 

테스트

테스트 먼저 이미지와 일반 파일을 업로드 시킨다.

콘솔창에 찍히는 파일 주소를 url 상에 다음과 같이 입력해 본다.

콘솔창

INFO : test.controller.UploadController - 파일 저장후 반환 값 - ,  /2017/10/22/1c9c0b2a-a16f-44fa-b548-f4f5f74d4ff8_최준호.hwp 
 

url 입력 값:

http://localhost:7070/displayFile?fileName=/2017/10/22/s_24bef728-c974-49c7-b2283d3754d3be0_%EC%9D%B4%EB%AF%B8%EC%A7%80%202.png

http://localhost:7070/displayFile?fileName=/2017/10/22/1c9c0b2a-a16f-44fa-b548-f4f5f74d4ff8_최준호.hwp

 

 

이미지 표시 , 파일 링크 처리

<script>

$(function(){
	

	
	
	$(".fileDrop").on("dragenter dragover", function(event){
		event.preventDefault();
	});

	$(".fileDrop").on("drop", function(event){
		event.preventDefault();
		
		var files=event.originalEvent.dataTransfer.files;
		
		var file=files[0];
		
		console.log(file);
		var formData=new FormData();
		formData.append("file", file);

		$.ajax({
			
			url:'/test/uploadAjax2',
			data:formData,
			dataType:'text',
			processData:false,
			contentType:false,
			type:'POST',
			success:function(data){
				
				var str="";
				
				if(checkImageType(data)){
					
					str="<div>"
					 +"<a href='/displayFile?fileName="+getImageLink(data)+"' ><img src='/displayFile?fileName="+data+"'/>"
					  +getOriginalName(data)+"</a>";		 
				}else{
					str ="<div>"+getOriginalName(data);
					
				}
				str +="<button data-src="+data+">X</button></div>";
				$(".uploadedList").append(str);
				
			}
		});
				
	});
	
	
});


//이미지인지 패턴으로 확인
function checkImageType(fileName){
	
	var pattern=/jpg|gif|png|jpeg/i;
	
	return fileName.match(pattern);
	
}


//원본이미지 이름 출력
function getOriginalName(fileName){
	
	if(checkImageType(fileName)){
		//이미지 일경우
		var idx=fileName.lastIndexOf("_")+1;
		return fileName.substr(idx);
	}
	
	//일반 파일 일 경우
	var idx=fileName.indexOf("_")+1;
	return fileName.substr(idx);
	
}


//이미지 원본 파일 찾아서 링크로 연결 시키기
// /2017/10/23/s_4beb6430-7dfb-4def-bcfd-35f4425b538f_a1.jpg
function getImageLink(fileName){
	
	//이미지가 아니면 반환
	if(!checkImageType(fileName)){
		return ;
	}
	
	var front =fileName.substr(0,12);
	var end=fileName.substr(14);
	return front +end;
}

</script>

 

 

 

 

 

 

 

 

 파일 삭제하기  

 

@Controller

	//삭제 처리
	@ResponseBody
	@RequestMapping(value="/deleteFile", method=RequestMethod.POST)
	public ResponseEntity<String> deleteFile(String fileName){
		
		logger.info("delete file {} " , fileName);
		
		String formatName =fileName.substring(fileName.lastIndexOf(".")+1);
		
		MediaType mType =MediaUtils.getMediaType(formatName);
		
		if(mType!=null){
			//이미지 파일인경우
			//이미지 원본 파일 삭제
			String front =fileName.substring(0, 12);
			String end=fileName.substring(14);
			new File(uploadPath+(front+end).replace('/', File.separatorChar)).delete();
		}
		
		//일반 파일 삭제, 썸네일 이미지 삭제
		new File(uploadPath+fileName.replace('/', File.separatorChar)).delete();
		
		return new ResponseEntity<String>("deleted", HttpStatus.OK);
	}

 

<div class="uploadedList">

<div><a href="/displayFile?fileName=/2017/10/23/4275f192-011f-469f-afba-e7e291e252b6_a1.jpg">
<img src="/displayFile?fileName=/2017/10/23/s_4275f192-011f-469f-afba-e7e291e252b6_a1.jpg">a1.jpg</a>
<button data-src="/2017/10/23/s_4275f192-011f-469f-afba-e7e291e252b6_a1.jpg">X</button></div>

<div>스타크래프트 시디키.txt
<button data-src="/2017/10/23/1ceb5610-b5ec-4032-bfd1-aa6c0bdb47a4_스타크래프트" 시디키.txt="">X</button>
</div>

</div>

 

 

	//첨부파일 삭제 처리
	$(".uploadedList").on("click", "button", function(event){
		
		var that =$(this);
		$.ajax({
			url:"/deleteFile",
			type:"post",
			data:{
				fileName:$(this).attr("data-src")
			},
			dataType:"text",
			success:function(result){
				if(result=='deleted'){	
					//삭제후 화면에서 제거
					that.parent("div").remove();
				}	
			}
			
		});
		
	});
	

 

 

 

 

 

 

제작 : macaronics.net - Developer  Jun Ho Choi

소스 :  소스가 필요한 분은 비밀 댓글로 작성해 주세요.

 

 

 

 

about author

PHRASE

Level 60  라이트

하나의 눈으로 보는 것보다는 두 개의 눈으로 보는 것이 더 잘 보이고, 하나의 귀로 듣는 것보다는 두 개의 귀로 듣는 것이 더 잘 들린다. 천하의 모든 백성의 실정을 보고 진실된 소리를 듣는 것이 나라를 다스리는 요도(要道)가 된다. -묵자

댓글 ( 9)

댓글 남기기

작성