blob: ded76dffc17dd799317fbee7a304516c053253b0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2009 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
}
};
}
}