본문 바로가기
개발/AWS

[AWS] AWS + S3 + ZIP형식 멀티다운로드

by 설이주인 2023. 8. 7.

S3에 저장된 자료들을 ZIP형식으로 다운로드 받고 싶다는 요청이 들어와서 작업한 부분을 정리했다.

 

 /**
   * File > ZIP > MultiDownload
   */
  public void multiDownload(List<String> filePathList, HttpServletResponse response) throws IOException {
    List<S3ObjectInputStream> inputList = new ArrayList<>();
    SimpleDateFormat dayFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
    Date nowDate = Calendar.getInstance().getTime();
    String nowDay = dayFormat.format(nowDate).replace(":", "");
    String zipFileName = "ALZ_" + nowDay;
    FileInputStream fileInputStream = null;

    for (String filePath : filePathList) {
      S3Object fullObject = s3Client.getObject(bucketName, filePath);
      S3ObjectInputStream objectInputStream = fullObject.getObjectContent();
      inputList.add(objectInputStream);
    }

    List<File> fileList = convertListS3ObjectInputStreamToFile(inputList);
    File zipFile = File.createTempFile(zipFileName, ".zip");
    zipFile.deleteOnExit(); //23.12 add
    
    try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile))) {
      byte[] buf = new byte[1024];

      for (File file : fileList) {
        if (file.exists()) {
          try (FileInputStream fis = new FileInputStream(file)) {
            ZipEntry zipEntry = new ZipEntry(file.getName());
            out.putNextEntry(zipEntry);
            int len;
            while ((len = fis.read(buf)) > 0) {
              out.write(buf, 0, len);
            }
            out.closeEntry();
          }
        }
      }
      for (File file : fileList) {
        deleteFile(file);
      }
    }

		//파일  bytes 변환
    byte[] bytes = FileCopyUtils.copyToByteArray(zipFile);
    response.setContentType("application/zip;");
    response.addHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + ".zip" + "\";");
    //파일  copy  (inputstream, outpustream)
		FileCopyUtils.copy(bytes, response.getOutputStream());
  }

//s3추출해온  s3Object를 File로 변환해줘야함
private List<File> convertListS3ObjectInputStreamToFile(List<String> fileNameList, List<S3ObjectInputStream> inputStream) throws IOException {
    List<File> files = new ArrayList<>();
    int count = 0;
    for (S3ObjectInputStream stream : inputStream) {
      String path = stream.getHttpRequest().getURI().getPath();
      String[] split = path.split("/");
      String fileName = fileNameList.get(count);
      count++;
      File file = convertS3ObjectInputStreamToFile(stream, fileName);
      stream.close();
      files.add(file);
    }
    return files;
  }

  private File convertS3ObjectInputStreamToFile(S3ObjectInputStream inputStream, String fileName) throws IOException{
    File file = new File(fileName);
    try (OutputStream outputStream = new FileOutputStream(file)) {
      byte[] buffer = new byte[1024];
      int bytesRead;
      while ((bytesRead = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, bytesRead);
      }
    }
    return file;
  }

 

  • 기억을 위한 해석본
더보기
/**
* KJE  File > ZIP > MultiDownload
* @param : filePathList > 파일 저장 위치 (from DB)
*/
public void multiDownload(List<String> filePathList, HttpServletResponse response) throws IOException {
SimpleDateFormat dayFormat = new SimpleDateFormat("yyyyMMddHH:mm:ss");
Date nowDate = Calendar.getInstance().getTime();
String nowDay = dayFormat.format(nowDate).replace(":", "");
String zipFileName = "CA_" + nowDay;  //zip으로 떤질 파일 명

List<S3ObjectInputStream> inputList = new ArrayList<>();  //s3에서 가져온 Content
for (String filePath : filePathList) {
  //s3에서 제공하는 aws sdk를 통해 s3버킷에서 객체 꺼내오는 getObject
  S3Object fullObject = s3Client.getObject(bucketName, filePath);
  
	//S3Object에 대해 getObjectContent를 호출하여 객체의 내용을 가져올 수 있습니다. 
  //다음 결과가 반환됩니다. S3오브젝트퍼트스트림표준 Java로 동작하는InputStream객체입니다.
  S3ObjectInputStream objectInputStream = fullObject.getObjectContent();
  
	//Zip안에 넣기 위해서 List에 s3에서 받아온 객체 넣기
  inputList.add(objectInputStream);
}

//S3ObjectInputStream를 바로 ZipEntry에 넣을 수 없기에 File로 convert 진행
List<File> fileList = convertListS3ObjectInputStreamToFile(inputList);

//임시 파일 생성 
File zipFile = File.createTempFile(zipFileName, ".zip");

//new ZipOutputStream zip파일 압축 시작
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile))) {
  byte[] buf = new byte[1024];

  for (File file : fileList) {
    if (file.exists()) {
      //압축 당할 파일 new FileInputStream(file)
      try (FileInputStream fis = new FileInputStream(file)) {
        ZipEntry zipEntry = new ZipEntry(file.getName());
        //압축 파일에 저장될 파일 : zipEntry
        out.putNextEntry(zipEntry);
        int len;
        //파일 읽기
        while ((len = fis.read(buf)) > 0) {
          //위에서 읽은 파일을 ZipOutputStream에 write
          out.write(buf, 0, len);
        }
        //zip파일 압축 종료
        out.closeEntry();
      }
    }
  }
  //위에서 저장을 위해 생성한 파일들 삭제
  for (File file : fileList) {
    deleteFile(file);
  }
}

