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 }