001/* 002 * Copyright 2020 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.contentio.archive; 017 018import java.io.FilterInputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.nio.file.DirectoryStream; 022import java.nio.file.FileSystem; 023import java.nio.file.FileSystems; 024import java.nio.file.Files; 025import java.nio.file.Path; 026import java.nio.file.attribute.BasicFileAttributes; 027import java.util.Optional; 028import java.util.function.BiPredicate; 029import java.util.stream.Stream; 030import java.util.stream.StreamSupport; 031import java.util.zip.ZipEntry; 032import java.util.zip.ZipFile; 033 034import org.apache.commons.lang.StringUtils; 035 036/** 037 * Utility class to retrieve {@link Path} representing folders or files under a ZIP archive. 038 */ 039public final class ZipEntryHelper 040{ 041 private ZipEntryHelper() 042 { /*empty*/ } 043 044 private static FileSystem _zipFileSystem(Path zipArchivePath) throws IOException 045 { 046 return FileSystems.newFileSystem(zipArchivePath, ClassLoader.getSystemClassLoader()); 047 } 048 049 /** 050 * Return the {@link Path} of the root of the given ZIP archive. 051 * @param zipArchivePath The zip path 052 * @return the PAth of the root 053 * @throws IOException if an I/O error is thrown when accessing a file. 054 */ 055 public static Path zipFileRoot(Path zipArchivePath) throws IOException 056 { 057 FileSystem zipFileSystem = _zipFileSystem(zipArchivePath); 058 Path[] roots = StreamSupport.stream(zipFileSystem.getRootDirectories().spliterator(), false) 059 .toArray(Path[]::new); 060 if (roots.length != 1) 061 { 062 throw new IllegalStateException(String.format("Unexpected error, the zip file '%s' should have 1 and only 1 root directory", zipArchivePath)); 063 } 064 065 Path zipFileRoot = roots[0]; 066 return zipFileRoot; 067 } 068 069 /** 070 * Returns a Stream that is lazily populated with Path by searching for files in a file tree rooted at a given starting path within the ZIP, or the ZIP root. 071 * @param zipArchivePath The zip path 072 * @param startingPath The (optional) starting path within the ZIP 073 * @param filter The filter 074 * @return the {@link Stream} of {@link Path} 075 * @throws IOException if an I/O error is thrown when accessing a file. 076 */ 077 public static Stream<Path> zipFileTree(Path zipArchivePath, Optional<String> startingPath, BiPredicate<Path, BasicFileAttributes> filter) throws IOException 078 { 079 return _zipFileTree(zipArchivePath, startingPath, Integer.MAX_VALUE, filter); 080 } 081 082 private static Stream<Path> _zipFileTree(Path zipArchivePath, Optional<String> startingPath, int maxDepth, BiPredicate<Path, BasicFileAttributes> filter) throws IOException 083 { 084 Path zipFileRoot = zipFileRoot(zipArchivePath); 085 Path start = startingPath 086 .map(zipFileRoot::resolve) 087 .orElse(zipFileRoot); 088 return Files.find(start, maxDepth, filter); 089 } 090 091 /** 092 * Opens a directory, returning a {@link DirectoryStream} to iterate over the entries in the directory. 093 * @param zipArchivePath The zip path 094 * @param startingPath The (optional) starting path within the ZIP 095 * @param filter The filter 096 * @return a new and opened {@link DirectoryStream} object 097 * @throws IOException if an I/O error is thrown when accessing a file. 098 */ 099 public static DirectoryStream<Path> children(Path zipArchivePath, Optional<String> startingPath, DirectoryStream.Filter<? super Path> filter) throws IOException 100 { 101 Path zipFileRoot = zipFileRoot(zipArchivePath); 102 Path start = startingPath 103 .map(zipFileRoot::resolve) 104 .orElse(zipFileRoot); 105 return Files.newDirectoryStream(start, filter); 106 } 107 108 /** 109 * Checks if the given folder entry exists in the ZIP archive. 110 * @param zipPath The zip path 111 * @param zipEntryFolderPath The path of the folder within the ZIP 112 * @return <code>true</code> if the entry exists 113 * @throws IOException if an I/O error is thrown when accessing a file. 114 */ 115 public static boolean zipEntryFolderExists(Path zipPath, String zipEntryFolderPath) throws IOException 116 { 117 Path zipEntryPath = _zipFileSystem(zipPath) 118 .getPath(zipEntryFolderPath); 119 return Files.exists(zipEntryPath) && Files.isDirectory(zipEntryPath); 120 } 121 122 /** 123 * Checks if the given file entry exists in the ZIP archive. 124 * @param zipPath The zip path 125 * @param zipEntryFilePath The path of the file within the ZIP 126 * @return <code>true</code> if the entry exists 127 * @throws IOException if an I/O error is thrown when accessing a file. 128 */ 129 public static boolean zipEntryFileExists(Path zipPath, String zipEntryFilePath) throws IOException 130 { 131 Path zipEntryPath = _zipFileSystem(zipPath) 132 .getPath(zipEntryFilePath); 133 return Files.exists(zipEntryPath) && Files.isRegularFile(zipEntryPath); 134 } 135 136 /** 137 * Returns an input stream for reading the content of an entry file of a ZIP archive. 138 * <br>You may check if the entry exist with {@link #zipEntryFileExists}, otherwise you may get an {@link IOException}. 139 * <br>It must be closed. 140 * @param zipPath The zip path 141 * @param zipEntryFilePath The path within the ZIP 142 * @return a new input stream 143 * @throws IOException if an I/O error is thrown when accessing a file. 144 */ 145 public static InputStream zipEntryFileInputStream(Path zipPath, String zipEntryFilePath) throws IOException 146 { 147 var zipFile = new ZipFile(zipPath.toFile()); 148 String name = StringUtils.stripStart(zipEntryFilePath, "/"); 149 ZipEntry zipEntry = zipFile.getEntry(name); 150 if (zipEntry == null) 151 { 152 throw new IOException(String.format("Wrong zip entry file path '%s', it cannot be found in the archive '%s'", zipEntryFilePath, zipPath)); 153 } 154 155 InputStream manifestIs = zipFile.getInputStream(zipEntry); 156 return new FilterInputStream(manifestIs) 157 { 158 // according to java.util.zip.ZipFile.getInputStream(ZipEntry), 159 // "Closing this ZIP file will, in turn, close all input streams that have been returned by invocations of this method" 160 // thus we need to override the close method of the returned InputStream in order to close the zip file 161 @Override 162 public void close() throws IOException 163 { 164 super.close(); 165 zipFile.close(); 166 } 167 }; 168 } 169} 170