본문 바로가기
Kotlin

kotlin 압축과 압축해제(zip, unzip, tar, unTar, gzip, unGzip, tarGzip, unTarGzip)

by 루에 2019. 11. 12.
반응형

나중에 보면 까먹는다. 미리 기록해 놓자.

 

Upzip

fun unZip(zipFilePath: String, targetPath: String) {
    ZipFile(zipFilePath).use { zip ->
        zip.entries().asSequence().forEach { entry ->
            if(entry.isDirectory){
                File(targetPath, entry.name).mkdirs()
            }else{
                zip.getInputStream(entry).use { input ->
                    File(targetPath, entry.name).outputStream().use { output ->
                        input.copyTo(output)
                    }
                }
            }
        }
    }
}

 

Zip은 고생을 좀 했다. 인터넷에 있는 소스들을 기반으로 만들었으나(찾는 것도 힘들었던게, 폴더는 FileInputStream으로 읽으면 오류가 나서 따로 처리하고 뭐하고... 대부분 파일 압축만 있다보니 -_-) 그나마도 만든게 스트림이 닫힌다던지, 참조가 안된다던지 등등...

 

그래서 처음에는 File List를 받아 use를 사용해 한 메소드 안에서 처리하려 했으나, 서브 폴더 리스트로 재귀하게 되는 구조인데, 하나의 ZipOutputStream을 사용하다보니 서브 폴더 리스트를 다 돌면 스트림이 닫힌다. 당연하게도 ㅜㅜ

 

이걸 모르고 

fun zip(files: List<File>, zos: ZipOutputStream){
	zos.use { zos ->
    //
    }
}

이런형태로 받아서 쓰다가 폭망!

 

구조를 바꿨다. 실제 zip을 실행하는 놈은 단일 파일을 받아서 압축하고 그걸 감싸는 함수를 만들기로.. 그래서 아래처럼 구성함

// 래핑 함수이자, 사용자가 실행하는 함수. unzip과 마찬가지로 file list와 타겟이 되는 경로만 받는다.
fun zip(files: List<File>, targetPath: String){
    logger.debug("call zip")
    ZipOutputStream(BufferedOutputStream(FileOutputStream(targetPath))).use {  output ->
        files.forEach { file -> excuteZip(file, output) }
    }
}

// 래핑 함수에서 forEach를 돌며 파일을 zip entry에 넣고, 파일일 경우 파일 쓰기까지 진행한다.
private fun excuteZip(file: File, zipOut: ZipOutputStream, parentPath: String = ""){
    if(file.isDirectory){
        val entryPath = parentPath + File.separator + file.name
        zipOut.putNextEntry(ZipEntry(if(file.name.endsWith("/")) entryPath else entryPath + File.separator))
        zipOut.closeEntry()
        file.listFiles()?.let {
            it.toList().forEach { f -> excuteZip(f, zipOut, entryPath)}
        }
    }else{
        val entry = ZipEntry(parentPath + File.separator + file.name)
        zipOut.putNextEntry(entry)
        FileInputStream(file).use { fileInputStream ->
            BufferedInputStream(fileInputStream).use { bufferedInputStream ->
                bufferedInputStream.copyTo(zipOut)
            }
        }
    }
}

 

tar

org.apache.commons:commons-compress:1.18 패키지를 추가한다.(이 패키지에 tar에 관련된 클래스가 있다)

사용될 클래스 목록은 아래와 같다.

TarArchiveOutputStream

TarArchiveEntry

나머지는 zip압축에서 사용하는 것과 동일하다.

 

그리고 위에 만든 zip관련 함수에 Stream만 변경하니 아래와 같은 Exception이 발생했다.

request to write 8192 bytes exceeds size in header of 0 bytes

 

원인은 내가 압축할 파일에 대한 헤더 사이즈가 비정상적으로 적용되었기 때문이다.

zip과 tar는 구조적으로 다르기 때문인지 서드파티 라이브러리가 그런것인지 좀 더 수동적인 것 같다.

entry에 제대로 파일이 쓰여지지 않은 것이기 때문에 내부를 살펴보면,

 

private TarArchiveEntry(boolean preserveAbsolutePath)
public TarArchiveEntry(final String name)
public TarArchiveEntry(String name, final boolean preserveAbsolutePath)
public TarArchiveEntry(final String name, final byte linkFlag)
public TarArchiveEntry(final String name, final byte linkFlag, final boolean preserveAbsolutePath)
public TarArchiveEntry(final File file)
public TarArchiveEntry(final File file, final String fileName)
public TarArchiveEntry(final byte[] headerBuf)

등등이 있다. 이 중에서 string을 파라미터로 하는 것은 제대로 인식이 안되는 것이므로 모두 넘기고 file을 파라미터로 하는 entry를 적용한다. 6번째 생성자면 될 것 같다.

 

적용하니 헤더 사이즈 부분은 넘어갔으나 archive에 close되지 않은 entry가 들어왔다고 exception이 생긴다. 위 zip코드에서보면 폴더일 경우만 close를 하고 있었는데 tar에서는 이것도 문제가 되는 것 같다. 파일을 쓰고 닫아준다. 테스트 해보니 폴더 구조도 정상적으로 생성하여 압축하는 것을 확인하였다.