//파일  bytes 변환
byte[] bytes = FileCopyUtils.copyToByteArray(zipFile);
response.setContentType("application/zip;");
response.addHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + ".zip" + "\";");
//파일  copy  (inputstream, outpustream)
FileCopyUtils.copy(bytes, response.getOutputStream());
}


//S3ObjectInputStream List >> FILE로 변환
private List<File> convertListS3ObjectInputStreamToFile(List<S3ObjectInputStream> inputStream) throws IOException {
List<File> files = new ArrayList<>();
for (S3ObjectInputStream stream : inputStream) {
  String path = stream.getHttpRequest().getURI().getPath();
  String[] split = path.split("/");
  String fileName = split[split.length - 1];
  File file = convertS3ObjectInputStreamToFile(stream,fileName);
  files.add(file);
}
return files;
}

//S3ObjectInputStream 단건 >> FILE 변환
private File convertS3ObjectInputStreamToFile(S3ObjectInputStream inputStream, String fileName) throws IOException{
File file = new File(fileName);
//FileOutputStream 데이터 파일을 바이트 스트림으로 저장하기 위해서 사용된다.
try (OutputStream outputStream = new FileOutputStream(file)) {
  byte[] buffer = new byte[1024];
  int bytesRead;
  //inputStream.read 메소드는 입력 스트림에서 데이터 바이트를 읽는다.
  //데이터의 다음 바이트를 반환하거나 파일 끝에 도달하면 -1을 반환한다.
  //계속 읽다가 파일 끝에 도달하면 읽은 아이를 bytesRead에 넣고 while문 안의 작업을 진행
  while ((bytesRead = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, bytesRead);
  }
}
//요기서 return 하는 file은 위의 new FileOutputStream(file) 안으로
return file;
}

- outputStream

https://www.baeldung.com/java-outputstream

 

- try with resources

https://www.baeldung.com/java-try-with-resources

 

파일 저장 및 다운로드 작업에서 이해하기 오래걸렸던 부분이 inputStream, outputStream이다.

  • inputStream
더보기

바이트 기반 입력 스트림의 최상 클래스

"this abstract class is the superclass of all classes representing an input stream of bytes"

이곳에서 주목할 부부은 represenging an input stream of bytes 이다.

 

그럼 stream은 무엇일까?

stream : 개별 바이트나 문자열인 데이터의 원헌, 파일을 읽거나 쓸 때, 네트워크 소켓을 거쳐 통신할때 쓰이는 추상적 개념 쉽게 생각하면 데이터 전송 통로이다.

bytes : 가볍게 데이터라고 생각하자 종합적으로 inputstream은 데이터가 들어오는 통로의 역할에 관해 규정하고 있는 추상 클래스이다. 지금까지 총 정리를 하자면 inputStream은 결론적으로 읽는 행위를 하는 아이이다.

 

현재 inputStream에는 데이터를 읽는 기능과 관련된 메서드가 3개 존재한다.

https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html

 

InputStream (Java Platform SE 8 )

Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes actually read is returned as an integer. This method blocks until input data is available, end of file is detected, or an exception is thrown. If

docs.oracle.com

 읽기의 메서드들의 확실한 부분은 inputStream으로 읽은 데이터는 다시 돌아가서 읽을 수 없다는 부분이다.

 

그외

데이터를 스킵하는 행위

 데이터가 얼마나 남았는지

읽었던 데이터를 특정 시점부터 다시 읽는다는 행위

 

그리고 종료를 지원하고 있다.

 

  • OutputStream
더보기

데이터가 나가는 통로 역할, 즉 데이터를 쓰는 행동이 가능하다.

보통 데이터를 파일, 메모리, 네트워크로 보내는 상황이 자주 발생한다.

 

write :

 

write 자세한 정보

//write(byte[] b, int off, int length) : 오버로드된 버전의 write()
//It can write “length” number of bytes from the byte array as specified by 
//the argument starting at an offset determined by “off” to the OutputStream

