Add tar.gz compression capability
[ccsdk/cds.git] / ms / blueprintsprocessor / modules / blueprints / blueprint-core / src / main / kotlin / org / onap / ccsdk / cds / controllerblueprints / core / utils / BluePrintArchiveUtils.kt
1 /*
2  * Copyright © 2017-2018 AT&T Intellectual Property.
3  * Modifications Copyright © 2019 Bell Canada.
4  * Modifications Copyright © 2019 Nordix Foundation.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18
19 package org.onap.ccsdk.cds.controllerblueprints.core.utils
20
21 import com.google.common.base.Predicates
22 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintProcessorException
23 import org.slf4j.LoggerFactory
24 import java.io.BufferedInputStream
25 import java.io.ByteArrayInputStream
26 import java.io.ByteArrayOutputStream
27 import java.io.Closeable
28 import java.io.File
29 import java.io.FileOutputStream
30 import java.io.InputStream
31 import java.io.InputStreamReader
32 import java.io.IOException
33 import java.io.OutputStream
34 import java.nio.file.FileVisitResult
35 import java.nio.file.Files
36 import java.nio.file.Path
37 import java.nio.file.SimpleFileVisitor
38 import java.nio.file.attribute.BasicFileAttributes
39 import java.util.function.Predicate
40 import org.apache.commons.compress.archivers.ArchiveEntry
41 import org.apache.commons.compress.archivers.ArchiveInputStream
42 import org.apache.commons.compress.archivers.ArchiveOutputStream
43 import org.apache.commons.compress.archivers.tar.TarArchiveEntry
44 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
45 import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
46 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
47 import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
48 import org.apache.commons.compress.archivers.zip.ZipFile
49 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
50 import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
51 import java.util.Enumeration
52 import java.util.zip.Deflater
53
54 enum class ArchiveType {
55     TarGz,
56     Zip
57 }
58
59 class BluePrintArchiveUtils {
60
61     companion object {
62         private val log = LoggerFactory.getLogger(BluePrintArchiveUtils::class.java)
63
64         /**
65          * Create a new Zip from a root directory
66          *
67          * @param source the base directory
68          * @param destination the output filename
69          * @return True if OK
70          */
71         fun compress(source: File, destination: File, archiveType: ArchiveType = ArchiveType.Zip): Boolean {
72             try {
73                 if (!destination.parentFile.exists()) {
74                     destination.parentFile.mkdirs()
75                 }
76                 destination.createNewFile()
77                 val ignoreZipFiles = Predicate<Path> { path -> !path.endsWith(".zip") && !path.endsWith(".ZIP") }
78                 FileOutputStream(destination).use { out ->
79                     compressFolder(source.toPath(), out, archiveType, pathFilter = ignoreZipFiles)
80                 }
81             } catch (e: Exception) {
82                 log.error("Fail to compress folder($source) to path(${destination.path})", e)
83                 return false
84             }
85             return true
86         }
87
88         /**
89          * In-memory compress an entire folder.
90          */
91         fun compressToBytes(
92             baseDir: Path,
93             archiveType: ArchiveType = ArchiveType.Zip,
94             compressionLevel: Int = Deflater.NO_COMPRESSION
95         ): ByteArray {
96             return compressFolder(baseDir, ByteArrayOutputStream(), archiveType, compressionLevel = compressionLevel)
97                 .toByteArray()
98         }
99
100         /**
101          * Compress an entire folder.
102          *
103          * @param baseDir path of base folder to be packaged.
104          * @param output the output stream
105          * @param pathFilter filter to ignore files based on its path.
106          * @param compressionLevel the wanted compression level.
107          * @param fixedModificationTime to force every entry to have this modification time.
108          * Useful for reproducible operations, like tests, for example.
109          */
110         private fun <T> compressFolder(
111             baseDir: Path,
112             output: T,
113             archiveType: ArchiveType,
114             pathFilter: Predicate<Path> = Predicates.alwaysTrue(),
115             compressionLevel: Int = Deflater.DEFAULT_COMPRESSION,
116             fixedModificationTime: Long? = null
117         ): T
118                 where T : OutputStream {
119             val stream: ArchiveOutputStream = if (archiveType == ArchiveType.Zip)
120                 ZipArchiveOutputStream(output).apply { setLevel(compressionLevel) }
121             else
122                 TarArchiveOutputStream(GzipCompressorOutputStream(output))
123             stream
124                 .use { aos ->
125                     Files.walkFileTree(baseDir, object : SimpleFileVisitor<Path>() {
126                         @Throws(IOException::class)
127                         override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
128                             if (pathFilter.test(file)) {
129                                 var archiveEntry: ArchiveEntry = aos.createArchiveEntry(file.toFile(),
130                                         baseDir.relativize(file).toString())
131                                 if (archiveType == ArchiveType.Zip) {
132                                     val entry = archiveEntry as ZipArchiveEntry
133                                     fixedModificationTime?.let {
134                                         entry.time = it
135                                     }
136                                     entry.time = 0
137                                 }
138                                 aos.putArchiveEntry(archiveEntry)
139                                 Files.copy(file, aos)
140                                 aos.closeArchiveEntry()
141                             }
142                             return FileVisitResult.CONTINUE
143                         }
144
145                         @Throws(IOException::class)
146                         override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult {
147                             var archiveEntry: ArchiveEntry?
148                             if (archiveType == ArchiveType.Zip) {
149                                 val entry = ZipArchiveEntry(baseDir.relativize(dir).toString() + "/")
150                                 fixedModificationTime?.let {
151                                     entry.time = it
152                                 }
153                                 archiveEntry = entry
154                             } else
155                                 archiveEntry = TarArchiveEntry(baseDir.relativize(dir).toString() + "/")
156                             aos.putArchiveEntry(archiveEntry)
157                             aos.closeArchiveEntry()
158                             return FileVisitResult.CONTINUE
159                         }
160                     })
161                 }
162             return output
163         }
164
165         private fun getDefaultEncoding(): String? {
166             val bytes = byteArrayOf('D'.toByte())
167             val inputStream: InputStream = ByteArrayInputStream(bytes)
168             val reader = InputStreamReader(inputStream)
169             return reader.encoding
170         }
171
172         fun deCompress(archiveFile: File, targetPath: String, archiveType: ArchiveType = ArchiveType.Zip): File {
173             var enumeration: ArchiveEnumerator? = null
174             if (archiveType == ArchiveType.Zip) {
175                 val zipArchive = ZipFile(archiveFile, getDefaultEncoding())
176                 enumeration = ArchiveEnumerator(zipArchive)
177             } else { // Tar Gz
178                 var tarGzArchiveIs: InputStream = BufferedInputStream(archiveFile.inputStream())
179                 tarGzArchiveIs = GzipCompressorInputStream(tarGzArchiveIs)
180                 val tarGzArchive: ArchiveInputStream = TarArchiveInputStream(tarGzArchiveIs)
181                 enumeration = ArchiveEnumerator(tarGzArchive)
182             }
183
184             enumeration.use {
185                 while (enumeration!!.hasMoreElements()) {
186                     val entry: ArchiveEntry? = enumeration.nextElement()
187                     val destFilePath = File(targetPath, entry!!.name)
188                     destFilePath.parentFile.mkdirs()
189
190                     if (entry!!.isDirectory)
191                         continue
192
193                     val bufferedIs = BufferedInputStream(enumeration.getInputStream(entry))
194                     destFilePath.outputStream().buffered(1024).use { bos ->
195                         bufferedIs.copyTo(bos)
196                     }
197
198                     if (!enumeration.getHasSharedEntryInputStream())
199                         bufferedIs.close()
200                 }
201             }
202
203             val destinationDir = File(targetPath)
204             check(destinationDir.isDirectory && destinationDir.exists()) {
205                 throw BluePrintProcessorException("failed to decompress blueprint(${archiveFile.absolutePath}) to ($targetPath) ")
206             }
207
208             return destinationDir
209         }
210     }
211
212     class ArchiveEnumerator : Enumeration<ArchiveEntry>, Closeable {
213         private val zipArchive: ZipFile?
214         private val zipEnumeration: Enumeration<ZipArchiveEntry>?
215         private val archiveStream: ArchiveInputStream?
216         private var nextEntry: ArchiveEntry? = null
217         private val hasSharedEntryInputStream: Boolean
218
219         constructor(zipFile: ZipFile) {
220             zipArchive = zipFile
221             zipEnumeration = zipFile.entries
222             archiveStream = null
223             hasSharedEntryInputStream = false
224         }
225
226         constructor(archiveStream: ArchiveInputStream) {
227             this.archiveStream = archiveStream
228             zipArchive = null
229             zipEnumeration = null
230             hasSharedEntryInputStream = true
231         }
232
233         fun getHasSharedEntryInputStream(): Boolean {
234             return hasSharedEntryInputStream
235         }
236
237         fun getInputStream(entry: ArchiveEntry): InputStream? {
238             return if (zipArchive != null)
239                 zipArchive?.getInputStream(entry as ZipArchiveEntry?)
240             else
241                 archiveStream
242         }
243
244         override fun hasMoreElements(): Boolean {
245             if (zipEnumeration != null)
246                 return zipEnumeration?.hasMoreElements()
247             else if (archiveStream != null) {
248                 nextEntry = archiveStream.nextEntry
249                 if (nextEntry != null && !archiveStream.canReadEntryData(nextEntry))
250                     return hasMoreElements()
251                 return nextEntry != null
252             }
253             return false
254         }
255
256         override fun nextElement(): ArchiveEntry? {
257             if (zipEnumeration != null)
258                 nextEntry = zipEnumeration.nextElement()
259             else if (archiveStream != null) {
260                 if (nextEntry == null)
261                     nextEntry = archiveStream.nextEntry
262             }
263             return nextEntry
264         }
265
266         override fun close() {
267             if (zipArchive != null)
268                 zipArchive.close()
269             else archiveStream?.close()
270         }
271     }
272 }