]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7125 use efficient delete dir method in sonar-process
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 11 Feb 2016 12:42:30 +0000 (13:42 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 12 Feb 2016 12:48:36 +0000 (13:48 +0100)
server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java
server/sonar-process/src/main/java/org/sonar/process/FileUtils.java [new file with mode: 0644]
server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java
server/sonar-process/src/test/java/org/sonar/process/FileUtilsTest.java [new file with mode: 0644]
sonar-application/src/main/java/org/sonar/application/AppFileSystem.java

index 045b4e581ca4a7ebef12b0a7c8b883724b6500a1..734a57f8adb06e969a6ac2d8647d1aef59353cc6 100644 (file)
  */
 package org.sonar.process;
 
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang.text.StrSubstitutor;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStreamReader;
@@ -31,6 +27,10 @@ import java.nio.charset.StandardCharsets;
 import java.util.Enumeration;
 import java.util.Map;
 import java.util.Properties;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.text.StrSubstitutor;
+
+import static org.apache.commons.io.FileUtils.deleteQuietly;
 
 public final class ConfigurationUtils {
 
@@ -66,7 +66,7 @@ public final class ConfigurationUtils {
       throw new IllegalStateException("Could not read properties from file: " + args[0], e);
     } finally {
       IOUtils.closeQuietly(reader);
-      FileUtils.deleteQuietly(propertyFile);
+      deleteQuietly(propertyFile);
     }
     return new Props(properties);
   }
diff --git a/server/sonar-process/src/main/java/org/sonar/process/FileUtils.java b/server/sonar-process/src/main/java/org/sonar/process/FileUtils.java
new file mode 100644 (file)
index 0000000..f25ac1f
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.process;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
+import javax.annotation.Nullable;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * This utility class provides Java NIO based replacement for some methods of
+ * {@link org.apache.commons.io.FileUtils Common IO FileUtils} class.
+ */
+public final class FileUtils {
+  private static final String DIRECTORY_CAN_NOT_BE_NULL = "Directory can not be null";
+  private static final EnumSet<FileVisitOption> FOLLOW_LINKS = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
+
+  private FileUtils() {
+    // prevents instantiation
+  }
+
+  /**
+   * Cleans a directory recursively.
+   *
+   * @param directory  directory to delete
+   * @throws IOException in case deletion is unsuccessful
+   */
+  public static void cleanDirectory(File directory) throws IOException {
+    requireNonNull(directory, DIRECTORY_CAN_NOT_BE_NULL);
+    if (!directory.exists()) {
+      return;
+    }
+
+    cleanDirectoryImpl(directory.toPath());
+  }
+
+  /**
+   * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories.
+   * <p>
+   * The difference between File.delete() and this method are:
+   * <ul>
+   * <li>A directory to be deleted does not have to be empty.</li>
+   * <li>No exceptions are thrown when a file or directory cannot be deleted.</li>
+   * </ul>
+   *
+   * @param file  file or directory to delete, can be {@code null}
+   * @return {@code true} if the file or directory was deleted, otherwise {@code false}
+   */
+  public static boolean deleteQuietly(@Nullable File file) {
+    if (file == null) {
+      return false;
+    }
+
+    try {
+      if (file.isDirectory()) {
+        deleteDirectory(file);
+      } else {
+        Files.delete(file.toPath());
+      }
+      return true;
+    } catch (IOException | SecurityException ignored) {
+      return false;
+    }
+  }
+
+  /**
+   * Deletes a directory recursively. Does not support symbolic link to directories.
+   *
+   * @param directory  directory to delete
+   * @throws IOException in case deletion is unsuccessful
+   */
+  public static void deleteDirectory(File directory) throws IOException {
+    requireNonNull(directory, DIRECTORY_CAN_NOT_BE_NULL);
+
+    Path path = directory.toPath();
+    if (!Files.exists(path)) {
+      return;
+    }
+
+    if (Files.isSymbolicLink(path)) {
+      throw new IOException(format("Directory '%s' is a symbolic link", directory));
+    }
+    if (Files.isRegularFile(path)) {
+      throw new IOException(format("Directory '%s' is a file", directory));
+    }
+    deleteDirectoryImpl(path);
+
+    if (Files.exists(path)) {
+      throw new IOException(format("Unable to delete directory '%s'", directory));
+    }
+  }
+
+  private static void cleanDirectoryImpl(Path path) throws IOException {
+    if (!Files.isDirectory(path)) {
+      throw new IllegalArgumentException(format("'%s' is not a directory", path));
+    }
+    Files.walkFileTree(path, FOLLOW_LINKS, CleanDirectoryFileVisitor.VISIT_MAX_DEPTH, new CleanDirectoryFileVisitor(path));
+  }
+
+  private static void deleteDirectoryImpl(Path path) throws IOException {
+    Files.walkFileTree(path, DeleteRecursivelyFileVisitor.INSTANCE);
+  }
+
+  /**
+   * This visitor is intended to be used to visit direct children of directory <strong>or a symLink to a directory</strong>,
+   * so, with a max depth of {@link #VISIT_MAX_DEPTH 1}. Each direct children will either be directly deleted (if file)
+   * or recursively deleted (if directory).
+   */
+  private static class CleanDirectoryFileVisitor extends SimpleFileVisitor<Path> {
+    public static final int VISIT_MAX_DEPTH = 1;
+
+    private final Path path;
+    private final boolean symLink;
+
+    public CleanDirectoryFileVisitor(Path path) {
+      this.path = path;
+      this.symLink = Files.isSymbolicLink(path);
+    }
+
+    @Override
+    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+      if (Files.isDirectory(file)) {
+        deleteDirectoryImpl(file);
+      } else if (!symLink || !file.equals(path)) {
+        Files.delete(file);
+      }
+      return FileVisitResult.CONTINUE;
+    }
+
+    @Override
+    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+      if (!dir.equals(path)) {
+        deleteDirectoryImpl(dir);
+      }
+      return FileVisitResult.CONTINUE;
+    }
+  }
+
+  private static final class DeleteRecursivelyFileVisitor extends SimpleFileVisitor<Path> {
+    public static final DeleteRecursivelyFileVisitor INSTANCE = new DeleteRecursivelyFileVisitor();
+
+    @Override
+    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+      Files.delete(file);
+      return FileVisitResult.CONTINUE;
+    }
+
+    @Override
+    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+      Files.delete(dir);
+      return FileVisitResult.CONTINUE;
+    }
+  }
+}
index 320bb133cee031d7d91ff15b2036819189588c03..3b20d57c5dc15c83e78b623544702e9e5b904873 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.process;
 
