1717
1818import java .io .File ;
1919import java .io .IOException ;
20+ import java .nio .file .AtomicMoveNotSupportedException ;
21+ import java .nio .file .DirectoryNotEmptyException ;
2022import java .nio .file .FileAlreadyExistsException ;
2123import java .nio .file .FileSystemException ;
2224import java .nio .file .FileVisitResult ;
2325import java .nio .file .Files ;
2426import java .nio .file .Path ;
2527import java .nio .file .Paths ;
2628import java .nio .file .SimpleFileVisitor ;
29+ import java .nio .file .StandardCopyOption ;
2730import java .nio .file .attribute .BasicFileAttributes ;
28- import java .time .Duration ;
29- import java .util .concurrent .TimeoutException ;
3031import java .util .function .Supplier ;
3132
3233import javax .annotation .Nonnull ;
@@ -55,70 +56,66 @@ private File shadowCopyRoot() {
5556 }
5657
5758 public void addEntry (String key , File orig ) {
58- // prevent concurrent adding of entry with same key
59- if (!reserveSubFolder (key )) {
60- logger .debug ("Shadow copy entry already in progress: {}. Awaiting finalization." , key );
59+ File target = entry (key , orig .getName ());
60+ if (target .exists ()) {
61+ logger .debug ("Shadow copy entry already exists, not overwriting: {}" , key );
62+ } else {
6163 try {
62- NpmResourceHelper .awaitFileDeleted (markerFilePath (key ).toFile (), Duration .ofSeconds (120 ));
63- } catch (TimeoutException e ) {
64- throw new RuntimeException (e );
64+ storeEntry (key , orig , target );
65+ } catch (Throwable ex ) {
66+ // Log but don't fail
67+ logger .warn ("Unable to store cache entry for {}" , key , ex );
6568 }
6669 }
70+ }
71+
72+ private void storeEntry (String key , File orig , File target ) throws IOException {
73+ // Create a temp directory in the same directory as target
74+ Files .createDirectories (target .toPath ().getParent ());
75+ Path tempDirectory = Files .createTempDirectory (target .toPath ().getParent (), key );
76+ logger .debug ("Will store entry {} to temporary directory {}, which is a sibling of the ultimate target {}" , orig , tempDirectory , target );
77+
6778 try {
68- storeEntry (key , orig );
79+ // Copy orig to temp dir
80+ Files .walkFileTree (orig .toPath (), new CopyDirectoryRecursively (tempDirectory , orig .toPath ()));
81+ try {
82+ logger .debug ("Finished storing entry {}. Atomically moving temporary directory {} into final place {}" , key , tempDirectory , target );
83+ // Atomically rename the completed cache entry into place
84+ Files .move (tempDirectory , target .toPath (), StandardCopyOption .ATOMIC_MOVE );
85+ } catch (FileAlreadyExistsException | DirectoryNotEmptyException e ) {
86+ // Someone already beat us to it
87+ logger .debug ("Shadow copy entry now exists, not overwriting: {}" , key );
88+ } catch (AtomicMoveNotSupportedException e ) {
89+ logger .warn ("The filesystem at {} does not support atomic moves. Spotless cannot safely cache on such a system due to race conditions. Caching has been skipped." , target .toPath ().getParent (), e );
90+ }
6991 } finally {
70- cleanupReservation (key );
92+ // Best effort to clean up
93+ if (Files .exists (tempDirectory )) {
94+ try {
95+ Files .walkFileTree (tempDirectory , new DeleteDirectoryRecursively ());
96+ } catch (Throwable ex ) {
97+ logger .warn ("Ignoring error while cleaning up temporary copy" , ex );
98+ }
99+ }
71100 }
72101 }
73102
74103 public File getEntry (String key , String fileName ) {
75104 return entry (key , fileName );
76105 }
77106
78- private void storeEntry (String key , File orig ) {
79- File target = entry (key , orig .getName ());
80- if (target .exists ()) {
81- logger .debug ("Shadow copy entry already exists: {}" , key );
82- // delete directory "target" recursively
83- // https://stackoverflow.com/questions/3775694/deleting-folder-from-java
84- ThrowingEx .run (() -> Files .walkFileTree (target .toPath (), new DeleteDirectoryRecursively ()));
85- }
86- // copy directory "orig" to "target" using hard links if possible or a plain copy otherwise
87- ThrowingEx .run (() -> Files .walkFileTree (orig .toPath (), new CopyDirectoryRecursively (target , orig )));
88- }
89-
90- private void cleanupReservation (String key ) {
91- ThrowingEx .run (() -> Files .delete (markerFilePath (key )));
92- }
93-
94- private Path markerFilePath (String key ) {
95- return Paths .get (shadowCopyRoot ().getAbsolutePath (), key + ".marker" );
96- }
97-
98107 private File entry (String key , String origName ) {
99108 return Paths .get (shadowCopyRoot ().getAbsolutePath (), key , origName ).toFile ();
100109 }
101110
102- private boolean reserveSubFolder (String key ) {
103- // put a marker file named "key".marker in "shadowCopyRoot" to make sure no other process is using it or return false if it already exists
104- try {
105- Files .createFile (Paths .get (shadowCopyRoot ().getAbsolutePath (), key + ".marker" ));
106- return true ;
107- } catch (FileAlreadyExistsException e ) {
108- return false ;
109- } catch (IOException e ) {
110- throw new RuntimeException (e );
111- }
112- }
113-
114111 public File copyEntryInto (String key , String origName , File targetParentFolder ) {
115112 File target = Paths .get (targetParentFolder .getAbsolutePath (), origName ).toFile ();
116113 if (target .exists ()) {
117114 logger .warn ("Shadow copy destination already exists, deleting! {}: {}" , key , target );
118115 ThrowingEx .run (() -> Files .walkFileTree (target .toPath (), new DeleteDirectoryRecursively ()));
119116 }
120117 // copy directory "orig" to "target" using hard links if possible or a plain copy otherwise
121- ThrowingEx .run (() -> Files .walkFileTree (entry (key , origName ).toPath (), new CopyDirectoryRecursively (target , entry (key , origName ))));
118+ ThrowingEx .run (() -> Files .walkFileTree (entry (key , origName ).toPath (), new CopyDirectoryRecursively (target . toPath () , entry (key , origName ). toPath ( ))));
122119 return target ;
123120 }
124121
@@ -127,20 +124,20 @@ public boolean entryExists(String key, String origName) {
127124 }
128125
129126 private static class CopyDirectoryRecursively extends SimpleFileVisitor <Path > {
130- private final File target ;
131- private final File orig ;
127+ private final Path target ;
128+ private final Path orig ;
132129
133130 private boolean tryHardLink = true ;
134131
135- public CopyDirectoryRecursively (File target , File orig ) {
132+ public CopyDirectoryRecursively (Path target , Path orig ) {
136133 this .target = target ;
137134 this .orig = orig ;
138135 }
139136
140137 @ Override
141138 public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) throws IOException {
142139 // create directory on target
143- Files .createDirectories (target .toPath (). resolve (orig . toPath () .relativize (dir )));
140+ Files .createDirectories (target .resolve (orig .relativize (dir )));
144141 return super .preVisitDirectory (dir , attrs );
145142 }
146143
@@ -149,7 +146,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
149146 // first try to hardlink, if that fails, copy
150147 if (tryHardLink ) {
151148 try {
152- Files .createLink (target .toPath (). resolve (orig . toPath () .relativize (file )), file );
149+ Files .createLink (target .resolve (orig .relativize (file )), file );
153150 return super .visitFile (file , attrs );
154151 } catch (UnsupportedOperationException | SecurityException | FileSystemException e ) {
155152 logger .debug ("Shadow copy entry does not support hard links: {}. Switching to 'copy'." , file , e );
@@ -160,11 +157,12 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
160157 }
161158 }
162159 // copy file to target
163- Files .copy (file , target .toPath (). resolve (orig . toPath () .relativize (file )));
160+ Files .copy (file , target .resolve (orig .relativize (file )));
164161 return super .visitFile (file , attrs );
165162 }
166163 }
167164
165+ // https://stackoverflow.com/questions/3775694/deleting-folder-from-java
168166 private static class DeleteDirectoryRecursively extends SimpleFileVisitor <Path > {
169167 @ Override
170168 public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) throws IOException {
0 commit comments