/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2007-2008 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS */ package org.opends.quicksetup.util; import static com.forgerock.opendj.cli.Utils.*; import static com.forgerock.opendj.util.OperatingSystem.*; import static org.opends.messages.QuickSetupMessages.*; import static org.opends.server.util.CollectionUtils.*; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.opends.quicksetup.Application; import org.opends.quicksetup.ApplicationException; import org.opends.quicksetup.ReturnCode; /** * Class for extracting the contents of a zip file and managing * the reporting of progress during extraction. */ public class ZipExtractor { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** Path separator for zip file entry names on Windows and *nix. */ private static final char ZIP_ENTRY_NAME_SEP = '/'; private InputStream is; private int minRatio; private int maxRatio; private int numberZipEntries; private String zipFileName; private Application application; /** * Creates an instance of an ZipExtractor. * @param zipFile File the zip file to extract * @throws FileNotFoundException if the specified file does not exist * @throws IllegalArgumentException if the zip file is not a zip file */ public ZipExtractor(File zipFile) throws FileNotFoundException, IllegalArgumentException { this(zipFile, 0, 0, 1, null); } /** * Creates an instance of an ZipExtractor. * @param in InputStream for zip content * @param zipFileName name of the input zip file * @throws FileNotFoundException if the specified file does not exist * @throws IllegalArgumentException if the zip file is not a zip file */ public ZipExtractor(InputStream in, String zipFileName) throws FileNotFoundException, IllegalArgumentException { this(in, 0, 0, 1, zipFileName, null); } /** * Creates an instance of an ZipExtractor. * @param zipFile File the zip file to extract * @param minRatio int indicating the max ration * @param maxRatio int indicating the min ration * @param numberZipEntries number of entries in the input stream * @param app application to be notified about progress * @throws FileNotFoundException if the specified file does not exist * @throws IllegalArgumentException if the zip file is not a zip file */ public ZipExtractor(File zipFile, int minRatio, int maxRatio, int numberZipEntries, Application app) throws FileNotFoundException, IllegalArgumentException { this(new FileInputStream(zipFile), minRatio, maxRatio, numberZipEntries, zipFile.getName(), app); if (!zipFile.getName().endsWith(".zip")) { throw new IllegalArgumentException("File must have extension .zip"); } } /** * Creates an instance of an ZipExtractor. * @param is InputStream of zip file content * @param minRatio int indicating the max ration * @param maxRatio int indicating the min ration * @param numberZipEntries number of entries in the input stream * @param zipFileName name of the input zip file * @param app application to be notified about progress */ public ZipExtractor(InputStream is, int minRatio, int maxRatio, int numberZipEntries, String zipFileName, Application app) { this.is = is; this.minRatio = minRatio; this.maxRatio = maxRatio; this.numberZipEntries = numberZipEntries; this.zipFileName = zipFileName; this.application = app; } /** * Performs the zip extraction. * @param destination File where the zip file will be extracted * @throws ApplicationException if something goes wrong */ public void extract(File destination) throws ApplicationException { extract(Utils.getPath(destination)); } /** * Performs the zip extraction. * @param destination File where the zip file will be extracted * @throws ApplicationException if something goes wrong */ public void extract(String destination) throws ApplicationException { extract(destination, true); } /** * Performs the zip extraction. * @param destDir String representing the directory where the zip file will * be extracted * @param removeFirstPath when true removes each zip entry's initial path * when copied to the destination folder. So for instance if the zip entry's * name was /OpenDJ-2.4.x/some_file the file would appear in the destination * directory as 'some_file'. * @throws ApplicationException if something goes wrong */ public void extract(String destDir, boolean removeFirstPath) throws ApplicationException { ZipInputStream zipIn = new ZipInputStream(is); int nEntries = 1; /* This map is updated in the copyZipEntry method with the permissions * of the files that have been copied. Once all the files have * been copied to the file system we will update the file permissions of * these files. This is done this way to group the number of calls to * Runtime.exec (which is required to update the file system permissions). */ Map<String, ArrayList<String>> permissions = new HashMap<>(); permissions.put(getProtectedDirectoryPermissionUnix(), newArrayList(destDir)); try { if(application != null) { application.checkAbort(); } ZipEntry entry = zipIn.getNextEntry(); while (entry != null) { if(application != null) { application.checkAbort(); } int ratioBeforeCompleted = minRatio + ((nEntries - 1) * (maxRatio - minRatio) / numberZipEntries); int ratioWhenCompleted = minRatio + (nEntries * (maxRatio - minRatio) / numberZipEntries); String name = entry.getName(); if (name != null && removeFirstPath) { int sepPos = name.indexOf(ZIP_ENTRY_NAME_SEP); if (sepPos != -1) { name = name.substring(sepPos + 1); } else { logger.warn(LocalizableMessage.raw( "zip entry name does not contain a path separator")); } } if (name != null && name.length() > 0) { try { File destination = new File(destDir, name); copyZipEntry(entry, destination, zipIn, ratioBeforeCompleted, ratioWhenCompleted, permissions); } catch (IOException ioe) { throw new ApplicationException( ReturnCode.FILE_SYSTEM_ACCESS_ERROR, getThrowableMsg(INFO_ERROR_COPYING.get(entry.getName()), ioe), ioe); } } zipIn.closeEntry(); entry = zipIn.getNextEntry(); nEntries++; } if (isUnix()) { // Change the permissions for UNIX systems for (String perm : permissions.keySet()) { ArrayList<String> paths = permissions.get(perm); try { int result = Utils.setPermissionsUnix(paths, perm); if (result != 0) { throw new IOException("Could not set permissions on files " + paths + ". The chmod error code was: " + result); } } catch (InterruptedException ie) { throw new IOException("Could not set permissions on files " + paths + ". The chmod call returned an InterruptedException.", ie); } } } } catch (IOException ioe) { throw new ApplicationException( ReturnCode.FILE_SYSTEM_ACCESS_ERROR, getThrowableMsg(INFO_ERROR_ZIP_STREAM.get(zipFileName), ioe), ioe); } } /** * Copies a zip entry in the file system. * @param entry the ZipEntry object. * @param destination File where the entry will be copied. * @param is the ZipInputStream that contains the contents to be copied. * @param ratioBeforeCompleted the progress ratio before the zip file is copied. * @param ratioWhenCompleted the progress ratio after the zip file is copied. * @param permissions an ArrayList with permissions whose contents will be updated. * @throws IOException if an error occurs. */ private void copyZipEntry(ZipEntry entry, File destination, ZipInputStream is, int ratioBeforeCompleted, int ratioWhenCompleted, Map<String, ArrayList<String>> permissions) throws IOException { if (application != null) { LocalizableMessage progressSummary = INFO_PROGRESS_EXTRACTING.get(Utils.getPath(destination)); if (application.isVerbose()) { application.notifyListenersWithPoints(ratioBeforeCompleted, progressSummary); } else { application.notifyListenersRatioChange(ratioBeforeCompleted); } } logger.info(LocalizableMessage.raw("extracting " + Utils.getPath(destination))); if (Utils.insureParentsExist(destination)) { if (entry.isDirectory()) { String perm = getDirectoryFileSystemPermissions(destination); ArrayList<String> list = permissions.get(perm); if (list == null) { list = new ArrayList<>(); } list.add(Utils.getPath(destination)); permissions.put(perm, list); if (!Utils.createDirectory(destination)) { throw new IOException("Could not create path: " + destination); } } else { String perm = Utils.getFileSystemPermissions(destination); ArrayList<String> list = permissions.get(perm); if (list == null) { list = new ArrayList<>(); } list.add(Utils.getPath(destination)); permissions.put(perm, list); Utils.createFile(destination, is); } } else { throw new IOException("Could not create parent path: " + destination); } if (application != null && application.isVerbose()) { application.notifyListenersDone(ratioWhenCompleted); } } /** * Returns the UNIX permissions to be applied to a protected directory. * @return the UNIX permissions to be applied to a protected directory. */ private String getProtectedDirectoryPermissionUnix() { return "700"; } /** * Returns the file system permissions for a directory. * @param path the directory for which we want the file permissions. * @return the file system permissions for the directory. */ private String getDirectoryFileSystemPermissions(File path) { // TODO We should get this dynamically during build? return "755"; } }