001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2007-2008 Sun Microsystems, Inc. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 */ 017package org.opends.quicksetup.util; 018 019import static com.forgerock.opendj.cli.Utils.*; 020import static com.forgerock.opendj.util.OperatingSystem.*; 021 022import static org.opends.messages.QuickSetupMessages.*; 023import static org.opends.server.util.CollectionUtils.*; 024 025import java.io.File; 026import java.io.FileInputStream; 027import java.io.FileNotFoundException; 028import java.io.IOException; 029import java.io.InputStream; 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.zip.ZipEntry; 035import java.util.zip.ZipInputStream; 036 037import org.forgerock.i18n.LocalizableMessage; 038import org.forgerock.i18n.slf4j.LocalizedLogger; 039import org.opends.quicksetup.Application; 040import org.opends.quicksetup.ApplicationException; 041import org.opends.quicksetup.ReturnCode; 042 043/** 044 * Class for extracting the contents of a zip file and managing 045 * the reporting of progress during extraction. 046 */ 047public class ZipExtractor { 048 049 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 050 051 /** Path separator for zip file entry names on Windows and *nix. */ 052 private static final char ZIP_ENTRY_NAME_SEP = '/'; 053 054 private InputStream is; 055 private int minRatio; 056 private int maxRatio; 057 private int numberZipEntries; 058 private String zipFileName; 059 private Application application; 060 061 /** 062 * Creates an instance of an ZipExtractor. 063 * @param zipFile File the zip file to extract 064 * @throws FileNotFoundException if the specified file does not exist 065 * @throws IllegalArgumentException if the zip file is not a zip file 066 */ 067 public ZipExtractor(File zipFile) 068 throws FileNotFoundException, IllegalArgumentException 069 { 070 this(zipFile, 0, 0, 1, null); 071 } 072 073 /** 074 * Creates an instance of an ZipExtractor. 075 * @param in InputStream for zip content 076 * @param zipFileName name of the input zip file 077 * @throws FileNotFoundException if the specified file does not exist 078 * @throws IllegalArgumentException if the zip file is not a zip file 079 */ 080 public ZipExtractor(InputStream in, String zipFileName) 081 throws FileNotFoundException, IllegalArgumentException 082 { 083 this(in, 0, 0, 1, zipFileName, null); 084 } 085 086 /** 087 * Creates an instance of an ZipExtractor. 088 * @param zipFile File the zip file to extract 089 * @param minRatio int indicating the max ration 090 * @param maxRatio int indicating the min ration 091 * @param numberZipEntries number of entries in the input stream 092 * @param app application to be notified about progress 093 * @throws FileNotFoundException if the specified file does not exist 094 * @throws IllegalArgumentException if the zip file is not a zip file 095 */ 096 public ZipExtractor(File zipFile, int minRatio, int maxRatio, 097 int numberZipEntries, 098 Application app) 099 throws FileNotFoundException, IllegalArgumentException 100 { 101 this(new FileInputStream(zipFile), 102 minRatio, 103 maxRatio, 104 numberZipEntries, 105 zipFile.getName(), 106 app); 107 if (!zipFile.getName().endsWith(".zip")) { 108 throw new IllegalArgumentException("File must have extension .zip"); 109 } 110 } 111 112 /** 113 * Creates an instance of an ZipExtractor. 114 * @param is InputStream of zip file content 115 * @param minRatio int indicating the max ration 116 * @param maxRatio int indicating the min ration 117 * @param numberZipEntries number of entries in the input stream 118 * @param zipFileName name of the input zip file 119 * @param app application to be notified about progress 120 */ 121 public ZipExtractor(InputStream is, int minRatio, int maxRatio, 122 int numberZipEntries, 123 String zipFileName, 124 Application app) { 125 this.is = is; 126 this.minRatio = minRatio; 127 this.maxRatio = maxRatio; 128 this.numberZipEntries = numberZipEntries; 129 this.zipFileName = zipFileName; 130 this.application = app; 131 } 132 133 /** 134 * Performs the zip extraction. 135 * @param destination File where the zip file will be extracted 136 * @throws ApplicationException if something goes wrong 137 */ 138 public void extract(File destination) throws ApplicationException { 139 extract(Utils.getPath(destination)); 140 } 141 142 /** 143 * Performs the zip extraction. 144 * @param destination File where the zip file will be extracted 145 * @throws ApplicationException if something goes wrong 146 */ 147 public void extract(String destination) throws ApplicationException { 148 extract(destination, true); 149 } 150 151 /** 152 * Performs the zip extraction. 153 * @param destDir String representing the directory where the zip file will 154 * be extracted 155 * @param removeFirstPath when true removes each zip entry's initial path 156 * when copied to the destination folder. So for instance if the zip entry's 157 * name was /OpenDJ-2.4.x/some_file the file would appear in the destination 158 * directory as 'some_file'. 159 * @throws ApplicationException if something goes wrong 160 */ 161 public void extract(String destDir, boolean removeFirstPath) 162 throws ApplicationException 163 { 164 ZipInputStream zipIn = new ZipInputStream(is); 165 int nEntries = 1; 166 167 /* This map is updated in the copyZipEntry method with the permissions 168 * of the files that have been copied. Once all the files have 169 * been copied to the file system we will update the file permissions of 170 * these files. This is done this way to group the number of calls to 171 * Runtime.exec (which is required to update the file system permissions). 172 */ 173 Map<String, List<String>> permissions = new HashMap<>(); 174 permissions.put(getProtectedDirectoryPermissionUnix(), newArrayList(destDir)); 175 try { 176 if(application != null) { 177 application.checkAbort(); 178 } 179 ZipEntry entry = zipIn.getNextEntry(); 180 while (entry != null) { 181 if(application != null) { 182 application.checkAbort(); 183 } 184 int ratioBeforeCompleted = minRatio 185 + ((nEntries - 1) * (maxRatio - minRatio) / numberZipEntries); 186 int ratioWhenCompleted = 187 minRatio + (nEntries * (maxRatio - minRatio) / numberZipEntries); 188 189 String name = entry.getName(); 190 if (name != null && removeFirstPath) { 191 int sepPos = name.indexOf(ZIP_ENTRY_NAME_SEP); 192 if (sepPos != -1) { 193 name = name.substring(sepPos + 1); 194 } else { 195 logger.warn(LocalizableMessage.raw( 196 "zip entry name does not contain a path separator")); 197 } 198 } 199 if (name != null && name.length() > 0) { 200 try { 201 File destination = new File(destDir, name); 202 copyZipEntry(entry, destination, zipIn, 203 ratioBeforeCompleted, ratioWhenCompleted, permissions); 204 } catch (IOException ioe) { 205 throw new ApplicationException( 206 ReturnCode.FILE_SYSTEM_ACCESS_ERROR, 207 getThrowableMsg(INFO_ERROR_COPYING.get(entry.getName()), ioe), 208 ioe); 209 } 210 } 211 212 zipIn.closeEntry(); 213 entry = zipIn.getNextEntry(); 214 nEntries++; 215 } 216 217 if (isUnix()) { 218 // Change the permissions for UNIX systems 219 for (String perm : permissions.keySet()) { 220 List<String> paths = permissions.get(perm); 221 try { 222 int result = Utils.setPermissionsUnix(paths, perm); 223 if (result != 0) { 224 throw new IOException("Could not set permissions on files " 225 + paths + ". The chmod error code was: " + result); 226 } 227 } catch (InterruptedException ie) { 228 throw new IOException("Could not set permissions on files " + paths 229 + ". The chmod call returned an InterruptedException.", ie); 230 } 231 } 232 } 233 } catch (IOException ioe) { 234 throw new ApplicationException( 235 ReturnCode.FILE_SYSTEM_ACCESS_ERROR, 236 getThrowableMsg(INFO_ERROR_ZIP_STREAM.get(zipFileName), ioe), 237 ioe); 238 } 239 } 240 241 /** 242 * Copies a zip entry in the file system. 243 * @param entry the ZipEntry object. 244 * @param destination File where the entry will be copied. 245 * @param is the ZipInputStream that contains the contents to be copied. 246 * @param ratioBeforeCompleted the progress ratio before the zip file is copied. 247 * @param ratioWhenCompleted the progress ratio after the zip file is copied. 248 * @param permissions an ArrayList with permissions whose contents will be updated. 249 * @throws IOException if an error occurs. 250 */ 251 private void copyZipEntry(ZipEntry entry, File destination, 252 ZipInputStream is, int ratioBeforeCompleted, 253 int ratioWhenCompleted, Map<String, List<String>> permissions) 254 throws IOException 255 { 256 if (application != null) { 257 LocalizableMessage progressSummary = 258 INFO_PROGRESS_EXTRACTING.get(Utils.getPath(destination)); 259 if (application.isVerbose()) 260 { 261 application.notifyListenersWithPoints(ratioBeforeCompleted, 262 progressSummary); 263 } 264 else 265 { 266 application.notifyListenersRatioChange(ratioBeforeCompleted); 267 } 268 } 269 logger.info(LocalizableMessage.raw("extracting " + Utils.getPath(destination))); 270 271 if (!Utils.ensureParentsExist(destination)) 272 { 273 throw new IOException("Could not create parent path: " + destination); 274 } 275 276 if (entry.isDirectory()) 277 { 278 String perm = getDirectoryFileSystemPermissions(destination); 279 addPermission(destination, permissions, perm); 280 if (!Utils.createDirectory(destination)) 281 { 282 throw new IOException("Could not create path: " + destination); 283 } 284 } else 285 { 286 String perm = Utils.getFileSystemPermissions(destination); 287 addPermission(destination, permissions, perm); 288 Utils.createFile(destination, is); 289 } 290 if (application != null && application.isVerbose()) 291 { 292 application.notifyListenersDone(ratioWhenCompleted); 293 } 294 } 295 296 private void addPermission(File destination, Map<String, List<String>> permissions, String perm) 297 { 298 List<String> list = permissions.get(perm); 299 if (list == null) 300 { 301 list = new ArrayList<>(); 302 permissions.put(perm, list); 303 } 304 list.add(Utils.getPath(destination)); 305 } 306 307 /** 308 * Returns the UNIX permissions to be applied to a protected directory. 309 * @return the UNIX permissions to be applied to a protected directory. 310 */ 311 private String getProtectedDirectoryPermissionUnix() 312 { 313 return "700"; 314 } 315 316 /** 317 * Returns the file system permissions for a directory. 318 * @param path the directory for which we want the file permissions. 319 * @return the file system permissions for the directory. 320 */ 321 private String getDirectoryFileSystemPermissions(File path) 322 { 323 // TODO We should get this dynamically during build? 324 return "755"; 325 } 326}