Skip to content

Commit 6cd1b45

Browse files
committed
SCANNERAPI-170 Properly handle case when SONAR_USER_HOME point to a symlink
1 parent 626fb8b commit 6cd1b45

4 files changed

Lines changed: 82 additions & 69 deletions

File tree

api/src/main/java/org/sonarsource/scanner/api/internal/cache/FileCache.java

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@
2121

2222
import java.io.File;
2323
import java.io.IOException;
24+
import java.nio.file.AtomicMoveNotSupportedException;
25+
import java.nio.file.FileAlreadyExistsException;
2426
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.nio.file.StandardCopyOption;
2529
import javax.annotation.CheckForNull;
2630

2731
/**
@@ -31,28 +35,25 @@
3135
*/
3236
public class FileCache {
3337

34-
/** Maximum loop count when creating temp directories. */
35-
private static final int TEMP_DIR_ATTEMPTS = 10000;
36-
37-
private final File dir;
38-
private final File tmpDir;
38+
private final Path dir;
39+
private final Path tmpDir;
3940
private final FileHashes hashes;
4041
private final Logger logger;
4142

42-
FileCache(File dir, FileHashes fileHashes, Logger logger) {
43+
FileCache(Path dir, FileHashes fileHashes, Logger logger) {
4344
this.hashes = fileHashes;
4445
this.logger = logger;
4546
this.dir = createDir(dir, "user cache");
46-
logger.info(String.format("User cache: %s", dir.getAbsolutePath()));
47-
this.tmpDir = createDir(new File(dir, "_tmp"), "temp dir");
47+
logger.info(String.format("User cache: %s", dir.toString()));
48+
this.tmpDir = createDir(dir.resolve("_tmp"), "temp dir");
4849
}
4950

50-
static FileCache create(File dir, Logger logger) {
51+
static FileCache create(Path dir, Logger logger) {
5152
return new FileCache(dir, new FileHashes(), logger);
5253
}
5354

5455
public File getDir() {
55-
return dir;
56+
return dir.toFile();
5657
}
5758

5859
/**
@@ -61,9 +62,9 @@ public File getDir() {
6162
*/
6263
@CheckForNull
6364
public File get(String filename, String hash) {
64-
File cachedFile = new File(new File(dir, hash), filename);
65-
if (cachedFile.exists()) {
66-
return cachedFile;
65+
Path cachedFile = dir.resolve(hash).resolve(filename);
66+
if (Files.exists(cachedFile)) {
67+
return cachedFile.toFile();
6768
}
6869
logger.debug(String.format("No file found in the cache with name %s and hash %s", filename, hash));
6970
return null;
@@ -76,84 +77,74 @@ public interface Downloader {
7677

7778
public File get(String filename, String hash, Downloader downloader) {
7879
// Does not fail if another process tries to create the directory at the same time.
79-
File hashDir = hashDir(hash);
80-
File targetFile = new File(hashDir, filename);
81-
if (!targetFile.exists()) {
82-
File tempFile = newTempFile();
80+
Path hashDir = hashDir(hash);
81+
Path targetFile = hashDir.resolve(filename);
82+
if (!Files.exists(targetFile)) {
83+
Path tempFile = newTempFile();
8384
download(downloader, filename, tempFile);
84-
String downloadedHash = hashes.of(tempFile);
85+
String downloadedHash = hashes.of(tempFile.toFile());
8586
if (!hash.equals(downloadedHash)) {
86-
throw new IllegalStateException("INVALID HASH: File " + tempFile.getAbsolutePath() + " was expected to have hash " + hash
87+
throw new IllegalStateException("INVALID HASH: File " + tempFile.toAbsolutePath() + " was expected to have hash " + hash
8788
+ " but was downloaded with hash " + downloadedHash);
8889
}
8990
mkdirQuietly(hashDir);
9091
renameQuietly(tempFile, targetFile);
9192
}
92-
return targetFile;
93+
return targetFile.toFile();
9394
}
9495

95-
private void download(Downloader downloader, String filename, File tempFile) {
96+
private static void download(Downloader downloader, String filename, Path tempFile) {
9697
try {
97-
downloader.download(filename, tempFile);
98+
downloader.download(filename, tempFile.toFile());
9899
} catch (IOException e) {
99100
throw new IllegalStateException("Fail to download " + filename + " to " + tempFile, e);
100101
}
101102
}
102103

103-
private void renameQuietly(File sourceFile, File targetFile) {
104-
boolean rename = sourceFile.renameTo(targetFile);
105-
// Check if the file was cached by another process during download
106-
if (!rename && !targetFile.exists()) {
107-
logger.warn(String.format("Unable to rename %s to %s", sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()));
104+
private void renameQuietly(Path sourceFile, Path targetFile) {
105+
try {
106+
Files.move(sourceFile, targetFile, StandardCopyOption.ATOMIC_MOVE);
107+
} catch (AtomicMoveNotSupportedException ex) {
108+
logger.warn(String.format("Unable to rename %s to %s", sourceFile.toAbsolutePath(), targetFile.toAbsolutePath()));
108109
logger.warn("A copy/delete will be tempted but with no guarantee of atomicity");
109110
try {
110-
Files.move(sourceFile.toPath(), targetFile.toPath());
111+
Files.move(sourceFile, targetFile);
111112
} catch (IOException e) {
112-
throw new IllegalStateException("Fail to move " + sourceFile.getAbsolutePath() + " to " + targetFile, e);
113+
throw new IllegalStateException("Fail to move " + sourceFile.toAbsolutePath() + " to " + targetFile, e);
113114
}
115+
} catch (FileAlreadyExistsException e) {
116+
// File was probably cached by another process in the mean time
117+
} catch (IOException e) {
118+
throw new IllegalStateException("Fail to move " + sourceFile.toAbsolutePath() + " to " + targetFile, e);
114119
}
115120
}
116121

117-
private File hashDir(String hash) {
118-
return new File(dir, hash);
122+
private Path hashDir(String hash) {
123+
return dir.resolve(hash);
119124
}
120125

121-
private static void mkdirQuietly(File hashDir) {
126+
private static void mkdirQuietly(Path hashDir) {
122127
try {
123-
Files.createDirectories(hashDir.toPath());
128+
Files.createDirectories(hashDir);
124129
} catch (IOException e) {
125130
throw new IllegalStateException("Fail to create cache directory: " + hashDir, e);
126131
}
127132
}
128133

129-
private File newTempFile() {
134+
private Path newTempFile() {
130135
try {
131-
return File.createTempFile("fileCache", null, tmpDir);
136+
return Files.createTempFile(tmpDir, "fileCache", null);
132137
} catch (IOException e) {
133138
throw new IllegalStateException("Fail to create temp file in " + tmpDir, e);
134139
}
135140
}
136141

137-
public File createTempDir() {
138-
String baseName = System.currentTimeMillis() + "-";
139-
140-
for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
141-
File tempDir = new File(tmpDir, baseName + counter);
142-
if (tempDir.mkdir()) {
143-
return tempDir;
144-
}
145-
}
146-
throw new IllegalStateException("Failed to create directory in " + tmpDir);
147-
}
148-
149-
private File createDir(File dir, String debugTitle) {
150-
if (!dir.isDirectory() || !dir.exists()) {
151-
logger.debug("Create : " + dir.getAbsolutePath());
152-
try {
153-
Files.createDirectories(dir.toPath());
154-
} catch (IOException e) {
155-
throw new IllegalStateException("Unable to create " + debugTitle + dir.getAbsolutePath(), e);
156-
}
142+
private Path createDir(Path dir, String debugTitle) {
143+
logger.debug("Create: " + dir.toString());
144+
try {
145+
Files.createDirectories(dir);
146+
} catch (IOException e) {
147+
throw new IllegalStateException("Unable to create " + debugTitle + dir.toString(), e);
157148
}
158149
return dir;
159150
}

api/src/main/java/org/sonarsource/scanner/api/internal/cache/FileCacheBuilder.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package org.sonarsource.scanner.api.internal.cache;
2121

2222
import java.io.File;
23-
2423
import javax.annotation.Nullable;
2524

2625
public class FileCacheBuilder {
@@ -46,9 +45,9 @@ public FileCache build() {
4645
userHome = findHome();
4746
}
4847
File cacheDir = new File(userHome, "cache");
49-
return FileCache.create(cacheDir, logger);
48+
return FileCache.create(cacheDir.toPath(), logger);
5049
}
51-
50+
5251
private static File findHome() {
5352
String path = System.getenv("SONAR_USER_HOME");
5453
if (path == null) {

api/src/test/java/org/sonarsource/scanner/api/internal/cache/FileCacheBuilderTest.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@
2020
package org.sonarsource.scanner.api.internal.cache;
2121

2222
import java.io.File;
23+
import java.io.IOException;
24+
import java.nio.file.Files;
2325
import org.junit.Rule;
2426
import org.junit.Test;
2527
import org.junit.rules.TemporaryFolder;
26-
import org.sonarsource.scanner.api.internal.cache.FileCache;
27-
import org.sonarsource.scanner.api.internal.cache.FileCacheBuilder;
28-
import org.sonarsource.scanner.api.internal.cache.Logger;
2928

3029
import static org.assertj.core.api.Assertions.assertThat;
3130
import static org.mockito.Mockito.mock;
@@ -53,6 +52,19 @@ public void user_home_property_can_be_null() {
5352
assertThat(cache.getDir().getName()).isEqualTo("cache");
5453
}
5554

55+
@Test
56+
public void user_home_property_can_be_a_symlink() throws IOException {
57+
File realSonarHome = temp.newFolder();
58+
File symlink = temp.newFolder();
59+
symlink.delete();
60+
Files.createSymbolicLink(symlink.toPath(), realSonarHome.toPath());
61+
62+
FileCache cache = new FileCacheBuilder(mock(Logger.class)).setUserHome(symlink).build();
63+
64+
assertThat(cache.getDir()).isDirectory().exists();
65+
assertThat(cache.getDir().getName()).isEqualTo("cache");
66+
}
67+
5668
@Test
5769
public void use_default_path_or_env_variable() {
5870
FileCache cache = new FileCacheBuilder(mock(Logger.class)).build();

api/src/test/java/org/sonarsource/scanner/api/internal/cache/FileCacheTest.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,32 @@
2323
import java.io.IOException;
2424
import java.nio.charset.StandardCharsets;
2525
import java.nio.file.Files;
26-
2726
import org.junit.Rule;
2827
import org.junit.Test;
2928
import org.junit.rules.ExpectedException;
3029
import org.junit.rules.TemporaryFolder;
3130

3231
import static org.assertj.core.api.Assertions.assertThat;
33-
import static org.mockito.Matchers.any;
32+
import static org.mockito.ArgumentMatchers.any;
3433
import static org.mockito.Mockito.mock;
3534
import static org.mockito.Mockito.when;
3635

3736
public class FileCacheTest {
3837
@Rule
39-
public TemporaryFolder tempFolder = new TemporaryFolder();
38+
public TemporaryFolder temp = new TemporaryFolder();
4039

4140
@Rule
4241
public ExpectedException thrown = ExpectedException.none();
4342

4443
@Test
4544
public void not_in_cache() throws IOException {
46-
FileCache cache = FileCache.create(tempFolder.newFolder(), mock(Logger.class));
45+
FileCache cache = FileCache.create(temp.newFolder().toPath(), mock(Logger.class));
4746
assertThat(cache.get("sonar-foo-plugin-1.5.jar", "ABCDE")).isNull();
4847
}
4948

5049
@Test
5150
public void found_in_cache() throws IOException {
52-
FileCache cache = FileCache.create(tempFolder.newFolder(), mock(Logger.class));
51+
FileCache cache = FileCache.create(temp.newFolder().toPath(), mock(Logger.class));
5352

5453
// populate the cache. Assume that hash is correct.
5554
File cachedFile = new File(new File(cache.getDir(), "ABCDE"), "sonar-foo-plugin-1.5.jar");
@@ -61,19 +60,31 @@ public void found_in_cache() throws IOException {
6160
@Test
6261
public void download_and_add_to_cache() throws IOException {
6362
FileHashes hashes = mock(FileHashes.class);
64-
FileCache cache = new FileCache(tempFolder.newFolder(), hashes, mock(Logger.class));
63+
FileCache cache = new FileCache(temp.newFolder().toPath(), hashes, mock(Logger.class));
6564
when(hashes.of(any(File.class))).thenReturn("ABCDE");
6665

6766
FileCache.Downloader downloader = new FileCache.Downloader() {
67+
boolean single = false;
68+
6869
public void download(String filename, File toFile) throws IOException {
70+
if (single) {
71+
throw new IllegalStateException("Already called");
72+
}
6973
write(toFile, "body");
74+
single = true;
7075
}
7176
};
7277
File cachedFile = cache.get("sonar-foo-plugin-1.5.jar", "ABCDE", downloader);
7378
assertThat(cachedFile).isNotNull().exists().isFile();
7479
assertThat(cachedFile.getName()).isEqualTo("sonar-foo-plugin-1.5.jar");
7580
assertThat(cachedFile.getParentFile().getParentFile()).isEqualTo(cache.getDir());
7681
assertThat(read(cachedFile)).isEqualTo("body");
82+
83+
File againFromCache = cache.get("sonar-foo-plugin-1.5.jar", "ABCDE", downloader);
84+
assertThat(againFromCache).isNotNull().exists().isFile();
85+
assertThat(againFromCache.getName()).isEqualTo("sonar-foo-plugin-1.5.jar");
86+
assertThat(againFromCache.getParentFile().getParentFile()).isEqualTo(cache.getDir());
87+
assertThat(read(againFromCache)).isEqualTo("body");
7788
}
7889

7990
@Test
@@ -82,7 +93,7 @@ public void download_corrupted_file() throws IOException {
8293
thrown.expectMessage("INVALID HASH");
8394

8495
FileHashes hashes = mock(FileHashes.class);
85-
FileCache cache = new FileCache(tempFolder.newFolder(), hashes, mock(Logger.class));
96+
FileCache cache = new FileCache(temp.newFolder().toPath(), hashes, mock(Logger.class));
8697
when(hashes.of(any(File.class))).thenReturn("VWXYZ");
8798

8899
FileCache.Downloader downloader = new FileCache.Downloader() {
@@ -97,7 +108,7 @@ public void download(String filename, File toFile) throws IOException {
97108
public void concurrent_download() throws IOException {
98109
FileHashes hashes = mock(FileHashes.class);
99110
when(hashes.of(any(File.class))).thenReturn("ABCDE");
100-
final FileCache cache = new FileCache(tempFolder.newFolder(), hashes, mock(Logger.class));
111+
final FileCache cache = new FileCache(temp.newFolder().toPath(), hashes, mock(Logger.class));
101112

102113
FileCache.Downloader downloader = new FileCache.Downloader() {
103114
public void download(String filename, File toFile) throws IOException {

0 commit comments

Comments
 (0)