-import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang.StringUtils;
 
 import java.io.File;
@@ -27,6 +26,7 @@ import java.io.IOException;
 import java.util.Map;
 
 import static java.lang.String.format;
+import static org.sonar.process.FileUtils.deleteQuietly;
 
 public class MinimumViableSystem {
 
@@ -42,7 +42,7 @@ public class MinimumViableSystem {
   void checkWritableDir(String tempPath) {
     try {
       File tempFile = File.createTempFile("check", "tmp", new File(tempPath));
-      FileUtils.deleteQuietly(tempFile);
+      deleteQuietly(tempFile);
     } catch (IOException e) {
       throw new IllegalStateException(format("Temp directory is not writable: %s", tempPath), e);
     }
diff --git a/server/sonar-process/src/test/java/org/sonar/process/FileUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/FileUtilsTest.java
new file mode 100644 (file)
index 0000000..9b1b0ce
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.process;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import javax.annotation.CheckForNull;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FileUtilsTest {
+  @Rule
+  public TemporaryFolder temporaryFolder = new TemporaryFolder();
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void cleanDirectory_throws_NPE_if_file_is_null() throws IOException {
+    expectDirectoryCanNotBeNullNPE();
+
+    FileUtils.cleanDirectory(null);
+  }
+
+  @Test
+  public void cleanDirectory_does_nothing_if_argument_does_not_exist() throws IOException {
+    FileUtils.cleanDirectory(new File("/a/b/ToDoSSS"));
+  }
+
+  @Test
+  public void cleanDirectory_throws_IAE_if_argument_is_a_file() throws IOException {
+    File file = temporaryFolder.newFile();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("'" + file.getAbsolutePath() + "' is not a directory");
+
+    FileUtils.cleanDirectory(file);
+  }
+
+  @Test
+  public void cleanDirectory_removes_directories_and_files_in_target_directory_but_not_target_directory() throws IOException {
+    Path target = temporaryFolder.newFolder().toPath();
+    Path childFile1 = Files.createFile(target.resolve("file1.txt"));
+    Path childDir1 = Files.createDirectory(target.resolve("subDir1"));
+    Path childFile2 = Files.createFile(childDir1.resolve("file2.txt"));
+    Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2"));
+
+    assertThat(target).isDirectory();
+    assertThat(childFile1).isRegularFile();
+    assertThat(childDir1).isDirectory();
+    assertThat(childFile2).isRegularFile();
+    assertThat(childDir2).isDirectory();
+
+    // on supporting FileSystem, target will change if directory is recreated
+    Object targetKey = getFileKey(target);
+
+    FileUtils.cleanDirectory(target.toFile());
+
+    assertThat(target).isDirectory();
+    assertThat(childFile1).doesNotExist();
+    assertThat(childDir1).doesNotExist();
+    assertThat(childFile2).doesNotExist();
+    assertThat(childDir2).doesNotExist();
+    assertThat(getFileKey(target)).isEqualTo(targetKey);
+  }
+
+  @Test
+  public void cleanDirectory_follows_symlink_to_target_directory() throws IOException {
+    Path target = temporaryFolder.newFolder().toPath();
+    Path symToDir = Files.createSymbolicLink(temporaryFolder.newFolder().toPath().resolve("sym_to_dir"), target);
+    Path childFile1 = Files.createFile(target.resolve("file1.txt"));
+    Path childDir1 = Files.createDirectory(target.resolve("subDir1"));
+    Path childFile2 = Files.createFile(childDir1.resolve("file2.txt"));
+    Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2"));
+
+    assertThat(target).isDirectory();
+    assertThat(symToDir).isSymbolicLink();
+    assertThat(childFile1).isRegularFile();
+    assertThat(childDir1).isDirectory();
+    assertThat(childFile2).isRegularFile();
+    assertThat(childDir2).isDirectory();
+
+    // on supporting FileSystem, target will change if directory is recreated
+    Object targetKey = getFileKey(target);
+    Object symLinkKey = getFileKey(symToDir);
+
+    FileUtils.cleanDirectory(symToDir.toFile());
+
+    assertThat(target).isDirectory();
+    assertThat(symToDir).isSymbolicLink();
+    assertThat(childFile1).doesNotExist();
+    assertThat(childDir1).doesNotExist();
+    assertThat(childFile2).doesNotExist();
+    assertThat(childDir2).doesNotExist();
+    assertThat(getFileKey(target)).isEqualTo(targetKey);
+    assertThat(getFileKey(symToDir)).isEqualTo(symLinkKey);
+  }
+
+  @Test
+  public void deleteQuietly_does_not_fail_if_argument_is_null() {
+    FileUtils.deleteQuietly(null);
+  }
+
+  @Test
+  public void deleteQuietly_does_not_fail_if_file_does_not_exist() throws IOException {
+    File file = new File(temporaryFolder.newFolder(), "blablabl");
+    assertThat(file).doesNotExist();
+
+    FileUtils.deleteQuietly(file);
+  }
+
+  @Test
+  public void deleteQuietly_deletes_directory_and_content() throws IOException {
+    Path target = temporaryFolder.newFolder().toPath();
+    Path childFile1 = Files.createFile(target.resolve("file1.txt"));
+    Path childDir1 = Files.createDirectory(target.resolve("subDir1"));
+    Path childFile2 = Files.createFile(childDir1.resolve("file2.txt"));
+    Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2"));
+
+    assertThat(target).isDirectory();
+    assertThat(childFile1).isRegularFile();
+    assertThat(childDir1).isDirectory();
+    assertThat(childFile2).isRegularFile();
+    assertThat(childDir2).isDirectory();
+
+    FileUtils.deleteQuietly(target.toFile());
+
+    assertThat(target).doesNotExist();
+    assertThat(childFile1).doesNotExist();
+    assertThat(childDir1).doesNotExist();
+    assertThat(childFile2).doesNotExist();
+    assertThat(childDir2).doesNotExist();
+  }
+
+  @Test
+  public void deleteQuietly_deletes_symbolicLink() throws IOException {
+    Path folder = temporaryFolder.newFolder().toPath();
+    Path file1 = Files.createFile(folder.resolve("file1.txt"));
+    Path symLink = Files.createSymbolicLink(folder.resolve("link1"), file1);
+
+    assertThat(file1).isRegularFile();
+    assertThat(symLink).isSymbolicLink();
+
+    FileUtils.deleteQuietly(symLink.toFile());
+
+    assertThat(symLink).doesNotExist();
+    assertThat(file1).isRegularFile();
+  }
+
+  @Test
+  public void deleteDirectory_throws_NPE_if_argument_is_null() throws IOException {
+    expectDirectoryCanNotBeNullNPE();
+
+    FileUtils.deleteDirectory(null);
+  }
+
+  @Test
+  public void deleteDirectory_does_not_fail_if_file_does_not_exist() throws IOException {
+    File file = new File(temporaryFolder.newFolder(), "foo.d");
+
+    FileUtils.deleteDirectory(file);
+  }
+
+  @Test
+  public void deleteDirectory_throws_IOE_if_argument_is_a_file() throws IOException {
+    File file = temporaryFolder.newFile();
+
+    expectedException.expect(IOException.class);
+    expectedException.expectMessage("Directory '" + file.getAbsolutePath() + "' is a file");
+
+    FileUtils.deleteDirectory(file);
+  }
+
+  @Test
+  public void deleteDirectory_throws_IOE_if_file_is_symbolicLink() throws IOException {
+    Path folder = temporaryFolder.newFolder().toPath();
+    Path file1 = Files.createFile(folder.resolve("file1.txt"));
+    Path symLink = Files.createSymbolicLink(folder.resolve("link1"), file1);
+
+    assertThat(file1).isRegularFile();
+    assertThat(symLink).isSymbolicLink();
+
+    expectedException.expect(IOException.class);
+    expectedException.expectMessage("Directory '" + symLink.toFile().getAbsolutePath() + "' is a symbolic link");
+
+    FileUtils.deleteDirectory(symLink.toFile());
+  }
+
+  @Test
+  public void deleteDirectory_deletes_directory_and_content() throws IOException {
+    Path target = temporaryFolder.newFolder().toPath();
+    Path childFile1 = Files.createFile(target.resolve("file1.txt"));
+    Path childDir1 = Files.createDirectory(target.resolve("subDir1"));
+    Path childFile2 = Files.createFile(childDir1.resolve("file2.txt"));
+    Path childDir2 = Files.createDirectory(childDir1.resolve("subDir2"));
+
+    assertThat(target).isDirectory();
+    assertThat(childFile1).isRegularFile();
+    assertThat(childDir1).isDirectory();
+    assertThat(childFile2).isRegularFile();
+    assertThat(childDir2).isDirectory();
+
+    FileUtils.deleteQuietly(target.toFile());
+
+    assertThat(target).doesNotExist();
+    assertThat(childFile1).doesNotExist();
+    assertThat(childDir1).doesNotExist();
+    assertThat(childFile2).doesNotExist();
+    assertThat(childDir2).doesNotExist();
+  }
+
+  private void expectDirectoryCanNotBeNullNPE() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("Directory can not be null");
+  }
+
+  @CheckForNull
+  private static Object getFileKey(Path path) throws IOException {
+    BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
+    return attrs.fileKey();
+  }
+}
index 0f513928c98aa97745c9ca42519446dd18f16856..c195008a6385dbafb14106b5dde88eba6ef7b2d3 100644 (file)
@@ -21,19 +21,13 @@ package org.sonar.application;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.sonar.process.Props;
 import org.sonar.process.monitor.FileSystem;
 
-import static java.nio.file.Files.delete;
-import static java.nio.file.Files.walkFileTree;
 import static org.apache.commons.io.FileUtils.forceMkdir;
+import static org.sonar.process.FileUtils.cleanDirectory;
 import static org.sonar.process.ProcessProperties.PATH_DATA;
 import static org.sonar.process.ProcessProperties.PATH_HOME;
 import static org.sonar.process.ProcessProperties.PATH_LOGS;
@@ -123,30 +117,4 @@ public class AppFileSystem implements FileSystem {
     }
   }
 
-  private static void cleanDirectory(File dir) throws IOException {
-    Path path = Paths.get(dir.toURI());
-    walkFileTree(path, new CleanRecursivelyFileVisitor(path));
-  }
-
-  private static class CleanRecursivelyFileVisitor extends SimpleFileVisitor<Path> {
-    private final Path path;
-
-    public CleanRecursivelyFileVisitor(Path path) {
-      this.path = path;
-    }
-
-    @Override
-    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-      delete(file);
-      return FileVisitResult.CONTINUE;
-    }
-
-    @Override
-    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
-      if (!dir.equals(path)) {
-        delete(dir);
-      }
-      return FileVisitResult.CONTINUE;
-    }
-  }
 }