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