최종 코드는 아래와 같다.

 

fun tar(files: List<File>, targetPath: String){
    logger.debug("call tar")
    TarArchiveOutputStream(BufferedOutputStream(FileOutputStream(targetPath))).use { output ->
        files.forEach { file ->
            logger.info(file.absolutePath)
            executeTar(file, output) }
    }
}

private fun executeTar(file: File, zipOut: TarArchiveOutputStream, parentPath: String = ""){
    if(file.isDirectory){
        val entryPath = parentPath + File.separator + file.name
        zipOut.putArchiveEntry(TarArchiveEntry(if(file.name.endsWith("/")) entryPath else entryPath + File.separator))
        zipOut.closeArchiveEntry()
        file.listFiles()?.let {
            it.toList().forEach { f -> executeTar(f, zipOut, entryPath) }
        }
    }else{
        val entry = TarArchiveEntry(file)
        zipOut.putArchiveEntry(entry)
        FileInputStream(file).use { fileInputStream ->
            BufferedInputStream(fileInputStream).use { bufferedInputStream ->
                bufferedInputStream.copyTo(zipOut)
            }
        }
        zipOut.closeArchiveEntry()
    }
}

 

gzip

간단하게 기존 코드를 거진 재활용한다.

java util package에 GZIPOutputStream이 있어 이를 이용해도 될 것 같으나, 어차피 라이브러리를 추가한김에 이쪽으로 활용해보았다. 

fun gzip(file: File, targetPath: String){
        logger.debug("call gzip")
        GzipCompressorOutputStream(BufferedOutputStream(FileOutputStream(targetPath))).use { output ->
            FileInputStream(file).use { fileInputStream ->
                BufferedInputStream(fileInputStream).use { bufferedInputStream ->
                    bufferedInputStream.copyTo(output)
                }
            }
        }
    }

 

이렇게하니 문제가 생긴다. File을 파라미터로 주니 루트 경로부터 폴더를 다 만든다. 예를 들면, D:\a\b\c\ 안에 있는 파일을 압축하면 압축파일 내 폴더구조도 a\b\c\가 되버린다. 파라미터는 string으로 지정하고 size를 세팅하는 것으로 해결했다.

 

그리고 추가 작업을 하여 untar, ungzip, untargzip을 만들었다. 결과물은 아래와 같다.

~ 환경에서 동작 보장 등을 써놨지만 사실 보장 안된다. ㅎㅎㅎ

import dra.logger.DebugTag
import dra.logger.TPLogger
import dra.logger.TaskTag
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import kotlin.math.pow

class Compress {
    /**
     * Windows 환경에서 동작 보장
     *
     * 압축(Zip)
     */
    fun zip(files: List<File>, targetPath: String){
        TPLogger.info(TaskTag.Status, "call zip")
        ZipOutputStream(BufferedOutputStream(FileOutputStream(targetPath))).use { output ->
            files.forEach { file ->
                TPLogger.info(DebugTag.Debug, file.absolutePath)
                executeZip(file, output) }
        }
    }

    /**
     * Windows 동작 보장
     *
     * 압축 해제(zip)
     */
    fun unZip(zipFilePath: String, targetPath: String) {
        TPLogger.info(DebugTag.Debug, "call unZip")
        ZipFile(zipFilePath).use { zip ->
            zip.entries().asSequence().forEach { entry ->
                if (entry.isDirectory) {
                    File(targetPath, entry.name).mkdirs()
                } else {
                    zip.getInputStream(entry).use { input ->
                        File(targetPath, entry.name).outputStream().use { output ->
                            input.copyTo(output)
                        }
                    }
                }
            }
        }
    }
    /**
     * Windows, Linux 동작 보장
     *
     * 묶음(tar)
     */
    fun tar(files: List<File>, targetPath: String): File?{
        TPLogger.info(TaskTag.Status, "call tar")
        try{
            // 압축하고자 하는 타겟이 폴더 1개일 때는 폴더 이하 리스트를 work list에 넣는다.
            var workList = if(files.size == 1 && files[0].isDirectory) files[0].listFiles()?.toList() else null
            // 타겟이 폴더 1개지만 폴더 이하 리스트가 없을 경우는 원래값을 work list에 넣는다.
            if(workList.isNullOrEmpty()) workList = files

            TarArchiveOutputStream(BufferedOutputStream(FileOutputStream(targetPath))).use { output ->
                workList.forEach { file ->
                    TPLogger.info(DebugTag.Debug, file.absolutePath)
                    executeTar(file, output)
                }
            }
        }catch(e: Exception){
            TPLogger.error(DebugTag.Debug, "${e.message}, ${e.cause}")
            return null
        }
        return File(targetPath)
    }

