17
17
18
18
import java .io .File ;
19
19
import java .io .IOException ;
20
+ import java .nio .file .AtomicMoveNotSupportedException ;
21
+ import java .nio .file .DirectoryNotEmptyException ;
20
22
import java .nio .file .FileAlreadyExistsException ;
21
23
import java .nio .file .FileSystemException ;
22
24
import java .nio .file .FileVisitResult ;
23
25
import java .nio .file .Files ;
24
26
import java .nio .file .Path ;
25
27
import java .nio .file .Paths ;
26
28
import java .nio .file .SimpleFileVisitor ;
29
+ import java .nio .file .StandardCopyOption ;
27
30
import java .nio .file .attribute .BasicFileAttributes ;
28
- import java .time .Duration ;
29
- import java .util .concurrent .TimeoutException ;
30
31
import java .util .function .Supplier ;
31
32
32
33
import javax .annotation .Nonnull ;
@@ -55,70 +56,66 @@ private File shadowCopyRoot() {
55
56
}
56
57
57
58
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 {
61
63
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 );
65
68
}
66
69
}
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
+
67
78
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
+ }
69
91
} 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
+ }
71
100
}
72
101
}
73
102
74
103
public File getEntry (String key , String fileName ) {
75
104
return entry (key , fileName );
76
105
}
77
106
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
-
98
107
private File entry (String key , String origName ) {
99
108
return Paths .get (shadowCopyRoot ().getAbsolutePath (), key , origName ).toFile ();
100
109
}
101
110
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
-
114
111
public File copyEntryInto (String key , String origName , File targetParentFolder ) {
115
112
File target = Paths .get (targetParentFolder .getAbsolutePath (), origName ).toFile ();
116
113
if (target .exists ()) {
117
114
logger .warn ("Shadow copy destination already exists, deleting! {}: {}" , key , target );
118
115
ThrowingEx .run (() -> Files .walkFileTree (target .toPath (), new DeleteDirectoryRecursively ()));
119
116
}
120
117
// 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 ( ))));
122
119
return target ;
123
120
}
124
121
@@ -127,20 +124,20 @@ public boolean entryExists(String key, String origName) {
127
124
}
128
125
129
126
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 ;
132
129
133
130
private boolean tryHardLink = true ;
134
131
135
- public CopyDirectoryRecursively (File target , File orig ) {
132
+ public CopyDirectoryRecursively (Path target , Path orig ) {
136
133
this .target = target ;
137
134
this .orig = orig ;
138
135
}
139
136
140
137
@ Override
141
138
public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) throws IOException {
142
139
// create directory on target
143
- Files .createDirectories (target .toPath (). resolve (orig . toPath () .relativize (dir )));
140
+ Files .createDirectories (target .resolve (orig .relativize (dir )));
144
141
return super .preVisitDirectory (dir , attrs );
145
142
}
146
143
@@ -149,7 +146,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
149
146
// first try to hardlink, if that fails, copy
150
147
if (tryHardLink ) {
151
148
try {
152
- Files .createLink (target .toPath (). resolve (orig . toPath () .relativize (file )), file );
149
+ Files .createLink (target .resolve (orig .relativize (file )), file );
153
150
return super .visitFile (file , attrs );
154
151
} catch (UnsupportedOperationException | SecurityException | FileSystemException e ) {
155
152
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
160
157
}
161
158
}
162
159
// copy file to target
163
- Files .copy (file , target .toPath (). resolve (orig . toPath () .relativize (file )));
160
+ Files .copy (file , target .resolve (orig .relativize (file )));
164
161
return super .visitFile (file , attrs );
165
162
}
166
163
}
167
164
165
+ // https://stackoverflow.com/questions/3775694/deleting-folder-from-java
168
166
private static class DeleteDirectoryRecursively extends SimpleFileVisitor <Path > {
169
167
@ Override
170
168
public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) throws IOException {
0 commit comments