1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 package org.eclipse.jgit.util;
45
46 import static java.nio.charset.StandardCharsets.UTF_8;
47 import static java.time.Instant.EPOCH;
48
49 import java.io.BufferedReader;
50 import java.io.ByteArrayInputStream;
51 import java.io.Closeable;
52 import java.io.File;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.InputStreamReader;
56 import java.io.OutputStream;
57 import java.io.OutputStreamWriter;
58 import java.io.PrintStream;
59 import java.io.Writer;
60 import java.nio.charset.Charset;
61 import java.nio.file.AccessDeniedException;
62 import java.nio.file.FileStore;
63 import java.nio.file.Files;
64 import java.nio.file.InvalidPathException;
65 import java.nio.file.Path;
66 import java.nio.file.attribute.BasicFileAttributes;
67 import java.nio.file.attribute.FileTime;
68 import java.security.AccessControlException;
69 import java.security.AccessController;
70 import java.security.PrivilegedAction;
71 import java.text.MessageFormat;
72 import java.time.Duration;
73 import java.time.Instant;
74 import java.util.ArrayList;
75 import java.util.Arrays;
76 import java.util.HashMap;
77 import java.util.Map;
78 import java.util.Objects;
79 import java.util.Optional;
80 import java.util.UUID;
81 import java.util.concurrent.CancellationException;
82 import java.util.concurrent.CompletableFuture;
83 import java.util.concurrent.ConcurrentHashMap;
84 import java.util.concurrent.ExecutionException;
85 import java.util.concurrent.ExecutorService;
86 import java.util.concurrent.Executors;
87 import java.util.concurrent.TimeUnit;
88 import java.util.concurrent.TimeoutException;
89 import java.util.concurrent.atomic.AtomicBoolean;
90 import java.util.concurrent.atomic.AtomicReference;
91 import java.util.concurrent.locks.Lock;
92 import java.util.concurrent.locks.ReentrantLock;
93
94 import org.eclipse.jgit.annotations.NonNull;
95 import org.eclipse.jgit.annotations.Nullable;
96 import org.eclipse.jgit.api.errors.JGitInternalException;
97 import org.eclipse.jgit.errors.CommandFailedException;
98 import org.eclipse.jgit.errors.ConfigInvalidException;
99 import org.eclipse.jgit.errors.LockFailedException;
100 import org.eclipse.jgit.internal.JGitText;
101 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
102 import org.eclipse.jgit.lib.Config;
103 import org.eclipse.jgit.lib.ConfigConstants;
104 import org.eclipse.jgit.lib.Constants;
105 import org.eclipse.jgit.lib.Repository;
106 import org.eclipse.jgit.lib.StoredConfig;
107 import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
108 import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy;
109 import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry;
110 import org.eclipse.jgit.util.ProcessResult.Status;
111 import org.slf4j.Logger;
112 import org.slf4j.LoggerFactory;
113
114
115
116
117 public abstract class FS {
118 private static final Logger LOG = LoggerFactory.getLogger(FS.class);
119
120
121
122
123
124
125
126 protected static final Entry[] NO_ENTRIES = {};
127
128 private volatile Boolean supportSymlinks;
129
130
131
132
133
134
135
136 public static class FSFactory {
137
138
139
140 protected FSFactory() {
141
142 }
143
144
145
146
147
148
149
150 public FS detect(Boolean cygwinUsed) {
151 if (SystemReader.getInstance().isWindows()) {
152 if (cygwinUsed == null) {
153 cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin());
154 }
155 if (cygwinUsed.booleanValue()) {
156 return new FS_Win32_Cygwin();
157 }
158 return new FS_Win32();
159 }
160 return new FS_POSIX();
161 }
162 }
163
164
165
166
167
168
169
170 public static class ExecutionResult {
171 private TemporaryBuffer stdout;
172
173 private TemporaryBuffer stderr;
174
175 private int rc;
176
177
178
179
180
181
182 public ExecutionResult(TemporaryBuffer./../org/eclipse/jgit/util/TemporaryBuffer.html#TemporaryBuffer">TemporaryBuffer stdout, TemporaryBuffer stderr,
183 int rc) {
184 this.stdout = stdout;
185 this.stderr = stderr;
186 this.rc = rc;
187 }
188
189
190
191
192 public TemporaryBuffer getStdout() {
193 return stdout;
194 }
195
196
197
198
199 public TemporaryBuffer getStderr() {
200 return stderr;
201 }
202
203
204
205
206 public int getRc() {
207 return rc;
208 }
209 }
210
211
212
213
214
215
216 public final static class FileStoreAttributes {
217
218 private static final Duration UNDEFINED_DURATION = Duration
219 .ofNanos(Long.MAX_VALUE);
220
221
222
223
224
225 public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration
226 .ofMillis(2000);
227
228
229
230
231
232
233 public static final FileStoreAttributes FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributes(
234 FALLBACK_TIMESTAMP_RESOLUTION);
235
236 private static final Map<FileStore, FileStoreAttributes> attributeCache = new ConcurrentHashMap<>();
237
238 private static final SimpleLruCache<Path, FileStoreAttributes> attrCacheByPath = new SimpleLruCache<>(
239 100, 0.2f);
240
241 private static AtomicBoolean background = new AtomicBoolean();
242
243 private static Map<FileStore, Lock> locks = new ConcurrentHashMap<>();
244
245 private static void setBackground(boolean async) {
246 background.set(async);
247 }
248
249 private static final String javaVersionPrefix = SystemReader
250 .getInstance().getHostname() + '|'
251 + System.getProperty("java.vendor") + '|'
252 + System.getProperty("java.version") + '|';
253
254 private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration
255 .ofMillis(10);
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272 public static void configureAttributesPathCache(int maxSize,
273 float purgeFactor) {
274 FileStoreAttributes.attrCacheByPath.configure(maxSize, purgeFactor);
275 }
276
277
278
279
280
281
282
283
284 public static FileStoreAttributes get(Path path) {
285 try {
286 path = path.toAbsolutePath();
287 Path dir = Files.isDirectory(path) ? path : path.getParent();
288 FileStoreAttributes cached = attrCacheByPath.get(dir);
289 if (cached != null) {
290 return cached;
291 }
292 FileStoreAttributes attrs = getFileStoreAttributes(dir);
293 attrCacheByPath.put(dir, attrs);
294 return attrs;
295 } catch (SecurityException e) {
296 return FALLBACK_FILESTORE_ATTRIBUTES;
297 }
298 }
299
300 private static FileStoreAttributes getFileStoreAttributes(Path dir) {
301 FileStore s;
302 try {
303 if (Files.exists(dir)) {
304 s = Files.getFileStore(dir);
305 FileStoreAttributes c = attributeCache.get(s);
306 if (c != null) {
307 return c;
308 }
309 if (!Files.isWritable(dir)) {
310
311 LOG.debug(
312 "{}: cannot measure timestamp resolution in read-only directory {}",
313 Thread.currentThread(), dir);
314 return FALLBACK_FILESTORE_ATTRIBUTES;
315 }
316 } else {
317
318 LOG.debug(
319 "{}: cannot measure timestamp resolution of unborn directory {}",
320 Thread.currentThread(), dir);
321 return FALLBACK_FILESTORE_ATTRIBUTES;
322 }
323
324 CompletableFuture<Optional<FileStoreAttributes>> f = CompletableFuture
325 .supplyAsync(() -> {
326 Lock lock = locks.computeIfAbsent(s,
327 l -> new ReentrantLock());
328 if (!lock.tryLock()) {
329 LOG.debug(
330 "{}: couldn't get lock to measure timestamp resolution in {}",
331 Thread.currentThread(), dir);
332 return Optional.empty();
333 }
334 Optional<FileStoreAttributes> attributes = Optional
335 .empty();
336 try {
337
338
339
340 FileStoreAttributes c = attributeCache
341 .get(s);
342 if (c != null) {
343 return Optional.of(c);
344 }
345 attributes = readFromConfig(s);
346 if (attributes.isPresent()) {
347 attributeCache.put(s, attributes.get());
348 return attributes;
349 }
350
351 Optional<Duration> resolution = measureFsTimestampResolution(
352 s, dir);
353 if (resolution.isPresent()) {
354 c = new FileStoreAttributes(
355 resolution.get());
356 attributeCache.put(s, c);
357
358
359 if (c.fsTimestampResolution
360 .toNanos() < 100_000_000L) {
361 c.minimalRacyInterval = measureMinimalRacyInterval(
362 dir);
363 }
364 if (LOG.isDebugEnabled()) {
365 LOG.debug(c.toString());
366 }
367 saveToConfig(s, c);
368 }
369 attributes = Optional.of(c);
370 } finally {
371 lock.unlock();
372 locks.remove(s);
373 }
374 return attributes;
375 });
376 f = f.exceptionally(e -> {
377 LOG.error(e.getLocalizedMessage(), e);
378 return Optional.empty();
379 });
380
381
382 Optional<FileStoreAttributes> d = background.get() ? f.get(
383 100, TimeUnit.MILLISECONDS) : f.get();
384 if (d.isPresent()) {
385 return d.get();
386 }
387
388 } catch (IOException | InterruptedException
389 | ExecutionException | CancellationException e) {
390 LOG.error(e.getMessage(), e);
391 } catch (TimeoutException | SecurityException e) {
392
393 }
394 LOG.debug("{}: use fallback timestamp resolution for directory {}",
395 Thread.currentThread(), dir);
396 return FALLBACK_FILESTORE_ATTRIBUTES;
397 }
398
399 @SuppressWarnings("boxing")
400 private static Duration measureMinimalRacyInterval(Path dir) {
401 LOG.debug("{}: start measure minimal racy interval in {}",
402 Thread.currentThread(), dir);
403 int n = 0;
404 int failures = 0;
405 long racyNanos = 0;
406 ArrayList<Long> deltas = new ArrayList<>();
407 Path probe = dir.resolve(".probe-" + UUID.randomUUID());
408 Instant end = Instant.now().plusSeconds(3);
409 try {
410 Files.createFile(probe);
411 do {
412 n++;
413 write(probe, "a");
414 FileSnapshot snapshot = FileSnapshot.save(probe.toFile());
415 read(probe);
416 write(probe, "b");
417 if (!snapshot.isModified(probe.toFile())) {
418 deltas.add(Long.valueOf(snapshot.lastDelta()));
419 racyNanos = snapshot.lastRacyThreshold();
420 failures++;
421 }
422 } while (Instant.now().compareTo(end) < 0);
423 } catch (IOException e) {
424 LOG.error(e.getMessage(), e);
425 return FALLBACK_MIN_RACY_INTERVAL;
426 } finally {
427 deleteProbe(probe);
428 }
429 if (failures > 0) {
430 Stats stats = new Stats();
431 for (Long d : deltas) {
432 stats.add(d);
433 }
434 LOG.debug(
435 "delta [ns] since modification FileSnapshot failed to detect\n"
436 + "count, failures, racy limit [ns], delta min [ns],"
437 + " delta max [ns], delta avg [ns],"
438 + " delta stddev [ns]\n"
439 + "{}, {}, {}, {}, {}, {}, {}",
440 n, failures, racyNanos, stats.min(), stats.max(),
441 stats.avg(), stats.stddev());
442 return Duration
443 .ofNanos(Double.valueOf(stats.max()).longValue());
444 }
445
446
447 LOG.debug("{}: no failures when measuring minimal racy interval",
448 Thread.currentThread());
449 return Duration.ZERO;
450 }
451
452 private static void write(Path p, String body) throws IOException {
453 FileUtils.mkdirs(p.getParent().toFile(), true);
454 try (Writer w = new OutputStreamWriter(Files.newOutputStream(p),
455 UTF_8)) {
456 w.write(body);
457 }
458 }
459
460 private static String read(Path p) throws IOException {
461 final byte[] body = IO.readFully(p.toFile());
462 return new String(body, 0, body.length, UTF_8);
463 }
464
465 private static Optional<Duration> measureFsTimestampResolution(
466 FileStore s, Path dir) {
467 LOG.debug("{}: start measure timestamp resolution {} in {}",
468 Thread.currentThread(), s, dir);
469 Path probe = dir.resolve(".probe-" + UUID.randomUUID());
470 try {
471 Files.createFile(probe);
472 FileTime t1 = Files.getLastModifiedTime(probe);
473 FileTime t2 = t1;
474 Instant t1i = t1.toInstant();
475 for (long i = 1; t2.compareTo(t1) <= 0; i += 1 + i / 20) {
476 Files.setLastModifiedTime(probe,
477 FileTime.from(t1i.plusNanos(i * 1000)));
478 t2 = Files.getLastModifiedTime(probe);
479 }
480 Duration fsResolution = Duration.between(t1.toInstant(), t2.toInstant());
481 Duration clockResolution = measureClockResolution();
482 fsResolution = fsResolution.plus(clockResolution);
483 LOG.debug("{}: end measure timestamp resolution {} in {}",
484 Thread.currentThread(), s, dir);
485 return Optional.of(fsResolution);
486 } catch (AccessDeniedException e) {
487 LOG.warn(e.getLocalizedMessage(), e);
488 } catch (IOException e) {
489 LOG.error(e.getLocalizedMessage(), e);
490 } finally {
491 deleteProbe(probe);
492 }
493 return Optional.empty();
494 }
495
496 private static Duration measureClockResolution() {
497 Duration clockResolution = Duration.ZERO;
498 for (int i = 0; i < 10; i++) {
499 Instant t1 = Instant.now();
500 Instant t2 = t1;
501 while (t2.compareTo(t1) <= 0) {
502 t2 = Instant.now();
503 }
504 Duration r = Duration.between(t1, t2);
505 if (r.compareTo(clockResolution) > 0) {
506 clockResolution = r;
507 }
508 }
509 return clockResolution;
510 }
511
512 private static void deleteProbe(Path probe) {
513 try {
514 FileUtils.delete(probe.toFile(),
515 FileUtils.SKIP_MISSING | FileUtils.RETRY);
516 } catch (IOException e) {
517 LOG.error(e.getMessage(), e);
518 }
519 }
520
521 private static Optional<FileStoreAttributes> readFromConfig(
522 FileStore s) {
523 StoredConfig userConfig;
524 try {
525 userConfig = SystemReader.getInstance().getUserConfig();
526 } catch (IOException | ConfigInvalidException e) {
527 LOG.error(JGitText.get().readFileStoreAttributesFailed, e);
528 return Optional.empty();
529 }
530 String key = getConfigKey(s);
531 Duration resolution = Duration.ofNanos(userConfig.getTimeUnit(
532 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
533 ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
534 UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
535 if (UNDEFINED_DURATION.equals(resolution)) {
536 return Optional.empty();
537 }
538 Duration minRacyThreshold = Duration.ofNanos(userConfig.getTimeUnit(
539 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
540 ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
541 UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
542 FileStoreAttributes c = new FileStoreAttributes(resolution);
543 if (!UNDEFINED_DURATION.equals(minRacyThreshold)) {
544 c.minimalRacyInterval = minRacyThreshold;
545 }
546 return Optional.of(c);
547 }
548
549 private static void saveToConfig(FileStore s,
550 FileStoreAttributes c) {
551 StoredConfig jgitConfig;
552 try {
553 jgitConfig = SystemReader.getInstance().getJGitConfig();
554 } catch (IOException | ConfigInvalidException e) {
555 LOG.error(JGitText.get().saveFileStoreAttributesFailed, e);
556 return;
557 }
558 long resolution = c.getFsTimestampResolution().toNanos();
559 TimeUnit resolutionUnit = getUnit(resolution);
560 long resolutionValue = resolutionUnit.convert(resolution,
561 TimeUnit.NANOSECONDS);
562
563 long minRacyThreshold = c.getMinimalRacyInterval().toNanos();
564 TimeUnit minRacyThresholdUnit = getUnit(minRacyThreshold);
565 long minRacyThresholdValue = minRacyThresholdUnit
566 .convert(minRacyThreshold, TimeUnit.NANOSECONDS);
567
568 final int max_retries = 5;
569 int retries = 0;
570 boolean succeeded = false;
571 String key = getConfigKey(s);
572 while (!succeeded && retries < max_retries) {
573 try {
574 jgitConfig.setString(
575 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
576 ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
577 String.format("%d %s",
578 Long.valueOf(resolutionValue),
579 resolutionUnit.name().toLowerCase()));
580 jgitConfig.setString(
581 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
582 ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
583 String.format("%d %s",
584 Long.valueOf(minRacyThresholdValue),
585 minRacyThresholdUnit.name().toLowerCase()));
586 jgitConfig.save();
587 succeeded = true;
588 } catch (LockFailedException e) {
589
590 try {
591 retries++;
592 if (retries < max_retries) {
593 Thread.sleep(100);
594 LOG.debug("locking {} failed, retries {}/{}",
595 jgitConfig, Integer.valueOf(retries),
596 Integer.valueOf(max_retries));
597 } else {
598 LOG.warn(MessageFormat.format(
599 JGitText.get().lockFailedRetry, jgitConfig,
600 Integer.valueOf(retries)));
601 }
602 } catch (InterruptedException e1) {
603 Thread.currentThread().interrupt();
604 break;
605 }
606 } catch (IOException e) {
607 LOG.error(MessageFormat.format(
608 JGitText.get().cannotSaveConfig, jgitConfig), e);
609 break;
610 }
611 }
612 }
613
614 private static String getConfigKey(FileStore s) {
615 final String storeKey;
616 if (SystemReader.getInstance().isWindows()) {
617 Object attribute = null;
618 try {
619 attribute = s.getAttribute("volume:vsn");
620 } catch (IOException ignored) {
621
622 }
623 if (attribute instanceof Integer) {
624 storeKey = attribute.toString();
625 } else {
626 storeKey = s.name();
627 }
628 } else {
629 storeKey = s.name();
630 }
631 return javaVersionPrefix + storeKey;
632 }
633
634 private static TimeUnit getUnit(long nanos) {
635 TimeUnit unit;
636 if (nanos < 200_000L) {
637 unit = TimeUnit.NANOSECONDS;
638 } else if (nanos < 200_000_000L) {
639 unit = TimeUnit.MICROSECONDS;
640 } else {
641 unit = TimeUnit.MILLISECONDS;
642 }
643 return unit;
644 }
645
646 private final @NonNull Duration fsTimestampResolution;
647
648 private Duration minimalRacyInterval;
649
650
651
652
653
654
655 public Duration getMinimalRacyInterval() {
656 return minimalRacyInterval;
657 }
658
659
660
661
662 @NonNull
663 public Duration getFsTimestampResolution() {
664 return fsTimestampResolution;
665 }
666
667
668
669
670
671
672
673 public FileStoreAttributes(
674 @NonNull Duration fsTimestampResolution) {
675 this.fsTimestampResolution = fsTimestampResolution;
676 this.minimalRacyInterval = Duration.ZERO;
677 }
678
679 @SuppressWarnings({ "nls", "boxing" })
680 @Override
681 public String toString() {
682 return String.format(
683 "FileStoreAttributes[fsTimestampResolution=%,d µs, "
684 + "minimalRacyInterval=%,d µs]",
685 fsTimestampResolution.toNanos() / 1000,
686 minimalRacyInterval.toNanos() / 1000);
687 }
688
689 }
690
691
692 public static final FS DETECTED = detect();
693
694 private volatile static FSFactory factory;
695
696
697
698
699
700
701 public static FS detect() {
702 return detect(null);
703 }
704
705
706
707
708
709
710
711
712
713
714 public static void setAsyncFileStoreAttributes(boolean asynch) {
715 FileStoreAttributes.setBackground(asynch);
716 }
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738 public static FS detect(Boolean cygwinUsed) {
739 if (factory == null) {
740 factory = new FS.FSFactory();
741 }
742 return factory.detect(cygwinUsed);
743 }
744
745
746
747
748
749
750
751
752
753
754
755 public static FileStoreAttributes getFileStoreAttributes(
756 @NonNull Path dir) {
757 return FileStoreAttributes.get(dir);
758 }
759
760 private volatile Holder<File> userHome;
761
762 private volatile Holder<File> gitSystemConfig;
763
764
765
766
767 protected FS() {
768
769 }
770
771
772
773
774
775
776
777 protected FSme="FS" href="../../../../org/eclipse/jgit/util/FS.html#FS">FS(FS src) {
778 userHome = src.userHome;
779 gitSystemConfig = src.gitSystemConfig;
780 }
781
782
783
784
785
786
787 public abstract FS newInstance();
788
789
790
791
792
793
794
795 public abstract boolean supportsExecute();
796
797
798
799
800
801
802
803
804
805
806
807
808 public boolean supportsAtomicCreateNewFile() {
809 return true;
810 }
811
812
813
814
815
816
817
818
819 public boolean supportsSymlinks() {
820 if (supportSymlinks == null) {
821 detectSymlinkSupport();
822 }
823 return Boolean.TRUE.equals(supportSymlinks);
824 }
825
826 private void detectSymlinkSupport() {
827 File tempFile = null;
828 try {
829 tempFile = File.createTempFile("tempsymlinktarget", "");
830 File linkName = new File(tempFile.getParentFile(), "tempsymlink");
831 createSymLink(linkName, tempFile.getPath());
832 supportSymlinks = Boolean.TRUE;
833 linkName.delete();
834 } catch (IOException | UnsupportedOperationException | SecurityException
835 | InternalError e) {
836 supportSymlinks = Boolean.FALSE;
837 } finally {
838 if (tempFile != null) {
839 try {
840 FileUtils.delete(tempFile);
841 } catch (IOException e) {
842 LOG.error(JGitText.get().cannotDeleteFile, tempFile);
843 }
844 }
845 }
846 }
847
848
849
850
851
852
853 public abstract boolean isCaseSensitive();
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869 public abstract boolean canExecute(File f);
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884 public abstract boolean setExecute(File f, boolean canExec);
885
886
887
888
889
890
891
892
893
894
895
896
897
898 @Deprecated
899 public long lastModified(File f) throws IOException {
900 return FileUtils.lastModified(f);
901 }
902
903
904
905
906
907
908
909
910
911
912
913 public Instant lastModifiedInstant(Path p) {
914 return FileUtils.lastModifiedInstant(p);
915 }
916
917
918
919
920
921
922
923
924
925
926
927 public Instant lastModifiedInstant(File f) {
928 return FileUtils.lastModifiedInstant(f.toPath());
929 }
930
931
932
933
934
935
936
937
938
939
940
941
942
943 @Deprecated
944 public void setLastModified(File f, long time) throws IOException {
945 FileUtils.setLastModified(f, time);
946 }
947
948
949
950
951
952
953
954
955
956
957
958
959 public void setLastModified(Path p, Instant time) throws IOException {
960 FileUtils.setLastModified(p, time);
961 }
962
963
964
965
966
967
968
969
970
971
972
973 public long length(File path) throws IOException {
974 return FileUtils.getLength(path);
975 }
976
977
978
979
980
981
982
983
984
985
986 public void delete(File f) throws IOException {
987 FileUtils.delete(f);
988 }
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008 public File resolve(File dir, String name) {
1009 final File abspn = new File(name);
1010 if (abspn.isAbsolute())
1011 return abspn;
1012 return new File(dir, name);
1013 }
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026 public File userHome() {
1027 Holder<File> p = userHome;
1028 if (p == null) {
1029 p = new Holder<>(userHomeImpl());
1030 userHome = p;
1031 }
1032 return p.value;
1033 }
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043 public FS setUserHome(File path) {
1044 userHome = new Holder<>(path);
1045 return this;
1046 }
1047
1048
1049
1050
1051
1052
1053 public abstract boolean retryFailedLockFileCommit();
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064 public BasicFileAttributes fileAttributes(File file) throws IOException {
1065 return FileUtils.fileAttributes(file);
1066 }
1067
1068
1069
1070
1071
1072
1073 protected File userHomeImpl() {
1074 final String home = AccessController.doPrivileged(
1075 (PrivilegedAction<String>) () -> System.getProperty("user.home")
1076 );
1077 if (home == null || home.length() == 0)
1078 return null;
1079 return new File(home).getAbsoluteFile();
1080 }
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093 protected static File searchPath(String path, String... lookFor) {
1094 if (path == null)
1095 return null;
1096
1097 for (String p : path.split(File.pathSeparator)) {
1098 for (String command : lookFor) {
1099 final File file = new File(p, command);
1100 try {
1101 if (file.isFile()) {
1102 return file.getAbsoluteFile();
1103 }
1104 } catch (SecurityException e) {
1105 LOG.warn(MessageFormat.format(
1106 JGitText.get().skipNotAccessiblePath,
1107 file.getPath()));
1108 }
1109 }
1110 }
1111 return null;
1112 }
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128 @Nullable
1129 protected static String readPipe(File dir, String[] command,
1130 String encoding) throws CommandFailedException {
1131 return readPipe(dir, command, encoding, null);
1132 }
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152 @Nullable
1153 protected static String readPipe(File dir, String[] command,
1154 String encoding, Map<String, String> env)
1155 throws CommandFailedException {
1156 final boolean debug = LOG.isDebugEnabled();
1157 try {
1158 if (debug) {
1159 LOG.debug("readpipe " + Arrays.asList(command) + ","
1160 + dir);
1161 }
1162 ProcessBuilder pb = new ProcessBuilder(command);
1163 pb.directory(dir);
1164 if (env != null) {
1165 pb.environment().putAll(env);
1166 }
1167 Process p;
1168 try {
1169 p = pb.start();
1170 } catch (IOException e) {
1171
1172 throw new CommandFailedException(-1, e.getMessage(), e);
1173 }
1174 p.getOutputStream().close();
1175 GobblerThread gobbler = new GobblerThread(p, command, dir);
1176 gobbler.start();
1177 String r = null;
1178 try (BufferedReader lineRead = new BufferedReader(
1179 new InputStreamReader(p.getInputStream(), encoding))) {
1180 r = lineRead.readLine();
1181 if (debug) {
1182 LOG.debug("readpipe may return '" + r + "'");
1183 LOG.debug("remaining output:\n");
1184 String l;
1185 while ((l = lineRead.readLine()) != null) {
1186 LOG.debug(l);
1187 }
1188 }
1189 }
1190
1191 for (;;) {
1192 try {
1193 int rc = p.waitFor();
1194 gobbler.join();
1195 if (rc == 0 && !gobbler.fail.get()) {
1196 return r;
1197 }
1198 if (debug) {
1199 LOG.debug("readpipe rc=" + rc);
1200 }
1201 throw new CommandFailedException(rc,
1202 gobbler.errorMessage.get(),
1203 gobbler.exception.get());
1204 } catch (InterruptedException ie) {
1205
1206 }
1207 }
1208 } catch (IOException e) {
1209 LOG.error("Caught exception in FS.readPipe()", e);
1210 } catch (AccessControlException e) {
1211 LOG.warn(MessageFormat.format(
1212 JGitText.get().readPipeIsNotAllowedRequiredPermission,
1213 command, dir, e.getPermission()));
1214 } catch (SecurityException e) {
1215 LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowed,
1216 command, dir));
1217 }
1218 if (debug) {
1219 LOG.debug("readpipe returns null");
1220 }
1221 return null;
1222 }
1223
1224 private static class GobblerThread extends Thread {
1225
1226
1227 private static final int PROCESS_EXIT_TIMEOUT = 5;
1228
1229 private final Process p;
1230 private final String desc;
1231 private final String dir;
1232 final AtomicBoolean fail = new AtomicBoolean();
1233 final AtomicReference<String> errorMessage = new AtomicReference<>();
1234 final AtomicReference<Throwable> exception = new AtomicReference<>();
1235
1236 GobblerThread(Process p, String[] command, File dir) {
1237 this.p = p;
1238 this.desc = Arrays.toString(command);
1239 this.dir = Objects.toString(dir);
1240 }
1241
1242 @Override
1243 public void run() {
1244 StringBuilder err = new StringBuilder();
1245 try (InputStream is = p.getErrorStream()) {
1246 int ch;
1247 while ((ch = is.read()) != -1) {
1248 err.append((char) ch);
1249 }
1250 } catch (IOException e) {
1251 if (waitForProcessCompletion(e) && p.exitValue() != 0) {
1252 setError(e, e.getMessage(), p.exitValue());
1253 fail.set(true);
1254 } else {
1255
1256
1257 }
1258 } finally {
1259 if (waitForProcessCompletion(null) && err.length() > 0) {
1260 setError(null, err.toString(), p.exitValue());
1261 if (p.exitValue() != 0) {
1262 fail.set(true);
1263 }
1264 }
1265 }
1266 }
1267
1268 @SuppressWarnings("boxing")
1269 private boolean waitForProcessCompletion(IOException originalError) {
1270 try {
1271 if (!p.waitFor(PROCESS_EXIT_TIMEOUT, TimeUnit.SECONDS)) {
1272 setError(originalError, MessageFormat.format(
1273 JGitText.get().commandClosedStderrButDidntExit,
1274 desc, PROCESS_EXIT_TIMEOUT), -1);
1275 fail.set(true);
1276 return false;
1277 }
1278 } catch (InterruptedException e) {
1279 setError(originalError, MessageFormat.format(
1280 JGitText.get().threadInterruptedWhileRunning, desc), -1);
1281 fail.set(true);
1282 return false;
1283 }
1284 return true;
1285 }
1286
1287 private void setError(IOException e, String message, int exitCode) {
1288 exception.set(e);
1289 errorMessage.set(MessageFormat.format(
1290 JGitText.get().exceptionCaughtDuringExecutionOfCommand,
1291 desc, dir, Integer.valueOf(exitCode), message));
1292 }
1293 }
1294
1295
1296
1297
1298
1299
1300
1301
1302 protected abstract File discoverGitExe();
1303
1304
1305
1306
1307
1308
1309
1310
1311 protected File discoverGitSystemConfig() {
1312 File gitExe = discoverGitExe();
1313 if (gitExe == null) {
1314 return null;
1315 }
1316
1317
1318 String v;
1319 try {
1320 v = readPipe(gitExe.getParentFile(),
1321 new String[] { "git", "--version" },
1322 Charset.defaultCharset().name());
1323 } catch (CommandFailedException e) {
1324 LOG.warn(e.getMessage());
1325 return null;
1326 }
1327 if (StringUtils.isEmptyOrNull(v)
1328 || (v != null && v.startsWith("jgit"))) {
1329 return null;
1330 }
1331
1332
1333
1334 Map<String, String> env = new HashMap<>();
1335 env.put("GIT_EDITOR", "echo");
1336
1337 String w;
1338 try {
1339 w = readPipe(gitExe.getParentFile(),
1340 new String[] { "git", "config", "--system", "--edit" },
1341 Charset.defaultCharset().name(), env);
1342 } catch (CommandFailedException e) {
1343 LOG.warn(e.getMessage());
1344 return null;
1345 }
1346 if (StringUtils.isEmptyOrNull(w)) {
1347 return null;
1348 }
1349
1350 return new File(w);
1351 }
1352
1353
1354
1355
1356
1357
1358
1359
1360 public File getGitSystemConfig() {
1361 if (gitSystemConfig == null) {
1362 gitSystemConfig = new Holder<>(discoverGitSystemConfig());
1363 }
1364 return gitSystemConfig.value;
1365 }
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375 public FS setGitSystemConfig(File configFile) {
1376 gitSystemConfig = new Holder<>(configFile);
1377 return this;
1378 }
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389 protected static File resolveGrandparentFile(File grandchild) {
1390 if (grandchild != null) {
1391 File parent = grandchild.getParentFile();
1392 if (parent != null)
1393 return parent.getParentFile();
1394 }
1395 return null;
1396 }
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407 public String readSymLink(File path) throws IOException {
1408 return FileUtils.readSymLink(path);
1409 }
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420 public boolean isSymLink(File path) throws IOException {
1421 return FileUtils.isSymlink(path);
1422 }
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433 public boolean exists(File path) {
1434 return FileUtils.exists(path);
1435 }
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446 public boolean isDirectory(File path) {
1447 return FileUtils.isDirectory(path);
1448 }
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459 public boolean isFile(File path) {
1460 return FileUtils.isFile(path);
1461 }
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474 public boolean isHidden(File path) throws IOException {
1475 return FileUtils.isHidden(path);
1476 }
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488 public void setHidden(File path, boolean hidden) throws IOException {
1489 FileUtils.setHidden(path, hidden);
1490 }
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502 public void createSymLink(File path, String target) throws IOException {
1503 FileUtils.createSymLink(path, target);
1504 }
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519 @Deprecated
1520 public boolean createNewFile(File path) throws IOException {
1521 return path.createNewFile();
1522 }
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533 public static class LockToken implements Closeable {
1534 private boolean isCreated;
1535
1536 private Optional<Path> link;
1537
1538 LockToken(boolean isCreated, Optional<Path> link) {
1539 this.isCreated = isCreated;
1540 this.link = link;
1541 }
1542
1543
1544
1545
1546 public boolean isCreated() {
1547 return isCreated;
1548 }
1549
1550 @Override
1551 public void close() {
1552 if (!link.isPresent()) {
1553 return;
1554 }
1555 Path p = link.get();
1556 if (!Files.exists(p)) {
1557 return;
1558 }
1559 try {
1560 Files.delete(p);
1561 } catch (IOException e) {
1562 LOG.error(MessageFormat
1563 .format(JGitText.get().closeLockTokenFailed, this), e);
1564 }
1565 }
1566
1567 @Override
1568 public String toString() {
1569 return "LockToken [lockCreated=" + isCreated +
1570 ", link="
1571 + (link.isPresent() ? link.get().getFileName() + "]"
1572 : "<null>]");
1573 }
1574 }
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588 public LockToken createNewFileAtomic(File path) throws IOException {
1589 return new LockToken(path.createNewFile(), Optional.empty());
1590 }
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606 public String relativize(String base, String other) {
1607 return FileUtils.relativizePath(base, other, File.separator, this.isCaseSensitive());
1608 }
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621 public Entry[] list(File directory, FileModeStrategy fileModeStrategy) {
1622 final File[] all = directory.listFiles();
1623 if (all == null) {
1624 return NO_ENTRIES;
1625 }
1626 final Entry[] result = new Entry[all.length];
1627 for (int i = 0; i < result.length; i++) {
1628 result[i] = new FileEntry(all[i], this, fileModeStrategy);
1629 }
1630 return result;
1631 }
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655 public ProcessResult runHookIfPresent(Repository repository,
1656 final String hookName,
1657 String[] args) throws JGitInternalException {
1658 return runHookIfPresent(repository, hookName, args, System.out, System.err,
1659 null);
1660 }
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690 public ProcessResult runHookIfPresent(Repository repository,
1691 final String hookName,
1692 String[] args, PrintStream outRedirect, PrintStream errRedirect,
1693 String stdinArgs) throws JGitInternalException {
1694 return new ProcessResult(Status.NOT_SUPPORTED);
1695 }
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726 protected ProcessResult internalRunHookIfPresent(Repository repository,
1727 final String hookName, String[] args, PrintStream outRedirect,
1728 PrintStream errRedirect, String stdinArgs)
1729 throws JGitInternalException {
1730 File hookFile = findHook(repository, hookName);
1731 if (hookFile == null || hookName == null) {
1732 return new ProcessResult(Status.NOT_PRESENT);
1733 }
1734
1735 File runDirectory = getRunDirectory(repository, hookName);
1736 if (runDirectory == null) {
1737 return new ProcessResult(Status.NOT_PRESENT);
1738 }
1739 String cmd = hookFile.getAbsolutePath();
1740 ProcessBuilder hookProcess = runInShell(cmd, args);
1741 hookProcess.directory(runDirectory.getAbsoluteFile());
1742 Map<String, String> environment = hookProcess.environment();
1743 environment.put(Constants.GIT_DIR_KEY,
1744 repository.getDirectory().getAbsolutePath());
1745 if (!repository.isBare()) {
1746 environment.put(Constants.GIT_WORK_TREE_KEY,
1747 repository.getWorkTree().getAbsolutePath());
1748 }
1749 try {
1750 return new ProcessResult(runProcess(hookProcess, outRedirect,
1751 errRedirect, stdinArgs), Status.OK);
1752 } catch (IOException e) {
1753 throw new JGitInternalException(MessageFormat.format(
1754 JGitText.get().exceptionCaughtDuringExecutionOfHook,
1755 hookName), e);
1756 } catch (InterruptedException e) {
1757 throw new JGitInternalException(MessageFormat.format(
1758 JGitText.get().exceptionHookExecutionInterrupted,
1759 hookName), e);
1760 }
1761 }
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775 public File findHook(Repository repository, String hookName) {
1776 if (hookName == null) {
1777 return null;
1778 }
1779 File hookDir = getHooksDirectory(repository);
1780 if (hookDir == null) {
1781 return null;
1782 }
1783 File hookFile = new File(hookDir, hookName);
1784 if (hookFile.isAbsolute()) {
1785 if (!hookFile.exists() || (FS.DETECTED.supportsExecute()
1786 && !FS.DETECTED.canExecute(hookFile))) {
1787 return null;
1788 }
1789 } else {
1790 try {
1791 File runDirectory = getRunDirectory(repository, hookName);
1792 if (runDirectory == null) {
1793 return null;
1794 }
1795 Path hookPath = runDirectory.getAbsoluteFile().toPath()
1796 .resolve(hookFile.toPath());
1797 FS fs = repository.getFS();
1798 if (fs == null) {
1799 fs = FS.DETECTED;
1800 }
1801 if (!Files.exists(hookPath) || (fs.supportsExecute()
1802 && !fs.canExecute(hookPath.toFile()))) {
1803 return null;
1804 }
1805 hookFile = hookPath.toFile();
1806 } catch (InvalidPathException e) {
1807 LOG.warn(MessageFormat.format(JGitText.get().invalidHooksPath,
1808 hookFile));
1809 return null;
1810 }
1811 }
1812 return hookFile;
1813 }
1814
1815 private File getRunDirectory(Repository repository,
1816 @NonNull String hookName) {
1817 if (repository.isBare()) {
1818 return repository.getDirectory();
1819 }
1820 switch (hookName) {
1821 case "pre-receive":
1822 case "update":
1823 case "post-receive":
1824 case "post-update":
1825 case "push-to-checkout":
1826 return repository.getDirectory();
1827 default:
1828 return repository.getWorkTree();
1829 }
1830 }
1831
1832 private File getHooksDirectory(Repository repository) {
1833 Config config = repository.getConfig();
1834 String hooksDir = config.getString(ConfigConstants.CONFIG_CORE_SECTION,
1835 null, ConfigConstants.CONFIG_KEY_HOOKS_PATH);
1836 if (hooksDir != null) {
1837 return new File(hooksDir);
1838 }
1839 File dir = repository.getDirectory();
1840 return dir == null ? null : new File(dir, Constants.HOOKS);
1841 }
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868 public int runProcess(ProcessBuilder processBuilder,
1869 OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
1870 throws IOException, InterruptedException {
1871 InputStream in = (stdinArgs == null) ? null : new ByteArrayInputStream(
1872 stdinArgs.getBytes(UTF_8));
1873 return runProcess(processBuilder, outRedirect, errRedirect, in);
1874 }
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904 public int runProcess(ProcessBuilder processBuilder,
1905 OutputStream outRedirect, OutputStream errRedirect,
1906 InputStream inRedirect) throws IOException,
1907 InterruptedException {
1908 final ExecutorService executor = Executors.newFixedThreadPool(2);
1909 Process process = null;
1910
1911
1912 IOException ioException = null;
1913 try {
1914 process = processBuilder.start();
1915 executor.execute(
1916 new StreamGobbler(process.getErrorStream(), errRedirect));
1917 executor.execute(
1918 new StreamGobbler(process.getInputStream(), outRedirect));
1919 @SuppressWarnings("resource")
1920 OutputStream outputStream = process.getOutputStream();
1921 try {
1922 if (inRedirect != null) {
1923 new StreamGobbler(inRedirect, outputStream).copy();
1924 }
1925 } finally {
1926 try {
1927 outputStream.close();
1928 } catch (IOException e) {
1929
1930
1931
1932
1933
1934
1935 }
1936 }
1937 return process.waitFor();
1938 } catch (IOException e) {
1939 ioException = e;
1940 } finally {
1941 shutdownAndAwaitTermination(executor);
1942 if (process != null) {
1943 try {
1944 process.waitFor();
1945 } catch (InterruptedException e) {
1946
1947
1948
1949
1950 Thread.interrupted();
1951 }
1952
1953
1954
1955 if (inRedirect != null) {
1956 inRedirect.close();
1957 }
1958 try {
1959 process.getErrorStream().close();
1960 } catch (IOException e) {
1961 ioException = ioException != null ? ioException : e;
1962 }
1963 try {
1964 process.getInputStream().close();
1965 } catch (IOException e) {
1966 ioException = ioException != null ? ioException : e;
1967 }
1968 try {
1969 process.getOutputStream().close();
1970 } catch (IOException e) {
1971 ioException = ioException != null ? ioException : e;
1972 }
1973 process.destroy();
1974 }
1975 }
1976
1977 throw ioException;
1978 }
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993 private static boolean shutdownAndAwaitTermination(ExecutorService pool) {
1994 boolean hasShutdown = true;
1995 pool.shutdown();
1996 try {
1997
1998 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
1999 pool.shutdownNow();
2000
2001 if (!pool.awaitTermination(60, TimeUnit.SECONDS))
2002 hasShutdown = false;
2003 }
2004 } catch (InterruptedException ie) {
2005
2006 pool.shutdownNow();
2007
2008 Thread.currentThread().interrupt();
2009 hasShutdown = false;
2010 }
2011 return hasShutdown;
2012 }
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026 public abstract ProcessBuilder runInShell(String cmd, String[] args);
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040 public ExecutionResult execute(ProcessBuilder pb, InputStream in)
2041 throws IOException, InterruptedException {
2042 try (TemporaryBuffer stdout = new TemporaryBuffer.LocalFile(null);
2043 TemporaryBuffer stderr = new TemporaryBuffer.Heap(1024,
2044 1024 * 1024)) {
2045 int rc = runProcess(pb, stdout, stderr, in);
2046 return new ExecutionResult(stdout, stderr, rc);
2047 }
2048 }
2049
2050 private static class Holder<V> {
2051 final V value;
2052
2053 Holder(V value) {
2054 this.value = value;
2055 }
2056 }
2057
2058
2059
2060
2061
2062
2063 public static class Attributes {
2064
2065
2066
2067
2068 public boolean isDirectory() {
2069 return isDirectory;
2070 }
2071
2072
2073
2074
2075 public boolean isExecutable() {
2076 return isExecutable;
2077 }
2078
2079
2080
2081
2082 public boolean isSymbolicLink() {
2083 return isSymbolicLink;
2084 }
2085
2086
2087
2088
2089 public boolean isRegularFile() {
2090 return isRegularFile;
2091 }
2092
2093
2094
2095
2096 public long getCreationTime() {
2097 return creationTime;
2098 }
2099
2100
2101
2102
2103
2104
2105 @Deprecated
2106 public long getLastModifiedTime() {
2107 return lastModifiedInstant.toEpochMilli();
2108 }
2109
2110
2111
2112
2113
2114 public Instant getLastModifiedInstant() {
2115 return lastModifiedInstant;
2116 }
2117
2118 private final boolean isDirectory;
2119
2120 private final boolean isSymbolicLink;
2121
2122 private final boolean isRegularFile;
2123
2124 private final long creationTime;
2125
2126 private final Instant lastModifiedInstant;
2127
2128 private final boolean isExecutable;
2129
2130 private final File file;
2131
2132 private final boolean exists;
2133
2134
2135
2136
2137 protected long length = -1;
2138
2139 final FS fs;
2140
2141 Attributes(FS fs, File file, boolean exists, boolean isDirectory,
2142 boolean isExecutable, boolean isSymbolicLink,
2143 boolean isRegularFile, long creationTime,
2144 Instant lastModifiedInstant, long length) {
2145 this.fs = fs;
2146 this.file = file;
2147 this.exists = exists;
2148 this.isDirectory = isDirectory;
2149 this.isExecutable = isExecutable;
2150 this.isSymbolicLink = isSymbolicLink;
2151 this.isRegularFile = isRegularFile;
2152 this.creationTime = creationTime;
2153 this.lastModifiedInstant = lastModifiedInstant;
2154 this.length = length;
2155 }
2156
2157
2158
2159
2160
2161
2162
2163
2164 public Attributes(File path, FS fs) {
2165 this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L);
2166 }
2167
2168
2169
2170
2171 public long getLength() {
2172 if (length == -1)
2173 return length = file.length();
2174 return length;
2175 }
2176
2177
2178
2179
2180 public String getName() {
2181 return file.getName();
2182 }
2183
2184
2185
2186
2187 public File getFile() {
2188 return file;
2189 }
2190
2191 boolean exists() {
2192 return exists;
2193 }
2194 }
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204 public Attributes getAttributes(File path) {
2205 boolean isDirectory = isDirectory(path);
2206 boolean isFile = !isDirectory && path.isFile();
2207 assert path.exists() == isDirectory || isFile;
2208 boolean exists = isDirectory || isFile;
2209 boolean canExecute = exists && !isDirectory && canExecute(path);
2210 boolean isSymlink = false;
2211 Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH;
2212 long createTime = 0L;
2213 return new Attributes(this, path, exists, isDirectory, canExecute,
2214 isSymlink, isFile, createTime, lastModified, -1);
2215 }
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225 public File normalize(File file) {
2226 return file;
2227 }
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237 public String normalize(String name) {
2238 return name;
2239 }
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253 private static class StreamGobbler implements Runnable {
2254 private InputStream in;
2255
2256 private OutputStream out;
2257
2258 public StreamGobbler(InputStream stream, OutputStream output) {
2259 this.in = stream;
2260 this.out = output;
2261 }
2262
2263 @Override
2264 public void run() {
2265 try {
2266 copy();
2267 } catch (IOException e) {
2268
2269 }
2270 }
2271
2272 void copy() throws IOException {
2273 boolean writeFailure = false;
2274 byte buffer[] = new byte[4096];
2275 int readBytes;
2276 while ((readBytes = in.read(buffer)) != -1) {
2277
2278
2279
2280 if (!writeFailure && out != null) {
2281 try {
2282 out.write(buffer, 0, readBytes);
2283 out.flush();
2284 } catch (IOException e) {
2285 writeFailure = true;
2286 }
2287 }
2288 }
2289 }
2290 }
2291 }