    /**
     * Windows, Linux 동작 보장
     *
     * 묶음 해제(tar)
     */
    fun unTar(tarFilePath: String, targetPath: String): String?{
        TPLogger.info(DebugTag.Debug, "call unTar")
        try {
            TarArchiveInputStream(BufferedInputStream(FileInputStream(tarFilePath))).use { tar ->
                var entry = tar.nextTarEntry
                while(entry != null){
                    if (entry.isDirectory) {
                        File(targetPath, entry.name).mkdirs()
                    } else {
                        File(targetPath, entry.name).outputStream().use { output ->
                            tar.copyTo(output)
                        }
                    }
                    entry = tar.nextTarEntry
                }
            }
        }catch(e: Exception){
            TPLogger.error(DebugTag.Debug, "${e.message}, ${e.cause}")
            return null
        }
        return targetPath
    }

    /**
     * Windows, Linux 동작 보장
     *
     * 압축 해제(tar.gz -> tar)
     */
    fun unGzip(gzipFilePath: String, targetPath: String): File?{
        val fileName: String?
        try{
            fileName = gzipFilePath.substringAfterLast(File.separator).split(".tar.gz")[0]
            GzipCompressorInputStream(BufferedInputStream(FileInputStream(gzipFilePath))).use {  gzip ->
                File(targetPath, "$fileName.tar").outputStream().use { output ->
                    gzip.copyTo(output)
                }
            }
        }catch (e: Exception){
            TPLogger.error(DebugTag.Debug, "${e.message}, ${e.cause}")
            return null
        }
        return File(targetPath, "$fileName.tar")
    }

    /**
     * Windows, Linux 동작 보장
     *
     * 압축 해제(tar.gz)
     */
    fun unTarGzip(tarGzipFilePath: String, targetPath: String): Boolean{
        try{
            unGzip(tarGzipFilePath, targetPath)?.let {
                unTar(it.absolutePath, targetPath)?.apply {
                    TPLogger.info(DebugTag.Debug, "tarGzip success path : $this")
                }.apply {
                    it.delete()
                }
            }
        }catch (e: Exception){
            TPLogger.error(DebugTag.Debug, "${e.message}, ${e.cause}")
            return false
        }
        return true
    }
    /**
     * Linux 동작 보장
     *
     * 압축(tar.gz)
     */
    fun tarGzip(files: List<File>, targetPath: String): File?{
        TPLogger.info(TaskTag.Status, "call tarGzip")
        return tar(files, "$targetPath.tar")?.let {
            TPLogger.info(DebugTag.Debug, "tar success : ${it.name}, ${it.length() /1024.0.pow(2)} MB")
            gzip(it, "$targetPath.tar.gz")?.apply {
                TPLogger.info(DebugTag.Debug, "tarGzip success : ${name}, ${length() / 1024.0.pow(2)} MB")
            }.apply {
                it.delete()
            }
        }
    }

    private fun executeZip(file: File, zipOut: ZipOutputStream, parentPath: String = ""){
        if(file.isDirectory){
            val entryPath = parentPath + File.separator + file.name
            zipOut.putNextEntry(ZipEntry(if(file.name.endsWith("/")) entryPath else entryPath + File.separator))
            zipOut.closeEntry()
            file.listFiles()?.let {
                it.toList().forEach { f -> executeZip(f, zipOut, entryPath)}
            }
        }else{
            val entry = ZipEntry(parentPath + File.separator + file.name)
            zipOut.putNextEntry(entry)
            FileInputStream(file).use { fileInputStream ->
                BufferedInputStream(fileInputStream).use { bufferedInputStream ->
                    bufferedInputStream.copyTo(zipOut)
                }
            }
        }
    }

    private fun gzip(file: File, targetPath: String): File?{
        TPLogger.info(TaskTag.Status, "call gzip")
        try{
            GzipCompressorOutputStream(BufferedOutputStream(FileOutputStream(targetPath))).use { output ->
                FileInputStream(file).use { fileInputStream ->
                    BufferedInputStream(fileInputStream).use { bufferedInputStream ->
                        bufferedInputStream.copyTo(output)
                    }
                }
            }
        }catch (e: Exception){
            TPLogger.error(DebugTag.Debug, "${e.message}, ${e.cause}")
            return null
        }
        return File(targetPath)
    }

    private fun executeTar(file: File, zipOut: TarArchiveOutputStream, parentPath: String = ""){
        TPLogger.info(DebugTag.Debug, "file : ${file.absolutePath} parent : $parentPath")
        if(file.isDirectory){
            val entryPath = parentPath + File.separator + if(file.name.endsWith(File.separatorChar)){
                    file.name
                } else {
                file.name + File.separator
            }
            zipOut.putArchiveEntry(TarArchiveEntry(entryPath))
            zipOut.closeArchiveEntry()
            file.listFiles()?.let {
                it.toList().forEach { f -> executeTar(f, zipOut, entryPath.substringBeforeLast(File.separatorChar)) }
            }
        }else{
            zipOut.putArchiveEntry(TarArchiveEntry("$parentPath${File.separator}${file.name}").apply {
                size = file.length()
            })
            FileInputStream(file).use { fileInputStream ->
                BufferedInputStream(fileInputStream).use { bufferedInputStream ->
                    bufferedInputStream.copyTo(zipOut)
                }
            }
            zipOut.closeArchiveEntry()
        }
    }
}

 

반응형

댓글