Search

Spring File

설정

Multipart - config

<multipart-config> <loaction>C:\\upload\\temp</location> <max-file-size>20971520</max-file-size> // 20MB <max-request-size>41943040</max-request-size> // 40MB <file-size-threshold>20971520</file-size-threshold> // 20MB </multipart-config>
XML
복사
이거는 WAS 설정일 뿐이고, 웹앱 설정은 따로 해줘야함.
스프링에
MultipartResolver라는 타입의 객체를 빈으로 등록해야한다.
@Bean public MultipartResolver multipartResolver(){ StandardServletMultipartResolver resolver = new StandardServletMultipartResolver(); return resolver; }
Java
복사
스프링에는 MultipartFile 타입을 제공해서 업로드되는 파일 데이터를 쉽게 처리할 수 있다.

MultipartFile

String getName() : 파라미터의 이름 <input> 태그의 이름
String getOriginalFileNAmE(): 업로드되는 파일의 이름
boolean isEmpty(): 파일이 존재하지 않는 경우 true
long getSize(): 업로드되는 파일의 크기
byte[] getBytes(): byte[]로 파일 데이터 변환
InputStream getInputStream(): 파일데이터와 연결된 InputStream을 반환
transferTo(File file): 파일의 저장

Client Ajax

<script> $(document).ready(function(){ $("#uploadBtn").on("click",function(e){ var formData = new FormData(); var inputFile = $("input[name='uploadFile']"); var files = inputFile[0].files; console.log(files); for(var i = 0; i< files.length;i++){ formData.append("uploadFile",files[i]); } $.ajax({ url: '/uploadAjaxAction', processData: false, contentType: false, data: formData, type: 'POST', success: function(result){ alert("Uploaded"); } }); }); }); </script>
JavaScript
복사
FormData()라는 객체를 만들고
input 태그의 속성 중 하나인 files를 선택한 다음
그 파일들을 돌면서 formData에 파일 데이터를 추가시킨 후
Ajax로 보내버린다.

Server Ajax

@PostMapping("/uploadAjaxAction") public void uploadAjaxPost(MultipartFile[] uploadFile)} log.info("update ajax post...."); String uploadFolder = "C:\\upload"; for(Multipartfile multipartFile : uploadFile){ String uploadFileName = multipartfile.getPriginalFilename(); uploadfilename = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1); File saveFile = new File(uploadFolder, uploadFileName); try{ multipartFile.transferTo(saveFile); }catch(Exception e){ log.error(e.getMessage()); } }
Java
복사

추가적으로 고쳐야할 점

동일한 이름으로 파일이 업로드 되었을 때 기존 파일이 사라지는 문제
이미지 파일의 경우에는 원본이 클 때, 썸네일을 생성해야하는 문제
이미지 파일과 일반 파일을 구분해서 다운로드 혹은 페이지에서 조회하도록 처리하는 문제
첨부파일 공격에 대비하기 위한 업로드 파일의 확장자 제한

파일 업로드 상세 처리

파일 확장자나 크기의 사전 처리

클라이언트 쪽에서 파일의 확장자는 차단시킨다.
서버 쪽에서도 한 번 더 체크한다. Regex

중복된 이름의 첨부파일 처리

1.
중복된 이름의 파일 처리 → UUID를 이용해서 중복이 발생할 가능성이 거의 없는 문자열을 생성해서 처리
2.
한 폴더 내에 너무 많은 파일의 생성 문제 → 년/월/일 단위의 폴더를 생성해서 파일을 저장

년/월/일 단위의 폴더를 생성

@PostMapping("/uploadAjaxAction") public void uploadAjaxPost(MultipartFile[] uploadFile)} log.info("update ajax post...."); String uploadFolder = "C:\\upload"; //make folder File uploadPath = new File(uploadFolder,getFolder()); log.info("upload path: " + uploadPath); if(uploadPath.exists() == false){ uploadPath.mkdirs(); } //make yyyy/MM/dd folder for(Multipartfile multipartFile : uploadFile){ String uploadFileName = multipartfile.getPriginalFilename(); uploadfilename = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1); File saveFile = new File(uploadFolder, uploadFileName); try{ multipartFile.transferTo(saveFile); }catch(Exception e){ log.error(e.getMessage()); } }
Java
복사
getFolder() 는 오늘 날짜의 경로를 문자열로 생성. 생성된 경로는 폴더 경로로 수정된 뒤에 반환한다.

