001package co.codewizards.cloudstore.core.util; 002 003import static co.codewizards.cloudstore.core.util.IOUtil.*; 004 005import java.io.BufferedInputStream; 006import java.io.BufferedOutputStream; 007import java.io.File; 008import java.io.FileInputStream; 009import java.io.FileOutputStream; 010import java.io.IOException; 011import java.io.InputStream; 012import java.io.OutputStream; 013import java.net.URL; 014import java.util.Properties; 015import java.util.zip.ZipEntry; 016import java.util.zip.ZipInputStream; 017import java.util.zip.ZipOutputStream; 018 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022import co.codewizards.cloudstore.core.progress.ProgressMonitor; 023import co.codewizards.cloudstore.core.progress.SubProgressMonitor; 024 025public final class ZipUtil { 026 private static final Logger logger = LoggerFactory.getLogger(ZipUtil.class); 027 028 private ZipUtil() { } 029 030 /** 031 * Recursively zips all entries of the given zipInputFolder to 032 * a zipFile defined by zipOutputFile. 033 * 034 * @param zipOutputFile The file to write to (will be deleted if existent). 035 * @param zipInputFolder The inputFolder to zip. 036 * @throws IOException in case of an I/O error. 037 */ 038 public static void zipFolder(File zipOutputFile, File zipInputFolder) 039 throws IOException 040 { 041 zipFolder(zipOutputFile, zipInputFolder, (ProgressMonitor) null); 042 } 043 044 /** 045 * Recursively zips all entries of the given zipInputFolder to 046 * a zipFile defined by zipOutputFile. 047 * 048 * @param zipOutputFile The file to write to (will be deleted if existent). 049 * @param zipInputFolder The inputFolder to zip. 050 * @param monitor an optional monitor for progress feedback (can be <code>null</code>). 051 * @throws IOException in case of an I/O error. 052 */ 053 public static void zipFolder(File zipOutputFile, File zipInputFolder, ProgressMonitor monitor) 054 throws IOException 055 { 056 zipFilesRecursively(zipOutputFile, zipInputFolder.listFiles(), zipInputFolder.getAbsoluteFile(), monitor); 057 } 058 059 /** 060 * Recursively zips all given files to a zipFile defined by zipOutputFile. 061 * 062 * @param zipOutputFile The file to write to (will be deleted if existent). 063 * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>, 064 * if <code>entryRoot</code> is <code>null</code>. 065 * @param entryRoot The root folder of all entries. Entries in subfolders will be 066 * added relative to this. If <code>entryRoot==null</code>, all given files will be 067 * added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not 068 * both be <code>null</code> at the same time. 069 * @throws IOException in case of an I/O error. 070 */ 071 public static void zipFilesRecursively(File zipOutputFile, File[] files, File entryRoot) 072 throws IOException 073 { 074 zipFilesRecursively(zipOutputFile, files, entryRoot, (ProgressMonitor) null); 075 } 076 077 /** 078 * Recursively zips all given files to a zipFile defined by zipOutputFile. 079 * 080 * @param zipOutputFile The file to write to (will be deleted if existent). 081 * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>, 082 * if <code>entryRoot</code> is <code>null</code>. 083 * @param entryRoot The root folder of all entries. Entries in subfolders will be 084 * added relative to this. If <code>entryRoot==null</code>, all given files will be 085 * added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not 086 * both be <code>null</code> at the same time. 087 * @param monitor an optional monitor for progress feedback (can be <code>null</code>). 088 * @throws IOException in case of an I/O error. 089 */ 090 public static void zipFilesRecursively(File zipOutputFile, File[] files, File entryRoot, ProgressMonitor monitor) 091 throws IOException 092 { 093 FileOutputStream fout = new FileOutputStream(zipOutputFile); 094 ZipOutputStream out = new ZipOutputStream(fout); 095 try { 096 zipFilesRecursively(out, zipOutputFile, files, entryRoot, monitor); 097 } finally { 098 out.close(); 099 } 100 } 101 102 /** 103 * Recursively writes all found files as entries into the given ZipOutputStream. 104 * 105 * @param out The ZipOutputStream to write to. 106 * @param zipOutputFile the output zipFile. optional. if it is null, this method cannot check whether 107 * your current output file is located within the zipped directory tree. You must not locate 108 * your zip-output file within the source directory, if you leave this <code>null</code>. 109 * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>, 110 * if <code>entryRoot</code> is <code>null</code>. 111 * @param entryRoot The root folder of all entries. Entries in subfolders will be 112 * added relative to this. If <code>entryRoot==null</code>, all given files will be 113 * added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not 114 * both be <code>null</code> at the same time. 115 * @throws IOException in case of an I/O error. 116 */ 117 public static void zipFilesRecursively(ZipOutputStream out, File zipOutputFile, File[] files, File entryRoot) 118 throws IOException 119 { 120 zipFilesRecursively(out, zipOutputFile, files, entryRoot, (ProgressMonitor) null); 121 } 122 123 /** 124 * Recursively writes all found files as entries into the given ZipOutputStream. 125 * 126 * @param out The ZipOutputStream to write to. 127 * @param zipOutputFile the output zipFile. optional. if it is null, this method cannot check whether 128 * your current output file is located within the zipped directory tree. You must not locate 129 * your zip-output file within the source directory, if you leave this <code>null</code>. 130 * @param files The files to zip (optional, defaults to all files recursively). It must not be <code>null</code>, 131 * if <code>entryRoot</code> is <code>null</code>. 132 * @param entryRoot The root folder of all entries. Entries in subfolders will be 133 * added relative to this. If <code>entryRoot==null</code>, all given files will be 134 * added without any path (directly into the zip's root). <code>entryRoot</code> and <code>files</code> must not 135 * both be <code>null</code> at the same time. 136 * @param monitor an optional monitor for progress feedback (can be <code>null</code>). 137 * @throws IOException in case of an I/O error. 138 */ 139 public static void zipFilesRecursively(ZipOutputStream out, File zipOutputFile, File[] files, File entryRoot, ProgressMonitor monitor) 140 throws IOException 141 { 142 if (entryRoot == null && files == null) 143 throw new IllegalArgumentException("entryRoot and files must not both be null!"); 144 145 if (entryRoot != null && !entryRoot.isDirectory()) 146 throw new IllegalArgumentException("entryRoot is not a directory: "+entryRoot.getAbsolutePath()); 147 148 if ( files == null ) { 149 files = new File[] { entryRoot }; 150 } 151 152 if (monitor != null) { 153 int dirCount = 0; 154 int fileCount = 0; 155 for (File file : files) { 156 if (file.isDirectory()) 157 ++dirCount; 158 else 159 ++fileCount; 160 } 161 162 monitor.beginTask("Zipping files", dirCount * 10 + fileCount); 163 } 164 try { 165 byte[] buf = new byte[1024 * 5]; 166 for (File file : files) { 167 if (zipOutputFile != null && file.equals(zipOutputFile)) { 168 if (monitor != null) 169 monitor.worked(1); 170 171 continue; 172 } 173 174 String relativePath = entryRoot == null ? file.getName() : getRelativePath(entryRoot, file.getAbsoluteFile()); 175 // The method ZipEntry.isDirectory checks for 'name.endsWith("/");' and thus seems not to take 176 // File.separator into account. Furthermore, I browsed the web for source codes (both implementation and 177 // usage of ZipFile/ZipEntry and it seems to always use '/' - even in Windows. 178 // Thus, I assume that all backslashes should be converted to slashes here. Marco. 179 relativePath = relativePath.replace('\\', '/'); 180 if ( file.isDirectory() ) { 181 // store directory (necessary, in case the directory is empty - otherwise it's lost) 182 relativePath += '/'; 183 ZipEntry entry = new ZipEntry(relativePath); 184 entry.setTime(file.lastModified()); 185 entry.setSize(0); 186 entry.setCompressedSize(0); 187 entry.setCrc(0); 188 entry.setMethod(ZipEntry.STORED); 189 out.putNextEntry(entry); 190 out.closeEntry(); 191 192 // recurse 193 File[] dirFiles = file.listFiles(); 194 if (dirFiles == null) { 195 logger.error("zipFilesRecursively: file.listFiles() returned null, even though file is a directory! file=\"{}\"", file.getAbsolutePath()); 196 if (monitor != null) 197 monitor.worked(10); 198 } 199 else { 200 zipFilesRecursively( 201 out, 202 zipOutputFile, 203 dirFiles, 204 entryRoot, 205 monitor == null ? null : new SubProgressMonitor(monitor, 10) 206 ); 207 } 208 } 209 else { 210 // Create a new zipEntry 211 BufferedInputStream in = new BufferedInputStream( new FileInputStream(file) ); 212 ZipEntry entry = new ZipEntry(relativePath); 213 entry.setTime(file.lastModified()); 214 out.putNextEntry(entry); 215 216 int len; 217 while ((len = in.read(buf)) > 0) { 218 out.write(buf, 0, len); 219 } 220 221 out.closeEntry(); 222 in.close(); 223 224 if (monitor != null) 225 monitor.worked(1); 226 } 227 } // end of for ( int i = 0; i < files.length; i++ ) 228 } finally { 229 if (monitor != null) 230 monitor.done(); 231 } 232 } 233 234 private static final String PROPERTY_KEY_ZIP_TIMESTAMP = "zip.timestamp"; 235 private static final String PROPERTY_KEY_ZIP_FILESIZE = "zip.size"; 236 237 /** 238 * Calls {@link #unzipArchiveIfModified(URL, File)} converting the File-parameter zipArchive to an url. 239 * 240 * @see #unzipArchiveIfModified(URL, File). 241 */ 242 public static synchronized void unzipArchiveIfModified(File zipArchive, File unzipRootFolder) 243 throws IOException 244 { 245 unzipArchive(zipArchive.toURI().toURL(), unzipRootFolder); 246 } 247 248 /** 249 * Unzip the given archive into the given folder, if the archive was modified 250 * after being unzipped the last time by this method. 251 * <p> 252 * The current implementation 253 * of this method creates a file named ".archive.properties" inside the 254 * <code>unzipRootFolder</code> and stores the <code>zipArchive</code>'s file size and 255 * last-modified-timestamp to decide whether a future call to this method needs 256 * to unzip the data again. 257 * </p> 258 * <p> 259 * Note, that this method deletes the <code>unzipRootFolder</code> prior to unzipping 260 * in order to guarantee that content which was removed from the <code>zipArchive</code> is not existing 261 * in the <code>unzipRootFolder</code> anymore, too. 262 * </p> 263 * TODO instead of being synchronized, this method should use lower (= operating-system) locking mechanisms. Marco. 264 * 265 * @param zipArchive The zip file to unzip. 266 * @param unzipRootFolder The folder to unzip to. 267 * @throws IOException in case of an I/O error. 268 */ 269 public static synchronized void unzipArchiveIfModified(URL zipArchive, File unzipRootFolder) 270 throws IOException 271 { 272 File metaFile = new File(unzipRootFolder, ".archive.properties"); 273 long timestamp = Long.MIN_VALUE; 274 long fileSize = Long.MIN_VALUE; 275 276 Properties properties = new Properties(); 277 if (metaFile.exists()) { 278 InputStream in = new FileInputStream(metaFile); 279 try { 280 properties.load(in); 281 } finally { 282 in.close(); 283 } 284 285 String timestampS = properties.getProperty(PROPERTY_KEY_ZIP_TIMESTAMP); 286 if (timestampS != null) { 287 try { 288 timestamp = Long.parseLong(timestampS, 36); 289 } catch (NumberFormatException x) { 290 // ignore 291 } 292 } 293 294 String fileSizeS = properties.getProperty(PROPERTY_KEY_ZIP_FILESIZE); 295 if (fileSizeS != null) { 296 try { 297 fileSize = Long.parseLong(fileSizeS, 36); 298 } catch (NumberFormatException x) { 299 // ignore 300 } 301 } 302 } 303 304 boolean doUnzip = true; 305 long zipLength = -1; 306 long zipLastModified = System.currentTimeMillis(); 307 308 if ("file".equals(zipArchive.getProtocol())) { 309 File fileToCheck = new File(Util.urlToUri(zipArchive)); 310 zipLastModified = fileToCheck.lastModified(); 311 zipLength = fileToCheck.length(); 312 doUnzip = !unzipRootFolder.exists() || zipLastModified != timestamp || zipLength != fileSize; 313 } 314 315 if (doUnzip) { 316 deleteDirectoryRecursively(unzipRootFolder); 317 unzipArchive(zipArchive, unzipRootFolder); 318 properties.setProperty(PROPERTY_KEY_ZIP_FILESIZE, Long.toString(zipLength, 36)); 319 properties.setProperty(PROPERTY_KEY_ZIP_TIMESTAMP, Long.toString(zipLastModified, 36)); 320 OutputStream out = new FileOutputStream(metaFile); 321 try { 322 properties.store(out, null); 323 } finally { 324 out.close(); 325 } 326 } 327 } 328 329 /** 330 * Unzip the given archive into the given folder. 331 * 332 * @param zipArchive The zip file to unzip. 333 * @param unzipRootFolder The folder to unzip to. 334 * @throws IOException in case of an I/O error. 335 */ 336 public static void unzipArchive(URL zipArchive, File unzipRootFolder) 337 throws IOException 338 { 339 ZipInputStream in = new ZipInputStream(zipArchive.openStream()); 340 try { 341 ZipEntry entry = null; 342 while ((entry = in.getNextEntry()) != null) { 343 if(entry.isDirectory()) { 344 // create the directory 345 File dir = new File(unzipRootFolder, entry.getName()); 346 if (!dir.exists() && !dir.mkdirs()) 347 throw new IllegalStateException("Could not create directory entry, possibly permission issues."); 348 } 349 else { 350 File file = new File(unzipRootFolder, entry.getName()); 351 352 File dir = file.getParentFile(); 353 if (dir.exists( )) { 354 assert (dir.isDirectory( )); 355 } 356 else { 357 dir.mkdirs( ); 358 } 359 360 BufferedOutputStream out = new BufferedOutputStream( new FileOutputStream(file) ); 361 362 int len; 363 byte[] buf = new byte[1024 * 5]; 364 while( (len = in.read(buf)) > 0 ) 365 { 366 out.write(buf, 0, len); 367 } 368 out.close(); 369 } 370 } 371 } finally { 372 if (in != null) 373 in.close(); 374 } 375 } 376 377 /** 378 * Calls {@link #unzipArchive(URL, File)} converting the File-parameter zipArchive to an url. 379 * 380 * @see #unzipArchive(URL, File). 381 */ 382 public static void unzipArchive(File zipArchive, File unzipRootFolder) 383 throws IOException 384 { 385 unzipArchive(zipArchive.toURI().toURL(), unzipRootFolder); 386 } 387}