//들어간 값은 Hello World!
public static void fileOutputStreamByteSubSequence(
  String file, String data) throws IOException {
    byte[] bytes = data.getBytes(); //hello world의 bytes를 읽고
    try (OutputStream out = new FileOutputStream(file)) {
        out.write(bytes, 6, 5); 
        // (length : 5) 5개 작성, (from bytes) hello world!, 
							//(starting at an offset) 6번재에서 시작 
        // 결과적으로 hello world의 bytes를 담아서 해당 bytes를 6에서 시작 5글자 작성을
        // out write가 처리 한다.
    }
}
결과 : World

 

flush (데이터 비우기) : 

 

close : 

 

 

  • FileOutPutStream
더보기

 FileOutputStream

https://docs.oracle.com/javase/8/docs/api/java/io/FileOutputStream.html

 

FileOutputStream (Java Platform SE 8 )

Returns the unique FileChannel object associated with this file output stream. The initial position of the returned channel will be equal to the number of bytes written to the file so far unless this stream is in append mode, in which case it will be equal

docs.oracle.com

FileOutputStream(String fileName) : 주어진 이름의 파일을 쓰기 위한 객체를 생성

 

FileOutputStream(String fileName, boolean append) : 주어진 append 값에 따라 새로운 파일을 생성하거나 기존 내용에 추가

 

FileOutputStream(String fileName) : 주어진 이름의 파일을 쓰기 위한 객체를 생성

 

 

  • ETC

 

2023.12 - temp file이 삭제되지 않은 상태를 확인, 아래 명령어를 추가해주자.

zipFile.deleteOnExit();

 

 

createTempFile의 공식 doc을 확인해보면 자동으로 삭제 되기를 원한다면 deleteOnExit 을 사용하도록 이야기한다....

언제나 공식 doc은 자세히 읽어보자..

 

 

작업 참고 자료

https://shanepark.tistory.com/273

 

java) 여러개의 파일 압축해 zip파일 생성

압축파일 생성 java에서도 파일을 압축 하고 압축을 해제 할 수 있습니다. 심지어 java.util.zip 에 기본적으로 포함 되어 있기 때문에 외부 라이브러리를 사용하지 않고도 손쉽게 구현 할 수 있습니

shanepark.tistory.com

https://madplay.github.io/post/java-file-zip

 

자바 파일 압축과 압축 해제(ZipInputStream, ZipOutputStream)

java.util.zip 패키지를 이용한 자바 파일 압축과 압축 해제

madplay.github.io

 

개선 작업 예정을 위한 링크

https://gksdudrb922.tistory.com/234

 

[Spring] AWS S3 파일 압축해서 다운로드 - 여러가지 방법 비교분석

이전에 Spring을 통해 S3에서 파일을 다운로드하는 API를 개발한 적이 있었다. 2021.08.05 - [java/spring] - [Spring] AWS S3에서 Spring Boot로 파일 다운로드 [Spring] AWS S3에서 Spring Boot로 파일 다운로드 프로젝트

gksdudrb922.tistory.com

 

S3 공식문서 참고 (getObjectContent 외 S3Object 관련 메소드)

https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/s3/model/S3Object.html#getObjectContent--

 

S3Object (AWS SDK for Java - 1.12.523)

Gets the input stream containing the contents of this object. Note: The method is a simple getter and does not actually create a stream. If you retrieve an S3Object, you should close this input stream as soon as possible, because the object contents aren't

docs.aws.amazon.com

 

S3Object 참고문서

https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-s3-objects.html#download-object

 

Performing Operations on Amazon S3 Objects - AWS SDK for Java 1.x

You can use copyObject with deleteObject to move or rename an object, by first copying the object to a new name (you can use the same bucket as both the source and destination) and then deleting the object from its old location.

docs.aws.amazon.com

 

InputStream 의문 해결을 위한 참고

https://kj84.tistory.com/entry/%EB%B0%94%EC%9D%B4%ED%8A%B8-%EA%B8%B0%EB%B0%98-%EC%8A%A4%ED%8A%B8%EB%A6%BC-File-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9E%84%EC%8B%9C%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%83%9D%EC%84%B1%EA%B3%BC-%EC%82%AD%EC%A0%9C

 

바이트 기반 스트림 - File 클래스를 이용한 임시파일의 생성과 삭제

File 클래스를 이용한 임시파일의 생성과 삭제 프로그램을 작성하다 보면 임시로 파일을 생성할 필요하 생긴다. 예를 들어 외부 파일을 이용해서 정렬할 경우가 대표적인 예다. 하지만 문제는 "

kj84.tistory.com

https://www.techiedelight.com/ko/how-to-read-file-using-inputstream-java/

 

Java에서 InputStream을 사용하여 파일 읽기

이 게시물은 파일의 내용을 읽는 방법에 대해 설명합니다. InputStream Java에서. InputStream 추상 클래스는 바이트의 입력 스트림을 나타내는 모든 클래스의 수퍼 클래스입니다. 다음을 사용하여 파일

www.techiedelight.com