최근 사내 프로젝트에서 압축 파일 해제 코드가 올바르게 처리 되고 있지 않아 컨플루언스에 올린 내용을 블로그에도 올려보겠다.
개요
특정 압축 파일을 업로드하여 자바 코드 내에서 압축 해제하는 경우 코드 동작이 정상적으로 진행되지 않는 현상
압축 대상 파일
zip, tar, tar.gz
압축 파일의 실제 포맷은?
file resource.tar.gz
리눅스 계열에서 file 명령어를 통해 파일이 어떤 포맷인지 확인할 수 있다.
file에 대한 포맷을 확인하는 이유는 가령 사용자가 zip으로 압축 한 뒤에 임의로 확장자를 .tar.gz로 바꾸는 경우가 있기 때문이다.
물론 이 경우에도 압축 파일로서의 동작은 잘 된다.
하지만 문제는 이 파일의 실체는 zip이다!
압축된 zip 파일을 .tar.gz로 강제로 바꾸는 경우 위의 명령어를 통해서 Zip archive data라는 Type으로 출력 된다. → 실체는 .zip이라는 뜻
그에 반해 tar 명령어로 묶은 tar.gz 파일은 POSIX tar archive(표준 약관을 따르는 tar 압축 파일)라는 Type으로 출력 된다. → 실체는 .tar 파일이라는 뜻
실제 코드에서 압축 파일의 포맷을 확인하는 방법(Magic Number)
이제 압축 파일이 어떤 포맷을 가지고 있는지 알았다.
이제 Apache Commons Library를 통해 해당 압축 파일의 유형에 맞게 압축을 해제해 줄 필요가 있다.
우선 압축 파일이 어떤 유형인지 알기 위해서는 파일에 대한 매직 넘버를 확인해야한다.
특정 압축 파일에 따라 InputStream 내에 시작 되는 16진수가 다르므로 시작되는 16진수 문자를 파악해야한다.
압축 형태 | 확장자 | 16진수 | 비고 |
pkzip format | .zip | 50 4b 03 04 | |
tar (POSIX) | .tar | 75 73 74 61 72 (start from 257 byte) | 257 byte 위치부터 ustar라는 글자를 가지고 있음. |
gzip format | .gz | 1f 8b |
.tar.gz의 이중성
위에서 한번 설명했던 것을 포함해서 다시 한번 .tar.gz에 대한 특징을 설명할 필요가 있다.
참고로 tar는 압축이 아닌 그냥 파일에 대한 묶음이다.
1. tar 명령어를 통해 압축해서 만든 .tar.gz
tar -cvf resource1.tar.gz resource
이런 경우에는 압축 유형이 tar (POSIX)로 분류되므로 자바에서 TarArchiveInputStream 클래스로 압축 해제 해주면 된다.
2. 윈도우에서 zip 파일을 .tar.gz로 만든 경우
이런 경우에는 실제 압축 유형은 zip이다. 그러므로 자바에서 매직 넘버를 확인 한 후에 ZipArchiveInputStream 클래스로 압축 해제 해주면 된다.
3. 실제로 .tar를 .gz로 2번 압축 한 경우
실제로 이런 경우의 .tar.gz는 별개의 특정 매직 넘버로 분류되지 않는다.
그냥 마지막에 압축한 .gz에 대한 매직 넘버만이 확인되기 때문이다.
그러므로 마지막 gzip format에 대한 16진수 코드만 파악할 수 있다.
이 유형은 GzipCompressorInputStream -> TarArchiveInputStream 순서의 클래스로 압축 해제 해주면 된다.
실제 구현 코드
public static InputStream getArchiveStream(InputStream inputStream) throws IOException {
ArchiveInputStream tar = null;
inputStream.mark(Integer.MAX_VALUE);
try {
tar = new TarArchiveInputStream(new GzipCompressorInputStream(inputStream));
} catch (IOException e) {
}
inputStream.reset(); // after the inputStream has been consumed once, it needs to be reset.
CompressedType compressedType = retrieveCompressedTypeFromStream(inputStream);
// converting from .zip should take precedence because while TAR can be converted to ZIP, the reverse is not possible.
if (compressedType == CompressedType.PKZIP && tar == null) {
tar = new ZipArchiveInputStream(inputStream);
}
if (compressedType == CompressedType.TAR && tar == null) {
tar = new TarArchiveInputStream(inputStream);
}
if (tar == null) {
throw new CommonExceptions.BadRequestException("Unsupported compression file format");
}
return tar;
}
public static CompressedType retrieveCompressedTypeFromStream(InputStream input) {
try {
CompressedType compressedType = null;
input.mark(Integer.MAX_VALUE);
byte[] bytes = new byte[300];
input.read(bytes);
if (bytes[0] == 0x50 && bytes[1] == 0x4b && bytes[2] == 0x03 && bytes[3] == 0x04) {
compressedType = CompressedType.PKZIP;
} else if (bytes[257] == 0x75 && bytes[258] == 0x73 && bytes[259] == 0x74 && bytes[260] == 0x61 && bytes[261] == 0x72) { // ustar
compressedType = CompressedType.TAR;
} else if (bytes[0] == 0x1f && bytes[1] == 0x8b) {
compressedType = CompressedType.GZIP;
}
input.reset();
return compressedType;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
try문 안에는 실제로 .tar와 .gz로 2번 압축한 경우에 대한 압축 해제 방법이다.
이런 경우엔 매직 넘버를 파악하는 방법이 없기 때문에 예외 처리를 통해 바로 패스시키는 방법을 선택했다.(분명 더 좋은 방법이 있을 것은 확실하다. 일단 비즈니스 로직이 아니기 때문에 패스.)
그 외는 매직 넘버가 파악이 되기 때문에 CompressedType을 구하는 메소드를 통해 포맷을 확인하고 압축 해제 해주면 된다.
'☕ Java' 카테고리의 다른 글
[Spring] DTO는 어디서 변환할까? (feat. DDD) (0) | 2024.09.19 |
---|---|
[Kotlin] Companion Object (0) | 2024.02.02 |
Java Stream 사용법 ( 당신의 Stream은 안녕하십니까? ) (0) | 2023.08.26 |
커스텀 어노테이션과 리플렉션 (0) | 2023.07.30 |
[Kotlin] Optional vs Kotlin Nullable 문법 비교하기 (0) | 2023.06.21 |