서버에서는 파일을 저장할 때 반드시 고려해야 하는 사항은 다음과 같다.
- 파일 이름의 중복 문제
- 파일의 저장 경로에 대한 문제
- 원본 파일을 그대로 보여주는 경우 브라우저에서 보여지는 파일의 크기 문제
- 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
소스 : 소스가 필요한 분은 비밀 댓글로 작성해 주세요.
댓글 ( 9)
댓글 남기기