From b7877f3f4ee8a6b9452ad197039459092d65f6e7 Mon Sep 17 00:00:00 2001 From: Jason Wells Date: Mon, 18 Mar 2024 12:08:42 -0700 Subject: [PATCH] smarter traversal while searching for case.fbi2 files When a case.fbi2 file is found in a directory traversal of sub-directories should now be skipped since in a valid situation there likely shouldn't be further case.fbi2 files within. This should help performance in some specific situations where traversing deeper can present a real performance impact. --- .../superutilities/cases/CaseUtility.java | 249 ++++++++++-------- .../src/test/java/CaseUtilityTests.java | 60 +++++ 2 files changed, 195 insertions(+), 114 deletions(-) create mode 100644 Java/SuperUtilities/src/test/java/CaseUtilityTests.java diff --git a/Java/SuperUtilities/src/main/java/com/nuix/superutilities/cases/CaseUtility.java b/Java/SuperUtilities/src/main/java/com/nuix/superutilities/cases/CaseUtility.java index c418bdb..adae358 100644 --- a/Java/SuperUtilities/src/main/java/com/nuix/superutilities/cases/CaseUtility.java +++ b/Java/SuperUtilities/src/main/java/com/nuix/superutilities/cases/CaseUtility.java @@ -1,18 +1,16 @@ package com.nuix.superutilities.cases; +import com.nuix.superutilities.misc.ZipHelper; +import org.apache.commons.io.FileUtils; +import org.apache.log4j.Logger; + import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.filefilter.RegexFileFilter; -import org.apache.commons.io.filefilter.TrueFileFilter; -import org.apache.log4j.Logger; - -import com.nuix.superutilities.misc.ZipHelper; - /*** * Class which provides some additional functionality regarding Nuix cases, mainly finding cases * present in directories and their sub directories. @@ -20,111 +18,134 @@ * */ public class CaseUtility { - private static Logger logger = Logger.getLogger(CaseUtility.class); - - protected CaseUtility(){} - - protected static CaseUtility instance = null; - public static CaseUtility getInstance(){ - if(instance == null){ - instance = new CaseUtility(); - } - return instance; - } - - /*** - * Searches for case directories in a given directory and sub-directories. - * @param rootSearchDirectory The root directory to search. - * @return A collection of File objects for case directories located. - */ - public Collection findCaseDirectories(File rootSearchDirectory){ - logger.info("Searching for cases in: "+rootSearchDirectory.getPath()); - return FileUtils - .listFiles(rootSearchDirectory, new RegexFileFilter("case\\.fbi2"), TrueFileFilter.TRUE) - .stream().map(f -> f.getParentFile()).collect(Collectors.toList()); - } - - /*** - * Searches for case directories in a given directory and sub-directories. - * @param rootSearchPath The path to the root directory to search. - * @return A collection of File objects for case directories located. - */ - public Collection findCaseDirectories(String rootSearchPath){ - return findCaseDirectories(new File(rootSearchPath)); - } - - /*** - * Searches for case directories in a given directory and sub-directories. - * @param rootSearchDirectory The root directory to search. - * @return A collection of String representing case directories located. - */ - public Collection findCaseDirectoryPaths(File rootSearchDirectory){ - return findCaseDirectories(rootSearchDirectory) - .stream().map(f -> f.getPath()).collect(Collectors.toList()); - } - - /*** - * Searches for case directories in a given directory and sub-directories. - * @param rootSearchPath The root directory to search. - * @return A collection of String representing case directories located. - */ - public Collection findCaseDirectoryPaths(String rootSearchPath){ - return findCaseDirectoryPaths(new File(rootSearchPath)); - } - - /*** - * Scans specified root directory and sub-directories for cases, returning {@link CaseInfo} objects - * for each case located. - * @param rootSearchDirectory The root directory to search in - * @return Case info objects for each case found - */ - public List findCaseInformation(File rootSearchDirectory){ - List result = null; - result = findCaseDirectories(rootSearchDirectory).stream().map(d -> new CaseInfo(d)).collect(Collectors.toList()); - return result; - } - - /*** - * Scans specified root directory and sub-directories for cases, returning {@link CaseInfo} objects - * for each case located. - * @param rootSearchPath The root directory to search in - * @return Case info objects for each case found - */ - public List findCaseInformation(String rootSearchPath){ - return findCaseInformation(new File(rootSearchPath)); - } - - /*** - * Archives a Nuix case into a Zip file, optionally deleting the case once completed. Based on: - * https://stackoverflow.com/questions/23318383/compress-directory-into-a-zipfile-with-commons-io - * @param nuixCaseDirectory Directory of the Nuix case - * @param archiveFile The Zip file to archive the case into - * @param deleteCaseOnCompletion Whether to delete the case upon completion - * @param compressionLevel The compression level (0-9) with 0 being no compression and 9 being full compression, values outside - * range will be clamped into range. - * @throws IOException Thrown if there are issues creating the archive or deleting the directory. - */ - public void archiveCase(String nuixCaseDirectory, String archiveFile, boolean deleteCaseOnCompletion, int compressionLevel) throws IOException { - archiveCase(new File(nuixCaseDirectory),new File(archiveFile),deleteCaseOnCompletion,compressionLevel); - } - - /*** - * Archives a Nuix case into a Zip file, optionally deleting the case once completed. Based on: - * https://stackoverflow.com/questions/23318383/compress-directory-into-a-zipfile-with-commons-io - * @param nuixCaseDirectory Directory of the Nuix case - * @param archiveFile The Zip file to archive the case into - * @param deleteCaseOnCompletion Whether to delete the case upon completion - * @param compressionLevel The compression level (0-9) with 0 being no compression and 9 being full compression, values outside - * range will be clamped into range. - * @throws IOException Thrown if there are issues creating the archive or deleting the directory. - */ - public void archiveCase(File nuixCaseDirectory, File archiveFile, boolean deleteCaseOnCompletion, int compressionLevel) throws IOException { - logger.info("Backing up case at " + nuixCaseDirectory.getAbsolutePath() + " to " + archiveFile.getAbsolutePath()); - archiveFile.getParentFile().mkdirs(); - ZipHelper.compressDirectoryToZipFile(nuixCaseDirectory.getAbsolutePath(), archiveFile.getAbsolutePath(), compressionLevel); - if(deleteCaseOnCompletion && archiveFile.exists()) { - logger.info("Deleting now archived case " + nuixCaseDirectory.getAbsolutePath()); - FileUtils.deleteDirectory(nuixCaseDirectory); - } - } + private static Logger logger = Logger.getLogger(CaseUtility.class); + + protected CaseUtility() { + } + + protected static CaseUtility instance = null; + + public static CaseUtility getInstance() { + if (instance == null) { + instance = new CaseUtility(); + } + return instance; + } + + /*** + * Searches for case directories in a given directory and sub-directories. + * @param rootSearchDirectory The root directory to search. + * @return A collection of File objects for case directories located. + */ + public Collection findCaseDirectories(File rootSearchDirectory) { + logger.info("Searching for cases in: " + rootSearchDirectory.getPath()); + if (!rootSearchDirectory.exists()) { + logger.info("Directory does not exist: " + rootSearchDirectory.getPath()); + return List.of(); + } else { + File[] children = rootSearchDirectory.listFiles(); + for (int i = 0; i < children.length; i++) { + File child = children[i]; + if(child.isFile() && child.getName().equalsIgnoreCase("case.fbi2")) { + return List.of(child); + } + } + + List result = new ArrayList<>(); + for (int i = 0; i < children.length; i++) { + File child = children[i]; + if(child.isDirectory()) { + Collection foundFiles = findCaseDirectories(child); + if(!foundFiles.isEmpty()) { + result.addAll(foundFiles); + } + } + } + return result; + } + } + + /*** + * Searches for case directories in a given directory and sub-directories. + * @param rootSearchPath The path to the root directory to search. + * @return A collection of File objects for case directories located. + */ + public Collection findCaseDirectories(String rootSearchPath) { + return findCaseDirectories(new File(rootSearchPath)); + } + + /*** + * Searches for case directories in a given directory and sub-directories. + * @param rootSearchDirectory The root directory to search. + * @return A collection of String representing case directories located. + */ + public Collection findCaseDirectoryPaths(File rootSearchDirectory) { + return findCaseDirectories(rootSearchDirectory) + .stream().map(f -> f.getPath()).collect(Collectors.toList()); + } + + /*** + * Searches for case directories in a given directory and sub-directories. + * @param rootSearchPath The root directory to search. + * @return A collection of String representing case directories located. + */ + public Collection findCaseDirectoryPaths(String rootSearchPath) { + return findCaseDirectoryPaths(new File(rootSearchPath)); + } + + /*** + * Scans specified root directory and sub-directories for cases, returning {@link CaseInfo} objects + * for each case located. + * @param rootSearchDirectory The root directory to search in + * @return Case info objects for each case found + */ + public List findCaseInformation(File rootSearchDirectory) { + List result = null; + result = findCaseDirectories(rootSearchDirectory).stream().map(d -> new CaseInfo(d)).collect(Collectors.toList()); + return result; + } + + /*** + * Scans specified root directory and sub-directories for cases, returning {@link CaseInfo} objects + * for each case located. + * @param rootSearchPath The root directory to search in + * @return Case info objects for each case found + */ + public List findCaseInformation(String rootSearchPath) { + return findCaseInformation(new File(rootSearchPath)); + } + + /*** + * Archives a Nuix case into a Zip file, optionally deleting the case once completed. Based on: + * https://stackoverflow.com/questions/23318383/compress-directory-into-a-zipfile-with-commons-io + * @param nuixCaseDirectory Directory of the Nuix case + * @param archiveFile The Zip file to archive the case into + * @param deleteCaseOnCompletion Whether to delete the case upon completion + * @param compressionLevel The compression level (0-9) with 0 being no compression and 9 being full compression, values outside + * range will be clamped into range. + * @throws IOException Thrown if there are issues creating the archive or deleting the directory. + */ + public void archiveCase(String nuixCaseDirectory, String archiveFile, boolean deleteCaseOnCompletion, int compressionLevel) throws IOException { + archiveCase(new File(nuixCaseDirectory), new File(archiveFile), deleteCaseOnCompletion, compressionLevel); + } + + /*** + * Archives a Nuix case into a Zip file, optionally deleting the case once completed. Based on: + * https://stackoverflow.com/questions/23318383/compress-directory-into-a-zipfile-with-commons-io + * @param nuixCaseDirectory Directory of the Nuix case + * @param archiveFile The Zip file to archive the case into + * @param deleteCaseOnCompletion Whether to delete the case upon completion + * @param compressionLevel The compression level (0-9) with 0 being no compression and 9 being full compression, values outside + * range will be clamped into range. + * @throws IOException Thrown if there are issues creating the archive or deleting the directory. + */ + public void archiveCase(File nuixCaseDirectory, File archiveFile, boolean deleteCaseOnCompletion, int compressionLevel) throws IOException { + logger.info("Backing up case at " + nuixCaseDirectory.getAbsolutePath() + " to " + archiveFile.getAbsolutePath()); + archiveFile.getParentFile().mkdirs(); + ZipHelper.compressDirectoryToZipFile(nuixCaseDirectory.getAbsolutePath(), archiveFile.getAbsolutePath(), compressionLevel); + if (deleteCaseOnCompletion && archiveFile.exists()) { + logger.info("Deleting now archived case " + nuixCaseDirectory.getAbsolutePath()); + FileUtils.deleteDirectory(nuixCaseDirectory); + } + } } diff --git a/Java/SuperUtilities/src/test/java/CaseUtilityTests.java b/Java/SuperUtilities/src/test/java/CaseUtilityTests.java new file mode 100644 index 0000000..f3ee0bb --- /dev/null +++ b/Java/SuperUtilities/src/test/java/CaseUtilityTests.java @@ -0,0 +1,60 @@ +import com.nuix.superutilities.cases.CaseUtility; +import org.apache.commons.io.FileUtils; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CaseUtilityTests extends TestFoundation { + @Test + public void testCaseSearch() throws Exception { + File exampleCaseFbi2 = new File(testDataDirectory, "ExampleCase.fbi2"); + File testDir = new File(testDataDirectory, "CaseSearchTest"); + if (!testDir.exists()) { + testDir.mkdirs(); + // TestData/Case1234 + File fakeCaseRoot = new File(testDir, "Case1234"); + fakeCaseRoot.mkdirs(); + + // TestData/Case1234/case.fbi2 + FileUtils.copyFile(exampleCaseFbi2, new File(fakeCaseRoot, "case.fbi2")); + + // TestData/Case1234/Sub-Directory + File fakeCaseInner = new File(fakeCaseRoot, "Sub-Directory"); + fakeCaseInner.mkdirs(); + + // TestData/Case1234/Sub-Directory/case.fbi2 + FileUtils.copyFile(exampleCaseFbi2, new File(fakeCaseInner, "case.fbi2")); + + // TestData/Case5678 + File fakeCaseRoot2 = new File(testDir, "Case5678"); + fakeCaseRoot2.mkdirs(); + + // TestData/Case5678/case.fbi2 + FileUtils.copyFile(exampleCaseFbi2, new File(fakeCaseRoot2, "case.fbi2")); + + // TestData/Case5678/Sub-Directory + File fakeCaseInner2 = new File(fakeCaseRoot2, "Sub-Directory"); + fakeCaseInner2.mkdirs(); + + // TestData/Case5678/Sub-Directory/case.fbi2 + FileUtils.copyFile(exampleCaseFbi2, new File(fakeCaseInner2, "case.fbi2")); + } + + // Test data should look like: + // TestData/Case1234/case.fbi2 + // TestData/Case1234/Sub-Directory/case.fbi2 + // TestData/Case5678/case.fbi2 + // TestData/Case5678/Sub-Directory/case.fbi2 + // The deeper case.fbi2 files should NOT be found since traversal logic should halt + // searching deeper once it finds each upper case.fbi2 file. The early traversal exit logic + // will hopefully help address a situation where file system stored binaries are in case directory + // which, without early exit logic, will do a large amount of unnecessary traversal when the case + // fbi2 file had been found much higher up. + + Collection caseDirs = CaseUtility.getInstance().findCaseDirectories(testDir); + assertEquals(2, caseDirs.size()); + } +}