| /******************************************************************************* |
| * Copyright (c) 2007, 2010 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.equinox.internal.p2.core.helpers; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.util.jar.JarFile; |
| import java.util.zip.*; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.osgi.util.NLS; |
| |
| public class FileUtils { |
| |
| private static File[] untarFile(File source, File outputDir) throws IOException, TarException { |
| TarFile tarFile = new TarFile(source); |
| List<File> untarredFiles = new ArrayList<File>(); |
| try { |
| for (Enumeration<TarEntry> e = tarFile.entries(); e.hasMoreElements();) { |
| TarEntry entry = e.nextElement(); |
| InputStream input = tarFile.getInputStream(entry); |
| try { |
| File outFile = new File(outputDir, entry.getName()); |
| outFile = outFile.getCanonicalFile(); //bug 266844 |
| untarredFiles.add(outFile); |
| if (entry.getFileType() == TarEntry.DIRECTORY) { |
| outFile.mkdirs(); |
| } else { |
| if (outFile.exists()) |
| outFile.delete(); |
| else |
| outFile.getParentFile().mkdirs(); |
| try { |
| copyStream(input, false, new FileOutputStream(outFile), true); |
| } catch (FileNotFoundException e1) { |
| // TEMP: ignore this for now in case we're trying to replace |
| // a running eclipse.exe |
| } |
| outFile.setLastModified(entry.getTime()); |
| } |
| } finally { |
| input.close(); |
| } |
| } |
| } finally { |
| tarFile.close(); |
| } |
| return untarredFiles.toArray(new File[untarredFiles.size()]); |
| } |
| |
| /** |
| * Unzip from a File to an output directory. |
| */ |
| public static File[] unzipFile(File zipFile, File outputDir) throws IOException { |
| // check to see if we have a tar'd and gz'd file |
| if (zipFile.getName().toLowerCase().endsWith(".tar.gz")) { //$NON-NLS-1$ |
| try { |
| return untarFile(zipFile, outputDir); |
| } catch (TarException e) { |
| throw new IOException(e.getMessage()); |
| } |
| } |
| InputStream in = new FileInputStream(zipFile); |
| try { |
| return unzipStream(in, zipFile.length(), outputDir, null, null); |
| } catch (IOException e) { |
| // add the file name to the message |
| throw new IOException(NLS.bind(Messages.Util_Error_Unzipping, zipFile, e.getMessage())); |
| } finally { |
| in.close(); |
| } |
| } |
| |
| /** |
| * Unzip from a File to an output directory, with progress indication. |
| * monitor may be null. |
| */ |
| public static File[] unzipFile(File zipFile, File outputDir, String taskName, IProgressMonitor monitor) throws IOException { |
| InputStream in = new FileInputStream(zipFile); |
| try { |
| return unzipStream(in, zipFile.length(), outputDir, taskName, monitor); |
| } catch (IOException e) { |
| // add the file name to the message |
| throw new IOException(NLS.bind(Messages.Util_Error_Unzipping, zipFile, e.getMessage())); |
| } finally { |
| in.close(); |
| } |
| } |
| |
| /** |
| * Unzip from an InputStream to an output directory. |
| */ |
| public static File[] unzipStream(InputStream stream, long size, File outputDir, String taskName, IProgressMonitor monitor) throws IOException { |
| InputStream is = monitor == null ? stream : stream; // new ProgressMonitorInputStream(stream, size, size, taskName, monitor); TODO Commented code |
| ZipInputStream in = new ZipInputStream(new BufferedInputStream(is)); |
| ZipEntry ze = in.getNextEntry(); |
| if (ze == null) { |
| // There must be at least one entry in a zip file. |
| // When there isn't getNextEntry returns null. |
| in.close(); |
| throw new IOException(Messages.Util_Invalid_Zip_File_Format); |
| } |
| ArrayList<File> unzippedFiles = new ArrayList<File>(); |
| do { |
| File outFile = new File(outputDir, ze.getName()); |
| unzippedFiles.add(outFile); |
| if (ze.isDirectory()) { |
| outFile.mkdirs(); |
| } else { |
| if (outFile.exists()) { |
| outFile.delete(); |
| } else { |
| outFile.getParentFile().mkdirs(); |
| } |
| try { |
| copyStream(in, false, new FileOutputStream(outFile), true); |
| } catch (FileNotFoundException e) { |
| // TEMP: ignore this for now in case we're trying to replace |
| // a running eclipse.exe |
| } |
| outFile.setLastModified(ze.getTime()); |
| } |
| in.closeEntry(); |
| } while ((ze = in.getNextEntry()) != null); |
| in.close(); |
| |
| return unzippedFiles.toArray(new File[unzippedFiles.size()]); |
| } |
| |
| // Delete empty directories under dir, including dir itself. |
| public static void deleteEmptyDirs(File dir) throws IOException { |
| File[] files = dir.listFiles(); |
| if (files != null) { |
| for (int i = 0; i < files.length; i += 1) { |
| deleteEmptyDirs(files[i]); |
| } |
| dir.getCanonicalFile().delete(); |
| } |
| } |
| |
| // Delete the given file whether it is a file or a directory |
| public static void deleteAll(File file) { |
| if (!file.exists()) |
| return; |
| if (file.isDirectory()) { |
| File[] files = file.listFiles(); |
| if (files != null) |
| for (int i = 0; i < files.length; i++) |
| deleteAll(files[i]); |
| } |
| file.delete(); |
| } |
| |
| /** |
| * Copy an input stream to an output stream. |
| * Optionally close the streams when done. |
| * Return the number of bytes written. |
| */ |
| public static int copyStream(InputStream in, boolean closeIn, OutputStream out, boolean closeOut) throws IOException { |
| try { |
| int written = 0; |
| byte[] buffer = new byte[16 * 1024]; |
| int len; |
| while ((len = in.read(buffer)) != -1) { |
| out.write(buffer, 0, len); |
| written += len; |
| } |
| return written; |
| } finally { |
| try { |
| if (closeIn) { |
| in.close(); |
| } |
| } finally { |
| if (closeOut) { |
| out.close(); |
| } |
| } |
| } |
| } |
| |
| public static void copy(File source, File destination, File root, boolean overwrite) throws IOException { |
| File sourceFile = new File(source, root.getPath()); |
| if (!sourceFile.exists()) |
| throw new FileNotFoundException("Source: " + sourceFile + " does not exist"); //$NON-NLS-1$//$NON-NLS-2$ |
| |
| File destinationFile = new File(destination, root.getPath()); |
| |
| if (destinationFile.exists()) |
| if (overwrite) |
| deleteAll(destinationFile); |
| else |
| throw new IOException("Destination: " + destinationFile + " already exists"); //$NON-NLS-1$//$NON-NLS-2$ |
| if (sourceFile.isDirectory()) { |
| destinationFile.mkdirs(); |
| File[] list = sourceFile.listFiles(); |
| for (int i = 0; i < list.length; i++) |
| copy(source, destination, new File(root, list[i].getName()), false); |
| } else { |
| destinationFile.getParentFile().mkdirs(); |
| InputStream in = null; |
| OutputStream out = null; |
| try { |
| in = new BufferedInputStream(new FileInputStream(sourceFile)); |
| out = new BufferedOutputStream(new FileOutputStream(destinationFile)); |
| copyStream(in, false, out, false); |
| } finally { |
| try { |
| if (in != null) |
| in.close(); |
| } finally { |
| if (out != null) |
| out.close(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates a zip archive at the given destination that contains all of the given inclusions |
| * except for the given exclusions. Inclusions and exclusions can be phrased as files or folders. |
| * Including a folder implies that all files and folders under the folder |
| * should be considered for inclusion. Excluding a folder implies that all files and folders |
| * under that folder will be excluded. Inclusions with paths deeper than an exclusion folder |
| * are filtered out and do not end up in the resultant archive. |
| * <p> |
| * All entries in the archive are computed using the given path computer. the path computer |
| * is reset between every explicit entry in the inclusions list. |
| * </p> |
| * @param inclusions the set of files and folders to be considered for inclusion in the result |
| * @param exclusions the set of files and folders to be excluded from the result. May be <code>null</code>. |
| * @param destinationArchive the location of the resultant archive |
| * @param pathComputer the path computer used to create the path of the files in the result |
| * @throws IOException if there is an IO issue during this operation. |
| */ |
| public static void zip(File[] inclusions, File[] exclusions, File destinationArchive, IPathComputer pathComputer) throws IOException { |
| FileOutputStream fileOutput = new FileOutputStream(destinationArchive); |
| ZipOutputStream output = new ZipOutputStream(fileOutput); |
| HashSet<File> exclusionSet = exclusions == null ? new HashSet<File>() : new HashSet<File>(Arrays.asList(exclusions)); |
| HashSet<IPath> directoryEntries = new HashSet<IPath>(); |
| try { |
| for (int i = 0; i < inclusions.length; i++) { |
| pathComputer.reset(); |
| zip(output, inclusions[i], exclusionSet, pathComputer, directoryEntries); |
| } |
| } finally { |
| try { |
| output.close(); |
| } catch (IOException e) { |
| // closing a zip file with no entries apparently fails on some JREs. |
| // if we failed closing the zip, try closing the raw file. |
| try { |
| fileOutput.close(); |
| } catch (IOException e2) { |
| // give up. |
| } |
| } |
| } |
| } |
| |
| /** |
| * Writes the given file or folder to the given ZipOutputStream. The stream is not closed, we recurse into folders |
| * @param output - the ZipOutputStream to write into |
| * @param source - the file or folder to zip |
| * @param exclusions - set of files or folders to exclude |
| * @param pathComputer - computer used to create the path of the files in the result. |
| * @throws IOException |
| */ |
| public static void zip(ZipOutputStream output, File source, Set<File> exclusions, IPathComputer pathComputer) throws IOException { |
| zip(output, source, exclusions, pathComputer, new HashSet<IPath>()); |
| } |
| |
| public static void zip(ZipOutputStream output, File source, Set<File> exclusions, IPathComputer pathComputer, Set<IPath> directoryEntries) throws IOException { |
| if (exclusions.contains(source)) |
| return; |
| if (source.isDirectory()) //if the file path is a URL then isDir and isFile are both false |
| zipDir(output, source, exclusions, pathComputer, directoryEntries); |
| else |
| zipFile(output, source, pathComputer, directoryEntries); |
| } |
| |
| private static void zipDirectoryEntry(ZipOutputStream output, IPath entry, long time, Set<IPath> directoryEntries) throws IOException { |
| entry = entry.addTrailingSeparator(); |
| if (!directoryEntries.contains(entry)) { |
| //make sure parent entries are in the zip |
| if (entry.segmentCount() > 1) |
| zipDirectoryEntry(output, entry.removeLastSegments(1), time, directoryEntries); |
| |
| try { |
| ZipEntry dirEntry = new ZipEntry(entry.toString()); |
| dirEntry.setTime(time); |
| output.putNextEntry(dirEntry); |
| directoryEntries.add(entry); |
| } catch (ZipException ze) { |
| //duplicate entries shouldn't happen because we checked the set |
| } finally { |
| try { |
| output.closeEntry(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| } |
| } |
| |
| /* |
| * Zip the contents of the given directory into the zip file represented by |
| * the given zip stream. Prepend the given prefix to the file paths. |
| */ |
| private static void zipDir(ZipOutputStream output, File source, Set<File> exclusions, IPathComputer pathComputer, Set<IPath> directoryEntries) throws IOException { |
| File[] files = source.listFiles(); |
| if (files.length == 0) { |
| zipDirectoryEntry(output, pathComputer.computePath(source), source.lastModified(), directoryEntries); |
| } |
| |
| // Different OSs return files in a different order. This affects the creation |
| // the dynamic path computer. To address this, we sort the files such that |
| // those with deeper paths appear later, and files are always before directories |
| // foo/bar.txt |
| // foo/something/bar2.txt |
| // foo/something/else/bar3.txt |
| Arrays.sort(files, new Comparator<File>() { |
| public int compare(File arg0, File arg1) { |
| Path a = new Path(arg0.getAbsolutePath()); |
| Path b = new Path(arg1.getAbsolutePath()); |
| if (a.segmentCount() == b.segmentCount()) { |
| if (arg0.isDirectory() && arg1.isFile()) |
| return 1; |
| else if (arg0.isDirectory() && arg1.isDirectory()) |
| return 0; |
| else if (arg0.isFile() && arg1.isDirectory()) |
| return -1; |
| else |
| return 0; |
| } |
| return a.segmentCount() - b.segmentCount(); |
| } |
| }); |
| |
| for (int i = 0; i < files.length; i++) |
| zip(output, files[i], exclusions, pathComputer, directoryEntries); |
| } |
| |
| /* |
| * Add the given file to the zip file represented by the specified stream. |
| * Prepend the given prefix to the path of the file. |
| */ |
| private static void zipFile(ZipOutputStream output, File source, IPathComputer pathComputer, Set<IPath> directoryEntries) throws IOException { |
| boolean isManifest = false; //manifest files are special |
| InputStream input = new BufferedInputStream(new FileInputStream(source)); |
| try { |
| IPath entryPath = pathComputer.computePath(source); |
| if (entryPath.isAbsolute()) |
| throw new IOException(Messages.Util_Absolute_Entry); |
| if (entryPath.segmentCount() == 0) |
| throw new IOException(Messages.Util_Empty_Zip_Entry); |
| |
| //make sure parent directory entries are in the zip |
| if (entryPath.segmentCount() > 1) { |
| //manifest files should be first, add their directory entry afterwards |
| isManifest = JarFile.MANIFEST_NAME.equals(entryPath.toString()); |
| if (!isManifest) |
| zipDirectoryEntry(output, entryPath.removeLastSegments(1), source.lastModified(), directoryEntries); |
| } |
| |
| ZipEntry zipEntry = new ZipEntry(entryPath.toString()); |
| zipEntry.setTime(source.lastModified()); |
| output.putNextEntry(zipEntry); |
| copyStream(input, true, output, false); |
| } catch (ZipException ze) { |
| //TODO: something about duplicate entries, and rethrow other exceptions |
| } finally { |
| try { |
| input.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| try { |
| output.closeEntry(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| |
| if (isManifest) { |
| zipDirectoryEntry(output, new Path("META-INF"), source.lastModified(), directoryEntries); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Path computers are used to transform a given File path into a path suitable for use |
| * as the to identify that file in an archive file or copy. |
| */ |
| public static interface IPathComputer { |
| /** |
| * Returns the path representing the given file. Often this trims or otherwise |
| * transforms the segments of the source file path. |
| * @param source the file path to be transformed |
| * @return the transformed path |
| */ |
| public IPath computePath(File source); |
| |
| /** |
| * Resets this path computer. Path computers can accumulate state or other information |
| * for use in computing subsequent paths. Resetting a computer causes it to flush that |
| * state and start afresh. The exact semantics of resetting depends on the nature of the |
| * computer itself. |
| */ |
| public void reset(); |
| } |
| |
| /** |
| * Creates a path computer that trims all paths according to the given root path. |
| * Paths that have no matching segments are returned unchanged. |
| * @param root the base path to use for trimming |
| * @return a path computer that trims according to the given root |
| */ |
| public static IPathComputer createRootPathComputer(final File root) { |
| return new IPathComputer() { |
| public IPath computePath(File source) { |
| IPath result = new Path(source.getAbsolutePath()); |
| IPath rootPath = new Path(root.getAbsolutePath()); |
| result = result.removeFirstSegments(rootPath.matchingFirstSegments(result)); |
| return result.setDevice(null); |
| } |
| |
| public void reset() { |
| //nothing |
| } |
| }; |
| } |
| |
| /** |
| * Creates a path computer that is a cross between the root and parent computers. |
| * When this computer is reset, the first path seen is considered a new root. That path |
| * is trimmed by the given number of segments and then used as in the same way as the |
| * root path computer. Every time this computer is reset, a new root is computed. |
| * <p> |
| * This is useful when handling several sets of disjoint files but for each set you want |
| * to have a common root. Rather than having to compute the roots ahead of time and |
| * then manage their relationships, you can simply reset the computer between groups. |
| * </p><p> |
| * For example, say you have the a list of folders { /a/b/c/eclipse/plugins/, /x/y/eclipse/features/} |
| * and want to end up with a zip containing plugins and features folders. Using a dynamic |
| * path computer and keeping 1 segment allows this to be done simply by resetting the computer |
| * between elements of the top level list. |
| * </p> |
| * @param segmentsToKeep the number of segments of encountered paths to keep |
| * relative to the dynamically computed roots. |
| * @return a path computer that trims but keeps the given number of segments relative |
| * to the dynamically computed roots. |
| */ |
| public static IPathComputer createDynamicPathComputer(final int segmentsToKeep) { |
| return new IPathComputer() { |
| IPathComputer computer = null; |
| |
| public IPath computePath(File source) { |
| if (computer == null) { |
| IPath sourcePath = new Path(source.getAbsolutePath()); |
| sourcePath = sourcePath.removeLastSegments(segmentsToKeep); |
| computer = createRootPathComputer(sourcePath.toFile()); |
| } |
| return computer.computePath(source); |
| } |
| |
| public void reset() { |
| computer = null; |
| } |
| }; |
| } |
| |
| /** |
| * Creates a path computer that retains the given number of trailing segments. |
| * @param segmentsToKeep number of segments to keep |
| * @return a path computer that retains the given number of trailing segments. |
| */ |
| public static IPathComputer createParentPrefixComputer(final int segmentsToKeep) { |
| return new IPathComputer() { |
| public IPath computePath(File source) { |
| IPath sourcePath = new Path(source.getAbsolutePath()); |
| sourcePath = sourcePath.removeFirstSegments(Math.max(0, sourcePath.segmentCount() - segmentsToKeep)); |
| return sourcePath.setDevice(null); |
| } |
| |
| public void reset() { |
| //nothing |
| } |
| }; |
| } |
| } |