UUID 적용

@PostMapping("/uploadAjaxAction") public void uploadAjaxPost(MultipartFile[] uploadFile)} log.info("update ajax post...."); String uploadFolder = "C:\\upload"; //make folder File uploadPath = new File(uploadFolder,getFolder()); log.info("upload path: " + uploadPath); if(uploadPath.exists() == false){ uploadPath.mkdirs(); } //make yyyy/MM/dd folder for(Multipartfile multipartFile : uploadFile){ String uploadFileName = multipartfile.getPriginalFilename(); //이름 자르기 uploadfileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1); //UUID 생성 UUID uuid = UUID.randomUUID(); //이름에 uuid 붙이기 uploadFileName = uuid.toString() + "_" + uploadFileName; //파일 생성 File saveFile = new File(uploadFolder, uploadFileName); try{ //저장 multipartFile.transferTo(saveFile); }catch(Exception e){ log.error(e.getMessage()); } }
Java
복사

썸네일 이미지 생성

Thumbnailator 라이브러리를 이용해서 썸네일 이미지를 생성
이 때, Controller에서는
1.
업로드된 파일이 이미지 종류의 파일인지 확인
2.
이미지 파일의 경우에는 섬네일 이미지 생성 및 저장함.

이미지 파일의 판단

private boolean checkImageType(File file){ try{ String contentType = Files.probeContentType(file.toPath()); return contentType.starsWith("image"); }catch(IOException e){ e.printStackTrace(); } return false; }
Java
복사
이미지 파일을 판단하는 메소드
File 객체의 MimeType을 반환하는 probeContentType()함수를 이용해서 image인지 아닌지 검사함
@PostMapping(value = "/uploadAjaxAction",produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public void uploadAjaxPost(MultipartFile[] uploadFile)} log.info("update ajax post...."); String uploadFolder = "C:\\upload"; //make folder File uploadPath = new File(uploadFolder,getFolder()); log.info("upload path: " + uploadPath); if(uploadPath.exists() == false){ uploadPath.mkdirs(); } //make yyyy/MM/dd folder for(Multipartfile multipartFile : uploadFile){ String uploadFileName = multipartfile.getPriginalFilename(); //이름 자르기 uploadfileName = uploadFileName.substring(uploadFileName.lastIndexOf("\\") + 1); //UUID 생성 UUID uuid = UUID.randomUUID(); //이름에 uuid 붙이기 uploadFileName = uuid.toString() + "_" + uploadFileName; try{ //파일 생성 File saveFile = new File(uploadFolder, uploadFileName); //저장 multipartFile.transferTo(saveFile); //이미지 인지 확인 if(checkImageType(saveFile)){ FileOutputStream thumbnail = new FileOutputStream(new File(uploadPath,"s_"+uploadFileName)); Thumnailator.createThumbnail(multipartfile.getInputStream(), thumbnail, 100, 100); thumbnail.close() }catch(Exception e){ log.error(e.getMessage()); } }
Java
복사

업로드된 파일의 데이터 반환

전송되야하는 데이터
업로드된 파일의 이름과 원본 파일의 이름
파일이 저장된 경로
업로드된 파일이 이미지인지 아닌지에 대한 정보
→ AttachFileDTO 클래스를 만들어서 반환하도록 하자
@Data public class AttachFileDTO{ private String fileName; private String uploadPath; private String uuid; privaet boolean image; }
Java
복사
—> 이러면 클라이언트에서 적당히 처리할 수 있다.

브라우저에서 썸네일 처리

