/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2007-2008 Sun Microsystems, Inc. * Portions Copyright 2011-2016 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.List; 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 */ private 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 */ private 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 */ private 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 */ private 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> 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 (Map.Entry> mapEntry : permissions.entrySet()) { String perm = mapEntry.getKey(); List paths = mapEntry.getValue(); 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> 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.ensureParentsExist(destination)) { throw new IOException("Could not create parent path: " + destination); } if (entry.isDirectory()) { String perm = getDirectoryFileSystemPermissions(destination); addPermission(destination, permissions, perm); if (!Utils.createDirectory(destination)) { throw new IOException("Could not create path: " + destination); } } else { String perm = Utils.getFileSystemPermissions(destination); addPermission(destination, permissions, perm); Utils.createFile(destination, is); } if (application != null && application.isVerbose()) { application.notifyListenersDone(ratioWhenCompleted); } } private void addPermission(File destination, Map> permissions, String perm) { List list = permissions.get(perm); if (list == null) { list = new ArrayList<>(); permissions.put(perm, list); } list.add(Utils.getPath(destination)); } /** * 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"; } }