Skip to main content
summaryrefslogtreecommitdiffstats
blob: 6f17bc0ddb1e68d6fdec4b1c1e8505c5710246f7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
/*******************************************************************************
 *  Copyright (c) 2007, 2018 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 {
		List<File> untarredFiles = new ArrayList<>();
		try (TarFile tarFile = new TarFile(source)) {
			for (Enumeration<TarEntry> e = tarFile.entries(); e.hasMoreElements();) {
				TarEntry entry = e.nextElement();
				try (InputStream input = tarFile.getInputStream(entry)) {
					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());
					}
				}
			}
		}
		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) {
				IOException ioException = new IOException(e.getMessage());
				ioException.initCause(e);
				throw ioException;
			}
		}
		try (InputStream in = new FileInputStream(zipFile)) {
			return unzipStream(in, zipFile.length(), outputDir, null, null);
		} catch (IOException e) {
			// add the file name to the message
			IOException ioException = new IOException(NLS.bind(Messages.Util_Error_Unzipping, zipFile, e.getMessage()));
			ioException.initCause(e);
			throw ioException;
		}
	}

	/**
	 * 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 {
		try (InputStream in = new FileInputStream(zipFile)) {
			return unzipStream(in, zipFile.length(), outputDir, taskName, monitor);
		} catch (IOException e) {
			// add the file name to the message
			IOException ioException = new IOException(NLS.bind(Messages.Util_Error_Unzipping, zipFile, e.getMessage()));
			ioException.initCause(e);
			throw ioException;
		}
	}

	/**
	 * 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
		ArrayList<File> unzippedFiles = new ArrayList<>();
		try (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);
			}
			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);
		}

		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();
			try (InputStream in = new BufferedInputStream(new FileInputStream(sourceFile)); OutputStream out = new BufferedOutputStream(new FileOutputStream(destinationFile));) {
				copyStream(in, false, out, false);
			}
		}
	}

	/**
	 * 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 {
		try (FileOutputStream fileOutput = new FileOutputStream(destinationArchive); ZipOutputStream output = new ZipOutputStream(fileOutput)) {
			HashSet<File> exclusionSet = exclusions == null ? new HashSet<>() : new HashSet<>(Arrays.asList(exclusions));
			HashSet<IPath> directoryEntries = new HashSet<>();
			for (int i = 0; i < inclusions.length; i++) {
				pathComputer.reset();
				zip(output, inclusions[i], exclusionSet, pathComputer, directoryEntries);
			}
		}
	}

	/**
	 * 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, (arg0, 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
		try (InputStream input = new BufferedInputStream(new FileInputStream(source))) {
			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 {
				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() {
			@Override
			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);
			}

			@Override
			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;

			@Override
			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);
			}

			@Override
			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() {
			@Override
			public IPath computePath(File source) {
				IPath sourcePath = new Path(source.getAbsolutePath());
				sourcePath = sourcePath.removeFirstSegments(Math.max(0, sourcePath.segmentCount() - segmentsToKeep));
				return sourcePath.setDevice(null);
			}

			@Override
			public void reset() {
				//nothing
			}
		};
	}
}

Back to the top