클라이언트 쪽에서 썸네일 path를 주면 GET으로 처리한다.
@GetMapping("/display") @ResponseBody public ResponseEntity<byte[]> getFile(String fileName){ log.info("fileName: " + fileName); Fie file = new File("c:\\upload\\" + fileName); log.info("file: " + file); ResponseEntity<byte[]> result = null; try{ HttpHeaders header = new HttpHeaders(); header.add("Content-Type", Files.probeContenttype(file.toPath())); result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK); }catch(IOException e){ e.printStackTrace(); } return result; }
Java
복사

첨부 파일의 다운로드 혹은 원본 보여주기

이미지 썸네일 클릭 → 원본 보여주기(새로운 <div> 태그를 생성해서 처리하는 방식)
기타 다른 파일들 → 해당 파일의 이름으로 다운로드

첨부 파일의 다운로드

@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @ResponseBody public ResponseEntity<Resource> downloadFile(String fileName){ log.info("download file: " + fileName); Resource resource = new FileSystemResource("c:\\upload\\" + fileName); log.info("resource: " + resource); String resourceName = resource.getFilename(); HttpHeaders headers = new HttpHeaders(); try{ headers.add("Content-Disposition", "attachment; filename=" + new String(resourceName.getBytes("UTF-8"),"ISO-8859-1")); }catch(UnsupportedEncodingException e)} e.printStackTrace(); } return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK); }
Java
복사

서버에서 파일 이름에 UUID가 붙은 부분을 제거하고 다운로드

@GetMapping(value = "/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE) @ResponseBody public ResponseEntity<Resource> downloadFile(@RequestHeader("User-Agent") String userAgent, String fileName){ log.info("download file: " + fileName); //Resource 객체 생성 Resource resource = new FileSystemResource("c:\\upload\\" + fileName); if(resource.exists() == false){ return new ResponseEntity<>(HttpStatus.NOT_FOUND); } log.info("resource: " + resource); String resourceName = resource.getFilename(); //UUID 제거 String resourceOriginalName = resourceName.substring(resourceName.indexof("_")+1); HttpHeaders headers = new HttpHeaders(); try{ String downloadName = null; if(userAgent.contains("Trident")){ log.info("IE browser"); downloadName = URLEncoder.encode(resourceOriginalName,"UTF-8").replaceAll("\\+"," "); }else if(userAgent.contains("Edge")){ log.info("Edge browser"); downloadName = URLEncoder.encode(resourceOriginalName.getBytes("UTF-8"),"ISO-8859-1"); }else{ log.info("Chrome browser"); downloadName = new String(resourceOriginalName.getBytes("UTF-8"),"ISO-8859-1"); } log.info("downloadname: " + downloadName); //헤더에 파일 이름 추가 headers.add("Content-Disposition", "attachment; filename=" + new String(resourceName.getBytes("UTF-8"),"ISO-8859-1")); }catch(UnsupportedEncodingException e)} e.printStackTrace(); } return new ResponseEntity<Resource>(resource, headers, HttpStatus.OK); }
Java
복사

첨부파일 삭제

고려해야할 점
이미지 파일의 경우에는 썸네일가지 같이 삭제됨
파일을 삭제한 후에는 브라우저에서도 섬네일이나 파일 아이콘이 삭제되도록 처리하는 점
비정상적으로 브라우저의 종료 시 업로드된 파일의 처리

일반 파일과 이미지 파일의 삭제

@PostMapping("/deleteFile") @ResponseBody public ResponseEntity<String> deleteFile(String fileName, String type){ log.info("deleteFile: " + fileName); File file; try{ file = new File("c:\\upload\\" + URLDecoder.decode(fileName, "UTF-8")); file.delete(); if(type.equals("image")){ String largeFileName = file.getAbsolutePath().replace("s_",""); log.info("largerFileName: " + largerFileName); file = new File(largeFileName); file.delete(); }catch(UnsupportedEncodingException e){ e.printStackTrace(); return new ResponseEntity<>(HttpStatus.NOT_FOUND); } return new ResponseEntity<String>("deleted",HttpStatus.OK); }
Java
복사

프로젝트의 첨부파일 - 등록

첨부 + 게시물의 테이블 설계

AttachedFileDTO 참고해서 짜보기