View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc.
3    * Copyright (C) 2010, Matthias Sohn <matthias.sohn@sap.com>
4    * Copyright (C) 2010, Jens Baumgart <jens.baumgart@sap.com> and others
5    *
6    * This program and the accompanying materials are made available under the
7    * terms of the Eclipse Distribution License v. 1.0 which is available at
8    * https://www.eclipse.org/org/documents/edl-v10.php.
9    *
10   * SPDX-License-Identifier: BSD-3-Clause
11   */
12  
13  package org.eclipse.jgit.util;
14  
15  import static java.nio.charset.StandardCharsets.UTF_8;
16  
17  import java.io.File;
18  import java.io.FileNotFoundException;
19  import java.io.IOException;
20  import java.nio.channels.FileChannel;
21  import java.nio.file.AtomicMoveNotSupportedException;
22  import java.nio.file.CopyOption;
23  import java.nio.file.Files;
24  import java.nio.file.InvalidPathException;
25  import java.nio.file.LinkOption;
26  import java.nio.file.NoSuchFileException;
27  import java.nio.file.Path;
28  import java.nio.file.StandardCopyOption;
29  import java.nio.file.StandardOpenOption;
30  import java.nio.file.attribute.BasicFileAttributeView;
31  import java.nio.file.attribute.BasicFileAttributes;
32  import java.nio.file.attribute.FileTime;
33  import java.nio.file.attribute.PosixFileAttributeView;
34  import java.nio.file.attribute.PosixFileAttributes;
35  import java.nio.file.attribute.PosixFilePermission;
36  import java.text.MessageFormat;
37  import java.text.Normalizer;
38  import java.text.Normalizer.Form;
39  import java.time.Instant;
40  import java.util.ArrayList;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Random;
44  import java.util.regex.Pattern;
45  
46  import org.eclipse.jgit.internal.JGitText;
47  import org.eclipse.jgit.lib.Constants;
48  import org.eclipse.jgit.util.FS.Attributes;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  /**
53   * File Utilities
54   */
55  public class FileUtils {
56  	private static final Logger LOG = LoggerFactory.getLogger(FileUtils.class);
57  
58  	private static final Random RNG = new Random();
59  
60  	/**
61  	 * Option to delete given {@code File}
62  	 */
63  	public static final int NONE = 0;
64  
65  	/**
66  	 * Option to recursively delete given {@code File}
67  	 */
68  	public static final int RECURSIVE = 1;
69  
70  	/**
71  	 * Option to retry deletion if not successful
72  	 */
73  	public static final int RETRY = 2;
74  
75  	/**
76  	 * Option to skip deletion if file doesn't exist
77  	 */
78  	public static final int SKIP_MISSING = 4;
79  
80  	/**
81  	 * Option not to throw exceptions when a deletion finally doesn't succeed.
82  	 * @since 2.0
83  	 */
84  	public static final int IGNORE_ERRORS = 8;
85  
86  	/**
87  	 * Option to only delete empty directories. This option can be combined with
88  	 * {@link #RECURSIVE}
89  	 *
90  	 * @since 3.0
91  	 */
92  	public static final int EMPTY_DIRECTORIES_ONLY = 16;
93  
94  	/**
95  	 * Safe conversion from {@link java.io.File} to {@link java.nio.file.Path}.
96  	 *
97  	 * @param f
98  	 *            {@code File} to be converted to {@code Path}
99  	 * @return the path represented by the file
100 	 * @throws java.io.IOException
101 	 *             in case the path represented by the file is not valid (
102 	 *             {@link java.nio.file.InvalidPathException})
103 	 * @since 4.10
104 	 */
105 	public static Path toPath(File f) throws IOException {
106 		try {
107 			return f.toPath();
108 		} catch (InvalidPathException ex) {
109 			throw new IOException(ex);
110 		}
111 	}
112 
113 	/**
114 	 * Delete file or empty folder
115 	 *
116 	 * @param f
117 	 *            {@code File} to be deleted
118 	 * @throws java.io.IOException
119 	 *             if deletion of {@code f} fails. This may occur if {@code f}
120 	 *             didn't exist when the method was called. This can therefore
121 	 *             cause java.io.IOExceptions during race conditions when
122 	 *             multiple concurrent threads all try to delete the same file.
123 	 */
124 	public static void delete(File f) throws IOException {
125 		delete(f, NONE);
126 	}
127 
128 	/**
129 	 * Delete file or folder
130 	 *
131 	 * @param f
132 	 *            {@code File} to be deleted
133 	 * @param options
134 	 *            deletion options, {@code RECURSIVE} for recursive deletion of
135 	 *            a subtree, {@code RETRY} to retry when deletion failed.
136 	 *            Retrying may help if the underlying file system doesn't allow
137 	 *            deletion of files being read by another thread.
138 	 * @throws java.io.IOException
139 	 *             if deletion of {@code f} fails. This may occur if {@code f}
140 	 *             didn't exist when the method was called. This can therefore
141 	 *             cause java.io.IOExceptions during race conditions when
142 	 *             multiple concurrent threads all try to delete the same file.
143 	 *             This exception is not thrown when IGNORE_ERRORS is set.
144 	 */
145 	public static void delete(File f, int options) throws IOException {
146 		FS fs = FS.DETECTED;
147 		if ((options & SKIP_MISSING) != 0 && !fs.exists(f))
148 			return;
149 
150 		if ((options & RECURSIVE) != 0 && fs.isDirectory(f)) {
151 			final File[] items = f.listFiles();
152 			if (items != null) {
153 				List<File> files = new ArrayList<>();
154 				List<File> dirs = new ArrayList<>();
155 				for (File c : items)
156 					if (c.isFile())
157 						files.add(c);
158 					else
159 						dirs.add(c);
160 				// Try to delete files first, otherwise options
161 				// EMPTY_DIRECTORIES_ONLY|RECURSIVE will delete empty
162 				// directories before aborting, depending on order.
163 				for (File file : files)
164 					delete(file, options);
165 				for (File d : dirs)
166 					delete(d, options);
167 			}
168 		}
169 
170 		boolean delete = false;
171 		if ((options & EMPTY_DIRECTORIES_ONLY) != 0) {
172 			if (f.isDirectory()) {
173 				delete = true;
174 			} else if ((options & IGNORE_ERRORS) == 0) {
175 				throw new IOException(MessageFormat.format(
176 						JGitText.get().deleteFileFailed, f.getAbsolutePath()));
177 			}
178 		} else {
179 			delete = true;
180 		}
181 
182 		if (delete) {
183 			Throwable t = null;
184 			Path p = f.toPath();
185 			try {
186 				Files.delete(p);
187 				return;
188 			} catch (FileNotFoundException e) {
189 				if ((options & (SKIP_MISSING | IGNORE_ERRORS)) == 0) {
190 					throw new IOException(MessageFormat.format(
191 							JGitText.get().deleteFileFailed,
192 							f.getAbsolutePath()), e);
193 				}
194 				return;
195 			} catch (IOException e) {
196 				t = e;
197 			}
198 			if ((options & RETRY) != 0) {
199 				for (int i = 1; i < 10; i++) {
200 					try {
201 						Thread.sleep(100);
202 					} catch (InterruptedException ex) {
203 						// ignore
204 					}
205 					try {
206 						Files.deleteIfExists(p);
207 						return;
208 					} catch (IOException e) {
209 						t = e;
210 					}
211 				}
212 			}
213 			if ((options & IGNORE_ERRORS) == 0) {
214 				throw new IOException(MessageFormat.format(
215 						JGitText.get().deleteFileFailed, f.getAbsolutePath()),
216 						t);
217 			}
218 		}
219 	}
220 
221 	/**
222 	 * Rename a file or folder. If the rename fails and if we are running on a
223 	 * filesystem where it makes sense to repeat a failing rename then repeat
224 	 * the rename operation up to 9 times with 100ms sleep time between two
225 	 * calls. Furthermore if the destination exists and is directory hierarchy
226 	 * with only directories in it, the whole directory hierarchy will be
227 	 * deleted. If the target represents a non-empty directory structure, empty
228 	 * subdirectories within that structure may or may not be deleted even if
229 	 * the method fails. Furthermore if the destination exists and is a file
230 	 * then the file will be deleted and then the rename is retried.
231 	 * <p>
232 	 * This operation is <em>not</em> atomic.
233 	 *
234 	 * @see FS#retryFailedLockFileCommit()
235 	 * @param src
236 	 *            the old {@code File}
237 	 * @param dst
238 	 *            the new {@code File}
239 	 * @throws java.io.IOException
240 	 *             if the rename has failed
241 	 * @since 3.0
242 	 */
243 	public static void rename(File src, File dst)
244 			throws IOException {
245 		rename(src, dst, StandardCopyOption.REPLACE_EXISTING);
246 	}
247 
248 	/**
249 	 * Rename a file or folder using the passed
250 	 * {@link java.nio.file.CopyOption}s. If the rename fails and if we are
251 	 * running on a filesystem where it makes sense to repeat a failing rename
252 	 * then repeat the rename operation up to 9 times with 100ms sleep time
253 	 * between two calls. Furthermore if the destination exists and is a
254 	 * directory hierarchy with only directories in it, the whole directory
255 	 * hierarchy will be deleted. If the target represents a non-empty directory
256 	 * structure, empty subdirectories within that structure may or may not be
257 	 * deleted even if the method fails. Furthermore if the destination exists
258 	 * and is a file then the file will be replaced if
259 	 * {@link java.nio.file.StandardCopyOption#REPLACE_EXISTING} has been set.
260 	 * If {@link java.nio.file.StandardCopyOption#ATOMIC_MOVE} has been set the
261 	 * rename will be done atomically or fail with an
262 	 * {@link java.nio.file.AtomicMoveNotSupportedException}
263 	 *
264 	 * @param src
265 	 *            the old file
266 	 * @param dst
267 	 *            the new file
268 	 * @param options
269 	 *            options to pass to
270 	 *            {@link java.nio.file.Files#move(java.nio.file.Path, java.nio.file.Path, CopyOption...)}
271 	 * @throws java.nio.file.AtomicMoveNotSupportedException
272 	 *             if file cannot be moved as an atomic file system operation
273 	 * @throws java.io.IOException
274 	 * @since 4.1
275 	 */
276 	public static void rename(final File src, final File dst,
277 			CopyOption... options)
278 					throws AtomicMoveNotSupportedException, IOException {
279 		int attempts = FS.DETECTED.retryFailedLockFileCommit() ? 10 : 1;
280 		while (--attempts >= 0) {
281 			try {
282 				Files.move(toPath(src), toPath(dst), options);
283 				return;
284 			} catch (AtomicMoveNotSupportedException e) {
285 				throw e;
286 			} catch (IOException e) {
287 				try {
288 					if (!dst.delete()) {
289 						delete(dst, EMPTY_DIRECTORIES_ONLY | RECURSIVE);
290 					}
291 					// On *nix there is no try, you do or do not
292 					Files.move(toPath(src), toPath(dst), options);
293 					return;
294 				} catch (IOException e2) {
295 					// ignore and continue retry
296 				}
297 			}
298 			try {
299 				Thread.sleep(100);
300 			} catch (InterruptedException e) {
301 				throw new IOException(
302 						MessageFormat.format(JGitText.get().renameFileFailed,
303 								src.getAbsolutePath(), dst.getAbsolutePath()),
304 						e);
305 			}
306 		}
307 		throw new IOException(
308 				MessageFormat.format(JGitText.get().renameFileFailed,
309 						src.getAbsolutePath(), dst.getAbsolutePath()));
310 	}
311 
312 	/**
313 	 * Creates the directory named by this abstract pathname.
314 	 *
315 	 * @param d
316 	 *            directory to be created
317 	 * @throws java.io.IOException
318 	 *             if creation of {@code d} fails. This may occur if {@code d}
319 	 *             did exist when the method was called. This can therefore
320 	 *             cause java.io.IOExceptions during race conditions when
321 	 *             multiple concurrent threads all try to create the same
322 	 *             directory.
323 	 */
324 	public static void mkdir(File d)
325 			throws IOException {
326 		mkdir(d, false);
327 	}
328 
329 	/**
330 	 * Creates the directory named by this abstract pathname.
331 	 *
332 	 * @param d
333 	 *            directory to be created
334 	 * @param skipExisting
335 	 *            if {@code true} skip creation of the given directory if it
336 	 *            already exists in the file system
337 	 * @throws java.io.IOException
338 	 *             if creation of {@code d} fails. This may occur if {@code d}
339 	 *             did exist when the method was called. This can therefore
340 	 *             cause java.io.IOExceptions during race conditions when
341 	 *             multiple concurrent threads all try to create the same
342 	 *             directory.
343 	 */
344 	public static void mkdir(File d, boolean skipExisting)
345 			throws IOException {
346 		if (!d.mkdir()) {
347 			if (skipExisting && d.isDirectory())
348 				return;
349 			throw new IOException(MessageFormat.format(
350 					JGitText.get().mkDirFailed, d.getAbsolutePath()));
351 		}
352 	}
353 
354 	/**
355 	 * Creates the directory named by this abstract pathname, including any
356 	 * necessary but nonexistent parent directories. Note that if this operation
357 	 * fails it may have succeeded in creating some of the necessary parent
358 	 * directories.
359 	 *
360 	 * @param d
361 	 *            directory to be created
362 	 * @throws java.io.IOException
363 	 *             if creation of {@code d} fails. This may occur if {@code d}
364 	 *             did exist when the method was called. This can therefore
365 	 *             cause java.io.IOExceptions during race conditions when
366 	 *             multiple concurrent threads all try to create the same
367 	 *             directory.
368 	 */
369 	public static void mkdirs(File d) throws IOException {
370 		mkdirs(d, false);
371 	}
372 
373 	/**
374 	 * Creates the directory named by this abstract pathname, including any
375 	 * necessary but nonexistent parent directories. Note that if this operation
376 	 * fails it may have succeeded in creating some of the necessary parent
377 	 * directories.
378 	 *
379 	 * @param d
380 	 *            directory to be created
381 	 * @param skipExisting
382 	 *            if {@code true} skip creation of the given directory if it
383 	 *            already exists in the file system
384 	 * @throws java.io.IOException
385 	 *             if creation of {@code d} fails. This may occur if {@code d}
386 	 *             did exist when the method was called. This can therefore
387 	 *             cause java.io.IOExceptions during race conditions when
388 	 *             multiple concurrent threads all try to create the same
389 	 *             directory.
390 	 */
391 	public static void mkdirs(File d, boolean skipExisting)
392 			throws IOException {
393 		if (!d.mkdirs()) {
394 			if (skipExisting && d.isDirectory())
395 				return;
396 			throw new IOException(MessageFormat.format(
397 					JGitText.get().mkDirsFailed, d.getAbsolutePath()));
398 		}
399 	}
400 
401 	/**
402 	 * Atomically creates a new, empty file named by this abstract pathname if
403 	 * and only if a file with this name does not yet exist. The check for the
404 	 * existence of the file and the creation of the file if it does not exist
405 	 * are a single operation that is atomic with respect to all other
406 	 * filesystem activities that might affect the file.
407 	 * <p>
408 	 * Note: this method should not be used for file-locking, as the resulting
409 	 * protocol cannot be made to work reliably. The
410 	 * {@link java.nio.channels.FileLock} facility should be used instead.
411 	 *
412 	 * @param f
413 	 *            the file to be created
414 	 * @throws java.io.IOException
415 	 *             if the named file already exists or if an I/O error occurred
416 	 */
417 	public static void createNewFile(File f) throws IOException {
418 		if (!f.createNewFile())
419 			throw new IOException(MessageFormat.format(
420 					JGitText.get().createNewFileFailed, f));
421 	}
422 
423 	/**
424 	 * Create a symbolic link
425 	 *
426 	 * @param path
427 	 *            the path of the symbolic link to create
428 	 * @param target
429 	 *            the target of the symbolic link
430 	 * @return the path to the symbolic link
431 	 * @throws java.io.IOException
432 	 * @since 4.2
433 	 */
434 	public static Path createSymLink(File path, String target)
435 			throws IOException {
436 		Path nioPath = toPath(path);
437 		if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS)) {
438 			BasicFileAttributes attrs = Files.readAttributes(nioPath,
439 					BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
440 			if (attrs.isRegularFile() || attrs.isSymbolicLink()) {
441 				delete(path);
442 			} else {
443 				delete(path, EMPTY_DIRECTORIES_ONLY | RECURSIVE);
444 			}
445 		}
446 		if (SystemReader.getInstance().isWindows()) {
447 			target = target.replace('/', '\\');
448 		}
449 		Path nioTarget = toPath(new File(target));
450 		return Files.createSymbolicLink(nioPath, nioTarget);
451 	}
452 
453 	/**
454 	 * Read target path of the symlink.
455 	 *
456 	 * @param path
457 	 *            a {@link java.io.File} object.
458 	 * @return target path of the symlink, or null if it is not a symbolic link
459 	 * @throws java.io.IOException
460 	 * @since 3.0
461 	 */
462 	public static String readSymLink(File path) throws IOException {
463 		Path nioPath = toPath(path);
464 		Path target = Files.readSymbolicLink(nioPath);
465 		String targetString = target.toString();
466 		if (SystemReader.getInstance().isWindows()) {
467 			targetString = targetString.replace('\\', '/');
468 		} else if (SystemReader.getInstance().isMacOS()) {
469 			targetString = Normalizer.normalize(targetString, Form.NFC);
470 		}
471 		return targetString;
472 	}
473 
474 	/**
475 	 * Create a temporary directory.
476 	 *
477 	 * @param prefix
478 	 *            prefix string
479 	 * @param suffix
480 	 *            suffix string
481 	 * @param dir
482 	 *            The parent dir, can be null to use system default temp dir.
483 	 * @return the temp dir created.
484 	 * @throws java.io.IOException
485 	 * @since 3.4
486 	 */
487 	public static File createTempDir(String prefix, String suffix, File dir)
488 			throws IOException {
489 		final int RETRIES = 1; // When something bad happens, retry once.
490 		for (int i = 0; i < RETRIES; i++) {
491 			File tmp = File.createTempFile(prefix, suffix, dir);
492 			if (!tmp.delete())
493 				continue;
494 			if (!tmp.mkdir())
495 				continue;
496 			return tmp;
497 		}
498 		throw new IOException(JGitText.get().cannotCreateTempDir);
499 	}
500 
501 	/**
502 	 * Expresses <code>other</code> as a relative file path from
503 	 * <code>base</code>. File-separator and case sensitivity are based on the
504 	 * current file system.
505 	 *
506 	 * See also
507 	 * {@link org.eclipse.jgit.util.FileUtils#relativizePath(String, String, String, boolean)}.
508 	 *
509 	 * @param base
510 	 *            Base path
511 	 * @param other
512 	 *            Destination path
513 	 * @return Relative path from <code>base</code> to <code>other</code>
514 	 * @since 4.8
515 	 */
516 	public static String relativizeNativePath(String base, String other) {
517 		return FS.DETECTED.relativize(base, other);
518 	}
519 
520 	/**
521 	 * Expresses <code>other</code> as a relative file path from
522 	 * <code>base</code>. File-separator and case sensitivity are based on Git's
523 	 * internal representation of files (which matches Unix).
524 	 *
525 	 * See also
526 	 * {@link org.eclipse.jgit.util.FileUtils#relativizePath(String, String, String, boolean)}.
527 	 *
528 	 * @param base
529 	 *            Base path
530 	 * @param other
531 	 *            Destination path
532 	 * @return Relative path from <code>base</code> to <code>other</code>
533 	 * @since 4.8
534 	 */
535 	public static String relativizeGitPath(String base, String other) {
536 		return relativizePath(base, other, "/", false); //$NON-NLS-1$
537 	}
538 
539 
540 	/**
541 	 * Expresses <code>other</code> as a relative file path from <code>base</code>
542 	 * <p>
543 	 * For example, if called with the two following paths :
544 	 *
545 	 * <pre>
546 	 * <code>base = "c:\\Users\\jdoe\\eclipse\\git\\project"</code>
547 	 * <code>other = "c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml"</code>
548 	 * </pre>
549 	 *
550 	 * This will return "..\\another_project\\pom.xml".
551 	 *
552 	 * <p>
553 	 * <b>Note</b> that this will return the empty String if <code>base</code>
554 	 * and <code>other</code> are equal.
555 	 * </p>
556 	 *
557 	 * @param base
558 	 *            The path against which <code>other</code> should be
559 	 *            relativized. This will be assumed to denote the path to a
560 	 *            folder and not a file.
561 	 * @param other
562 	 *            The path that will be made relative to <code>base</code>.
563 	 * @param dirSeparator
564 	 *            A string that separates components of the path. In practice, this is "/" or "\\".
565 	 * @param caseSensitive
566 	 *            Whether to consider differently-cased directory names as distinct
567 	 * @return A relative path that, when resolved against <code>base</code>,
568 	 *         will yield the original <code>other</code>.
569 	 * @since 4.8
570 	 */
571 	public static String relativizePath(String base, String other, String dirSeparator, boolean caseSensitive) {
572 		if (base.equals(other))
573 			return ""; //$NON-NLS-1$
574 
575 		final String[] baseSegments = base.split(Pattern.quote(dirSeparator));
576 		final String[] otherSegments = other.split(Pattern
577 				.quote(dirSeparator));
578 
579 		int commonPrefix = 0;
580 		while (commonPrefix < baseSegments.length
581 				&& commonPrefix < otherSegments.length) {
582 			if (caseSensitive
583 					&& baseSegments[commonPrefix]
584 					.equals(otherSegments[commonPrefix]))
585 				commonPrefix++;
586 			else if (!caseSensitive
587 					&& baseSegments[commonPrefix]
588 							.equalsIgnoreCase(otherSegments[commonPrefix]))
589 				commonPrefix++;
590 			else
591 				break;
592 		}
593 
594 		final StringBuilder builder = new StringBuilder();
595 		for (int i = commonPrefix; i < baseSegments.length; i++)
596 			builder.append("..").append(dirSeparator); //$NON-NLS-1$
597 		for (int i = commonPrefix; i < otherSegments.length; i++) {
598 			builder.append(otherSegments[i]);
599 			if (i < otherSegments.length - 1)
600 				builder.append(dirSeparator);
601 		}
602 		return builder.toString();
603 	}
604 
605 	/**
606 	 * Determine if an IOException is a Stale NFS File Handle
607 	 *
608 	 * @param ioe
609 	 *            an {@link java.io.IOException} object.
610 	 * @return a boolean true if the IOException is a Stale NFS FIle Handle
611 	 * @since 4.1
612 	 */
613 	public static boolean isStaleFileHandle(IOException ioe) {
614 		String msg = ioe.getMessage();
615 		return msg != null
616 				&& msg.toLowerCase(Locale.ROOT)
617 						.matches("stale .*file .*handle"); //$NON-NLS-1$
618 	}
619 
620 	/**
621 	 * Determine if a throwable or a cause in its causal chain is a Stale NFS
622 	 * File Handle
623 	 *
624 	 * @param throwable
625 	 *            a {@link java.lang.Throwable} object.
626 	 * @return a boolean true if the throwable or a cause in its causal chain is
627 	 *         a Stale NFS File Handle
628 	 * @since 4.7
629 	 */
630 	public static boolean isStaleFileHandleInCausalChain(Throwable throwable) {
631 		while (throwable != null) {
632 			if (throwable instanceof IOException
633 					&& isStaleFileHandle((IOException) throwable)) {
634 				return true;
635 			}
636 			throwable = throwable.getCause();
637 		}
638 		return false;
639 	}
640 
641 	/**
642 	 * @param file
643 	 * @return {@code true} if the passed file is a symbolic link
644 	 */
645 	static boolean isSymlink(File file) {
646 		return Files.isSymbolicLink(file.toPath());
647 	}
648 
649 	/**
650 	 * @param file
651 	 * @return lastModified attribute for given file, not following symbolic
652 	 *         links
653 	 * @throws IOException
654 	 * @deprecated use {@link #lastModifiedInstant(Path)} instead which returns
655 	 *             FileTime
656 	 */
657 	@Deprecated
658 	static long lastModified(File file) throws IOException {
659 		return Files.getLastModifiedTime(toPath(file), LinkOption.NOFOLLOW_LINKS)
660 				.toMillis();
661 	}
662 
663 	/**
664 	 * @param path
665 	 * @return lastModified attribute for given file, not following symbolic
666 	 *         links
667 	 */
668 	static Instant lastModifiedInstant(Path path) {
669 		try {
670 			return Files.getLastModifiedTime(path, LinkOption.NOFOLLOW_LINKS)
671 					.toInstant();
672 		} catch (NoSuchFileException e) {
673 			LOG.debug(
674 					"Cannot read lastModifiedInstant since path {} does not exist", //$NON-NLS-1$
675 					path);
676 			return Instant.EPOCH;
677 		} catch (IOException e) {
678 			LOG.error(MessageFormat
679 					.format(JGitText.get().readLastModifiedFailed, path), e);
680 			return Instant.ofEpochMilli(path.toFile().lastModified());
681 		}
682 	}
683 
684 	/**
685 	 * Return all the attributes of a file, without following symbolic links.
686 	 *
687 	 * @param file
688 	 * @return {@link BasicFileAttributes} of the file
689 	 * @throws IOException in case of any I/O errors accessing the file
690 	 *
691 	 * @since 4.5.6
692 	 */
693 	static BasicFileAttributes fileAttributes(File file) throws IOException {
694 		return Files.readAttributes(file.toPath(), BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
695 	}
696 
697 	/**
698 	 * @param file
699 	 * @param time
700 	 * @throws IOException
701 	 */
702 	@Deprecated
703 	static void setLastModified(File file, long time) throws IOException {
704 		Files.setLastModifiedTime(toPath(file), FileTime.fromMillis(time));
705 	}
706 
707 	/**
708 	 * @param path
709 	 * @param time
710 	 * @throws IOException
711 	 */
712 	static void setLastModified(Path path, Instant time)
713 			throws IOException {
714 		Files.setLastModifiedTime(path, FileTime.from(time));
715 	}
716 
717 	/**
718 	 * @param file
719 	 * @return {@code true} if the given file exists, not following symbolic
720 	 *         links
721 	 */
722 	static boolean exists(File file) {
723 		return Files.exists(file.toPath(), LinkOption.NOFOLLOW_LINKS);
724 	}
725 
726 	/**
727 	 * @param file
728 	 * @return {@code true} if the given file is hidden
729 	 * @throws IOException
730 	 */
731 	static boolean isHidden(File file) throws IOException {
732 		return Files.isHidden(toPath(file));
733 	}
734 
735 	/**
736 	 * Set a file hidden (on Windows)
737 	 *
738 	 * @param file
739 	 *            a {@link java.io.File} object.
740 	 * @param hidden
741 	 *            a boolean.
742 	 * @throws java.io.IOException
743 	 * @since 4.1
744 	 */
745 	public static void setHidden(File file, boolean hidden) throws IOException {
746 		Files.setAttribute(toPath(file), "dos:hidden", Boolean.valueOf(hidden), //$NON-NLS-1$
747 				LinkOption.NOFOLLOW_LINKS);
748 	}
749 
750 	/**
751 	 * Get file length
752 	 *
753 	 * @param file
754 	 *            a {@link java.io.File}.
755 	 * @return length of the given file
756 	 * @throws java.io.IOException
757 	 * @since 4.1
758 	 */
759 	public static long getLength(File file) throws IOException {
760 		Path nioPath = toPath(file);
761 		if (Files.isSymbolicLink(nioPath))
762 			return Files.readSymbolicLink(nioPath).toString()
763 					.getBytes(UTF_8).length;
764 		return Files.size(nioPath);
765 	}
766 
767 	/**
768 	 * @param file
769 	 * @return {@code true} if the given file is a directory, not following
770 	 *         symbolic links
771 	 */
772 	static boolean isDirectory(File file) {
773 		return Files.isDirectory(file.toPath(), LinkOption.NOFOLLOW_LINKS);
774 	}
775 
776 	/**
777 	 * @param file
778 	 * @return {@code true} if the given file is a file, not following symbolic
779 	 *         links
780 	 */
781 	static boolean isFile(File file) {
782 		return Files.isRegularFile(file.toPath(), LinkOption.NOFOLLOW_LINKS);
783 	}
784 
785 	/**
786 	 * Whether the given file can be executed.
787 	 *
788 	 * @param file
789 	 *            a {@link java.io.File} object.
790 	 * @return {@code true} if the given file can be executed.
791 	 * @since 4.1
792 	 */
793 	public static boolean canExecute(File file) {
794 		if (!isFile(file)) {
795 			return false;
796 		}
797 		return Files.isExecutable(file.toPath());
798 	}
799 
800 	/**
801 	 * @param fs
802 	 * @param file
803 	 * @return non null attributes object
804 	 */
805 	static Attributes getFileAttributesBasic(FS fs, File file) {
806 		try {
807 			Path nioPath = toPath(file);
808 			BasicFileAttributes readAttributes = nioPath
809 					.getFileSystem()
810 					.provider()
811 					.getFileAttributeView(nioPath,
812 							BasicFileAttributeView.class,
813 							LinkOption.NOFOLLOW_LINKS).readAttributes();
814 			Attributes attributes = new Attributes(fs, file,
815 					true,
816 					readAttributes.isDirectory(),
817 					fs.supportsExecute() ? file.canExecute() : false,
818 					readAttributes.isSymbolicLink(),
819 					readAttributes.isRegularFile(), //
820 					readAttributes.creationTime().toMillis(), //
821 					readAttributes.lastModifiedTime().toInstant(),
822 					readAttributes.isSymbolicLink() ? Constants
823 							.encode(readSymLink(file)).length
824 							: readAttributes.size());
825 			return attributes;
826 		} catch (IOException e) {
827 			return new Attributes(file, fs);
828 		}
829 	}
830 
831 	/**
832 	 * Get file system attributes for the given file.
833 	 *
834 	 * @param fs
835 	 *            a {@link org.eclipse.jgit.util.FS} object.
836 	 * @param file
837 	 *            a {@link java.io.File}.
838 	 * @return file system attributes for the given file.
839 	 * @since 4.1
840 	 */
841 	public static Attributes getFileAttributesPosix(FS fs, File file) {
842 		try {
843 			Path nioPath = toPath(file);
844 			PosixFileAttributes readAttributes = nioPath
845 					.getFileSystem()
846 					.provider()
847 					.getFileAttributeView(nioPath,
848 							PosixFileAttributeView.class,
849 							LinkOption.NOFOLLOW_LINKS).readAttributes();
850 			Attributes attributes = new Attributes(
851 					fs,
852 					file,
853 					true, //
854 					readAttributes.isDirectory(), //
855 					readAttributes.permissions().contains(
856 							PosixFilePermission.OWNER_EXECUTE),
857 					readAttributes.isSymbolicLink(),
858 					readAttributes.isRegularFile(), //
859 					readAttributes.creationTime().toMillis(), //
860 					readAttributes.lastModifiedTime().toInstant(),
861 					readAttributes.size());
862 			return attributes;
863 		} catch (IOException e) {
864 			return new Attributes(file, fs);
865 		}
866 	}
867 
868 	/**
869 	 * NFC normalize a file (on Mac), otherwise do nothing
870 	 *
871 	 * @param file
872 	 *            a {@link java.io.File}.
873 	 * @return on Mac: NFC normalized {@link java.io.File}, otherwise the passed
874 	 *         file
875 	 * @since 4.1
876 	 */
877 	public static File normalize(File file) {
878 		if (SystemReader.getInstance().isMacOS()) {
879 			// TODO: Would it be faster to check with isNormalized first
880 			// assuming normalized paths are much more common
881 			String normalized = Normalizer.normalize(file.getPath(),
882 					Normalizer.Form.NFC);
883 			return new File(normalized);
884 		}
885 		return file;
886 	}
887 
888 	/**
889 	 * On Mac: get NFC normalized form of given name, otherwise the given name.
890 	 *
891 	 * @param name
892 	 *            a {@link java.lang.String} object.
893 	 * @return on Mac: NFC normalized form of given name
894 	 * @since 4.1
895 	 */
896 	public static String normalize(String name) {
897 		if (SystemReader.getInstance().isMacOS()) {
898 			if (name == null)
899 				return null;
900 			return Normalizer.normalize(name, Normalizer.Form.NFC);
901 		}
902 		return name;
903 	}
904 
905 	/**
906 	 * Best-effort variation of {@link java.io.File#getCanonicalFile()}
907 	 * returning the input file if the file cannot be canonicalized instead of
908 	 * throwing {@link java.io.IOException}.
909 	 *
910 	 * @param file
911 	 *            to be canonicalized; may be {@code null}
912 	 * @return canonicalized file, or the unchanged input file if
913 	 *         canonicalization failed or if {@code file == null}
914 	 * @throws java.lang.SecurityException
915 	 *             if {@link java.io.File#getCanonicalFile()} throws one
916 	 * @since 4.2
917 	 */
918 	public static File canonicalize(File file) {
919 		if (file == null) {
920 			return null;
921 		}
922 		try {
923 			return file.getCanonicalFile();
924 		} catch (IOException e) {
925 			return file;
926 		}
927 	}
928 
929 	/**
930 	 * Convert a path to String, replacing separators as necessary.
931 	 *
932 	 * @param file
933 	 *            a {@link java.io.File}.
934 	 * @return file's path as a String
935 	 * @since 4.10
936 	 */
937 	public static String pathToString(File file) {
938 		final String path = file.getPath();
939 		if (SystemReader.getInstance().isWindows()) {
940 			return path.replace('\\', '/');
941 		}
942 		return path;
943 	}
944 
945 	/**
946 	 * Touch the given file
947 	 *
948 	 * @param f
949 	 *            the file to touch
950 	 * @throws IOException
951 	 * @since 5.1.8
952 	 */
953 	public static void touch(Path f) throws IOException {
954 		try (FileChannel fc = FileChannel.open(f, StandardOpenOption.CREATE,
955 				StandardOpenOption.APPEND, StandardOpenOption.SYNC)) {
956 			// touch
957 		}
958 		Files.setLastModifiedTime(f, FileTime.from(Instant.now()));
959 	}
960 
961 	/**
962 	 * Compute a delay in a {@code min..max} interval with random jitter.
963 	 *
964 	 * @param last
965 	 *            amount of delay waited before the last attempt. This is used
966 	 *            to seed the next delay interval. Should be 0 if there was no
967 	 *            prior delay.
968 	 * @param min
969 	 *            shortest amount of allowable delay between attempts.
970 	 * @param max
971 	 *            longest amount of allowable delay between attempts.
972 	 * @return new amount of delay to wait before the next attempt.
973 	 *
974 	 * @since 5.6
975 	 */
976 	public static long delay(long last, long min, long max) {
977 		long r = Math.max(0, last * 3 - min);
978 		if (r > 0) {
979 			int c = (int) Math.min(r + 1, Integer.MAX_VALUE);
980 			r = RNG.nextInt(c);
981 		}
982 		return Math.max(Math.min(min + r, max), min);
983 	}
984 }