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