PageRenderTime 27ms CodeModel.GetById 0ms RepoModel.GetById 0ms app.codeStats 0ms

/amps-maven-plugin/src/main/java/com/atlassian/maven/plugins/amps/util/ZipUtils.java

https://bitbucket.org/atlassian/amps
Java | 436 lines | 262 code | 51 blank | 123 comment | 49 complexity | 24a1762e8f8e7a27ca5c205cbe966b3f MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. package com.atlassian.maven.plugins.amps.util;
  2. import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
  3. import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
  4. import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
  5. import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
  6. import org.apache.commons.compress.archivers.zip.ZipFile;
  7. import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
  8. import org.apache.commons.lang3.StringUtils;
  9. import org.apache.commons.lang3.SystemUtils;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import java.io.File;
  13. import java.io.FileInputStream;
  14. import java.io.FileOutputStream;
  15. import java.io.IOException;
  16. import java.io.InputStream;
  17. import java.io.OutputStream;
  18. import java.nio.file.Files;
  19. import java.nio.file.LinkOption;
  20. import java.nio.file.Path;
  21. import java.nio.file.attribute.BasicFileAttributes;
  22. import java.util.Enumeration;
  23. import java.util.List;
  24. import java.util.regex.Pattern;
  25. import java.util.stream.Collectors;
  26. import java.util.stream.Stream;
  27. import java.util.zip.ZipEntry;
  28. import static com.atlassian.maven.plugins.amps.util.FileUtils.makeExecutable;
  29. import static com.atlassian.maven.plugins.amps.util.FileUtils.setLastModified;
  30. import static org.apache.commons.io.IOUtils.copy;
  31. /**
  32. * Static utility methods relating to zip files.
  33. */
  34. public final class ZipUtils {
  35. private static final Logger LOGGER = LoggerFactory.getLogger(ZipUtils.class);
  36. /**
  37. * A mask to test against to determine whether the executable bit is set in a file's mode.
  38. * <p>
  39. * Note that this value needs to be specified in octal (leading 0), not decimal, to set the correct bits.
  40. */
  41. @SuppressWarnings("OctalInteger")
  42. private static final int MASK_EXECUTABLE = 0100;
  43. /**
  44. * The mode to use when marking a file executable when creating a zipfile.
  45. * <p>
  46. * Note that this value needs to be specified in octal (leading 0), not decimal, to set the correct bits.
  47. */
  48. @SuppressWarnings("OctalInteger")
  49. private static final int MODE_EXECUTABLE = 0700;
  50. /**
  51. * Ungzips and extracts the specified tar.gz file into the specified directory, trimming
  52. * the specified number of leading path segments from the extraction path.
  53. *
  54. * @param targz the tar.gz file to use
  55. * @param destDir the directory to contain the extracted contents
  56. * @param leadingPathSegmentsToTrim the number of leading path segments to remove from the
  57. * extracted path
  58. * @throws IOException if the archive cannot be expanded
  59. */
  60. public static void untargz(File targz, String destDir, int leadingPathSegmentsToTrim) throws IOException {
  61. try (final FileInputStream fin = new FileInputStream(targz);
  62. final GzipCompressorInputStream gzIn = new GzipCompressorInputStream(fin);
  63. final TarArchiveInputStream tarIn = new TarArchiveInputStream(gzIn)) {
  64. for (TarArchiveEntry entry = tarIn.getNextTarEntry(); entry != null; entry = tarIn.getNextTarEntry()) {
  65. final File entryFile = new File(destDir + File.separator +
  66. trimPathSegments(entry.getName(), leadingPathSegmentsToTrim));
  67. if (entry.isDirectory()) {
  68. makeDirectories(entryFile);
  69. } else {
  70. copyFile(tarIn, entry, entryFile);
  71. }
  72. }
  73. }
  74. }
  75. private static void copyFile(final TarArchiveInputStream tarIn, final TarArchiveEntry entry, final File entryFile)
  76. throws IOException {
  77. if (!entryFile.getParentFile().exists()) {
  78. makeDirectories(entryFile.getParentFile());
  79. }
  80. try (final FileOutputStream fos = new FileOutputStream(entryFile)) {
  81. copy(tarIn, fos);
  82. // check for user-executable bit on entry and apply to file
  83. if (isExecutable(entry)) {
  84. makeExecutable(entryFile);
  85. }
  86. }
  87. }
  88. private static void makeDirectories(final File file) {
  89. if (!file.mkdirs()) {
  90. LOGGER.debug("Did not create directories for {}", file);
  91. }
  92. }
  93. public static void unzip(File zipFile, String destDir) throws IOException {
  94. unzip(zipFile, destDir, 0);
  95. }
  96. /**
  97. * Unzips a file
  98. *
  99. * @param zipFile the Zip file
  100. * @param destDir the destination folder
  101. * @param leadingPathSegmentsToTrim number of root folders to skip. Example: If all files are in generated-resources/home/*,
  102. * then you may want to skip 2 folders.
  103. * @throws IOException if the archive cannot be expanded
  104. */
  105. public static void unzip(File zipFile, String destDir, int leadingPathSegmentsToTrim) throws IOException {
  106. unzip(zipFile, destDir, leadingPathSegmentsToTrim, false, null);
  107. }
  108. /**
  109. * Unzips a file
  110. *
  111. * @param zipFile the Zip file
  112. * @param destDir the destination folder
  113. * @param leadingPathSegmentsToTrim number of root folders to skip. Example: If all files are in generated-resources/home/*,
  114. * then you may want to skip 2 folders.
  115. * @param flatten if true all files from zip are extracted directly to destDir without keeping the subdirectories
  116. * structure from the zip file
  117. * @param pattern pattern that must be meet by zip entry to be extracted
  118. * @throws IOException if the archive cannot be expanded
  119. */
  120. public static void unzip(final File zipFile, final String destDir, final int leadingPathSegmentsToTrim,
  121. final boolean flatten, final Pattern pattern) throws IOException {
  122. try (final ZipFile zip = new ZipFile(zipFile)) {
  123. final Enumeration<? extends ZipArchiveEntry> entries = zip.getEntries();
  124. while (entries.hasMoreElements()) {
  125. final ZipArchiveEntry zipEntry = entries.nextElement();
  126. if (matches(zipEntry, pattern)) {
  127. final File file = getFile(destDir, leadingPathSegmentsToTrim, flatten, zipEntry);
  128. if (zipEntry.isDirectory()) {
  129. makeDirectories(file);
  130. } else {
  131. copyFile(zip, zipEntry, file);
  132. }
  133. }
  134. }
  135. }
  136. }
  137. private static boolean matches(final ZipArchiveEntry zipEntry, final Pattern pattern) {
  138. return pattern == null || pattern.matcher(zipEntry.getName()).matches();
  139. }
  140. /**
  141. * Copies the given entry from the given ZIP file to the given file, preserving the executable flag and the "last
  142. * modified" time.
  143. *
  144. * @param zip the ZIP file
  145. * @param zipEntry the entry to be copied
  146. * @param file the destination file
  147. * @throws IOException if any I/O fails
  148. */
  149. private static void copyFile(final ZipFile zip, final ZipArchiveEntry zipEntry, final File file) throws IOException {
  150. // make sure our parent exists in case zipentries are out of order
  151. if (!file.getParentFile().exists()) {
  152. makeDirectories(file.getParentFile());
  153. }
  154. try (final InputStream is = zip.getInputStream(zipEntry);
  155. final OutputStream fos = new FileOutputStream(file)) {
  156. copy(is, fos);
  157. if (isExecutable(zipEntry)) {
  158. makeExecutable(file);
  159. }
  160. }
  161. setLastModified(file, zipEntry.getTime());
  162. }
  163. private static File getFile(
  164. final String destDir, final int leadingPathSegmentsToTrim, final boolean flatten, final ZipEntry zipEntry) {
  165. String zipPath = trimPathSegments(zipEntry.getName(), leadingPathSegmentsToTrim);
  166. if (flatten) {
  167. zipPath = flattenPath(zipPath);
  168. }
  169. return new File(destDir + File.separatorChar + zipPath);
  170. }
  171. private static boolean isExecutable(final TarArchiveEntry tarArchiveEntry) {
  172. return isExecutable(tarArchiveEntry.getMode());
  173. }
  174. private static boolean isExecutable(final ZipArchiveEntry zipArchiveEntry) {
  175. return isExecutable(zipArchiveEntry.getUnixMode());
  176. }
  177. private static boolean isExecutable(final int mode) {
  178. return (mode & MASK_EXECUTABLE) != 0;
  179. }
  180. /**
  181. * Count the number of nested root folders. A root folder is a folder which contains 0 or 1 file or folder.
  182. * <p>
  183. * Example: A zip with only "generated-resources/home/database.log" has 2 root folders.
  184. *
  185. * @param zip the zip file
  186. * @return the number of root folders.
  187. */
  188. public static int countNestingLevel(File zip) throws IOException {
  189. try (ZipFile zipFile = new ZipFile(zip)) {
  190. List<String> filenames = toList(zipFile.getEntries());
  191. return countNestingLevel(filenames);
  192. }
  193. }
  194. /**
  195. * Count the number of nested root directories in the filenames.
  196. * <p>
  197. * A root directory is a directory that has no sibling.
  198. *
  199. * @param filenames the list of filenames, using / as a separator. Must be a mutable copy,
  200. * as it will be modified.
  201. */
  202. static int countNestingLevel(List<String> filenames) {
  203. String prefix = StringUtils.getCommonPrefix(filenames.toArray(new String[0]));
  204. if (!prefix.endsWith("/")) {
  205. prefix = prefix.substring(0, prefix.lastIndexOf('/') + 1);
  206. }
  207. // The first prefix may be wrong, example:
  208. // root/ <- to be discarded
  209. // root/nested/ <- to be discarded
  210. // root/nested/folder1/file.txt <- the root "root/nested/" will be detected properly
  211. // root/nested/folder2/file.txt
  212. if (filenames.remove(prefix)) {
  213. return countNestingLevel(filenames);
  214. }
  215. // The client can't use these filenames anymore.
  216. filenames.clear();
  217. return StringUtils.countMatches(prefix, "/");
  218. }
  219. private static List<String> toList(Enumeration<? extends ZipEntry> entries) {
  220. return StreamUtils.stream(entries)
  221. .map(ZipEntry::getName)
  222. .collect(Collectors.toList());
  223. }
  224. /**
  225. * Recursively zips the <i>children</i> of the specified root directory.
  226. * <p>
  227. * Unlike {@link #zipDir}, this method does not include any prefixes. Files and subdirectories under the specified
  228. * root directory are included <i>without prefixes</i> in the resulting zip.
  229. * <p>
  230. * Consider the following directory structure:
  231. * <code><pre>
  232. * /tmp
  233. * |- /tmp/foo
  234. * |- /tmp/foo/file.txt
  235. * |- /tmp/foo/bar
  236. * |- /tmp/foo/bar/file.txt
  237. * </pre></code>
  238. * Given {@code /tmp/foo} as {@code rootDir}, the resulting zip will contain the following entries:
  239. * <ul>
  240. * <li>{@code file.txt}</li>
  241. * <li>{@code bar/} (directories get explicit entries in the zip file)</li>
  242. * <li>{@code bar/file.txt}</li>
  243. * </ul>
  244. * Using {@code zipDir(zipFile, new File("/tmp/foo"), null)} would include the same files and directories, but
  245. * each entry would start with {@code foo/}. There is no way to disable using <i>some</i> prefix.
  246. *
  247. * @param zipFile the zip file to create
  248. * @param rootDir the root directory, from which all files and subdirectories should be zipped
  249. * @throws IOException if the zip cannot be created
  250. * @since 8.0
  251. */
  252. public static void zipChildren(File zipFile, File rootDir) throws IOException {
  253. Path root = rootDir.toPath();
  254. try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(zipFile);
  255. Stream<Path> children = Files.walk(root).skip(1L)) // Drop the root directory
  256. {
  257. byte[] buffer = new byte[8192]; //ZipArchiveOutputStream internally supports blocks up to 8K
  258. for (Path child : (Iterable<Path>) children::iterator) {
  259. BasicFileAttributes attributes = Files.readAttributes(child, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
  260. if (attributes.isOther() || attributes.isSymbolicLink()) {
  261. // Symbolic links and "other" file types won't zipped (or extracted) faithfully,
  262. // so they are ignored
  263. continue;
  264. }
  265. String path = root.relativize(child).toString();
  266. if (SystemUtils.IS_OS_WINDOWS) {
  267. // Always use forward slashes for the zip's entries, even on Windows
  268. path = path.replace('\\', '/');
  269. }
  270. if (attributes.isDirectory()) {
  271. path += "/";
  272. }
  273. ZipArchiveEntry entry = new ZipArchiveEntry(path);
  274. out.putArchiveEntry(entry);
  275. if (attributes.isRegularFile()) {
  276. if (Files.isExecutable(child)) {
  277. entry.setUnixMode(MODE_EXECUTABLE);
  278. }
  279. try (InputStream content = Files.newInputStream(child)) {
  280. copyViaBuffer(content, out, buffer);
  281. }
  282. }
  283. out.closeArchiveEntry();
  284. }
  285. }
  286. }
  287. /**
  288. * @param prefix the prefix. If empty, uses the srcDir's name. That means you can't create a zip with no
  289. * root folder.
  290. */
  291. public static void zipDir(File zipFile, File srcDir, String prefix) throws IOException {
  292. try (ZipArchiveOutputStream out = new ZipArchiveOutputStream(zipFile)) {
  293. addZipPrefixes(srcDir, out, prefix);
  294. addZipDir(srcDir, out, prefix);
  295. }
  296. }
  297. private static void addZipPrefixes(File dirObj, ZipArchiveOutputStream out, String prefix) throws IOException {
  298. // need to manually add the prefix folders
  299. String entryPrefix = ensurePrefixWithSlash(dirObj, prefix);
  300. String[] prefixes = entryPrefix.split("/");
  301. String lastPrefix = "";
  302. for (String p : prefixes) {
  303. ZipArchiveEntry entry = new ZipArchiveEntry(lastPrefix + p + "/");
  304. out.putArchiveEntry(entry);
  305. out.closeArchiveEntry();
  306. lastPrefix = p + "/";
  307. }
  308. }
  309. private static void addZipDir(File dirObj, ZipArchiveOutputStream out, String prefix) throws IOException {
  310. File[] files = dirObj.listFiles();
  311. if (files == null || files.length == 0) {
  312. return;
  313. }
  314. byte[] tmpBuf = new byte[8192]; //ZipArchiveOutputStream internally supports blocks up to 8K
  315. String entryPrefix = ensurePrefixWithSlash(dirObj, prefix);
  316. for (File currentFile : files) {
  317. if (currentFile.isDirectory()) {
  318. String entryName = entryPrefix + currentFile.getName() + "/";
  319. // need to manually add folders so entries are in order
  320. ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
  321. out.putArchiveEntry(entry);
  322. out.closeArchiveEntry();
  323. // add the files in the folder
  324. addZipDir(currentFile, out, entryName);
  325. } else if (currentFile.isFile()) {
  326. String entryName = entryPrefix + currentFile.getName();
  327. try (FileInputStream in = new FileInputStream(currentFile.getAbsolutePath())) {
  328. ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
  329. out.putArchiveEntry(entry);
  330. if (currentFile.canExecute()) {
  331. entry.setUnixMode(MODE_EXECUTABLE);
  332. }
  333. // Transfer from the file to the ZIP file
  334. copyViaBuffer(in, out, tmpBuf);
  335. // Complete the entry
  336. out.closeArchiveEntry();
  337. }
  338. }
  339. }
  340. }
  341. private static void copyViaBuffer(InputStream inputStream, OutputStream outputStream, byte[] buffer)
  342. throws IOException {
  343. int read;
  344. while ((read = inputStream.read(buffer)) != -1) {
  345. outputStream.write(buffer, 0, read);
  346. }
  347. }
  348. /**
  349. * Make sure 'prefix' is in format 'entry/' or, by default, 'rootDir/'
  350. * (not '', '/', '/entry', or 'entry').
  351. */
  352. private static String ensurePrefixWithSlash(File rootDir, String prefix) {
  353. String entryPrefix = prefix;
  354. if (StringUtils.isNotBlank(entryPrefix) && !entryPrefix.equals("/")) {
  355. // strip leading '/'
  356. if (entryPrefix.charAt(0) == '/') {
  357. entryPrefix = entryPrefix.substring(1);
  358. }
  359. // ensure trailing '/'
  360. if (entryPrefix.charAt(entryPrefix.length() - 1) != '/') {
  361. entryPrefix = entryPrefix + "/";
  362. }
  363. } else {
  364. entryPrefix = rootDir.getName() + "/";
  365. }
  366. return entryPrefix;
  367. }
  368. private static String trimPathSegments(String zipPath, int trimLeadingPathSegments) {
  369. int startIndex = 0;
  370. for (int i = 0; i < trimLeadingPathSegments; i++) {
  371. int nextSlash = zipPath.indexOf('/', startIndex);
  372. if (nextSlash == -1) {
  373. break;
  374. } else {
  375. startIndex = nextSlash + 1;
  376. }
  377. }
  378. return zipPath.substring(startIndex);
  379. }
  380. private static String flattenPath(String zipPath) {
  381. return zipPath.substring(Math.max(zipPath.lastIndexOf('/'), 0));
  382. }
  383. private ZipUtils() {
  384. }
  385. }