Merge pull request #4647 from rcordovano/improved-case-deletion-part2

4578 Improved case deletion
This commit is contained in:
Richard Cordovano 2019-04-03 19:06:46 -04:00 committed by GitHub
commit 2f4799d937
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 3351 additions and 1282 deletions

View File

@ -11,7 +11,7 @@ Case.deleteCaseFailureMessageBox.message=Error deleting case: {0}
Case.deleteCaseFailureMessageBox.title=Failed to Delete Case
Case.exceptionMessage.cancelledByUser=Cancelled by user.
Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first.
Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or there is a problem with the coordination service.
Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host.
Case.exceptionMessage.cannotLocateMainWindow=Cannot locate main application window
Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case.
# {0} - exception message
@ -32,12 +32,19 @@ Case.exceptionMessage.couldNotSaveCaseMetadata=Failed to save case metadata:\n{0
Case.exceptionMessage.couldNotSaveDbNameToMetadataFile=Failed to save case database name to case metadata file:\n{0}.
# {0} - exception message
Case.exceptionMessage.couldNotUpdateCaseNodeData=Failed to update coordination service node data:\n{0}.
# {0} - case display name
Case.exceptionMessage.deletionInterrupted=Deletion of the case {0} was cancelled.
Case.exceptionMessage.emptyCaseDir=Must specify a case directory path.
Case.exceptionMessage.emptyCaseName=Must specify a case name.
Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details
Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details.
# {0} - exception message
Case.exceptionMessage.execExceptionWrapperMessage={0}
Case.exceptionMessage.failedToDeleteCoordinationServiceNodes=Failed to delete the coordination service nodes for the case.
# {0} - exception message
Case.exceptionMessage.failedToConnectToCoordSvc=Failed to connect to coordination service:\n{0}.
# {0} - exception message
Case.exceptionMessage.failedToFetchCoordSvcNodeData=Failed to fetch coordination service node data:\n{0}.
# {0} - exception message
Case.exceptionMessage.failedToLockCaseForDeletion=Failed to exclusively lock case for deletion:\n{0}.
# {0} - exception message
Case.exceptionMessage.failedToReadMetadata=Failed to read case metadata:\n{0}.
Case.exceptionMessage.metadataUpdateError=Failed to update case metadata
@ -50,23 +57,26 @@ Case.progressIndicatorTitle.creatingCase=Creating Case
Case.progressIndicatorTitle.deletingCase=Deleting Case
Case.progressIndicatorTitle.openingCase=Opening Case
Case.progressMessage.cancelling=Cancelling...
Case.progressMessage.checkingForOtherUser=Checking to see if another user has the case open...
Case.progressMessage.clearingTempDirectory=Clearing case temp directory...
Case.progressMessage.closingApplicationServiceResources=Closing case-specific application service resources...
Case.progressMessage.closingCaseDatabase=Closing case database...
Case.progressMessage.closingCaseLevelServices=Closing case-level services...
Case.progressMessage.connectingToCoordSvc=Connecting to coordination service...
Case.progressMessage.creatingCaseDatabase=Creating case database...
Case.progressMessage.creatingCaseDirectory=Creating case directory...
Case.progressMessage.creatingCaseNodeData=Creating coordination service node data...
Case.progressMessage.deletingCaseDatabase=Deleting case database...
Case.progressMessage.deletingCaseDirCoordSvcNode=Deleting case directory coordination service node...
Case.progressMessage.deletingCaseDirectory=Deleting case directory...
Case.progressMessage.deletingCoordinationServiceNodes=Deleting coordination service nodes...
Case.progressMessage.deletingResourcesCoordSvcNode=Deleting case resources coordination service node...
Case.progressMessage.deletingTextIndex=Deleting text index...
Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case...
Case.progressMessage.openingApplicationServiceResources=Opening application service case resources...
Case.progressMessage.openingCaseDatabase=Opening case database...
Case.progressMessage.openingCaseLevelServices=Opening case-level services...
Case.progressMessage.preparing=Preparing...
Case.progressMessage.preparingToOpenCaseResources=<html>Preparing to open case resources.<br>This may take time if another user is upgrading the case.</html>
Case.progressMessage.removingCaseFromRecentCases=Removing case from Recent Cases menu...
Case.progressMessage.savingCaseMetadata=Saving case metadata to file...
Case.progressMessage.settingUpNetworkCommunications=Setting up network communications...
Case.progressMessage.shuttingDownNetworkCommunications=Shutting down network communications...

View File

@ -18,6 +18,7 @@
*/
package org.sleuthkit.autopsy.casemodule;
import com.google.common.annotations.Beta;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import java.awt.Frame;
import java.awt.event.ActionEvent;
@ -31,6 +32,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.ParseException;
@ -79,6 +81,7 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.DataSourceNameChangedEvent;
import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent;
import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils;
import org.sleuthkit.autopsy.casemodule.services.Services;
import org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributeSearchAction;
import org.sleuthkit.autopsy.communications.OpenCommVisualizationToolAction;
@ -119,7 +122,6 @@ import org.sleuthkit.datamodel.Report;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskUnsupportedSchemaVersionException;
import org.sleuthkit.autopsy.coreutils.StopWatch;
/**
* An Autopsy case. Currently, only one case at a time may be open.
@ -139,6 +141,7 @@ public class Case {
private static final String MODULE_FOLDER = "ModuleOutput"; //NON-NLS
private static final String CASE_ACTION_THREAD_NAME = "%s-case-action";
private static final String CASE_RESOURCES_THREAD_NAME = "%s-manage-case-resources";
private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode";
private static final Logger logger = Logger.getLogger(Case.class.getName());
private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher();
private static final Object caseActionSerializationLock = new Object();
@ -699,38 +702,28 @@ public class Case {
}
/**
* Deletes a case. This method cannot be used to delete the current case;
* deleting the current case must be done by calling Case.deleteCurrentCase.
* Deletes a case. The case to be deleted must not be the "current case."
* Deleting the current case must be done by calling Case.deleteCurrentCase.
*
* @param metadata The metadata for the case to delete.
* @param metadata The case metadata.
*
* @throws CaseActionException if there is a problem deleting the case. The
* exception will have a user-friendly message
* and may be a wrapper for a lower-level
* exception.
* @throws CaseActionException If there were one or more errors deleting the
* case. The exception will have a user-friendly
* message and may be a wrapper for a
* lower-level exception.
*/
@Messages({
"Case.progressIndicatorTitle.deletingCase=Deleting Case",
"Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first.",
"Case.progressMessage.checkingForOtherUser=Checking to see if another user has the case open...",
"Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or there is a problem with the coordination service.",
"Case.exceptionMessage.failedToDeleteCoordinationServiceNodes=Failed to delete the coordination service nodes for the case."
"# {0} - case display name", "Case.exceptionMessage.deletionInterrupted=Deletion of the case {0} was cancelled."
})
public static void deleteCase(CaseMetadata metadata) throws CaseActionException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
synchronized (caseActionSerializationLock) {
if (null != currentCase) {
throw new CaseActionException(Bundle.Case_exceptionMessage_cannotDeleteCurrentCase());
}
}
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to acquire caseActionSerializationLock (Java monitor in Case class) for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
/*
* Set up either a GUI progress indicator without a cancel button (can't
* cancel deleting a case) or a logging progress indicator.
*/
ProgressIndicator progressIndicator;
if (RuntimeProperties.runningWithGUI()) {
progressIndicator = new ModalDialogProgressIndicator(mainFrame, Bundle.Case_progressIndicatorTitle_deletingCase());
@ -740,32 +733,17 @@ public class Case {
progressIndicator.start(Bundle.Case_progressMessage_preparing());
try {
if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
deleteCase(metadata, progressIndicator);
deleteSingleUserCase(metadata, progressIndicator);
} else {
/*
* First, acquire an exclusive case directory lock. The case
* cannot be deleted if another node has it open.
*/
progressIndicator.progress(Bundle.Case_progressMessage_checkingForOtherUser());
stopWatch.reset();
stopWatch.start();
try (CoordinationService.Lock dirLock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) {
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to acquire case directory coordination service lock for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
if (dirLock != null) {
deleteCase(metadata, progressIndicator);
} else {
throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireDirLock());
}
} catch (CoordinationServiceException ex) {
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to fail to acquire case directory coordination service lock for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
throw new CaseActionException(Bundle.Case_exceptionMessage_failedToDeleteCoordinationServiceNodes(), ex);
}
try {
deleteCoordinationServiceNodes(metadata, progressIndicator);
} catch (CoordinationServiceException ex) {
throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireDirLock(), ex);
deleteMultiUserCase(metadata, progressIndicator);
} catch (InterruptedException ex) {
/*
* Note that task cancellation is not currently supported
* for this code path, so this catch block is not expected
* to be executed.
*/
throw new CaseActionException(Bundle.Case_exceptionMessage_deletionInterrupted(metadata.getCaseDisplayName()), ex);
}
}
} finally {
@ -773,43 +751,6 @@ public class Case {
}
}
/**
* Deletes the coordination nodes for a multi-user case.
*
* @param metadata The metadata for the case to delete.
* @param progressIndicator The progress indicator for the deletion
* operation.
*
* @throws CoordinationServiceException If there is a problem getting the
* coordination service.
*/
@Messages({
"Case.progressMessage.deletingCoordinationServiceNodes=Deleting coordination service nodes..."
})
static void deleteCoordinationServiceNodes(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CoordinationServiceException {
progressIndicator.progress(Bundle.Case_progressMessage_deletingCoordinationServiceNodes());
CoordinationService coordinationService;
coordinationService = CoordinationService.getInstance();
String resourcesLockNodePath = metadata.getCaseDirectory() + "_resources";
try {
coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath);
} catch (CoordinationServiceException ex) {
/*
* Log but do not notify the user.
*/
logger.log(Level.SEVERE, String.format("Failed to delete resources lock coordination service node for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
}
String caseDirectoryLockNodePath = metadata.getCaseDirectory();
try {
coordinationService.deleteNode(CategoryNode.CASES, caseDirectoryLockNodePath);
} catch (CoordinationServiceException ex) {
/*
* Log but do not notify the user.
*/
logger.log(Level.SEVERE, String.format("Failed to delete case directory lock coordination service node for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
}
}
/**
* Opens a new or existing case as the current case.
*
@ -987,105 +928,6 @@ public class Case {
return imgPaths;
}
/**
*
* Deletes the case directory of a deleted case and removes the case form
* the Recent Cases menu.
*
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
*
* @throws UserPreferencesException if there is a problem getting the case
* databse connection info for a multi-user
* case.
* @throws ClassNotFoundException if there is a problem loading the JDBC
* driver for PostgreSQL for a multi-user
* case.
* @throws SQLException If there is a problem
*/
@Messages({
"Case.progressMessage.deletingTextIndex=Deleting text index...",
"Case.progressMessage.deletingCaseDatabase=Deleting case database...",
"Case.progressMessage.deletingCaseDirectory=Deleting case directory...",
"Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details"
})
private static void deleteCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException {
StopWatch stopWatch = new StopWatch();
boolean errorsOccurred = false;
if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) {
/*
* Delete the case database from the database server.
*/
stopWatch.start();
try {
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase());
CaseDbConnectionInfo db;
db = UserPreferences.getDatabaseConnectionInfo();
Class.forName("org.postgresql.Driver"); //NON-NLS
try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS
Statement statement = connection.createStatement();) {
String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS
statement.execute(deleteCommand);
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to delete case database for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
}
} catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
logger.log(Level.SEVERE, String.format("Failed to delete case database %s for %s (%s) in %s", metadata.getCaseDatabaseName(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
errorsOccurred = true;
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to fail delete case database for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
}
}
/*
* Delete the text index.
*/
progressIndicator.progress(Bundle.Case_progressMessage_deletingTextIndex());
for (KeywordSearchService searchService : Lookup.getDefault().lookupAll(KeywordSearchService.class)) {
try {
stopWatch.reset();
stopWatch.start();
searchService.deleteTextIndex(metadata);
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to delete text index for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
} catch (KeywordSearchServiceException ex) {
logger.log(Level.SEVERE, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
errorsOccurred = true;
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to fail to delete text index for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
}
}
/*
* Delete the case directory.
*/
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirectory());
stopWatch.reset();
stopWatch.start();
if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) {
logger.log(Level.SEVERE, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
errorsOccurred = true;
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to fail to delete case directory for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
} else {
stopWatch.stop();
logger.log(Level.INFO, String.format("Used %d s to delete case directory for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
}
/*
* If running in a GUI, remove the case from the Recent Cases menu
*/
if (RuntimeProperties.runningWithGUI()) {
SwingUtilities.invokeLater(() -> {
RecentCases.getInstance().removeRecentCase(metadata.getCaseDisplayName(), metadata.getFilePath().toString());
});
}
if (errorsOccurred) {
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
}
}
/**
* Acquires an exclusive case resources lock.
*
@ -1101,7 +943,8 @@ public class Case {
})
private static CoordinationService.Lock acquireExclusiveCaseResourcesLock(String caseDir) throws CaseActionException {
try {
String resourcesNodeName = caseDir + "_resources";
Path caseDirPath = Paths.get(caseDir);
String resourcesNodeName = CoordinationServiceUtils.getCaseResourcesNodePath(caseDirPath);
Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, resourcesNodeName, RESOURCES_LOCK_TIMOUT_HOURS, TimeUnit.HOURS);
return lock;
} catch (InterruptedException ex) {
@ -2346,7 +2189,6 @@ public class Case {
"# {0} - service name", "Case.servicesException.notificationTitle={0} Error"
})
private void openAppServiceCaseResources(ProgressIndicator progressIndicator) throws CaseActionException {
progressIndicator.progress(Bundle.Case_progressMessage_openingApplicationServiceResources());
/*
* Each service gets its own independently cancellable/interruptible
* task, running in a named thread managed by an executor service, with
@ -2355,6 +2197,7 @@ public class Case {
* possible to ensure that each service task completes before the next
* one starts by awaiting termination of the executor service.
*/
progressIndicator.progress(Bundle.Case_progressMessage_openingApplicationServiceResources());
for (AutopsyService service : Lookup.getDefault().lookupAll(AutopsyService.class)) {
/*
* Create a progress indicator for the task and start the task. If
@ -2704,6 +2547,365 @@ public class Case {
}
/**
* Deletes a single-user case.
*
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
*
* @throws CaseActionException If there were one or more errors deleting the
* case. The exception will have a user-friendly
* message and may be a wrapper for a
* lower-level exception.
*/
@Messages({
"Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details."
})
private static void deleteSingleUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException {
boolean errorsOccurred = false;
try {
deleteTextIndex(metadata, progressIndicator);
} catch (KeywordSearchServiceException ex) {
errorsOccurred = true;
logger.log(Level.WARNING, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
}
try {
deleteCaseDirectory(metadata, progressIndicator);
} catch (CaseActionException ex) {
errorsOccurred = true;
logger.log(Level.WARNING, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
}
deleteFromRecentCases(metadata, progressIndicator);
if (errorsOccurred) {
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
}
}
/**
* Deletes a multi-user case. This method does so after acquiring the case
* directory coordination service lock and is intended to be used for
* deleting simple multi-user cases without auto ingest input. Note that the
* case directory coordination service node for the case is only deleted if
* no errors occurred.
*
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
*
* @throws CaseActionException If there were one or more errors deleting
* the case. The exception will have a
* user-friendly message and may be a wrapper
* for a lower-level exception.
* @throws InterruptedException If the thread this code is running in is
* interrupted while blocked, i.e., if
* cancellation of the operation is detected
* during a wait.
*/
@Messages({
"Case.progressMessage.connectingToCoordSvc=Connecting to coordination service...",
"# {0} - exception message", "Case.exceptionMessage.failedToConnectToCoordSvc=Failed to connect to coordination service:\n{0}.",
"Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host.",
"# {0} - exception message", "Case.exceptionMessage.failedToLockCaseForDeletion=Failed to exclusively lock case for deletion:\n{0}.",
"Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case...",
"# {0} - exception message", "Case.exceptionMessage.failedToFetchCoordSvcNodeData=Failed to fetch coordination service node data:\n{0}.",
"Case.progressMessage.deletingResourcesCoordSvcNode=Deleting case resources coordination service node...",
"Case.progressMessage.deletingCaseDirCoordSvcNode=Deleting case directory coordination service node..."
})
private static void deleteMultiUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException, InterruptedException {
progressIndicator.progress(Bundle.Case_progressMessage_connectingToCoordSvc());
CoordinationService coordinationService;
try {
coordinationService = CoordinationService.getInstance();
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Failed to connect to coordination service when attempting to delete %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
throw new CaseActionException(Bundle.Case_exceptionMessage_failedToConnectToCoordSvc(ex.getLocalizedMessage()));
}
CaseNodeData caseNodeData;
boolean errorsOccurred = false;
try (CoordinationService.Lock dirLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) {
if (dirLock == null) {
logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because a case directory lock was held by another host", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); //NON-NLS
throw new CaseActionException(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase());
}
progressIndicator.progress(Bundle.Case_progressMessage_fetchingCoordSvcNodeData());
try {
byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, metadata.getCaseDirectory());
caseNodeData = new CaseNodeData(nodeBytes);
} catch (CoordinationServiceException | InterruptedException | IOException ex) {
logger.log(Level.SEVERE, String.format("Failed to get coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
throw new CaseActionException(Bundle.Case_exceptionMessage_failedToFetchCoordSvcNodeData(ex.getLocalizedMessage()));
}
errorsOccurred = deleteMultiUserCase(caseNodeData, metadata, progressIndicator, logger);
progressIndicator.progress(Bundle.Case_progressMessage_deletingResourcesCoordSvcNode());
try {
String resourcesLockNodePath = CoordinationServiceUtils.getCaseResourcesNodePath(caseNodeData.getDirectory());
coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath);
} catch (CoordinationServiceException ex) {
if (!isNoNodeException(ex)) {
errorsOccurred = true;
logger.log(Level.WARNING, String.format("Error deleting the case resources coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
}
} catch (InterruptedException ex) {
logger.log(Level.WARNING, String.format("Error deleting the case resources coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
}
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error exclusively locking the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
throw new CaseActionException(Bundle.Case_exceptionMessage_failedToLockCaseForDeletion(ex.getLocalizedMessage()));
}
if (!errorsOccurred) {
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirCoordSvcNode());
try {
String casDirNodePath = CoordinationServiceUtils.getCaseDirectoryNodePath(caseNodeData.getDirectory());
coordinationService.deleteNode(CategoryNode.CASES, casDirNodePath);
} catch (CoordinationServiceException | InterruptedException ex) {
logger.log(Level.SEVERE, String.format("Error deleting the case directory lock node for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
errorsOccurred = true;
}
}
if (errorsOccurred) {
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
}
}
/**
* IMPORTANT: This is a "beta" method and is subject to change or removal
* without notice!
*
* Deletes a mulit-user case by attempting to delete the case database, the
* text index, the case directory, and the case resources coordination
* service node for a case, and removes the case from the recent cases menu
* of the main application window. Callers of this method MUST acquire and
* release the case directory lock for the case and are responsible for
* deleting the corresponding coordination service nodes, if desired.
*
* @param caseNodeData The coordination service node data for the case.
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
* @param logger A logger.
*
* @return True if one or more errors occurred (see log for details), false
* otherwise.
*
* @throws InterruptedException If the thread this code is running in is
* interrupted while blocked, i.e., if
* cancellation of the operation is detected
* during a wait.
*/
@Beta
public static boolean deleteMultiUserCase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws InterruptedException {
boolean errorsOccurred = false;
try {
deleteMultiUserCaseDatabase(caseNodeData, metadata, progressIndicator, logger);
deleteMultiUserCaseTextIndex(caseNodeData, metadata, progressIndicator, logger);
deleteMultiUserCaseDirectory(caseNodeData, metadata, progressIndicator, logger);
deleteFromRecentCases(metadata, progressIndicator);
} catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
errorsOccurred = true;
logger.log(Level.WARNING, String.format("Failed to delete the case database for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
} catch (KeywordSearchServiceException ex) {
errorsOccurred = true;
logger.log(Level.WARNING, String.format("Failed to delete the text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
} catch (CaseActionException ex) {
errorsOccurred = true;
logger.log(Level.WARNING, String.format("Failed to delete the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS
}
return errorsOccurred;
}
/**
* Attempts to delete the case database for a multi-user case.
*
* @param caseNodeData The coordination service node data for the case.
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
* @param logger A logger.
*
* @throws UserPreferencesException if there is an error getting the
* database server connection info.
* @throws ClassNotFoundException if there is an error gettting the
* required JDBC driver.
* @throws SQLException if there is an error executing the SQL
* to drop the database from the database
* server.
* @throws InterruptedException If interrupted while blocked waiting for
* coordination service data to be written
* to the coordination service node
* database.
*/
@Messages({
"Case.progressMessage.deletingCaseDatabase=Deleting case database..."
})
private static void deleteMultiUserCaseDatabase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws UserPreferencesException, ClassNotFoundException, SQLException, InterruptedException {
if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB)) {
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase());
logger.log(Level.INFO, String.format("Deleting case database for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); //NON-NLS
CaseDbConnectionInfo info = UserPreferences.getDatabaseConnectionInfo();
String url = "jdbc:postgresql://" + info.getHost() + ":" + info.getPort() + "/postgres"; //NON-NLS
Class.forName("org.postgresql.Driver"); //NON-NLS
try (Connection connection = DriverManager.getConnection(url, info.getUserName(), info.getPassword()); Statement statement = connection.createStatement()) {
String dbExistsQuery = "SELECT 1 from pg_database WHERE datname = '" + metadata.getCaseDatabaseName() + "'"; //NON-NLS
try (ResultSet queryResult = statement.executeQuery(dbExistsQuery)) {
if (queryResult.next()) {
String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS
statement.execute(deleteCommand);
}
}
}
setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.CASE_DB);
}
}
/**
* Attempts to delete the text index for a multi-user case.
*
* @param caseNodeData The coordination service node data for the case.
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
* @param logger A logger.
*
* @throws KeywordSearchServiceException If there is an error deleting the
* text index.
* @throws InterruptedException If interrupted while blocked
* waiting for coordination service
* data to be written to the
* coordination service node database.
*/
private static void deleteMultiUserCaseTextIndex(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws KeywordSearchServiceException, InterruptedException {
if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.TEXT_INDEX)) {
logger.log(Level.INFO, String.format("Deleting text index for %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); //NON-NLS
deleteTextIndex(metadata, progressIndicator);
setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.TEXT_INDEX);
}
}
/**
* Attempts to delete the text index for a case.
*
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
*
* @throws KeywordSearchServiceException If there is an error deleting the
* text index.
*/
@Messages({
"Case.progressMessage.deletingTextIndex=Deleting text index..."
})
private static void deleteTextIndex(CaseMetadata metadata, ProgressIndicator progressIndicator) throws KeywordSearchServiceException {
progressIndicator.progress(Bundle.Case_progressMessage_deletingTextIndex());
for (KeywordSearchService searchService : Lookup.getDefault().lookupAll(KeywordSearchService.class)) {
searchService.deleteTextIndex(metadata);
}
}
/**
* Attempts to delete the case directory for a multi-user case.
*
* @param caseNodeData The coordination service node data for the case.
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
* @param logger A logger.
*
* @throws CaseActionException if there is an error deleting the case
* directory.
* @throws InterruptedException If interrupted while blocked waiting for
* coordination service data to be written to
* the coordination service node database.
*/
private static void deleteMultiUserCaseDirectory(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws CaseActionException, InterruptedException {
if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR)) {
logger.log(Level.INFO, String.format("Deleting case directory for %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); //NON-NLS
deleteCaseDirectory(metadata, progressIndicator);
setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.CASE_DIR);
}
}
/**
* Attempts to delete the case directory for a case.
*
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
*
* @throws CaseActionException If there is an error deleting the case
* directory.
*/
@Messages({
"Case.progressMessage.deletingCaseDirectory=Deleting case directory..."
})
private static void deleteCaseDirectory(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException {
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirectory());
if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) {
throw new CaseActionException(String.format("Failed to delete %s", metadata.getCaseDirectory())); //NON-NLS
}
}
/**
* Attempts to remove a case from the recent cases menu if the main
* application window is present.
*
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
*/
@Messages({
"Case.progressMessage.removingCaseFromRecentCases=Removing case from Recent Cases menu..."
})
private static void deleteFromRecentCases(CaseMetadata metadata, ProgressIndicator progressIndicator) {
if (RuntimeProperties.runningWithGUI()) {
progressIndicator.progress(Bundle.Case_progressMessage_removingCaseFromRecentCases());
SwingUtilities.invokeLater(() -> {
RecentCases.getInstance().removeRecentCase(metadata.getCaseDisplayName(), metadata.getFilePath().toString());
});
}
}
/**
* Examines a coordination service exception to try to determine if it is a
* "no node" exception, i.e., an operation was attempted on a node that does
* not exist.
*
* @param ex A coordination service exception.
*
* @return True or false.
*/
private static boolean isNoNodeException(CoordinationServiceException ex) {
boolean isNodeNodeEx = false;
Throwable cause = ex.getCause();
if (cause != null) {
String causeMessage = cause.getMessage();
isNodeNodeEx = causeMessage.contains(NO_NODE_ERROR_MSG_FRAGMENT);
}
return isNodeNodeEx;
}
/**
* Sets a deleted item flag in the coordination service node data for a
* multi-user case.
*
* @param caseNodeData The coordination service node data for the case.
* @param flag The flag to set.
*
* @throws InterruptedException If interrupted while blocked waiting for
* coordination service data to be written to
* the coordination service node database.
*/
private static void setDeletedItemFlag(CaseNodeData caseNodeData, CaseNodeData.DeletedFlags flag) throws InterruptedException {
try {
caseNodeData.setDeletedFlag(flag);
CoordinationService coordinationService = CoordinationService.getInstance();
coordinationService.setNodeData(CategoryNode.CASES, caseNodeData.getDirectory().toString(), caseNodeData.toArray());
} catch (IOException | CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s (%s) in %s", flag.name(), caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex);
}
}
/**
* A case operation Cancel button listener for use with a
* ModalDialogProgressIndicator when running with a GUI.

View File

@ -144,10 +144,10 @@ public final class CaseMetadata {
* persisted to the case metadata file until writeFile or a setX method is
* called.
*
* @param caseType The type of case.
* @param caseDirectory The case directory.
* @param caseName The immutable name of the case.
* @param caseDetails The details for the case
* @param caseType The type of case.
* @param caseDirectory The case directory.
* @param caseName The immutable name of the case.
* @param caseDetails The details for the case
*/
CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails) {
this(caseType, caseDirectory, caseName, caseDetails, null);
@ -158,11 +158,11 @@ public final class CaseMetadata {
* persisted to the case metadata file until writeFile or a setX method is
* called.
*
* @param caseType The type of case.
* @param caseDirectory The case directory.
* @param caseName The immutable name of the case.
* @param caseDetails The details for the case
* @param originalMetadata The metadata object from the original case
* @param caseType The type of case.
* @param caseDirectory The case directory.
* @param caseName The immutable name of the case.
* @param caseDetails The details for the case
* @param originalMetadata The metadata object from the original case
*/
CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails, CaseMetadata originalMetadata) {
metadataFilePath = Paths.get(caseDirectory, caseDetails.getCaseDisplayName() + FILE_EXTENSION);
@ -192,23 +192,23 @@ public final class CaseMetadata {
}
/**
* Locate the case meta data file in the supplied directory. If the file does
* not exist, null is returned.
* Locate the case meta data file in the supplied directory. If the file
* does not exist, null is returned.
*
* @param directoryPath Directory path to search
* @return case meta data file path or null
* @param directoryPath Directory path to search.
*
* @return Case metadata file path or null.
*/
public static Path getCaseMetadataFile(Path directoryPath) {
final File[] caseFiles = directoryPath.toFile().listFiles();
if(caseFiles != null) {
for (File file : caseFiles) {
public static Path getCaseMetadataFilePath(Path directoryPath) {
final File[] files = directoryPath.toFile().listFiles();
if (files != null) {
for (File file : files) {
final String fileName = file.getName().toLowerCase();
if (fileName.endsWith(CaseMetadata.getFileExtension())) {
if (fileName.endsWith(CaseMetadata.getFileExtension()) && file.isFile()) {
return file.toPath();
}
}
}
return null;
}
@ -478,9 +478,9 @@ public final class CaseMetadata {
/**
* Write the case element children for the given metadata object
*
* @param doc The document.
* @param caseElement The case element parent
* @param metadataToWrite The CaseMetadata object to read from
* @param doc The document.
* @param caseElement The case element parent
* @param metadataToWrite The CaseMetadata object to read from
*/
private void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite) {
CaseDetails caseDetailsToWrite = metadataToWrite.caseDetails;
@ -573,8 +573,7 @@ public final class CaseMetadata {
caseNotes = getElementTextContent(caseElement, CASE_ELEMENT_NOTES, false);
}
this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail,
caseNotes);
this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, caseNotes);
this.caseType = Case.CaseType.fromString(getElementTextContent(caseElement, CASE_TYPE_ELEMENT_NAME, true));
if (null == this.caseType) {
throw new CaseMetadataException("Case metadata file corrupted");

View File

@ -238,6 +238,27 @@ public final class CaseNodeData {
this.displayName = displayName;
}
/**
* Checks whether a deleted item flag is set for the case represented by
* this node data.
*
* @param flag The flag to check.
*
* @return
*/
public boolean isDeletedFlagSet(DeletedFlags flag) {
return (this.deletedItemFlags & flag.getValue()) == flag.getValue();
}
/**
* Sets a deleted item flag for the case represented by this node data.
*
* @param flag The flag to set.
*/
public void setDeletedFlag(DeletedFlags flag) {
this.deletedItemFlags |= flag.getValue();
}
/**
* Gets the node data as a byte array that can be sent to the coordination
* service.
@ -262,16 +283,37 @@ public final class CaseNodeData {
return byteStream.toByteArray();
}
public final static class InvalidDataException extends Exception {
/**
* Flags for the various components of a case that can be deleted.
*/
public enum DeletedFlags {
private static final long serialVersionUID = 1L;
TEXT_INDEX(1),
CASE_DB(2),
CASE_DIR(4),
DATA_SOURCES(8),
MANIFEST_FILE_NODES(16);
private InvalidDataException(String message) {
super(message);
private final short value;
/**
* Constructs a flag for a case component that can be deleted.
*
* @param value
*/
private DeletedFlags(int value) {
this.value = (short) value;
}
private InvalidDataException(String message, Throwable cause) {
super(message, cause);
/**
* Gets the value of the flag.
*
* @return The value as a short.
*/
private short getValue() {
return value;
}
}
}

View File

@ -0,0 +1,187 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.casemodule.multiusercases;
import java.io.File;
import java.io.IOException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException;
import org.sleuthkit.autopsy.coreutils.Logger;
/**
* Queries the coordination service to collect the multi-user case node data
* stored in the case directory lock ZooKeeper nodes.
*/
final public class CaseNodeDataCollector {
private static final Logger logger = Logger.getLogger(CaseNodeDataCollector.class.getName());
/**
* Queries the coordination service to collect the multi-user case node data
* stored in the case directory lock ZooKeeper nodes.
*
* @return The node data for the multi-user cases known to the coordination
* service.
*
* @throws CoordinationServiceException If there is an error interacting
* with the coordination service.
* @throws InterruptedException If the current thread is interrupted
* while waiting for the coordination
* service.
*/
public static List<CaseNodeData> getNodeData() throws CoordinationServiceException, InterruptedException {
final List<CaseNodeData> cases = new ArrayList<>();
final CoordinationService coordinationService = CoordinationService.getInstance();
final List<String> nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES);
for (String nodeName : nodeList) {
if (CoordinationServiceUtils.isCaseNameNodePath(nodeName)
|| CoordinationServiceUtils.isCaseResourcesNodePath(nodeName)
|| CoordinationServiceUtils.isCaseAutoIngestLogNodePath(nodeName)) {
continue;
}
/*
* Get the data from the case directory lock node. This data may not
* exist or may exist only in an older version. If it is missing or
* incomplete, create or update it.
*/
try {
CaseNodeData nodeData;
final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName);
if (nodeBytes != null && nodeBytes.length > 0) {
nodeData = new CaseNodeData(nodeBytes);
if (nodeData.getVersion() < CaseNodeData.getCurrentVersion()) {
nodeData = updateNodeData(nodeName, nodeData);
}
} else {
nodeData = updateNodeData(nodeName, null);
}
if (nodeData != null) {
cases.add(nodeData);
}
} catch (CoordinationService.CoordinationServiceException | InterruptedException | IOException | ParseException | CaseMetadata.CaseMetadataException ex) {
logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex);
}
}
return cases;
}
/**
* Updates the case directory lock coordination service node data for a
* case.
*
* @param nodeName The coordination service node name, i.e., the case
* directory path.
* @param oldNodeData The node data to be updated.
*
* @return A CaseNodedata object or null if the coordination service node is
* an "orphan" with no corresponding case directry.
*
* @throws IOException If there is an error writing the
* node data to a byte array.
* @throws CaseMetadataException If there is an error reading the
* case metadata file.
* @throws ParseException If there is an error parsing a date
* from the case metadata file.
* @throws CoordinationServiceException If there is an error interacting
* with the coordination service.
* @throws InterruptedException If a coordination service operation
* is interrupted.
*/
private static CaseNodeData updateNodeData(String nodeName, CaseNodeData oldNodeData) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException {
Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS);
File caseDirectory = caseDirectoryPath.toFile();
if (!caseDirectory.exists()) {
logger.log(Level.WARNING, String.format("Found orphan coordination service node %s, attempting clean up", caseDirectoryPath));
deleteLockNodes(CoordinationService.getInstance(), caseDirectoryPath);
return null;
}
CaseNodeData nodeData = null;
if (oldNodeData == null || oldNodeData.getVersion() == 0) {
File[] files = caseDirectory.listFiles();
for (File file : files) {
String name = file.getName().toLowerCase();
if (name.endsWith(CaseMetadata.getFileExtension())) {
CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath()));
nodeData = new CaseNodeData(metadata);
if (oldNodeData != null) {
/*
* Version 0 case node data was only written if errors
* occurred during an auto ingest job.
*/
nodeData.setErrorsOccurred(true);
}
break;
}
}
}
if (nodeData != null) {
CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray());
}
return nodeData;
}
/**
* Attempts to delete the coordination service lock nodes for a case,
* logging any failures.
*
* @param coordinationService The coordination service.
* @param caseDirectoryPath The case directory path.
*/
private static void deleteLockNodes(CoordinationService coordinationService, Path caseDirectoryPath) {
deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseResourcesNodePath(caseDirectoryPath));
deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseAutoIngestLogNodePath(caseDirectoryPath));
deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseDirectoryNodePath(caseDirectoryPath));
deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseNameNodePath(caseDirectoryPath));
}
/**
* Attempts to delete a coordination service node, logging failure.
*
* @param coordinationService The coordination service.
* @param nodeName A node name.
*/
private static void deleteCoordinationServiceNode(CoordinationService coordinationService, String nodeName) {
try {
coordinationService.deleteNode(CoordinationService.CategoryNode.CASES, nodeName);
} catch (CoordinationService.CoordinationServiceException | InterruptedException ex) {
logger.log(Level.WARNING, String.format("Error deleting coordination service node %s", nodeName), ex);
}
}
/**
* Private constructor to prevent instantiation of this utility class.
*/
private CaseNodeDataCollector() {
}
}

View File

@ -0,0 +1,133 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.casemodule.multiusercases;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.sleuthkit.autopsy.coreutils.TimeStampUtils;
/**
* Utility methods for using the coordination service for multi-user cases.
*/
public final class CoordinationServiceUtils {
private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS
private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS
/**
* Gets the path of the case resources coordination service node for a case.
* This coordiantion service node is used for case resource locking.
*
* @param caseDirectoryPath The case directory path.
*
* @return The case resources coordination service node path.
*/
public static String getCaseResourcesNodePath(Path caseDirectoryPath) {
return caseDirectoryPath + RESOURCES_LOCK_SUFFIX;
}
/**
* Gets the path of the case auto ingest log coordination service node for a
* case. This coordination service node is used for serializing case auto
* ingest log writes.
*
* @param caseDirectoryPath The case directory path.
*
* @return The case auto ingest log coordination service node path.
*/
public static String getCaseAutoIngestLogNodePath(Path caseDirectoryPath) {
return Paths.get(caseDirectoryPath.toString(), CASE_AUTO_INGEST_LOG_NAME).toString();
}
/**
* Gets the path of the case directory coordination service node for a case.
* This coordination service node is used for locking the case directory and
* for storing data about the case.
*
* @param caseDirectoryPath The case directory path.
*
* @return The case directory coordination service node path.
*/
public static String getCaseDirectoryNodePath(Path caseDirectoryPath) {
return caseDirectoryPath.toString();
}
/**
* Gets the path of the case name coordination service node for a case. This
* coordination service node is used to lock the case name so that only one
* node at a time can create a case with a particular name.
*
* @param caseDirectoryPath The case directory path.
*
* @return The case name coordination service node path.
*/
public static String getCaseNameNodePath(Path caseDirectoryPath) {
String caseName = caseDirectoryPath.getFileName().toString();
if (TimeStampUtils.endsWithTimeStamp(caseName)) {
caseName = TimeStampUtils.removeTimeStamp(caseName);
if (caseName.endsWith("_")) {
caseName = caseName.substring(0, caseName.length() - 1);
}
}
return caseName;
}
/**
* Determines whether or not a coordination service node path is a case auto
* ingest node path.
*
* @param nodePath The node path.
*
* @return True or false.
*/
public static boolean isCaseAutoIngestLogNodePath(String nodePath) {
return Paths.get(nodePath).getFileName().toString().equals(CASE_AUTO_INGEST_LOG_NAME);
}
/**
* Determines whether or not a coordination service node path is a case
* resources node path.
*
* @param nodePath The node path.
*
* @return True or false.
*/
public static boolean isCaseResourcesNodePath(String nodePath) {
return Paths.get(nodePath).getFileName().toString().endsWith(RESOURCES_LOCK_SUFFIX);
}
/**
* Determines whether or not a coordination service node path is a case name
* node path.
*
* @param nodePath The node path.
*
* @return True or false.
*/
public static boolean isCaseNameNodePath(String nodePath) {
return !(nodePath.contains("\\") || nodePath.contains("//"));
}
/**
* Prevents instantiation of this uitlity class.
*/
private CoordinationServiceUtils() {
}
}

View File

@ -1,164 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.casemodule.multiusercases;
import java.io.File;
import java.io.IOException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coreutils.Logger;
/**
* Queries the coordination service to collect the multi-user case node data
* stored in the case directory lock ZooKeeper nodes.
*/
final public class MultiUserCaseNodeDataCollector {
private static final Logger logger = Logger.getLogger(MultiUserCaseNodeDataCollector.class.getName());
private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS
private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS
/**
* Queries the coordination service to collect the multi-user case node data
* stored in the case directory lock ZooKeeper nodes.
*
* @return A list of CaseNodedata objects that convert data for a case
* directory lock coordination service node to and from byte arrays.
*
* @throws CoordinationServiceException If there is an error
*/
public static List<CaseNodeData> getNodeData() throws CoordinationService.CoordinationServiceException {
final List<CaseNodeData> cases = new ArrayList<>();
final CoordinationService coordinationService = CoordinationService.getInstance();
final List<String> nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES);
for (String nodeName : nodeList) {
/*
* Ignore auto ingest case name lock nodes.
*/
final Path nodeNameAsPath = Paths.get(nodeName);
if (!(nodeNameAsPath.toString().contains("\\") || nodeNameAsPath.toString().contains("//"))) {
continue;
}
/*
* Ignore case auto ingest log lock nodes and resource lock nodes.
*/
final String lastNodeNameComponent = nodeNameAsPath.getFileName().toString();
if (lastNodeNameComponent.equals(CASE_AUTO_INGEST_LOG_NAME)) {
continue;
}
/*
* Ignore case resources lock nodes.
*/
if (lastNodeNameComponent.endsWith(RESOURCES_LOCK_SUFFIX)) {
continue;
}
/*
* Get the data from the case directory lock node. This data may not
* exist for "legacy" nodes. If it is missing, create it.
*/
try {
CaseNodeData nodeData;
byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName);
if (nodeBytes != null && nodeBytes.length > 0) {
nodeData = new CaseNodeData(nodeBytes);
if (nodeData.getVersion() == 0) {
/*
* Version 0 case node data was only written if errors
* occurred during an auto ingest job and consisted of
* only the set errors flag.
*/
nodeData = createNodeDataFromCaseMetadata(nodeName, true);
}
} else {
nodeData = createNodeDataFromCaseMetadata(nodeName, false);
}
cases.add(nodeData);
} catch (CoordinationService.CoordinationServiceException | InterruptedException | IOException | ParseException | CaseMetadata.CaseMetadataException ex) {
logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex);
}
}
return cases;
}
/**
* Creates and saves case directory lock coordination service node data from
* the metadata file for the case associated with the node.
*
* @param nodeName The coordination service node name, i.e., the case
* directory path.
* @param errorsOccurred Whether or not errors occurred during an auto
* ingest job for the case.
*
* @return A CaseNodedata object.
*
* @throws IOException If there is an error writing the
* node data to a byte array.
* @throws CaseMetadataException If there is an error reading the
* case metadata file.
* @throws ParseException If there is an error parsing a date
* from the case metadata file.
* @throws CoordinationServiceException If there is an error interacting
* with the coordination service.
* @throws InterruptedException If a coordination service operation
* is interrupted.
*/
private static CaseNodeData createNodeDataFromCaseMetadata(String nodeName, boolean errorsOccurred) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException {
CaseNodeData nodeData = null;
Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS);
File caseDirectory = caseDirectoryPath.toFile();
if (caseDirectory.exists()) {
File[] files = caseDirectory.listFiles();
for (File file : files) {
String name = file.getName().toLowerCase();
if (name.endsWith(CaseMetadata.getFileExtension())) {
CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath()));
nodeData = new CaseNodeData(metadata);
nodeData.setErrorsOccurred(errorsOccurred);
break;
}
}
}
if (nodeData != null) {
CoordinationService coordinationService = CoordinationService.getInstance();
coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray());
return nodeData;
} else {
throw new IOException(String.format("Could not find case metadata file for %s", nodeName));
}
}
/**
* Private constructor to prevent instantiation of this utility class.
*/
private MultiUserCaseNodeDataCollector() {
}
}

View File

@ -1,5 +1,12 @@
MultiUserCaseBrowserCustomizer.column.caseDbDeleteStatus=Case Database Deleted
MultiUserCaseBrowserCustomizer.column.caseDirDeleteStatus=Case Directory Deleted
MultiUserCaseBrowserCustomizer.column.createTime=Create Time
MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Deleted
MultiUserCaseBrowserCustomizer.column.directory=Directory
MultiUserCaseBrowserCustomizer.column.displayName=Name
MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time
MultiUserCaseBrowserCustomizer.column.manifestFileZNodesDeleteStatus=Manifest Znodes Deleted
MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted
MultiUserCaseNode.column.createTime=False
MultiUserCaseNode.columnValue.true=True
MultiUserCasesBrowserPanel.waitNode.message=Please Wait...

View File

@ -141,13 +141,23 @@ public interface MultiUserCaseBrowserCustomizer {
"MultiUserCaseBrowserCustomizer.column.displayName=Name",
"MultiUserCaseBrowserCustomizer.column.createTime=Create Time",
"MultiUserCaseBrowserCustomizer.column.directory=Directory",
"MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time"
"MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time",
"MultiUserCaseBrowserCustomizer.column.manifestFileZNodesDeleteStatus=Manifest Znodes Deleted",
"MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Deleted",
"MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted",
"MultiUserCaseBrowserCustomizer.column.caseDbDeleteStatus=Case Database Deleted",
"MultiUserCaseBrowserCustomizer.column.caseDirDeleteStatus=Case Directory Deleted"
})
public enum Column {
DISPLAY_NAME(Bundle.MultiUserCaseBrowserCustomizer_column_displayName()),
CREATE_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_createTime()),
DIRECTORY(Bundle.MultiUserCaseBrowserCustomizer_column_directory()),
LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime());
LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime()),
MANIFEST_FILE_ZNODES_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_manifestFileZNodesDeleteStatus()),
DATA_SOURCES_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_dataSourcesDeleteStatus()),
TEXT_INDEX_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_textIndexDeleteStatus()),
CASE_DB_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_caseDbDeleteStatus()),
CASE_DIR_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_caseDirDeleteStatus());
private final String displayName;

View File

@ -25,8 +25,10 @@ import javax.swing.Action;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.Sheet;
import org.openide.util.NbBundle;
import org.openide.util.lookup.Lookups;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.DeletedFlags;
import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrowserCustomizer.Column;
import org.sleuthkit.autopsy.datamodel.NodeProperty;
@ -75,6 +77,21 @@ final class MultiUserCaseNode extends AbstractNode {
case LAST_ACCESS_DATE:
sheetSet.put(new NodeProperty<>(propName, propName, propName, caseNodeData.getLastAccessDate()));
break;
case MANIFEST_FILE_ZNODES_DELETE_STATUS:
sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.MANIFEST_FILE_NODES)));
break;
case DATA_SOURCES_DELETE_STATUS:
sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.DATA_SOURCES)));
break;
case TEXT_INDEX_DELETE_STATUS:
sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.TEXT_INDEX)));
break;
case CASE_DB_DELETE_STATUS:
sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.CASE_DB)));
break;
case CASE_DIR_DELETE_STATUS:
sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.CASE_DIR)));
break;
default:
break;
}
@ -95,4 +112,20 @@ final class MultiUserCaseNode extends AbstractNode {
return customizer.getPreferredAction(caseNodeData);
}
/**
* Interprets the deletion status of part of a case.
*
* @param flag The coordination service node data deleted items flag
* to interpret.
*
* @return A string stating "True" or "False."
*/
@NbBundle.Messages({
"MultiUserCaseNode.columnValue.true=True",
"MultiUserCaseNode.column.createTime=False",
})
private String isDeleted(CaseNodeData.DeletedFlags flag) {
return caseNodeData.isDeletedFlagSet(flag) ? "True" : "False";
}
}

View File

@ -25,7 +25,7 @@ import org.openide.nodes.ChildFactory;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.casemodule.multiusercases.MultiUserCaseNodeDataCollector;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeDataCollector;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coreutils.Logger;
@ -63,9 +63,9 @@ final class MultiUserCasesRootNode extends AbstractNode {
@Override
protected boolean createKeys(List<CaseNodeData> keys) {
try {
List<CaseNodeData> caseNodeData = MultiUserCaseNodeDataCollector.getNodeData();
List<CaseNodeData> caseNodeData = CaseNodeDataCollector.getNodeData();
keys.addAll(caseNodeData);
} catch (CoordinationService.CoordinationServiceException ex) {
} catch (CoordinationService.CoordinationServiceException | InterruptedException ex) {
logger.log(Level.SEVERE, "Failed to get case node data from coodination service", ex);
}
return true;

View File

@ -114,7 +114,16 @@ public final class CoordinationService {
}
try {
instance = new CoordinationService(rootNode);
} catch (IOException | InterruptedException | KeeperException | CoordinationServiceException ex) {
} catch (IOException | KeeperException | CoordinationServiceException ex) {
throw new CoordinationServiceException("Failed to create coordination service", ex);
} catch (InterruptedException ex) {
/*
* The interrupted exception should be propagated to support
* task cancellation. To avoid a public API change here, restore
* the interrupted flag and then throw the InterruptedException
* in its wrapper.
*/
Thread.currentThread().interrupt();
throw new CoordinationServiceException("Failed to create coordination service", ex);
}
}
@ -363,15 +372,22 @@ public final class CoordinationService {
* @param category The desired category in the namespace.
* @param nodePath The node to be deleted.
*
* @throws CoordinationServiceException If there is an error deleting the
* node.
* @throws CoordinationServiceException If there is an error deleting the
* node.
* @throws java.lang.InterruptedException If a thread interrupt occurs while
* blocked waiting for the operation
* to complete.
*/
public void deleteNode(CategoryNode category, String nodePath) throws CoordinationServiceException {
public void deleteNode(CategoryNode category, String nodePath) throws CoordinationServiceException, InterruptedException {
String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try {
curator.delete().forPath(fullNodePath);
} catch (Exception ex) {
throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex);
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex);
}
}
}
@ -382,15 +398,22 @@ public final class CoordinationService {
*
* @return A list of child node names.
*
* @throws CoordinationServiceException If there is an error getting the
* node list.
* @throws CoordinationServiceException If there is an error getting the
* node list.
* @throws java.lang.InterruptedException If a thread interrupt occurs while
* blocked waiting for the operation
* to complete.
*/
public List<String> getNodeList(CategoryNode category) throws CoordinationServiceException {
public List<String> getNodeList(CategoryNode category) throws CoordinationServiceException, InterruptedException {
try {
List<String> list = curator.getChildren().forPath(categoryNodeToPath.get(category.getDisplayName()));
return list;
} catch (Exception ex) {
throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex);
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex);
}
}
}
@ -404,9 +427,9 @@ public final class CoordinationService {
*/
private String getFullyQualifiedNodePath(CategoryNode category, String nodePath) {
// nodePath on Unix systems starts with a "/" and ZooKeeper doesn't like two slashes in a row
if(nodePath.startsWith("/")){
if (nodePath.startsWith("/")) {
return categoryNodeToPath.get(category.getDisplayName()) + nodePath.toUpperCase();
}else{
} else {
return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase();
}
}

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2011-2017 Basis Technology Corp.
* Copyright 2013-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -59,7 +59,19 @@ public final class ExecUtil {
}
/**
* Process terminator that can be used to kill a processes after it exceeds
* A process terminator that can be used to kill a process spawned by a
* thread that has been interrupted.
*/
public static class InterruptedThreadProcessTerminator implements ProcessTerminator {
@Override
public boolean shouldTerminateProcess() {
return Thread.currentThread().isInterrupted();
}
}
/**
* A process terminator that can be used to kill a process after it exceeds
* a maximum allowable run time.
*/
public static class TimedProcessTerminator implements ProcessTerminator {
@ -212,9 +224,6 @@ public final class ExecUtil {
}
}
/**
* EVERYTHING FOLLOWING THIS LINE IS DEPRECATED AND SLATED FOR REMOVAL
*/
private static final Logger logger = Logger.getLogger(ExecUtil.class.getName());
private Process proc = null;
private ExecUtil.StreamToStringRedirect errorStringRedirect = null;

View File

@ -0,0 +1,124 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* 9
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.progress;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable;
/**
* A progress indicator that displays progress using a progress bar in the lower
* right hand corner of the application main frame, i.e., a NetBeans
* ProgressHandle.
*/
public final class AppFrameProgressBar implements ProgressIndicator {
private final String displayName;
private Cancellable cancellationBehavior;
private ProgressHandle progressHandle;
private volatile boolean cancelling;
/**
* Constructs a progress indicator that displays progress using a progress
* bar in the lower right hand corner of the application main frame, i.e., a
* NetBeans ProgressHandle.
*
* @param displayName The display name for the progress bar (a fixed name
* that appears above the current progress message).
*/
public AppFrameProgressBar(String displayName) {
this.displayName = displayName;
}
/**
* Sets the cancellation behavior that should happen when a user clicks on
* the "x" button of the progress bar.
*
* @param cancellationBehavior A org.openide.util.Cancellable that
* implements the desired cancellation behavior.
*/
public void setCancellationBehavior(Cancellable cancellationBehavior) {
this.cancellationBehavior = cancellationBehavior;
}
@Override
public void start(String message, int totalWorkUnits) {
cancelling = false;
this.progressHandle = ProgressHandle.createHandle(displayName, cancellationBehavior);
progressHandle.start(totalWorkUnits);
progressHandle.progress(message);
}
@Override
public void start(String message) {
cancelling = false;
this.progressHandle = ProgressHandle.createHandle(displayName, cancellationBehavior);
progressHandle.start();
progressHandle.progress(message);
}
@Override
public void switchToIndeterminate(String message) {
if (!cancelling) {
progressHandle.switchToIndeterminate();
progressHandle.progress(message);
}
}
@Override
public void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits) {
if (!cancelling) {
progressHandle.switchToDeterminate(totalWorkUnits);
progressHandle.progress(message, workUnitsCompleted);
}
}
@Override
public void progress(String message) {
if (!cancelling) {
progressHandle.progress(message);
}
}
@Override
public void progress(int workUnitsCompleted) {
if (!cancelling) {
progressHandle.progress(workUnitsCompleted);
}
}
@Override
public void progress(String message, int workUnitsCompleted) {
if (!cancelling) {
progressHandle.progress(message, workUnitsCompleted);
}
}
@Override
public void setCancelling(String cancellingMessage) {
cancelling = true;
progressHandle.switchToIndeterminate();
progressHandle.progress(cancellingMessage);
}
@Override
public void finish() {
progressHandle.finish();
}
}

View File

@ -3,3 +3,4 @@
# and open the template in the editor.
ProgressPanel.progressMessage.text=Message
TaskCanceller.progress.cancellingMessage=Cancelling...

View File

@ -128,6 +128,7 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator {
*
* @param cancellingMessage
*/
@Override
public synchronized void setCancelling(String cancellingMessage) {
cancelling = true;
SwingUtilities.invokeLater(() -> {

View File

@ -49,7 +49,7 @@ public interface ProgressIndicator {
*
* @param message The initial progress message.
*/
public void switchToIndeterminate(String message);
void switchToIndeterminate(String message);
/**
* Switches the progress indicator to determinate mode (the total number of
@ -59,14 +59,14 @@ public interface ProgressIndicator {
* @param workUnitsCompleted The number of work units completed so far.
* @param totalWorkUnits The total number of work units to be completed.
*/
public void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits);
void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits);
/**
* Updates the progress indicator with a progress message.
*
* @param message The progress message.
*/
public void progress(String message);
void progress(String message);
/**
* Updates the progress indicator with the number of work units completed so
@ -75,7 +75,7 @@ public interface ProgressIndicator {
*
* @param workUnitsCompleted Number of work units completed so far.
*/
public void progress(int workUnitsCompleted);
void progress(int workUnitsCompleted);
/**
* Updates the progress indicator with a progress message and the number of
@ -85,7 +85,24 @@ public interface ProgressIndicator {
* @param message The progress message.
* @param workUnitsCompleted Number of work units completed so far.
*/
public void progress(String message, int workUnitsCompleted);
void progress(String message, int workUnitsCompleted);
/**
* If the progress indicator supports cancelling the underlying task, sets a
* cancelling message and causes the progress indicator to no longer accept
* updates unless start is called again.
*
* The default implementation assumes that cancelling the underlying task is
* not supported.
*
* @param cancellingMessage The cancelling messages.
*/
default void setCancelling(String cancellingMessage) {
/*
* The default implementation assumes that cancelling the underlying
* task is not supported.
*/
}
/**
* Finishes the progress indicator when the task is completed.

View File

@ -0,0 +1,68 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp. Contact: carrier <at> sleuthkit
* <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.sleuthkit.autopsy.progress;
import java.util.concurrent.Future;
import org.openide.util.Cancellable;
import org.openide.util.NbBundle;
/**
* Pluggable cancellation behavior for use in progress indicators (such as the
* application frame progress indicator) that support cancelling a task using an
* implementation of org.openide.util.Cancellable. Encapsulates a Future<?> to
* be cancelled and sets the cancelling flag and message of the progress
* indicator.
*/
public class TaskCancellable implements Cancellable {
private final ProgressIndicator progress;
private Future<?> future;
/**
* Constructs a pluggable cancellation behavior for use in progress
* indicators (such as the application frame progress indicator) that
* support cancelling a task using an implementation of
* org.openide.util.Cancellable. Encapsulates a Future<?> to be cancelled
* and sets the cancelling flag and message of the progress indicator.
*
* @param progress
*/
public TaskCancellable(ProgressIndicator progress) {
this.progress = progress;
}
/**
* Sets the Future<?> used to cancel the associated task.
*
* @param future The future for the associated task.
*/
public synchronized void setFuture(Future<?> future) {
this.future = future;
}
@Override
@NbBundle.Messages({
"TaskCanceller.progress.cancellingMessage=Cancelling..."
})
public synchronized boolean cancel() {
progress.setCancelling(Bundle.TaskCanceller_progress_cancellingMessage());
return future.cancel(true);
}
}

View File

@ -26,7 +26,6 @@ import org.netbeans.swing.outline.DefaultOutlineModel;
import org.netbeans.swing.outline.Outline;
import org.openide.explorer.ExplorerManager;
import org.openide.nodes.Node;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobsNode.JobNode;
/**
* A panel which displays an outline view with all auto ingest nodes and their

View File

@ -293,75 +293,6 @@ final class AutoIngestAdminActions {
}
}
@NbBundle.Messages({"AutoIngestAdminActions.deleteCaseAction.title=Delete Case",
"AutoIngestAdminActions.deleteCaseAction.error=Failed to delete case."})
static final class DeleteCaseAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private final AutoIngestJob job;
DeleteCaseAction(AutoIngestJob selectedJob) {
super(Bundle.AutoIngestAdminActions_deleteCaseAction_title());
this.job = selectedJob;
}
@Override
public void actionPerformed(ActionEvent e) {
if (job == null) {
return;
}
final AutoIngestDashboardTopComponent tc = (AutoIngestDashboardTopComponent) WindowManager.getDefault().findTopComponent(AutoIngestDashboardTopComponent.PREFERRED_ID);
if (tc == null) {
return;
}
AutoIngestDashboard dashboard = tc.getAutoIngestDashboard();
if (dashboard != null) {
String caseName = job.getManifest().getCaseName();
Object[] options = {
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.Delete"),
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DoNotDelete")
};
Object[] msgContent = {org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DeleteAreYouSure") + "\"" + caseName + "\"?"};
int reply = JOptionPane.showOptionDialog(dashboard,
msgContent,
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.ConfirmDeletionHeader"),
JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE,
null,
options,
options[JOptionPane.NO_OPTION]);
if (reply == JOptionPane.YES_OPTION) {
EventQueue.invokeLater(() -> {
dashboard.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
AutoIngestManager.CaseDeletionResult result = dashboard.getMonitor().deleteCase(job);
dashboard.getCompletedJobsPanel().refresh(new AutoIngestNodeRefreshEvents.RefreshChildrenEvent(dashboard.getMonitor()));
dashboard.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
if (AutoIngestManager.CaseDeletionResult.FAILED == result) {
JOptionPane.showMessageDialog(dashboard,
String.format("Could not delete case %s. It may be in use.", caseName),
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"),
JOptionPane.INFORMATION_MESSAGE);
} else if (AutoIngestManager.CaseDeletionResult.PARTIALLY_DELETED == result) {
JOptionPane.showMessageDialog(dashboard,
String.format("Could not fully delete case %s. See log for details.", caseName),
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"),
JOptionPane.INFORMATION_MESSAGE);
}
});
}
}
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); //To change body of generated methods, choose Tools | Templates.
}
}
@NbBundle.Messages({"AutoIngestAdminActions.showCaseLogAction.title=Show Case Log",
"AutoIngestAdminActions.showCaseLogActionFailed.title=Unable to display case log",
"AutoIngestAdminActions.showCaseLogActionFailed.message=Case log file does not exist",

View File

@ -57,14 +57,15 @@
<Component id="runningScrollPane" min="-2" pref="1021" max="-2" attributes="0"/>
<Component id="completedScrollPane" min="-2" pref="1021" max="-2" attributes="0"/>
</Group>
<EmptySpace type="unrelated" min="-2" max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" max="-2" attributes="0">
<Component id="bnCancelJob" linkSize="1" max="32767" attributes="0"/>
<Component id="bnShowProgress" linkSize="1" max="32767" attributes="0"/>
<Component id="bnCancelModule" linkSize="1" alignment="0" max="32767" attributes="0"/>
<Component id="bnDeleteCase" linkSize="1" alignment="0" max="32767" attributes="0"/>
<Component id="bnShowCaseLog" max="32767" attributes="0"/>
<Component id="bnReprocessJob" alignment="0" max="32767" attributes="0"/>
<EmptySpace type="unrelated" max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Group type="103" groupAlignment="0" max="-2" attributes="0">
<Component id="bnCancelJob" linkSize="1" max="32767" attributes="0"/>
<Component id="bnShowProgress" linkSize="1" max="32767" attributes="0"/>
<Component id="bnCancelModule" linkSize="1" alignment="0" max="32767" attributes="0"/>
<Component id="bnReprocessJob" alignment="0" max="32767" attributes="0"/>
</Group>
<Component id="bnShowCaseLog" alignment="1" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<Group type="102" alignment="0" attributes="0">
@ -135,8 +136,6 @@
<EmptySpace min="-2" pref="68" max="-2" attributes="0"/>
<Component id="bnReprocessJob" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="bnDeleteCase" linkSize="2" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="bnShowCaseLog" min="-2" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
@ -255,28 +254,6 @@
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="bnCancelJobActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JButton" name="bnDeleteCase">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties" key="AutoIngestControlPanel.bnDeleteCase.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
<Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties" key="AutoIngestControlPanel.bnDeleteCase.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
<Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[162, 23]"/>
</Property>
<Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[162, 23]"/>
</Property>
<Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
<Dimension value="[162, 23]"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="bnDeleteCaseActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="lbPending">
<Properties>
<Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor">

View File

@ -63,7 +63,6 @@ import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.NetworkUtils;
import org.sleuthkit.autopsy.coreutils.PlatformUtil;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.CaseDeletionResult;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.JobsSnapshot;
import org.sleuthkit.autopsy.guiutils.DurationCellRenderer;
import org.sleuthkit.autopsy.guiutils.LongDateCellRenderer;
@ -621,7 +620,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
}
int row = completedTable.getSelectedRow();
boolean enabled = row >= 0 && row < completedTable.getRowCount();
bnDeleteCase.setEnabled(enabled);
bnShowCaseLog.setEnabled(enabled);
bnReprocessJob.setEnabled(enabled);
});
@ -632,7 +630,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
*/
private void initButtons() {
bnOptions.setEnabled(true);
bnDeleteCase.setEnabled(false);
enablePrioritizeButtons(false);
enableDeprioritizeButtons(false);
bnShowCaseLog.setEnabled(false);
@ -1229,7 +1226,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
completedScrollPane = new javax.swing.JScrollPane();
completedTable = new javax.swing.JTable();
bnCancelJob = new javax.swing.JButton();
bnDeleteCase = new javax.swing.JButton();
lbPending = new javax.swing.JLabel();
lbRunning = new javax.swing.JLabel();
lbCompleted = new javax.swing.JLabel();
@ -1317,17 +1313,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
}
});
org.openide.awt.Mnemonics.setLocalizedText(bnDeleteCase, org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.bnDeleteCase.text")); // NOI18N
bnDeleteCase.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.bnDeleteCase.toolTipText")); // NOI18N
bnDeleteCase.setMaximumSize(new java.awt.Dimension(162, 23));
bnDeleteCase.setMinimumSize(new java.awt.Dimension(162, 23));
bnDeleteCase.setPreferredSize(new java.awt.Dimension(162, 23));
bnDeleteCase.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
bnDeleteCaseActionPerformed(evt);
}
});
lbPending.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N
org.openide.awt.Mnemonics.setLocalizedText(lbPending, org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.lbPending.text")); // NOI18N
@ -1547,13 +1532,13 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
.addComponent(runningScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 1021, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(completedScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 1021, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addComponent(bnCancelJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnShowProgress, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnCancelModule, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnDeleteCase, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnShowCaseLog, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnReprocessJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addComponent(bnCancelJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnShowProgress, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnCancelModule, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(bnReprocessJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addComponent(bnShowCaseLog, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
.addGroup(layout.createSequentialGroup()
.addComponent(pendingScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 1021, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
@ -1565,7 +1550,7 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))))
);
layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnDeleteCase, bnShowProgress});
layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnShowProgress});
layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnClusterMetrics, bnExit, bnOpenLogDir, bnOptions, bnPause, bnRefresh});
@ -1612,8 +1597,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
.addGap(68, 68, 68)
.addComponent(bnReprocessJob, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(bnDeleteCase, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(bnShowCaseLog, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
.addGroup(layout.createSequentialGroup()
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
@ -1631,7 +1614,7 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
.addContainerGap())
);
layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnClusterMetrics, bnDeleteCase, bnExit, bnOpenLogDir, bnOptions, bnPrioritizeCase, bnPrioritizeJob, bnRefresh, bnShowProgress});
layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnClusterMetrics, bnExit, bnOpenLogDir, bnOptions, bnPrioritizeCase, bnPrioritizeJob, bnRefresh, bnShowProgress});
}// </editor-fold>//GEN-END:initComponents
@ -1649,60 +1632,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}//GEN-LAST:event_bnRefreshActionPerformed
/**
* Handles a click on the delete case button. If an entry is selected that
* can be deleted, pops up a confirmation dialog. Upon confirmation, asks
* AutoIngestManager to delete the entry and asks for an updated view.
*
* @param evt The button click event.
*/
@Messages({
"AutoIngestControlPanel.DeletionFailed=Deletion failed for job"
})
private void bnDeleteCaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnDeleteCaseActionPerformed
if (completedTable.getModel().getRowCount() < 0 || completedTable.getSelectedRow() < 0) {
return;
}
String caseName = (String) completedTable.getModel().getValueAt(completedTable.convertRowIndexToModel(completedTable.getSelectedRow()), JobsTableModelColumns.CASE.ordinal());
Object[] options = {
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.Delete"),
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DoNotDelete")
};
Object[] msgContent = {org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DeleteAreYouSure") + "\"" + caseName + "\"?"};
int reply = JOptionPane.showOptionDialog(this,
msgContent,
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.ConfirmDeletionHeader"),
JOptionPane.DEFAULT_OPTION,
JOptionPane.WARNING_MESSAGE,
null,
options,
options[JOptionPane.NO_OPTION]);
if (reply == JOptionPane.YES_OPTION) {
bnDeleteCase.setEnabled(false);
bnShowCaseLog.setEnabled(false);
if (completedTable.getModel().getRowCount() > 0 && completedTable.getSelectedRow() >= 0) {
Path caseDirectoryPath = (Path) completedTable.getModel().getValueAt(completedTable.convertRowIndexToModel(completedTable.getSelectedRow()), JobsTableModelColumns.CASE_DIRECTORY_PATH.ordinal());
completedTable.clearSelection();
this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
CaseDeletionResult result = manager.deleteCase(caseName, caseDirectoryPath);
refreshTables();
this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
if (CaseDeletionResult.FAILED == result) {
JOptionPane.showMessageDialog(this,
String.format("Could not delete case %s. It may be in use.", caseName),
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"),
JOptionPane.INFORMATION_MESSAGE);
} else if (CaseDeletionResult.PARTIALLY_DELETED == result) {
JOptionPane.showMessageDialog(this,
String.format("Could not fully delete case %s. See system log for details.", caseName),
org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"),
JOptionPane.INFORMATION_MESSAGE);
}
}
}
}//GEN-LAST:event_bnDeleteCaseActionPerformed
/**
* Handles a click on the cancel auto ingest job button. Cancels the
* selected job.
@ -1976,7 +1905,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer {
private javax.swing.JButton bnCancelJob;
private javax.swing.JButton bnCancelModule;
private javax.swing.JButton bnClusterMetrics;
private javax.swing.JButton bnDeleteCase;
private javax.swing.JButton bnDeprioritizeCase;
private javax.swing.JButton bnDeprioritizeJob;
private javax.swing.JButton bnExit;

View File

@ -0,0 +1,92 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.autoingest;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Timestamp;
import java.util.logging.FileHandler;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import javax.annotation.concurrent.GuardedBy;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.PlatformUtil;
/**
* A logger for the auto ingest dashboard log.
*/
final class AutoIngestDashboardLogger {
private static final int LOG_SIZE = 50000000; // In bytes, zero is unlimited.
private static final int LOG_FILE_COUNT = 10;
private static final Logger logger = Logger.getLogger("AutoIngestDashboardLogger"); //NON-NLS
private static final String NEWLINE = System.lineSeparator();
@GuardedBy("AutoIngestDashboardLogger")
private static boolean configured;
/**
* Gets a logger for the auto ingest dashboard log.
*
* @return The logger.
*/
synchronized static Logger getLogger() {
if (!configured) {
Path logFilePath = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "var", "log", "auto_ingest_dashboard.log"); //NON-NLS
try {
FileHandler fileHandler = new FileHandler(logFilePath.toString(), LOG_SIZE, LOG_FILE_COUNT);
fileHandler.setEncoding(PlatformUtil.getLogFileEncoding());
fileHandler.setFormatter(new Formatter() {
@Override
public String format(LogRecord record) {
Throwable thrown = record.getThrown();
String stackTrace = ""; //NON-NLS
while (thrown != null) {
stackTrace += thrown.toString() + NEWLINE;
for (StackTraceElement traceElem : record.getThrown().getStackTrace()) {
stackTrace += "\t" + traceElem.toString() + NEWLINE; //NON-NLS
}
thrown = thrown.getCause();
}
return (new Timestamp(record.getMillis())).toString() + " " //NON-NLS
+ record.getSourceClassName() + " " //NON-NLS
+ record.getSourceMethodName() + NEWLINE
+ record.getLevel() + ": " //NON-NLS
+ this.formatMessage(record) + NEWLINE
+ stackTrace;
}
});
logger.addHandler(fileHandler);
logger.setUseParentHandlers(false);
} catch (IOException ex) {
throw new UncheckedIOException(String.format("Error initializing file handler for %s", logFilePath), ex); //NON-NLS
}
configured = true;
}
return logger;
}
/**
* Prevents instantiation of this utility class.
*/
private AutoIngestDashboardLogger() {
}
}

View File

@ -193,6 +193,7 @@ final class AutoIngestJob implements Comparable<AutoIngestJob>, IngestProgressSn
this.ingestThreadsSnapshot = Collections.emptyList();
this.ingestJobsSnapshot = Collections.emptyList();
this.moduleRunTimesSnapshot = Collections.emptyMap();
} catch (Exception ex) {
throw new AutoIngestJobException(String.format("Error creating automated ingest job"), ex);
}
@ -651,7 +652,7 @@ final class AutoIngestJob implements Comparable<AutoIngestJob>, IngestProgressSn
PENDING,
PROCESSING,
COMPLETED,
DELETED
DELETED // No longer used, retained for legacy jobs only.
}
/**

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2018 Basis Technology Corp.
* Copyright 2018-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -372,7 +372,6 @@ final class AutoIngestJobsNode extends AbstractNode {
break;
case COMPLETED_JOB:
actions.add(new AutoIngestAdminActions.ReprocessJobAction(jobWrapper.getJob()));
actions.add(new AutoIngestAdminActions.DeleteCaseAction(jobWrapper.getJob()));
actions.add(new AutoIngestAdminActions.ShowCaseLogAction(jobWrapper.getJob()));
break;
default:

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2011-2018 Basis Technology Corp.
* Copyright 2011-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import static java.nio.file.FileVisitOption.FOLLOW_LINKS;
import java.nio.file.FileVisitResult;
@ -39,11 +40,9 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Set;
import java.util.UUID;
@ -127,6 +126,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
private static final int NUM_INPUT_SCAN_SCHEDULING_THREADS = 1;
private static final String INPUT_SCAN_SCHEDULER_THREAD_NAME = "AIM-input-scan-scheduler-%d";
private static final String INPUT_SCAN_THREAD_NAME = "AIM-input-scan-%d";
private static final int INPUT_SCAN_LOCKING_TIMEOUT_MINS = 5;
private static final String AUTO_INGEST_THREAD_NAME = "AIM-job-processing-%d";
private static final String LOCAL_HOST_NAME = NetworkUtils.getLocalHostName();
private static final String EVENT_CHANNEL_NAME = "Auto-Ingest-Manager-Events";
@ -146,6 +146,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
private static final String JOB_STATUS_PUBLISHING_THREAD_NAME = "AIM-job-status-event-publisher-%d";
private static final long MAX_MISSED_JOB_STATUS_UPDATES = 10;
private static final int DEFAULT_PRIORITY = 0;
private static String CASE_MANIFESTS_LIST_FILE_NAME = "auto-ingest-job-manifests.txt";
private static final Logger sysLogger = AutoIngestSystemLogger.getLogger();
private static AutoIngestManager instance;
private final AutopsyEventPublisher eventPublisher;
@ -158,8 +159,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
private final ConcurrentHashMap<String, AutoIngestJob> hostNamesToRunningJobs;
private final Object jobsLock;
@GuardedBy("jobsLock")
private final Map<String, Set<Path>> casesToManifests;
@GuardedBy("jobsLock")
private List<AutoIngestJob> pendingJobs;
@GuardedBy("jobsLock")
private AutoIngestJob currentJob;
@ -175,6 +174,10 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
private volatile AutoIngestNodeStateEvent lastPublishedStateEvent;
static String getCaseManifestsListFileName() {
return CASE_MANIFESTS_LIST_FILE_NAME;
}
/**
* Gets a singleton auto ingest manager responsible for processing auto
* ingest jobs defined by manifest files that can be added to any level of a
@ -206,7 +209,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
hostNamesToRunningJobs = new ConcurrentHashMap<>();
hostNamesToLastMsgTime = new ConcurrentHashMap<>();
jobsLock = new Object();
casesToManifests = new HashMap<>();
pendingJobs = new ArrayList<>();
completedJobs = new ArrayList<>();
try {
@ -695,7 +697,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
int oldPriority = job.getPriority();
job.setPriority(DEFAULT_PRIORITY);
try {
this.updateCoordinationServiceManifestNode(job);
this.updateAutoIngestJobData(job);
} catch (CoordinationServiceException | InterruptedException ex) {
job.setPriority(oldPriority);
throw new AutoIngestManagerException("Error updating case priority", ex);
@ -745,7 +747,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
int oldPriority = job.getPriority();
job.setPriority(maxPriority);
try {
this.updateCoordinationServiceManifestNode(job);
this.updateAutoIngestJobData(job);
} catch (CoordinationServiceException | InterruptedException ex) {
job.setPriority(oldPriority);
throw new AutoIngestManagerException("Error updating case priority", ex);
@ -797,7 +799,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
int oldPriority = jobToDeprioritize.getPriority();
jobToDeprioritize.setPriority(DEFAULT_PRIORITY);
try {
this.updateCoordinationServiceManifestNode(jobToDeprioritize);
this.updateAutoIngestJobData(jobToDeprioritize);
} catch (CoordinationServiceException | InterruptedException ex) {
jobToDeprioritize.setPriority(oldPriority);
throw new AutoIngestManagerException("Error updating job priority", ex);
@ -855,7 +857,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
int oldPriority = jobToPrioritize.getPriority();
jobToPrioritize.setPriority(maxPriority);
try {
this.updateCoordinationServiceManifestNode(jobToPrioritize);
this.updateAutoIngestJobData(jobToPrioritize);
} catch (CoordinationServiceException | InterruptedException ex) {
jobToPrioritize.setPriority(oldPriority);
throw new AutoIngestManagerException("Error updating job priority", ex);
@ -910,7 +912,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
completedJob.setCompletedDate(new Date(0));
completedJob.setProcessingStatus(PENDING);
completedJob.setProcessingStage(AutoIngestJob.Stage.PENDING, Date.from(Instant.now()));
updateCoordinationServiceManifestNode(completedJob);
updateAutoIngestJobData(completedJob);
pendingJobs.add(completedJob);
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Coordination service error while reprocessing %s", manifestPath), ex);
@ -925,123 +927,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
}
/**
* Deletes a case. This includes deleting the case directory, the text
* index, and the case database. This does not include the directories
* containing the data sources and their manifests.
*
* @param caseName The name of the case.
* @param caseDirectoryPath The path to the case directory.
*
* @return A result code indicating success, partial success, or failure.
*/
CaseDeletionResult deleteCase(String caseName, Path caseDirectoryPath) {
if (state != State.RUNNING) {
return CaseDeletionResult.FAILED;
}
CaseDeletionResult result = CaseDeletionResult.FULLY_DELETED;
List<Lock> manifestFileLocks = new ArrayList<>();
try {
synchronized (jobsLock) {
/*
* Get the case metadata.
*/
CaseMetadata metaData;
Path caseMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), caseName + CaseMetadata.getFileExtension());
try {
metaData = new CaseMetadata(caseMetaDataFilePath);
} catch (CaseMetadata.CaseMetadataException ex) {
sysLogger.log(Level.SEVERE, String.format("Failed to get case metadata file %s for case %s at %s", caseMetaDataFilePath, caseName, caseDirectoryPath), ex);
return CaseDeletionResult.FAILED;
}
/*
* Do a fresh input directory scan.
*/
InputDirScanner scanner = new InputDirScanner();
scanner.scan();
Set<Path> manifestPaths = casesToManifests.get(caseName);
if (null == manifestPaths) {
sysLogger.log(Level.SEVERE, String.format("No manifest paths found for case %s at %s", caseName, caseDirectoryPath));
return CaseDeletionResult.FAILED;
}
/*
* Get exclusive locks on all of the manifests for the case.
* This will exclude other auot ingest nodes from doing anything
* with the case.
*/
for (Path manifestPath : manifestPaths) {
try {
Lock lock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString());
if (null != lock) {
manifestFileLocks.add(lock);
} else {
return CaseDeletionResult.FAILED;
}
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to acquire manifest lock for %s for case %s at %s", manifestPath, caseName, caseDirectoryPath), ex);
return CaseDeletionResult.FAILED;
}
}
try {
/*
* Physically delete the case.
*/
Case.deleteCase(metaData);
} catch (CaseActionException ex) {
sysLogger.log(Level.SEVERE, String.format("Failed to physically delete case %s at %s", caseName, caseDirectoryPath), ex);
return CaseDeletionResult.FAILED;
}
/*
* Mark each job (manifest file) as deleted
*/
for (Path manifestPath : manifestPaths) {
try {
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()));
AutoIngestJob deletedJob = new AutoIngestJob(nodeData);
deletedJob.setProcessingStatus(AutoIngestJob.ProcessingStatus.DELETED);
this.updateCoordinationServiceManifestNode(deletedJob);
} catch (AutoIngestJobNodeData.InvalidDataException | AutoIngestJobException ex) {
sysLogger.log(Level.WARNING, String.format("Invalid auto ingest job node data for %s", manifestPath), ex);
return CaseDeletionResult.PARTIALLY_DELETED;
} catch (InterruptedException | CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to set delete flag on manifest data for %s for case %s at %s", manifestPath, caseName, caseDirectoryPath), ex);
return CaseDeletionResult.PARTIALLY_DELETED;
}
}
/*
* Remove the jobs for the case from the pending jobs queue and
* completed jobs list.
*/
removeJobs(manifestPaths, pendingJobs);
removeJobs(manifestPaths, completedJobs);
casesToManifests.remove(caseName);
}
eventPublisher.publishRemotely(new AutoIngestCaseDeletedEvent(caseName, LOCAL_HOST_NAME, getSystemUserNameProperty()));
setChanged();
notifyObservers(Event.CASE_DELETED);
return result;
} finally {
/*
* Always release the manifest locks, regardless of the outcome.
*/
for (Lock lock : manifestFileLocks) {
try {
lock.release();
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Failed to release manifest file lock when deleting case %s at %s", caseName, caseDirectoryPath), ex);
}
}
}
}
/**
* Get the current snapshot of the job lists.
*
@ -1114,15 +999,12 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
/**
* Sets the coordination service manifest node.
*
* Note that a new auto ingest job node data object will be created from the
* job passed in. Thus, if the data version of the node has changed, the
* node will be "upgraded" as well as updated.
* Writes the node data for an auto ingest job to the job's manifest file
* lock coordination service node.
*
* @param job The auto ingest job.
*/
void updateCoordinationServiceManifestNode(AutoIngestJob job) throws CoordinationServiceException, InterruptedException {
void updateAutoIngestJobData(AutoIngestJob job) throws CoordinationServiceException, InterruptedException {
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(job);
String manifestNodePath = job.getManifest().getFilePath().toString();
byte[] rawData = nodeData.toArray();
@ -1134,14 +1016,21 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
*
* @param caseDirectoryPath The case directory path.
*
* @throws CoordinationService.CoordinationServiceException
* @throws InterruptedException
* @throws IOException
* @throws CoordinationServiceException If there was an error getting the
* node data from the cooordination
* service.
* @throws IOException If the node data was missing or
* there was an error interpreting it.
* @throws InterruptedException If the thread running the input
* directory scan task is interrupted
* while blocked, i.e., if auto ingest
* is shutting down.
*/
private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws CoordinationServiceException, InterruptedException, IOException {
CaseNodeData caseNodeData = new CaseNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString()));
private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws IOException, CoordinationServiceException, InterruptedException {
byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString());
CaseNodeData caseNodeData = new CaseNodeData(rawData);
caseNodeData.setErrorsOccurred(true);
byte[] rawData = caseNodeData.toArray();
rawData = caseNodeData.toArray();
coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString(), rawData);
}
@ -1206,6 +1095,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
private final List<AutoIngestJob> newPendingJobsList = new ArrayList<>();
private final List<AutoIngestJob> newCompletedJobsList = new ArrayList<>();
private Lock currentDirLock;
/**
* Searches the input directories for manifest files. The search results
@ -1227,9 +1117,9 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
} catch (Exception ex) {
/*
* NOTE: Need to catch all exceptions here. Otherwise
* uncaught exceptions will propagate up to the calling
* thread and may stop it from running.
* NOTE: Need to catch all unhandled exceptions here.
* Otherwise uncaught exceptions will propagate up to the
* calling thread and may stop it from running.
*/
sysLogger.log(Level.SEVERE, String.format("Error scanning the input directory %s", rootInputDirectory), ex);
}
@ -1263,20 +1153,15 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
/**
* Invoked for a file in a directory. If the file is a manifest file,
* creates a pending pending or completed auto ingest job for the
* manifest, based on the data stored in the coordination service node
* for the manifest.
* <p>
* Note that the mapping of case names to manifest paths that is used
* for case deletion is updated as well.
* Creates a pending or completed auto ingest job if the file visited is
* a manifest file, based on the data stored in the coordination service
* node for the manifest.
*
* @param filePath The path of the file.
* @param attrs The file system attributes of the file.
*
* @return TERMINATE if auto ingest is shutting down, CONTINUE if it has
* not.
*
*/
@Override
public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) {
@ -1285,6 +1170,11 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
try {
/*
* Determine whether or not the file is an auto ingest job
* manifest file. If it is, then parse it. Otherwise, move on to
* the next file in the directory.
*/
Manifest manifest = null;
for (ManifestFileParser parser : Lookup.getDefault().lookupAll(ManifestFileParser.class)) {
if (parser.fileIsManifest(filePath)) {
@ -1304,76 +1194,95 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
return TERMINATE;
}
if (null != manifest) {
/*
* Update the mapping of case names to manifest paths that
* is used for case deletion.
*/
String caseName = manifest.getCaseName();
Path manifestPath = manifest.getFilePath();
if (casesToManifests.containsKey(caseName)) {
Set<Path> manifestPaths = casesToManifests.get(caseName);
manifestPaths.add(manifestPath);
} else {
Set<Path> manifestPaths = new HashSet<>();
manifestPaths.add(manifestPath);
casesToManifests.put(caseName, manifestPaths);
}
if (manifest == null) {
return CONTINUE;
}
/*
* Add a job to the pending jobs queue, the completed jobs
* list, or do crashed job recovery, as required.
*/
try {
byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString());
/*
* If a manifest file has been found, get a manifest file lock,
* analyze the job state, and put a job into the appropriate job
* list. There is a short wait here in case the input directory
* scanner file visitor of another auto ingest node (AIN) has
* the lock. If the lock ultmiately can't be obtained, the wait
* was not long enough, or another auto ingest node (AIN) is
* holding the lock because it is executing the job, or a case
* deletion task has aquired the lock. In all of these cases the
* manifest can be skipped for this scan.
*/
try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString(), INPUT_SCAN_LOCKING_TIMEOUT_MINS, TimeUnit.MINUTES)) {
if (null != manifestLock) {
/*
* Now that the lock has been acquired, make sure the
* manifest is still here. This is a way to resolve the
* race condition between this task and case deletion
* tasks without resorting to a protocol using locking
* of the input directory.
*/
if (!filePath.toFile().exists()) {
return CONTINUE;
}
byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString());
if (null != rawData && rawData.length > 0) {
try {
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(rawData);
AutoIngestJob.ProcessingStatus processingStatus = nodeData.getProcessingStatus();
switch (processingStatus) {
case PENDING:
addPendingJob(manifest, nodeData);
break;
case PROCESSING:
doRecoveryIfCrashed(manifest, nodeData);
break;
case COMPLETED:
addCompletedJob(manifest, nodeData);
break;
case DELETED:
/*
* Ignore jobs marked as "deleted."
*/
break;
default:
sysLogger.log(Level.SEVERE, "Unknown ManifestNodeData.ProcessingStatus");
break;
}
} catch (AutoIngestJobNodeData.InvalidDataException | AutoIngestJobException ex) {
sysLogger.log(Level.SEVERE, String.format("Invalid auto ingest job node data for %s", manifestPath), ex);
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(rawData);
AutoIngestJob.ProcessingStatus processingStatus = nodeData.getProcessingStatus();
switch (processingStatus) {
case PENDING:
addPendingJob(manifest, nodeData);
break;
case PROCESSING:
/*
* If an exclusive manifest file lock was
* obtained for an auto ingest job in the
* processing state, the auto ingest node
* (AIN) executing the job crashed and the
* lock was released when the coordination
* service detected that the AIN was no
* longer alive.
*/
doCrashRecovery(manifest, nodeData);
break;
case COMPLETED:
addCompletedJob(manifest, nodeData);
break;
case DELETED:
/*
* Ignore jobs marked as deleted. Note that
* this state is no longer used and is
* retained for legacy jobs only.
*/
break;
default:
sysLogger.log(Level.SEVERE, "Unknown ManifestNodeData.ProcessingStatus");
break;
}
} else {
try {
addNewPendingJob(manifest);
} catch (AutoIngestJobException ex) {
sysLogger.log(Level.SEVERE, String.format("Invalid manifest data for %s", manifestPath), ex);
sysLogger.log(Level.SEVERE, String.format("Invalid manifest data for %s", manifest.getFilePath()), ex);
}
}
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error transmitting node data for %s", manifestPath), ex);
return CONTINUE;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return TERMINATE;
}
} catch (CoordinationServiceException | AutoIngestJobException | AutoIngestJobNodeData.InvalidDataException ex) {
sysLogger.log(Level.SEVERE, String.format("Error handling manifest at %s", manifest.getFilePath()), ex);
} catch (InterruptedException ex) {
/*
* The thread running the input directory scan task was
* interrupted while blocked, i.e., auto ingest is shutting
* down.
*/
return TERMINATE;
}
} catch (Exception ex) {
// Catch all unhandled and unexpected exceptions. Otherwise one bad file
// can stop the entire input folder scanning. Given that the exception is unexpected,
// I'm hesitant to add logging which requires accessing or de-referencing data.
sysLogger.log(Level.SEVERE, "Unexpected exception in file visitor", ex);
return CONTINUE;
/*
* This is an exception firewall so that an unexpected runtime
* exception from the handling of a single manifest file does
* not take out the input directory scanner.
*/
sysLogger.log(Level.SEVERE, String.format("Unexpected exception handling %s", filePath), ex);
}
if (!Thread.currentThread().isInterrupted()) {
@ -1384,49 +1293,36 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
/**
* Adds an existing job to the pending jobs queue.
* Adds an auto ingest job to the pending jobs queue.
*
* @param manifest The manifest for the job.
* @param nodeData The data stored in the coordination service node for
* the job.
* @param nodeData The data stored in the manifest file lock
* coordination service node for the job.
*
* @throws InterruptedException if the thread running the input
* directory scan task is interrupted while
* blocked, i.e., if auto ingest is
* shutting down.
* @throws AutoIngestJobException If there was an error working
* with the node data.
* @throws CoordinationServiceException If a lock node data version
* update was required and there
* was an error writing the node
* data by the coordination
* service.
* @throws InterruptedException If the thread running the input
* directory scan task is
* interrupted while blocked, i.e.,
* if auto ingest is shutting down.
*/
private void addPendingJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws InterruptedException, AutoIngestJobException {
private void addPendingJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws AutoIngestJobException, CoordinationServiceException, InterruptedException {
AutoIngestJob job;
if (nodeData.getVersion() == AutoIngestJobNodeData.getCurrentVersion()) {
job = new AutoIngestJob(nodeData);
} else {
job = new AutoIngestJob(manifest);
job.setPriority(nodeData.getPriority()); // Retain priority, present in all versions of the node data.
job.setPriority(nodeData.getPriority());
Path caseDirectory = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName());
if (null != caseDirectory) {
job.setCaseDirectoryPath(caseDirectory);
}
/*
* Try to upgrade/update the coordination service manifest node
* data for the job.
*
* An exclusive lock is obtained before doing so because another
* host may have already found the job, obtained an exclusive
* lock, and started processing it. However, this locking does
* make it possible that two processing hosts will both try to
* obtain the lock to do the upgrade operation at the same time.
* If this happens, the host that is holding the lock will
* complete the upgrade operation, so there is nothing more for
* this host to do.
*/
try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) {
if (null != manifestLock) {
updateCoordinationServiceManifestNode(job);
}
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifest.getFilePath()), ex);
}
updateAutoIngestJobData(job);
}
newPendingJobsList.add(job);
}
@ -1436,150 +1332,117 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
*
* @param manifest The manifest for the job.
*
* @throws InterruptedException if the thread running the input
* directory scan task is interrupted while
* blocked, i.e., if auto ingest is
* shutting down.
* @throws AutoIngestJobException If there was an error creating
* the node data.
* @throws CoordinationServiceException If there was an error writing
* the node data by the
* coordination service.
* @throws InterruptedException If the thread running the input
* directory scan task is
* interrupted while blocked, i.e.,
* if auto ingest is shutting down.
*/
private void addNewPendingJob(Manifest manifest) throws InterruptedException, AutoIngestJobException {
/*
* Create the coordination service manifest node data for the job.
* Note that getting the lock will create the node for the job (with
* no data) if it does not already exist.
*
* An exclusive lock is obtained before creating the node data
* because another host may have already found the job, obtained an
* exclusive lock, and started processing it. However, this locking
* does make it possible that two hosts will both try to obtain the
* lock to do the create operation at the same time. If this
* happens, the host that is locked out will not add the job to its
* pending queue for this scan of the input directory, but it will
* be picked up on the next scan.
*/
try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) {
if (null != manifestLock) {
AutoIngestJob job = new AutoIngestJob(manifest);
updateCoordinationServiceManifestNode(job);
newPendingJobsList.add(job);
}
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifest.getFilePath()), ex);
}
private void addNewPendingJob(Manifest manifest) throws AutoIngestJobException, CoordinationServiceException, InterruptedException {
AutoIngestJob job = new AutoIngestJob(manifest);
updateAutoIngestJobData(job);
newPendingJobsList.add(job);
}
/**
* Does crash recovery for a manifest, if required. The criterion for
* crash recovery is a manifest with coordination service node data
* indicating it is being processed for which an exclusive lock on the
* node can be acquired. If this condition is true, it is probable that
* the node that was processing the job crashed and the processing
* status was not updated.
* Does recovery for an auto ingest job that was left in the processing
* state by an auot ingest node (AIN) that crashed.
*
* @param manifest The manifest for upgrading the node.
* @param jobNodeData The auto ingest job node data.
* @param manifest The manifest for the job.
* @param nodeData The data stored in the manifest file lock
* coordination service node for the job.
*
* @throws InterruptedException if the thread running the input
* directory scan task is interrupted
* while blocked, i.e., if auto ingest is
* shutting down.
* @throws AutoIngestJobException if there is an issue creating a new
* AutoIngestJob object.
* @throws AutoIngestJobException If there was an error working
* with the node data.
* @throws CoordinationServiceException If there was an error writing
* updated node data by the
* coordination service.
* @throws InterruptedException If the thread running the input
* directory scan task is
* interrupted while blocked, i.e.,
* if auto ingest is shutting down.
*/
private void doRecoveryIfCrashed(Manifest manifest, AutoIngestJobNodeData jobNodeData) throws InterruptedException, AutoIngestJobException {
/*
* Try to get an exclusive lock on the coordination service node for
* the job. If the lock cannot be obtained, another host in the auto
* ingest cluster is already doing the recovery, so there is nothing
* to do.
*/
private void doCrashRecovery(Manifest manifest, AutoIngestJobNodeData jobNodeData) throws AutoIngestJobException, CoordinationServiceException, InterruptedException {
String manifestPath = manifest.getFilePath().toString();
try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath)) {
if (null != manifestLock) {
sysLogger.log(Level.SEVERE, "Attempting crash recovery for {0}", manifestPath);
Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName());
sysLogger.log(Level.SEVERE, "Attempting crash recovery for {0}", manifestPath);
AutoIngestJob job = new AutoIngestJob(jobNodeData);
/*
* Create the recovery job.
*/
AutoIngestJob job = new AutoIngestJob(jobNodeData);
int numberOfCrashes = job.getNumberOfCrashes();
if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) {
++numberOfCrashes;
job.setNumberOfCrashes(numberOfCrashes);
if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) {
job.setCompletedDate(new Date(0));
} else {
job.setCompletedDate(Date.from(Instant.now()));
}
}
/*
* Try to set the error flags that indicate incomplete or messy data
* in displays for the job and the case. Note that if the job
* crashed before a case directory was created, the job was a no-op,
* so the data quality flags do not need to be set.
*/
Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName());
if (null != caseDirectoryPath) {
job.setCaseDirectoryPath(caseDirectoryPath);
job.setErrorsOccurred(true);
try {
setCaseNodeDataErrorsOccurred(caseDirectoryPath);
} catch (IOException ex) {
sysLogger.log(Level.WARNING, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex);
}
} else {
job.setErrorsOccurred(false);
}
if (null != caseDirectoryPath) {
job.setCaseDirectoryPath(caseDirectoryPath);
job.setErrorsOccurred(true);
try {
setCaseNodeDataErrorsOccurred(caseDirectoryPath);
} catch (IOException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex);
}
} else {
job.setErrorsOccurred(false);
}
if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) {
job.setProcessingStatus(AutoIngestJob.ProcessingStatus.PENDING);
if (null != caseDirectoryPath) {
try {
new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryWithRetry();
} catch (AutoIngestJobLoggerException ex) {
sysLogger.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex);
}
}
} else {
job.setProcessingStatus(AutoIngestJob.ProcessingStatus.COMPLETED);
if (null != caseDirectoryPath) {
try {
new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryNoRetry();
} catch (AutoIngestJobLoggerException ex) {
sysLogger.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex);
}
}
}
/*
* Update the coordination service node for the job. If this
* fails, leave the recovery to another host.
*/
/*
* Update the crash count for the job, determine whether or not to
* retry processing its data source, and deal with the job
* accordingly.
*/
int numberOfCrashes = job.getNumberOfCrashes();
++numberOfCrashes;
job.setNumberOfCrashes(numberOfCrashes);
if (numberOfCrashes < AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) {
job.setProcessingStatus(AutoIngestJob.ProcessingStatus.PENDING);
job.setCompletedDate(new Date(0));
if (null != caseDirectoryPath) {
try {
updateCoordinationServiceManifestNode(job);
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifestPath), ex);
return;
}
jobNodeData = new AutoIngestJobNodeData(job);
if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) {
newPendingJobsList.add(job);
} else {
newCompletedJobsList.add(new AutoIngestJob(jobNodeData));
new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryWithRetry();
} catch (AutoIngestJobLoggerException ex) {
sysLogger.log(Level.SEVERE, String.format("Error writing case auto ingest log entry for crashed job for %s", manifestPath), ex);
}
}
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to get exclusive lock for %s", manifestPath), ex);
updateAutoIngestJobData(job);
newPendingJobsList.add(job);
} else {
job.setProcessingStatus(AutoIngestJob.ProcessingStatus.COMPLETED);
job.setCompletedDate(Date.from(Instant.now()));
if (null != caseDirectoryPath) {
try {
new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryNoRetry();
} catch (AutoIngestJobLoggerException ex) {
sysLogger.log(Level.SEVERE, String.format("Error writing case auto ingest log entry for crashed job for %s", manifestPath), ex);
}
}
updateAutoIngestJobData(job);
newCompletedJobsList.add(new AutoIngestJob(jobNodeData));
}
}
/**
* Adds a job to process a manifest to the completed jobs list.
*
* @param nodeData The data stored in the coordination service node for
* the manifest.
* @param manifest The manifest for upgrading the node.
* @param manifest The manifest for the job.
* @param nodeData The data stored in the manifest file lock
* coordination service node for the job.
*
* @throws CoordinationServiceException
* @throws InterruptedException
* @throws AutoIngestJobException If there was an error working
* with the node data.
* @throws CoordinationServiceException If there was an error writing
* updated node data by the
* coordination service.
* @throws InterruptedException If the thread running the input
* directory scan task is
* interrupted while blocked, i.e.,
* if auto ingest is shutting down.
*/
private void addCompletedJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws CoordinationServiceException, InterruptedException, AutoIngestJobException {
private void addCompletedJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws AutoIngestJobException, CoordinationServiceException, InterruptedException {
Path caseDirectoryPath = nodeData.getCaseDirectoryPath();
if (!caseDirectoryPath.toFile().exists()) {
sysLogger.log(Level.WARNING, String.format("Job completed for %s, but cannot find case directory %s, ignoring job", nodeData.getManifestFilePath(), caseDirectoryPath.toString()));
@ -1611,21 +1474,9 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
job.setProcessingStage(AutoIngestJob.Stage.COMPLETED, nodeData.getCompletedDate());
job.setProcessingStatus(AutoIngestJob.ProcessingStatus.COMPLETED);
/*
* Try to upgrade/update the coordination service manifest node
* data for the job. It is possible that two hosts will both try
* to obtain the lock to do the upgrade operation at the same
* time. If this happens, the host that is holding the lock will
* complete the upgrade operation.
*/
try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) {
if (null != manifestLock) {
updateCoordinationServiceManifestNode(job);
}
} catch (CoordinationServiceException ex) {
sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifest.getFilePath()), ex);
}
updateAutoIngestJobData(job);
}
newCompletedJobsList.add(job);
}
@ -1654,17 +1505,17 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
/**
* Invoked for an input directory after entries in the directory are
* Invoked for an input directory after the files in the directory are
* visited. Checks if the task thread has been interrupted because auto
* ingest is shutting down and terminates the scan if that is the case.
*
* @param dirPath The directory about to be visited.
* @param unused Unused.
*
* @return TERMINATE if the task thread has been interrupted, CONTINUE
* if it has not.
* @return FileVisitResult.TERMINATE if the task thread has been
* interrupted, FileVisitResult.CONTINUE if it has not.
*
* @throws IOException if an I/O error occurs, but this implementation
* @throws IOException If an I/O error occurs, but this implementation
* does not throw.
*/
@Override
@ -2123,11 +1974,12 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
try {
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()));
if (!nodeData.getProcessingStatus().equals(PENDING)) {
/*
* Due to a timing issue or a missed event, a
* non-pending job has ended up on the pending
* queue. Skip the job and remove it from the queue.
*/
iterator.remove();
continue;
}
File manifestFile = nodeData.getManifestFilePath().toFile();
if (!manifestFile.exists()) {
iterator.remove();
continue;
}
@ -2145,11 +1997,13 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
continue;
}
}
iterator.remove();
currentJob = job;
break;
} catch (AutoIngestJobNodeData.InvalidDataException ex) {
sysLogger.log(Level.WARNING, String.format("Unable to use node data for %s", manifestPath), ex); // JCTODO: Is this right?
sysLogger.log(Level.WARNING, String.format("Unable to use node data for %s", manifestPath), ex);
}
}
}
@ -2220,7 +2074,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
currentJob.setProcessingStatus(AutoIngestJob.ProcessingStatus.PROCESSING);
currentJob.setProcessingStage(AutoIngestJob.Stage.STARTING, Date.from(Instant.now()));
currentJob.setProcessingHostName(AutoIngestManager.LOCAL_HOST_NAME);
updateCoordinationServiceManifestNode(currentJob);
updateAutoIngestJobData(currentJob);
setChanged();
notifyObservers(Event.JOB_STARTED);
eventPublisher.publishRemotely(new AutoIngestJobStartedEvent(currentJob));
@ -2244,7 +2098,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
currentJob.setProcessingStatus(AutoIngestJob.ProcessingStatus.PENDING);
}
currentJob.setProcessingHostName("");
updateCoordinationServiceManifestNode(currentJob);
updateAutoIngestJobData(currentJob);
boolean retry = (!currentJob.isCanceled() && !currentJob.isCompleted());
sysLogger.log(Level.INFO, "Completed processing of {0}, retry = {1}", new Object[]{manifestPath, retry});
@ -2410,9 +2264,9 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
sysLogger.log(Level.INFO, "Opening case {0} for {1}", new Object[]{caseName, manifest.getFilePath()});
currentJob.setProcessingStage(AutoIngestJob.Stage.OPENING_CASE, Date.from(Instant.now()));
/*
* Acquire and hold a case name lock so that only one node at as
* time can scan the output directory at a time. This prevents
* making duplicate cases for the saem auto ingest case.
* Acquire and hold a case name lock so that only one node at a time
* can search the output directory for an existing case. This
* prevents making duplicate cases for the same auto ingest case.
*/
try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseName, 30, TimeUnit.MINUTES)) {
if (null != caseLock) {
@ -2440,13 +2294,16 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
Thread.sleep(AutoIngestUserPreferences.getSecondsToSleepBetweenCases() * 1000);
}
currentJob.setCaseDirectoryPath(caseDirectoryPath);
updateCoordinationServiceManifestNode(currentJob); // update case directory path
updateAutoIngestJobData(currentJob);
recordManifest(caseDirectoryPath, manifest.getFilePath());
Case caseForJob = Case.getCurrentCase();
sysLogger.log(Level.INFO, "Opened case {0} for {1}", new Object[]{caseForJob.getName(), manifest.getFilePath()});
return caseForJob;
} catch (KeywordSearchModuleException ex) {
throw new CaseManagementException(String.format("Error creating solr settings file for case %s for %s", caseName, manifest.getFilePath()), ex);
} catch (IOException ex) {
throw new CaseManagementException(String.format("Error recording manifest file path for case %s for %s", caseName, manifest.getFilePath()), ex);
} catch (CaseActionException ex) {
throw new CaseManagementException(String.format("Error creating or opening case %s for %s", caseName, manifest.getFilePath()), ex);
}
@ -2456,6 +2313,22 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
}
/**
* Writes the path of the manifest file for the current job to a list of
* manifest file paths for the case in file in the case directory.
*
* @param caseDirectoryPath The case directory path.
*
* @throws IOException If the file cannot be created or opened and
* updated.
*/
private void recordManifest(Path caseDirectoryPath, Path manifestFilePath) throws IOException {
final Path manifestsListFilePath = Paths.get(caseDirectoryPath.toString(), AutoIngestManager.getCaseManifestsListFileName());
try (FileWriter fileWriter = new FileWriter(manifestsListFilePath.toString(), true)) {
fileWriter.write(manifestFilePath.toString() + "\n");
}
}
/**
* Runs the ingest process for the current job.
*
@ -3096,7 +2969,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
currentJob.setModuleRuntimesSnapshot(IngestManager.getInstance().getModuleRunTimes());
setChanged();
notifyObservers(Event.JOB_STATUS_UPDATED);
updateCoordinationServiceManifestNode(currentJob);
updateAutoIngestJobData(currentJob);
eventPublisher.publishRemotely(new AutoIngestJobStatusEvent(currentJob));
}
}
@ -3256,12 +3129,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
}
enum CaseDeletionResult {
FAILED,
PARTIALLY_DELETED,
FULLY_DELETED
}
static final class AutoIngestManagerException extends Exception {
private static final long serialVersionUID = 1L;

View File

@ -72,7 +72,7 @@ final class AutoIngestMetricsCollector {
switch (processingStatus) {
case PENDING:
case PROCESSING:
case DELETED:
case DELETED: // No longer used, retained for legacy jobs only.
/*
* These are not jobs we care about for metrics, so
* we will ignore them.
@ -96,7 +96,7 @@ final class AutoIngestMetricsCollector {
return newMetricsSnapshot;
} catch (CoordinationService.CoordinationServiceException ex) {
} catch (CoordinationService.CoordinationServiceException | InterruptedException ex) {
LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex);
return new MetricsSnapshot();
}

View File

@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.experimental.autoingest;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
@ -38,20 +37,15 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.annotation.concurrent.GuardedBy;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.CaseActionException;
import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.NetworkUtils;
import org.sleuthkit.autopsy.coreutils.StopWatch;
import org.sleuthkit.autopsy.events.AutopsyEventException;
import org.sleuthkit.autopsy.events.AutopsyEventPublisher;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJob.ProcessingStatus;
import static org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJob.ProcessingStatus.DELETED;
import static org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJob.ProcessingStatus.PENDING;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.CaseDeletionResult;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.Event;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestNodeControlEvent.ControlEventType;
@ -360,7 +354,7 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen
case COMPLETED:
newJobsSnapshot.addOrReplaceCompletedJob(job);
break;
case DELETED:
case DELETED: // No longer used, retained for legacy jobs only.
break;
default:
LOGGER.log(Level.SEVERE, "Unknown AutoIngestJobData.ProcessingStatus");
@ -378,7 +372,7 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen
return newJobsSnapshot;
} catch (CoordinationServiceException ex) {
} catch (CoordinationServiceException | InterruptedException ex) {
LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex);
return new JobsSnapshot();
}
@ -659,88 +653,6 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen
}
}
/**
* Deletes a case. This includes deleting the case directory, the text
* index, and the case database. This does not include the directories
* containing the data sources and their manifests.
*
* @param job The job whose case you want to delete
*
* @return A result code indicating success, partial success, or failure.
*/
CaseDeletionResult deleteCase(AutoIngestJob job) {
String caseName = job.getManifest().getCaseName();
Path caseDirectoryPath = job.getCaseDirectoryPath();
Path metadataFilePath = caseDirectoryPath.resolve(caseName + CaseMetadata.getFileExtension());
StopWatch stopWatch = new StopWatch();
stopWatch.start();
synchronized (jobsLock) {
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to acquire jobsLock (Java monitor in AutoIngestMonitor class) for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath));
stopWatch.reset();
stopWatch.start();
try {
CaseMetadata metadata = new CaseMetadata(metadataFilePath);
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to read case metadata for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath));
stopWatch.reset();
stopWatch.start();
Case.deleteCase(metadata);
} catch (CaseMetadata.CaseMetadataException ex) {
LOGGER.log(Level.SEVERE, String.format("Failed to read case metadata file %s for case %s at %s", metadataFilePath, caseName, caseDirectoryPath), ex);
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to fail to read case metadata file %s for case %s at %s", stopWatch.getElapsedTimeSecs(), metadataFilePath, caseName, caseDirectoryPath));
return CaseDeletionResult.FAILED;
} catch (CaseActionException ex) {
LOGGER.log(Level.SEVERE, String.format("Failed to delete case %s at %s", caseName, caseDirectoryPath), ex);
return CaseDeletionResult.FAILED;
}
// Update the state of completed jobs associated with this case to indicate
// that the case has been deleted
stopWatch.reset();
stopWatch.start();
List<AutoIngestJob> completedJobs = getCompletedJobs();
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to get completed jobs listing for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath));
stopWatch.reset();
stopWatch.start();
for (AutoIngestJob completedJob : completedJobs) {
if (caseName.equals(completedJob.getManifest().getCaseName())) {
try {
completedJob.setProcessingStatus(DELETED);
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(completedJob);
coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, completedJob.getManifest().getFilePath().toString(), nodeData.toArray());
} catch (CoordinationServiceException | InterruptedException ex) {
LOGGER.log(Level.SEVERE, String.format("Failed to update completed job node data for %s when deleting case %s at %s", completedJob.getManifest().getFilePath(), caseName, caseDirectoryPath), ex);
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to fail to update job node data for completed jobs for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath));
return CaseDeletionResult.PARTIALLY_DELETED;
}
}
}
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to update job node data for completed jobs for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath));
// Remove jobs associated with this case from the completed jobs collection.
stopWatch.reset();
stopWatch.start();
completedJobs.removeIf((AutoIngestJob completedJob)
-> completedJob.getManifest().getCaseName().equals(caseName));
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to remove completed jobs for case %s at %s from current jobs snapshot", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath));
// Publish a message to update auto ingest nodes.
stopWatch.reset();
stopWatch.start();
eventPublisher.publishRemotely(new AutoIngestCaseDeletedEvent(caseName, LOCAL_HOST_NAME, AutoIngestManager.getSystemUserNameProperty()));
stopWatch.stop();
LOGGER.log(Level.INFO, String.format("Used %d s to publish job deletion event for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath));
}
return CaseDeletionResult.FULLY_DELETED;
}
/**
* Send the given control event to the given node.
*

View File

@ -228,7 +228,6 @@ AutoIngestControlPanel.bnShowProgress.text=Ingest Progress
AutoIngestControlPanel.bnCancelJob.text=&Cancel Job
AutoIngestControlPanel.bnCancelModule.text=Cancel &Module
AutoIngestControlPanel.bnReprocessJob.text=Reprocess Job
AutoIngestControlPanel.bnDeleteCase.text=&Delete Case
AutoIngestControlPanel.bnShowCaseLog.text=Show Case &Log
AutoIngestControlPanel.bnPause.text=Pause
AutoIngestControlPanel.bnRefresh.text=&Refresh
@ -255,3 +254,4 @@ AinStatusDashboard.clusterMetricsButton.text=Auto Ingest &Metrics
AinStatusDashboard.nodeStatusTableTitle.text=Auto Ingest Nodes
AinStatusDashboard.healthMonitorButton.text=Health Monitor
CasesDashboardTopComponent.refreshButton.text=Refresh
AutoIngestCasesDeletionDialog.jLabel1.text=Progress

View File

@ -10,8 +10,6 @@ AinStatusNode.status.title=Status
AinStatusNode.status.unknown=Unknown
AutoIngestAdminActions.cancelJobAction.title=Cancel Job
AutoIngestAdminActions.cancelModuleAction.title=Cancel Module
AutoIngestAdminActions.deleteCaseAction.error=Failed to delete case.
AutoIngestAdminActions.deleteCaseAction.title=Delete Case
AutoIngestAdminActions.pause.title=Pause Node
AutoIngestAdminActions.progressDialogAction.title=Ingest Progress
AutoIngestAdminActions.reprocessJobAction.error=Failed to reprocess job
@ -56,7 +54,6 @@ AutoIngestControlPanel.Cancelling=Cancelling...
AutoIngestControlPanel.completedTable.toolTipText=The Completed table shows all Jobs that have been processed already
AutoIngestControlPanel.ConfigLocked=The shared configuration directory is locked because upload from another node is in progress. \nIf this is an error, you can unlock the directory and then retry the upload.
AutoIngestControlPanel.ConfigLockedTitle=Configuration directory locked
AutoIngestControlPanel.DeletionFailed=Deletion failed for job
AutoIngestControlPanel.EnableConfigurationSettings=Enable shared configuration from the options panel before uploading
AutoIngestControlPanel.errorMessage.caseDeprioritization=An error occurred when deprioritizing the case. Some or all jobs may not have been deprioritized.
AutoIngestControlPanel.errorMessage.casePrioritization=An error occurred when prioritizing the case. Some or all jobs may not have been prioritized.
@ -167,9 +164,43 @@ CTL_AutoIngestDashboardOpenAction=Auto Ingest Dashboard
CTL_AutoIngestDashboardTopComponent=Auto Ingest Jobs
CTL_CasesDashboardAction=Multi-User Cases Dashboard
CTL_CasesDashboardTopComponent=Cases
DeleteCaseInputDirectoriesAction.menuItemText=Delete Input Directories
DeleteCasesAction.menuItemText=Delete Case and Jobs
DeleteCasesForReprocessingAction.menuItemText=Delete for Reprocessing
DeleteCaseInputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n
DeleteCaseInputAction.menuItemText=Delete Input
DeleteCaseInputAction.progressDisplayName=Delete Input
DeleteCaseInputAction.taskName=input
DeleteCaseInputAndOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes
DeleteCaseInputAndOutputAction.menuItemText=Delete Input and Output
DeleteCaseInputAndOutputAction.progressDisplayName=Delete Input and Output
DeleteCaseInputAndOutputAction.taskName=input-and-output
DeleteCaseOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes
DeleteCaseOutputAction.menuItemText=Delete Output
DeleteCaseOutputAction.progressDisplayName=Delete Output
DeleteCaseOutputAction.taskName=output
DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock...
DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock...
DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks...
DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service...
DeleteCaseTask.progress.deletingCaseDirCoordSvcNode=Deleting case directory znode...
DeleteCaseTask.progress.deletingCaseNameCoordSvcNode=Deleting case name znode...
# {0} - data source name
# {1} - device id
DeleteCaseTask.progress.deletingDataSource=Deleting data source {0} with device id {1}...
DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest log znode...
# {0} - manifest file path
DeleteCaseTask.progress.deletingManifest=Deleting manifest file {0}...
# {0} - manifest file path
DeleteCaseTask.progress.deletingManifestFileNode=Deleting the manifest file znode for {0}...
DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources znode...
DeleteCaseTask.progress.gettingManifestPaths=Getting manifest file paths...
# {0} - manifest file path
DeleteCaseTask.progress.lockingManifest=Locking manifest file {0}...
DeleteCaseTask.progress.openingCaseDatabase=Opening the case database...
DeleteCaseTask.progress.openingCaseMetadataFile=Opening case metadata file...
# {0} - manifest file path
DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}...
# {0} - manifest file path
DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}...
DeleteCaseTask.progress.startMessage=Starting deletion...
HINT_CasesDashboardTopComponent=This is an adminstrative dashboard for multi-user cases
OpenAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted.
OpenAutoIngestLogAction.logOpenFailedErrorMsg=Failed to open case auto ingest log. See application log for details.
@ -286,7 +317,6 @@ PrioritizationAction.prioritizeJobAction.error=Failed to prioritize job "%s".
PrioritizationAction.prioritizeJobAction.title=Prioritize Job
PrioritizedIconCellRenderer.notPrioritized.tooltiptext=This job has not been prioritized.
PrioritizedIconCellRenderer.prioritized.tooltiptext=This job has been prioritized. The most recently prioritized job should be processed next.
ShowCaseDeletionStatusAction.menuItemText=Show Deletion Status
SingleUserCaseImporter.NonUniqueOutputFolder=Output folder not unique. Skipping
SingleUserCaseImporter.WillImport=Will import:
SingleUserCaseImporter.None=None
@ -380,7 +410,6 @@ AutoIngestControlPanel.bnShowProgress.text=Ingest Progress
AutoIngestControlPanel.bnCancelJob.text=&Cancel Job
AutoIngestControlPanel.bnCancelModule.text=Cancel &Module
AutoIngestControlPanel.bnReprocessJob.text=Reprocess Job
AutoIngestControlPanel.bnDeleteCase.text=&Delete Case
AutoIngestControlPanel.bnShowCaseLog.text=Show Case &Log
AutoIngestControlPanel.bnPause.text=Pause
AutoIngestControlPanel.bnRefresh.text=&Refresh
@ -407,3 +436,4 @@ AinStatusDashboard.clusterMetricsButton.text=Auto Ingest &Metrics
AinStatusDashboard.nodeStatusTableTitle.text=Auto Ingest Nodes
AinStatusDashboard.healthMonitorButton.text=Health Monitor
CasesDashboardTopComponent.refreshButton.text=Refresh
AutoIngestCasesDeletionDialog.jLabel1.text=Progress

View File

@ -31,23 +31,26 @@ import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrows
*/
final class CasesDashboardCustomizer implements MultiUserCaseBrowserCustomizer {
private final DeleteCaseInputDirectoriesAction deleteCaseInputAction;
private final DeleteCasesForReprocessingAction deleteCaseOutputAction;
private final DeleteCasesAction deleteCaseAction;
private final DeleteCaseInputAction deleteCaseInputAction;
private final DeleteCaseOutputAction deleteCaseOutputAction;
private final DeleteCaseInputAndOutputAction deleteCaseAction;
/**
* Constructs a customizer for the multi-user case browser panel used in the
* administrative dashboard for auto ingest cases to present a tabular view
* of the multi-user cases known to the coordination service.
*
* @param executor An executor for tasks for actions that do work in the
* background.
*/
CasesDashboardCustomizer() {
/*
* These actions are shared by all nodes in order to support multiple
* selection.
*/
deleteCaseInputAction = new DeleteCaseInputDirectoriesAction();
deleteCaseOutputAction = new DeleteCasesForReprocessingAction();
deleteCaseAction = new DeleteCasesAction();
deleteCaseInputAction = new DeleteCaseInputAction();
deleteCaseOutputAction = new DeleteCaseOutputAction();
deleteCaseAction = new DeleteCaseInputAndOutputAction();
}
@Override
@ -56,6 +59,11 @@ final class CasesDashboardCustomizer implements MultiUserCaseBrowserCustomizer {
properties.add(Column.CREATE_DATE);
properties.add(Column.LAST_ACCESS_DATE);
properties.add(Column.DIRECTORY);
properties.add(Column.MANIFEST_FILE_ZNODES_DELETE_STATUS);
properties.add(Column.DATA_SOURCES_DELETE_STATUS);
properties.add(Column.TEXT_INDEX_DELETE_STATUS);
properties.add(Column.CASE_DB_DELETE_STATUS);
properties.add(Column.CASE_DIR_DELETE_STATUS);
return properties;
}
@ -79,7 +87,6 @@ final class CasesDashboardCustomizer implements MultiUserCaseBrowserCustomizer {
actions.add(deleteCaseInputAction);
actions.add(deleteCaseOutputAction);
actions.add(deleteCaseAction);
actions.add(new ShowCaseDeletionStatusAction(nodeData));
return actions;
}

View File

@ -34,7 +34,7 @@ import org.sleuthkit.autopsy.coreutils.Logger;
*/
@TopComponent.Description(
preferredID = "CasesDashboardTopComponent",
persistenceType = TopComponent.PERSISTENCE_ALWAYS
persistenceType = TopComponent.PERSISTENCE_NEVER
)
@TopComponent.Registration(
mode = "dashboard",
@ -62,8 +62,6 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex
* for multi-user cases. The top component is docked into the "dashboard
* mode" defined by the auto ingest jobs top component.
*/
// RJCTODO: Consider moving all of the dashboard code into its own
// admindashboards or dashboards package.
public static void openTopComponent() {
CasesDashboardTopComponent topComponent = (CasesDashboardTopComponent) WindowManager.getDefault().findTopComponent("CasesDashboardTopComponent"); // NON-NLS
if (topComponent == null) {

View File

@ -0,0 +1,106 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp. Contact: carrier <at> sleuthkit
* <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.sleuthkit.autopsy.experimental.autoingest;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.AbstractAction;
import org.openide.util.Utilities;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.progress.AppFrameProgressBar;
import org.sleuthkit.autopsy.progress.TaskCancellable;
import org.sleuthkit.autopsy.progress.ProgressIndicator;
/**
* An abstract class for an action that deletes one or more auto ingest cases
* using a thread pool, one task per case. Uses the Template Method design
* pattern to allow subclasses to specify the deletion task to be performed.
*
* This cases to delete are discovered by querying the actions global context
* lookup for CaseNodeData objects. See
* https://platform.netbeans.org/tutorials/nbm-selection-1.html and
* https://platform.netbeans.org/tutorials/nbm-selection-2.html for details.
*/
abstract class DeleteCaseAction extends AbstractAction {
private static final long serialVersionUID = 1L;
private static final int NUMBER_OF_THREADS = 4;
private static final String THREAD_NAME_SUFFIX = "-task-%d"; //NON-NLS
private static final String PROGRESS_DISPLAY_NAME = "%s for %s"; //NON-NLS
private final String taskDisplayName;
private final ExecutorService executor;
/**
* Constructs an abstract class for an action that deletes one or more auto
* ingest cases using a thread pool, one task per case. Uses the Template
* Method design pattern to allow subclasses to specify the deletion task to
* be performed.
*
* @param menuItemText The menu item text for the action.
* @param taskDisplayName The task display name for the progress indicator
* for the task, to be inserted in the first position
* of "%s for %s", where the second substitution is
* the case name.
* @param taskName The task name, to be inserted in the first
* position of "%s-task-%d", where the second
* substitution is the pool thread number.
*/
DeleteCaseAction(String menuItemText, String taskDisplayName, String taskName) {
super(menuItemText);
this.taskDisplayName = taskDisplayName;
String threadNameFormat = taskName + THREAD_NAME_SUFFIX;
executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS, new ThreadFactoryBuilder().setNameFormat(threadNameFormat).build());
}
@Override
public void actionPerformed(ActionEvent event) {
Collection<CaseNodeData> selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class));
for (CaseNodeData nodeData : selectedNodeData) {
AppFrameProgressBar progress = new AppFrameProgressBar(String.format(PROGRESS_DISPLAY_NAME, taskDisplayName, nodeData.getDisplayName()));
TaskCancellable taskCanceller = new TaskCancellable(progress);
progress.setCancellationBehavior(taskCanceller);
Future<?> future = executor.submit(getTask(nodeData, progress));
taskCanceller.setFuture(future);
}
}
/**
* Uses the Template Method design pattern to allow subclasses to specify
* the deletion task to be performed in a worker thread by this action.
*
* @param caseNodeData The case directory lock coordination service node
* data for the case to be deleted.
* @param progress A progress indicator for the task.
*
* @return A case deletion task, ready to be executed.
*/
abstract DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress);
@Override
public DeleteCaseAction clone() throws CloneNotSupportedException {
super.clone();
throw new CloneNotSupportedException();
}
}

View File

@ -19,12 +19,11 @@
package org.sleuthkit.autopsy.experimental.autoingest;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collection;
import javax.swing.AbstractAction;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions;
import org.sleuthkit.autopsy.progress.ProgressIndicator;
/**
* An action that deletes the auto ingest job input directories associated with
@ -32,13 +31,8 @@ import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
* ingest jobs are not deleted. This supports the use case where the directories
* may need to be directed to reclaim space, but the option to restore the
* directories without having the jobs be reprocessed is retained.
*
* This cases to delete are discovered by querying the actions global context
* lookup for CaseNodeData objects. See
* https://platform.netbeans.org/tutorials/nbm-selection-1.html and
* https://platform.netbeans.org/tutorials/nbm-selection-2.html for details.
*/
final class DeleteCaseInputDirectoriesAction extends AbstractAction {
final class DeleteCaseInputAction extends DeleteCaseAction {
private static final long serialVersionUID = 1L;
@ -51,27 +45,27 @@ final class DeleteCaseInputDirectoriesAction extends AbstractAction {
* reprocessed is retained.
*/
@NbBundle.Messages({
"DeleteCaseInputDirectoriesAction.menuItemText=Delete Input Directories"
"DeleteCaseInputAction.menuItemText=Delete Input",
"DeleteCaseInputAction.progressDisplayName=Delete Input",
"DeleteCaseInputAction.taskName=input"
})
DeleteCaseInputDirectoriesAction() {
super(Bundle.DeleteCaseInputDirectoriesAction_menuItemText());
setEnabled(false); // RJCTODO: Enable when implemented
DeleteCaseInputAction() {
super(Bundle.DeleteCaseInputAction_menuItemText(), Bundle.DeleteCaseInputAction_progressDisplayName(), Bundle.DeleteCaseInputAction_taskName());
}
@NbBundle.Messages({
"DeleteCaseInputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n"
})
@Override
public void actionPerformed(ActionEvent event) {
final Collection<CaseNodeData> selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class));
// if (!selectedNodeData.isEmpty()) {
// /*
// * RJCTODO: Create a background task that does the deletion and
// * displays results in a dialog with a scrolling text pane.
// */
// }
if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseInputAction_confirmationText())) {
super.actionPerformed(event);
}
}
@Override
public DeleteCaseInputDirectoriesAction clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) {
return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_INPUT, progress);
}
}

View File

@ -0,0 +1,67 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.autoingest;
import java.awt.event.ActionEvent;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions;
import org.sleuthkit.autopsy.progress.ProgressIndicator;
/**
* An action that completely deletes one or more multi-user cases, including any
* associated auto ingest job input directories and all coordination service
* nodes.
*/
final class DeleteCaseInputAndOutputAction extends DeleteCaseAction {
private static final long serialVersionUID = 1L;
/**
* Constructs an action that completely deletes one or more multi-user
* cases, including any associated auto ingest job input directories and
* coordination service nodes.
*/
@Messages({
"DeleteCaseInputAndOutputAction.menuItemText=Delete Input and Output",
"DeleteCaseInputAndOutputAction.progressDisplayName=Delete Input and Output",
"DeleteCaseInputAndOutputAction.taskName=input-and-output"
})
DeleteCaseInputAndOutputAction() {
super(Bundle.DeleteCaseInputAndOutputAction_menuItemText(), Bundle.DeleteCaseInputAndOutputAction_progressDisplayName(), Bundle.DeleteCaseInputAndOutputAction_taskName());
}
@NbBundle.Messages({
"DeleteCaseInputAndOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes"
})
@Override
public void actionPerformed(ActionEvent event) {
if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseInputAndOutputAction_confirmationText())) {
super.actionPerformed(event);
}
}
@Override
DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) {
return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_ALL, progress);
}
}

View File

@ -19,12 +19,11 @@
package org.sleuthkit.autopsy.experimental.autoingest;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collection;
import javax.swing.AbstractAction;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions;
import org.sleuthkit.autopsy.progress.ProgressIndicator;
/**
* An action that deletes everything except the auto ingest job input
@ -32,13 +31,8 @@ import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
* where a case needs to be reprocessed, so the input directories are not
* deleted even though the coordination service nodes for the auto ingest jobs
* are deleted.
*
* This cases to delete are discovered by querying the actions global context
* lookup for CaseNodeData objects. See
* https://platform.netbeans.org/tutorials/nbm-selection-1.html and
* https://platform.netbeans.org/tutorials/nbm-selection-2.html for details.
*/
final class DeleteCasesForReprocessingAction extends AbstractAction {
final class DeleteCaseOutputAction extends DeleteCaseAction {
private static final long serialVersionUID = 1L;
@ -50,27 +44,27 @@ final class DeleteCasesForReprocessingAction extends AbstractAction {
* ingest jobs are deleted.
*/
@NbBundle.Messages({
"DeleteCasesForReprocessingAction.menuItemText=Delete for Reprocessing"
"DeleteCaseOutputAction.menuItemText=Delete Output",
"DeleteCaseOutputAction.progressDisplayName=Delete Output",
"DeleteCaseOutputAction.taskName=output"
})
DeleteCasesForReprocessingAction() {
super(Bundle.DeleteCasesForReprocessingAction_menuItemText());
setEnabled(false); // RJCTODO: Enable when implemented
DeleteCaseOutputAction() {
super(Bundle.DeleteCaseOutputAction_menuItemText(), Bundle.DeleteCaseOutputAction_progressDisplayName(), Bundle.DeleteCaseOutputAction_taskName());
}
@NbBundle.Messages({
"DeleteCaseOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes"
})
@Override
public void actionPerformed(ActionEvent event) {
final Collection<CaseNodeData> selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class));
// if (!selectedNodeData.isEmpty()) {
// /*
// * RJCTODO: Create a background task that does the deletion and
// * displays results in a dialog with a scrolling text pane.
// */
// }
if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseOutputAction_confirmationText())) {
super.actionPerformed(event);
}
}
@Override
public DeleteCasesForReprocessingAction clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) {
return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_OUTPUT, progress);
}
}

View File

@ -0,0 +1,809 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.autoingest;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock;
import org.sleuthkit.autopsy.core.UserPreferences;
import org.sleuthkit.autopsy.core.UserPreferencesException;
import org.sleuthkit.autopsy.coreutils.FileUtil;
import org.sleuthkit.autopsy.progress.ProgressIndicator;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobNodeData.InvalidDataException;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.TskCoreException;
/**
* A task that deletes part or all of a given case. Note that all logging is
* directed to the dedicated auto ingest dashboard log instead of to the general
* application log.
*/
final class DeleteCaseTask implements Runnable {
private static final int MANIFEST_FILE_LOCKING_TIMEOUT_MINS = 5;
private static final int MANIFEST_DELETE_TRIES = 3;
private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode";
private static final Logger logger = AutoIngestDashboardLogger.getLogger();
private final CaseNodeData caseNodeData;
private final DeleteOptions deleteOption;
private final ProgressIndicator progress;
private final List<Path> manifestFilePaths;
private final List<Lock> manifestFileLocks;
private CoordinationService coordinationService;
private CaseMetadata caseMetadata;
/**
* Options to support implementing different case deletion use cases.
*/
enum DeleteOptions {
/**
* Delete the auto ingest job manifests and corresponding data sources,
* while leaving the manifest file coordination service nodes and the
* rest of the case intact. The use case is freeing auto ingest input
* directory space while retaining the option to restore the data
* sources, effectively restoring the case.
*/
DELETE_INPUT,
/**
* Delete the manifest file coordination service nodes and the output
* for a case, while leaving the auto ingest job manifests and
* corresponding data sources intact. The use case is auto ingest
* reprocessing of a case with a clean slate without having to restore
* the manifests and data sources.
*/
DELETE_OUTPUT,
/**
* Delete everything.
*/
DELETE_ALL
}
/**
* Constructs a task that deletes part or all of a given case. Note that all
* logging is directed to the dedicated auto ingest dashboard log instead of
* to the general application log.
*
* @param caseNodeData The case directory coordination service node data for
* the case.
* @param deleteOption The deletion option for the task.
* @param progress A progress indicator.
*/
DeleteCaseTask(CaseNodeData caseNodeData, DeleteOptions deleteOption, ProgressIndicator progress) {
this.caseNodeData = caseNodeData;
this.deleteOption = deleteOption;
this.progress = progress;
manifestFilePaths = new ArrayList<>();
manifestFileLocks = new ArrayList<>();
}
@Override
@NbBundle.Messages({
"DeleteCaseTask.progress.startMessage=Starting deletion..."
})
public void run() {
try {
progress.start(Bundle.DeleteCaseTask_progress_startMessage());
logger.log(Level.INFO, String.format("Starting deletion of %s (%s)", caseNodeData.getDisplayName(), deleteOption));
deleteCase();
logger.log(Level.INFO, String.format("Finished deletion of %s (%s)", caseNodeData.getDisplayName(), deleteOption));
} catch (CoordinationServiceException | IOException ex) {
logger.log(Level.SEVERE, String.format("Error deleting %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex);
} catch (InterruptedException ex) {
logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex);
Thread.currentThread().interrupt();
} catch (Exception ex) {
/*
* This is an unexpected runtime exceptions firewall. It is here
* because this task is designed to be able to be run in scenarios
* where there is no call to get() on a Future<Void> associated with
* the task, so this ensures that any such errors get logged.
*/
logger.log(Level.SEVERE, String.format("Unexpected error deleting %s", caseNodeData.getDisplayName()), ex);
throw ex;
} finally {
releaseManifestFileLocks();
progress.finish();
}
}
/**
* Deletes part or all of the given case.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
@NbBundle.Messages({
"DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service...",
"DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock...",
"DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock...",
"DeleteCaseTask.progress.gettingManifestPaths=Getting manifest file paths...",
"DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks...",
"DeleteCaseTask.progress.openingCaseMetadataFile=Opening case metadata file...",
"DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources znode...",
"DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest log znode...",
"DeleteCaseTask.progress.deletingCaseDirCoordSvcNode=Deleting case directory znode...",
"DeleteCaseTask.progress.deletingCaseNameCoordSvcNode=Deleting case name znode..."
})
private void deleteCase() throws CoordinationServiceException, IOException, InterruptedException {
progress.progress(Bundle.DeleteCaseTask_progress_connectingToCoordSvc());
logger.log(Level.INFO, String.format("Connecting to the coordination service for deletion of %s", caseNodeData.getDisplayName()));
coordinationService = CoordinationService.getInstance();
checkForCancellation();
/*
* Acquire an exclusive case name lock. The case name lock is the lock
* that auto ingest node (AIN) job processing tasks acquire exclusively
* when creating or opening a case specified in an auto ingest job
* manifest file. The reason AINs do this is to ensure that only one of
* them at a time can search the auto ingest output directory for an
* existing case matching the one in the manifest file. If a matching
* case is found, it is opened, otherwise the case is created. Acquiring
* this lock effectively disables this AIN job processing task behavior
* while the case is being deleted.
*/
progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseNameLock());
logger.log(Level.INFO, String.format("Acquiring an exclusive case name lock for %s", caseNodeData.getDisplayName()));
String caseNameLockName = CoordinationServiceUtils.getCaseNameNodePath(caseNodeData.getDirectory());
try (CoordinationService.Lock nameLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, caseNameLockName)) {
if (nameLock == null) {
logger.log(Level.INFO, String.format("Could not delete %s because a case name lock was already held by another host", caseNodeData.getDisplayName()));
return;
}
checkForCancellation();
/*
* Acquire an exclusive case directory lock. A shared case directory
* lock is acquired by each auto ingest node (AIN) and examiner node
* (EIN) when it opens a case. The shared locks are held by the AINs
* and EINs for as long as they have the case open. Acquiring this
* lock exclusively ensures that no AIN or EIN has the case to be
* deleted open and prevents another node from trying to open the
* case while it is being deleted.
*/
progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseDirLock());
logger.log(Level.INFO, String.format("Acquiring an exclusive case directory lock for %s", caseNodeData.getDisplayName()));
String caseDirLockName = CoordinationServiceUtils.getCaseDirectoryNodePath(caseNodeData.getDirectory());
try (CoordinationService.Lock caseDirLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseDirLockName)) {
if (caseDirLock == null) {
logger.log(Level.INFO, String.format("Could not delete %s because a case directory lock was already held by another host", caseNodeData.getDisplayName()));
return;
}
checkForCancellation();
getManifestFilePaths();
checkForCancellation();
/*
* Acquire exclusive locks for the auto ingest job manifest
* files for the case, if any. Manifest file locks are acquired
* by the auto ingest node (AIN) input directory scanning tasks
* when they look for auto ingest jobs to enqueue, and by the
* AIN job execution tasks when they do a job. Acquiring these
* locks here ensures that the scanning tasks and job execution
* tasks cannot do anything with the auto ingest jobs for a case
* during case deletion.
*/
if (!acquireManifestFileLocks()) {
logger.log(Level.INFO, String.format("Could not delete %s because at least one manifest file lock was already held by another host", caseNodeData.getDisplayName()));
return;
}
checkForCancellation();
deleteCaseContents();
checkForCancellation();
deleteCaseResourcesNode();
checkForCancellation();
deleteCaseAutoIngestLogNode();
checkForCancellation();
deleteManifestFileNodes();
checkForCancellation();
}
deleteCaseDirectoryNode();
checkForCancellation();
}
deleteCaseNameNode();
}
/**
* Gets the manifest file paths for the case, if there are any.
*
* @throws CoordinationServiceException If there is an error completing a
* coordination service operation.
* @throws InterruptedException If the thread in which this task is
* running is interrupted while blocked
* waiting for a coordination service
* operation to complete.
* @throws IOException If there is an error reading the
* manifests list file.
*/
private void getManifestFilePaths() throws IOException, CoordinationServiceException, InterruptedException {
progress.progress(Bundle.DeleteCaseTask_progress_gettingManifestPaths());
logger.log(Level.INFO, String.format("Getting manifest file paths for %s", caseNodeData.getDisplayName()));
final Path manifestsListFilePath = Paths.get(caseNodeData.getDirectory().toString(), AutoIngestManager.getCaseManifestsListFileName());
final File manifestListsFile = manifestsListFilePath.toFile();
if (manifestListsFile.exists()) {
getManifestPathsFromFile(manifestsListFilePath);
} else {
getManifestPathsFromNodes();
}
if (manifestFilePaths.isEmpty()) {
setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES);
}
logger.log(Level.INFO, String.format("Found %d manifest file path(s) for %s", manifestFilePaths.size(), caseNodeData.getDisplayName()));
}
/**
* Gets a list of the manifest file paths for the case by reading them from
* the manifests list file for the case.
*
* @param manifestsListFilePath The path of the manifests list file.
*
* @throws IOException If there is an error reading the manifests
* list file.
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
private void getManifestPathsFromFile(Path manifestsListFilePath) throws IOException, InterruptedException {
try (final Scanner manifestsListFileScanner = new Scanner(manifestsListFilePath)) {
while (manifestsListFileScanner.hasNextLine()) {
checkForCancellation();
final Path manifestFilePath = Paths.get(manifestsListFileScanner.nextLine());
if (manifestFilePath.toFile().exists()) {
manifestFilePaths.add(manifestFilePath);
}
}
}
}
/**
* Gets a list of the manifest file paths for the case by sifting through
* the node data of the manifest file coordination service nodes and
* matching on case name.
*
* @throws CoordinationServiceException If there is an error completing a
* coordination service operation.
* @throws InterruptedException If the thread in which this task is
* running is interrupted while blocked
* waiting for a coordination service
* operation to complete.
*/
private void getManifestPathsFromNodes() throws CoordinationServiceException, InterruptedException {
/*
* Get the original, undecorated case name from the case directory. This
* is necessary because the case display name can be changed and the
* original case name may have a time stamp added to make it unique,
* depending on how the case was created. An alternative aproach would
* be to strip off any time stamp from the case name in the case node
* data.
*/
String caseName = CoordinationServiceUtils.getCaseNameNodePath(caseNodeData.getDirectory());
final List<String> nodeNames = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS);
for (String manifestNodeName : nodeNames) {
checkForCancellation();
try {
final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodeName);
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes);
if (caseName.equals(nodeData.getCaseName())) {
Path manifestFilePath = nodeData.getManifestFilePath();
if (manifestFilePath.toFile().exists()) {
manifestFilePaths.add(manifestFilePath);
}
}
} catch (CoordinationServiceException | InvalidDataException ex) {
logger.log(Level.WARNING, String.format("Error getting coordination service node data from %s", manifestNodeName), ex);
}
}
}
/**
* Acquires either all or none of the auto ingest job manifest file locks
* for a case.
*
* @return True if all of the locks were acquired; false otherwise.
*
* @throws CoordinationServiceException If there is an error completing a
* coordination service operation.
* @throws InterruptedException If the thread in which this task is
* running is interrupted while blocked
* waiting for a coordination service
* operation to complete.
*/
@NbBundle.Messages({
"# {0} - manifest file path", "DeleteCaseTask.progress.lockingManifest=Locking manifest file {0}..."
})
private boolean acquireManifestFileLocks() throws IOException, CoordinationServiceException, InterruptedException {
boolean allLocksAcquired = true;
if (!manifestFilePaths.isEmpty()) {
progress.progress(Bundle.DeleteCaseTask_progress_acquiringManifestLocks());
logger.log(Level.INFO, String.format("Acquiring exclusive manifest file locks for %s", caseNodeData.getDisplayName()));
/*
* When acquiring the locks, it is reasonable to block briefly,
* since the auto ingest node (AIN) input directory scanning tasks
* do a lot of short-term acquiring and releasing of the same locks.
* The assumption here is that the originator of this case deletion
* task is not asking for deletion of a case that has a job that an
* auto ingest node (AIN) job execution task is working on and that
* MANIFEST_FILE_LOCKING_TIMEOUT_MINS is not very long anyway, so
* waiting a bit should be fine.
*/
try {
for (Path manifestPath : manifestFilePaths) {
checkForCancellation();
progress.progress(Bundle.DeleteCaseTask_progress_lockingManifest(manifestPath.toString()));
logger.log(Level.INFO, String.format("Exclusively locking the manifest %s for %s", manifestPath, caseNodeData.getDisplayName()));
CoordinationService.Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString(), MANIFEST_FILE_LOCKING_TIMEOUT_MINS, TimeUnit.MINUTES);
if (null != manifestLock) {
manifestFileLocks.add(manifestLock);
} else {
logger.log(Level.INFO, String.format("Failed to exclusively lock the manifest %s because it was already held by another host", manifestPath, caseNodeData.getDisplayName()));
allLocksAcquired = false;
releaseManifestFileLocks();
break;
}
}
} catch (CoordinationServiceException | InterruptedException ex) {
releaseManifestFileLocks();
throw ex;
}
}
return allLocksAcquired;
}
/**
* Deletes case contents, based on the specified deletion option.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
private void deleteCaseContents() throws InterruptedException {
final File caseDirectory = caseNodeData.getDirectory().toFile();
if (caseDirectory.exists()) {
progress.progress(Bundle.DeleteCaseTask_progress_openingCaseMetadataFile());
logger.log(Level.INFO, String.format("Opening case metadata file for %s", caseNodeData.getDisplayName()));
Path caseMetadataPath = CaseMetadata.getCaseMetadataFilePath(caseNodeData.getDirectory());
if (caseMetadataPath != null) {
try {
caseMetadata = new CaseMetadata(caseMetadataPath);
checkForCancellation();
if (!manifestFilePaths.isEmpty() && (deleteOption == DeleteOptions.DELETE_INPUT || deleteOption == DeleteOptions.DELETE_ALL)) {
deleteAutoIngestInput();
}
checkForCancellation();
if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) {
Case.deleteMultiUserCase(caseNodeData, caseMetadata, progress, logger);
}
} catch (CaseMetadata.CaseMetadataException ex) {
logger.log(Level.SEVERE, String.format("Error reading metadata file for %s", caseNodeData.getDisplayName()), ex);
}
} else {
logger.log(Level.WARNING, String.format("No case metadata file found for %s", caseNodeData.getDisplayName()));
}
} else {
setDeletedItemFlag(CaseNodeData.DeletedFlags.CASE_DIR);
logger.log(Level.INFO, String.format("No case directory found for %s", caseNodeData.getDisplayName()));
}
}
/**
* Deletes the auto ingest job input manifests for the case along with the
* corresponding data sources.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
@NbBundle.Messages({
"DeleteCaseTask.progress.openingCaseDatabase=Opening the case database...",
"# {0} - manifest file path", "DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}...",
"# {0} - manifest file path", "DeleteCaseTask.progress.deletingManifest=Deleting manifest file {0}..."
})
private void deleteAutoIngestInput() throws InterruptedException {
SleuthkitCase caseDb = null;
try {
progress.progress(Bundle.DeleteCaseTask_progress_openingCaseDatabase());
logger.log(Level.INFO, String.format("Opening the case database for %s", caseNodeData.getDisplayName()));
caseDb = SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory());
List<DataSource> dataSources = caseDb.getDataSources();
checkForCancellation();
/*
* For every manifest file associated with the case, attempt to
* delete both the data source referenced by the manifest and the
* manifest.
*/
boolean allInputDeleted = true;
for (Path manifestFilePath : manifestFilePaths) {
checkForCancellation();
final File manifestFile = manifestFilePath.toFile();
if (manifestFile.exists()) {
Manifest manifest = parseManifestFile(manifestFilePath);
if (manifest != null) {
if (deleteDataSources(manifest, dataSources)) {
if (!deleteManifestFile(manifestFile)) {
logger.log(Level.WARNING, String.format("Failed to delete manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
allInputDeleted = false;
}
} else {
logger.log(Level.WARNING, String.format("Failed to delete manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
allInputDeleted = false;
}
} else {
logger.log(Level.WARNING, String.format("Failed to parse manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
allInputDeleted = false;
}
} else {
logger.log(Level.WARNING, String.format("Did not find manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
}
}
if (allInputDeleted) {
setDeletedItemFlag(CaseNodeData.DeletedFlags.DATA_SOURCES);
}
} catch (TskCoreException | UserPreferencesException ex) {
logger.log(Level.INFO, String.format("Failed to open or query the case database for %s", caseNodeData.getDisplayName()), ex);
} finally {
if (caseDb != null) {
caseDb.close();
}
}
}
/**
* Parses a manifest file.
*
* @param manifestFilePath The manifest file path.
*
* @return A manifest, if the parsing is successful, null otherwise.
*/
private Manifest parseManifestFile(Path manifestFilePath) {
progress.progress(Bundle.DeleteCaseTask_progress_parsingManifest(manifestFilePath));
logger.log(Level.INFO, String.format("Parsing manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
Manifest manifest = null;
for (ManifestFileParser parser : Lookup.getDefault().lookupAll(ManifestFileParser.class)) {
if (parser.fileIsManifest(manifestFilePath)) {
try {
manifest = parser.parse(manifestFilePath);
break;
} catch (ManifestFileParser.ManifestFileParserException ex) {
logger.log(Level.WARNING, String.format("Error parsing manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex);
}
}
}
return manifest;
}
/**
* Deletes a manifest file.
*
* @param manifestFile The manifest file.
*
* @return True if the file was deleted, false otherwise.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
private boolean deleteManifestFile(File manifestFile) throws InterruptedException {
/*
* Delete the manifest file, allowing a few retries. This is a way to
* resolve the race condition between this task and auto ingest node
* (AIN) input directory scanning tasks, which parse manifests (actually
* all files) before getting a coordination service lock, without
* resorting to a protocol using locking of the input directory.
*/
Path manifestFilePath = manifestFile.toPath();
progress.progress(Bundle.DeleteCaseTask_progress_deletingManifest(manifestFilePath));
logger.log(Level.INFO, String.format("Deleting manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
int tries = 0;
boolean deleted = false;
while (!deleted && tries < MANIFEST_DELETE_TRIES) {
deleted = manifestFile.delete();
if (!deleted) {
++tries;
Thread.sleep(1000);
}
}
return deleted;
}
/**
* Locates and deletes the data source files referenced by a manifest.
*
* @param manifest A manifest.
* @param dataSources The data sources in the case as obtained from the case
* database.
*
* @return True if all of the data source files werre deleted, false
* otherwise.
*/
@NbBundle.Messages({
"# {0} - data source name", "# {1} - device id", "DeleteCaseTask.progress.deletingDataSource=Deleting data source {0} with device id {1}...",})
private boolean deleteDataSources(Manifest manifest, List<DataSource> dataSources) {
final String dataSourceFileName = manifest.getDataSourceFileName();
final String dataSourceDeviceId = manifest.getDeviceId();
progress.progress(Bundle.DeleteCaseTask_progress_deletingDataSource(dataSourceFileName, dataSourceDeviceId));
logger.log(Level.INFO, String.format("Deleting data source %s with device id %s from %s", dataSourceFileName, dataSourceDeviceId, caseNodeData.getDisplayName()));
/*
* There are two possibilities here. The data source may be an image,
* and if so, it may be split into multiple files. In this case, all of
* the files for the image need to be deleted. Otherwise, the data
* source is a single directory or file (a logical file, logical file
* set, report file, archive file, etc.). In this case, just the file
* referenced by the manifest will be deleted.
*/
boolean allFilesDeleted = true;
Set<Path> filesToDelete = new HashSet<>();
for (DataSource dataSource : dataSources) {
if (dataSource instanceof Image) {
Image image = (Image) dataSource;
if (image.getName().equals(dataSourceFileName) && image.getDeviceId().equals(dataSourceDeviceId)) {
String[] imageFilePaths = image.getPaths();
for (String path : imageFilePaths) {
Path imageFilePath = Paths.get(path);
filesToDelete.add(imageFilePath);
}
break;
}
}
}
if (filesToDelete.isEmpty()) {
final Path dataSourcePath = manifest.getDataSourcePath();
filesToDelete.add(dataSourcePath);
}
for (Path path : filesToDelete) {
File fileOrDir = path.toFile();
if (fileOrDir.exists() && !FileUtil.deleteFileDir(fileOrDir)) {
allFilesDeleted = false;
logger.log(Level.INFO, String.format("Failed to delete data source file at %s for %s", path, caseNodeData.getDisplayName()));
}
}
return allFilesDeleted;
}
/**
* Deletes the case resources coordination service node.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
private void deleteCaseResourcesNode() throws InterruptedException {
if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) {
progress.progress(Bundle.DeleteCaseTask_progress_deletingResourcesLockNode());
logger.log(Level.INFO, String.format("Deleting case resources log znode for %s", caseNodeData.getDisplayName()));
String resourcesNodePath = CoordinationServiceUtils.getCaseResourcesNodePath(caseNodeData.getDirectory());
try {
coordinationService.deleteNode(CategoryNode.CASES, resourcesNodePath);
} catch (CoordinationServiceException ex) {
if (!isNoNodeException(ex)) {
logger.log(Level.SEVERE, String.format("Error deleting case resources znode for %s", caseNodeData.getDisplayName()), ex);
}
}
}
}
/**
* Deletes the case auto ingest log coordination service node.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
private void deleteCaseAutoIngestLogNode() throws InterruptedException {
if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) {
progress.progress(Bundle.DeleteCaseTask_progress_deletingJobLogLockNode());
logger.log(Level.INFO, String.format("Deleting case auto ingest job log znode for %s", caseNodeData.getDisplayName()));
String logFilePath = CoordinationServiceUtils.getCaseAutoIngestLogNodePath(caseNodeData.getDirectory());
try {
coordinationService.deleteNode(CategoryNode.CASES, logFilePath);
} catch (CoordinationServiceException ex) {
if (!isNoNodeException(ex)) {
logger.log(Level.SEVERE, String.format("Error deleting case auto ingest job log znode for %s", caseNodeData.getDisplayName()), ex);
}
}
}
}
/**
* Deletes the case directory coordination service node if everything that
* was supposed to be deleted was deleted. Otherwise, leave the node so that
* what was and was not deleted can be inspected.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
private void deleteCaseDirectoryNode() throws InterruptedException {
if ((deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL)
&& caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.DATA_SOURCES)
&& caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB)
&& caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR)
&& caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES)) {
progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseDirCoordSvcNode());
logger.log(Level.INFO, String.format("Deleting case directory znode for %s", caseNodeData.getDisplayName()));
String caseDirNodePath = CoordinationServiceUtils.getCaseDirectoryNodePath(caseNodeData.getDirectory());
try {
coordinationService.deleteNode(CategoryNode.CASES, caseDirNodePath);
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error deleting case directory lock node for %s", caseNodeData.getDisplayName()), ex);
}
}
}
/**
* Deletes the case name coordiation service node.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
private void deleteCaseNameNode() throws InterruptedException {
if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) {
progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseNameCoordSvcNode());
logger.log(Level.INFO, String.format("Deleting case name znode for %s", caseNodeData.getDisplayName()));
try {
String caseNameLockNodeName = CoordinationServiceUtils.getCaseNameNodePath(caseNodeData.getDirectory());
coordinationService.deleteNode(CategoryNode.CASES, caseNameLockNodeName);
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error deleting case name lock node for %s", caseNodeData.getDisplayName()), ex);
}
}
}
/**
* Examines a coordination service exception to try to determine if it is a
* no node exception.
*
* @param ex A coordination service exception.
*
* @return True or false.
*/
private boolean isNoNodeException(CoordinationServiceException ex) {
boolean isNodeNodeEx = false;
Throwable cause = ex.getCause();
if (cause != null) {
String causeMessage = cause.getMessage();
isNodeNodeEx = causeMessage.contains(NO_NODE_ERROR_MSG_FRAGMENT);
}
return isNodeNodeEx;
}
/**
* Releases all of the manifest file locks that have been acquired by this
* task.
*/
@NbBundle.Messages({
"# {0} - manifest file path", "DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}..."
})
private void releaseManifestFileLocks() {
for (Lock manifestFileLock : manifestFileLocks) {
String manifestFilePath = manifestFileLock.getNodePath();
try {
progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLock(manifestFilePath));
logger.log(Level.INFO, String.format("Releasing the exclusive coordination service lock on the manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
manifestFileLock.release();
} catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Error releasing the exclusive coordination service lock on the manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex);
}
}
manifestFileLocks.clear();
}
/**
* Releases all of the manifest file locks that have been acquired by this
* task and attempts to delete the corresponding coordination service nodes.
*
* @return True if all of the manifest file coordianiton service nodes have
* been deleted, false otherwise.
*
* @throws InterruptedException If the thread in which this task is running
* is interrupted while blocked waiting for a
* coordination service operation to complete.
*/
@Messages({
"# {0} - manifest file path", "DeleteCaseTask.progress.deletingManifestFileNode=Deleting the manifest file znode for {0}..."
})
private void deleteManifestFileNodes() throws InterruptedException {
if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) {
boolean allINodesDeleted = true;
Iterator<Lock> iterator = manifestFileLocks.iterator();
while (iterator.hasNext()) {
Lock manifestFileLock = iterator.next();
String manifestFilePath = manifestFileLock.getNodePath();
try {
progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLock(manifestFilePath));
logger.log(Level.INFO, String.format("Releasing the lock on the manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
manifestFileLock.release();
progress.progress(Bundle.DeleteCaseTask_progress_deletingManifestFileNode(manifestFilePath));
logger.log(Level.INFO, String.format("Deleting the manifest file znode for %s for %s", manifestFilePath, caseNodeData.getDisplayName()));
coordinationService.deleteNode(CoordinationService.CategoryNode.MANIFESTS, manifestFilePath);
} catch (CoordinationServiceException ex) {
allINodesDeleted = false;
logger.log(Level.WARNING, String.format("Error deleting the manifest file znode for %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex);
}
iterator.remove();
}
if (allINodesDeleted) {
setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES);
}
}
}
/**
* Sets a deleted item flag in the coordination service node data for the
* case.
*
* @param flag The flag to set.
*/
private void setDeletedItemFlag(CaseNodeData.DeletedFlags flag) {
try {
caseNodeData.setDeletedFlag(flag);
coordinationService.setNodeData(CategoryNode.CASES, caseNodeData.getDirectory().toString(), caseNodeData.toArray());
} catch (IOException | CoordinationServiceException | InterruptedException ex) {
logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s", flag.name(), caseNodeData.getDisplayName()), ex);
}
}
/**
* Checks whether the interrupted flag of the current thread is set.
*
* @throws InterruptedException If the interrupted flag is set.
*/
private void checkForCancellation() throws InterruptedException {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException("Interrupt detected");
}
}
}

View File

@ -1,71 +0,0 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.experimental.autoingest;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collection;
import javax.swing.AbstractAction;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
/**
* An action that completely deletes one or more multi-user cases, including any
* associated auto ingest job input directories and coordination service nodes.
*
* This cases to delete are discovered by querying the actions global context
* lookup for CaseNodeData objects. See
* https://platform.netbeans.org/tutorials/nbm-selection-1.html and
* https://platform.netbeans.org/tutorials/nbm-selection-2.html for details.
*/
final class DeleteCasesAction extends AbstractAction {
private static final long serialVersionUID = 1L;
/**
* Constructs an action that completely deletes one or more multi-user
* cases, including any associated auto ingest job input directories and
* coordination service nodes.
*/
@NbBundle.Messages({
"DeleteCasesAction.menuItemText=Delete Case and Jobs"
})
DeleteCasesAction() {
super(Bundle.DeleteCasesAction_menuItemText());
setEnabled(false); // RJCTODO: Enable when implemented
}
@Override
public void actionPerformed(ActionEvent event) {
// final Collection<CaseNodeData> selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class));
// if (!selectedNodeData.isEmpty()) {
// /*
// * RJCTODO: Create a background task that does the deletion and
// * displays results in a dialog with a scrolling text pane.
// */
// }
}
@Override
public DeleteCasesAction clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}

View File

@ -1,48 +0,0 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.sleuthkit.autopsy.experimental.autoingest;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
/**
* An action that shows a popup that enumerates the deletion status of the
* various parts of a multi-user case known to the coordination service.
*/
final class ShowCaseDeletionStatusAction extends AbstractAction {
private static final long serialVersionUID = 1L;
// private final CaseNodeData caseNodeData;
/**
* Constructs an action that shows a popup that enumerates the deletion
* status of the various parts of a multi-user case known to the
* coordination service.
*
* @param caseNodeData The coordination service node data for the case.
*/
@NbBundle.Messages({
"ShowCaseDeletionStatusAction.menuItemText=Show Deletion Status"
})
ShowCaseDeletionStatusAction(CaseNodeData caseNodeData) {
super(Bundle.ShowCaseDeletionStatusAction_menuItemText());
// this.caseNodeData = caseNodeData; // RJCTODO: Use
setEnabled(false); // RJCTODO: Enable when implemented
}
@Override
public void actionPerformed(ActionEvent e) {
// RJCTODO: Implement
}
@Override
public ShowCaseDeletionStatusAction clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}

View File

@ -0,0 +1,775 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.keywordsearch.multicase;
import com.google.common.eventbus.EventBus;
import java.io.File;
import java.io.IOException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.lang.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.request.CoreAdminRequest;
import org.apache.solr.client.solrj.response.CoreAdminResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.params.CoreAdminParams;
import org.apache.solr.common.params.CursorMarkParams;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.core.UserPreferences;
import org.sleuthkit.autopsy.core.UserPreferencesException;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.UNCPathUtilities;
import org.sleuthkit.autopsy.keywordsearch.Server;
import org.sleuthkit.autopsy.progress.ProgressIndicator;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.CaseDbConnectionInfo;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.Report;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Performs keyword searches across multiple cases
*/
final class MultiCaseSearcher {
private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS
private static final String SEARCH_COMPLETE_MESSAGE = "SEARCH_COMPLETE";
private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS
private static final int CASE_DIR_READ_LOCK_TIMEOUT_HOURS = 12; //NON-NLS
private static final String SOLR_SERVER_URL_FORMAT_STRING = "http://%s:%s/solr"; //NON-NLS
private static final String SOLR_CORE_URL_FORMAT_STRING = "http://%s:%s/solr/%s"; //NON-NLS
private final static String SOLR_METADATA_FILE_NAME = "SolrCore.properties"; //NON-NLS
private static final String SOLR_CORE_NAME_XPATH = "/SolrCores/Core/CoreName/text()"; //NON-NLS
private static final String TEXT_INDEX_NAME_XPATH = "/SolrCores/Core/TextIndexPath/text()"; //NON-NLS
private static final String SOLR_CORE_INSTANCE_PATH_PROPERTY = "instanceDir"; //NON-NLS
private static final String SOLR_CONFIG_SET_NAME = "AutopsyConfig"; //NON-NLS
private static final int MAX_RESULTS_PER_CURSOR_MARK = 512;
private static final String SOLR_DOC_ID_FIELD = Server.Schema.ID.toString(); //NON-NLS
private static final String SOLR_DOC_CONTENT_STR_FIELD = Server.Schema.CONTENT_STR.toString(); //NON-NLS
private static final String SOLR_DOC_CHUNK_SIZE_FIELD = Server.Schema.CHUNK_SIZE.toString(); //NON-NLS
private static final String SOLR_DOC_ID_PARTS_SEPARATOR = "_";
private static final Logger logger = Logger.getLogger(MultiCaseSearcher.class.getName());
private final EventBus eventBus = new EventBus("MultiCaseSearcherEventBus");
private static final UNCPathUtilities pathUtils = new UNCPathUtilities();
private volatile boolean searchStopped = true;
MultiCaseSearcher() {
}
static String getSearchCompleteMessage() {
return SEARCH_COMPLETE_MESSAGE;
}
/**
*
* Performs keyword searches across multiple cases
*
* @param caseNames The names of the cases to search.
* @param query The keyword search query to perform.
* @param progressIndicator A progrss indicator for the search.
*
* @return The search results.
*
* @throws MultiCaseSearcherException
* @throws InterruptedException
*/
@NbBundle.Messages({
"MultiCaseSearcher.progressMessage.findingCases=Finding selected cases",
"MultiCaseSearcher.progressMessage.creatingSolrQuery=Creating search query for Solr server",
"# {0} - total cases",
"MultiCaseSearcher.progressMessage.startingCaseSearches=Searching {0} case(s)",
"# {0} - case name",
"# {1} - case counter",
"# {2} - total cases",
"MultiCaseSearcher.progressMessage.acquiringSharedLockForCase=Acquiring shared lock for \"{0}\" ({1} of {2} case(s))",
"# {0} - case name",
"# {1} - case counter",
"# {2} - total cases",
"MultiCaseSearcher.progressMessage.loadingSolrCoreForCase=Loading Solr core for \"{0}\" ({1} of {2} case(s))",
"# {0} - case name",
"# {1} - case counter",
"# {2} - total cases",
"MultiCaseSearcher.progressMessage.openingCaseDbForCase=Opening case database for \"{0}\" ({1} of {2} case(s))",
"# {0} - case name",
"# {1} - case counter",
"# {2} - total cases",
"MultiCaseSearcher.progressMessage.executingSolrQueryForCase=Getting keyword hits for \"{0}\" ({1} of {2} case(s))",
"# {0} - case directory path",
"MultiCaseSearcher.exceptionMessage.failedToGetCaseDirReadlock=Failed to obtain read lock for case directory at {0}",
"MultiCaseSearcher.exceptionMessage.cancelledMessage=Search cancelled"
})
void performKeywordSearch(final Collection<CaseNodeData> caseNodes, final SearchQuery query, final ProgressIndicator progressIndicator) {
progressIndicator.start(Bundle.MultiCaseSearcher_progressMessage_findingCases());
try {
searchStopped = false; //mark the search as started
final List<MultiCaseMetadata> caseMetadata = getMultiCaseMetadata(caseNodes);
checkForCancellation();
//eventBus.post("number of cases to search determined");
progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_creatingSolrQuery());
final SolrQuery solrQuery = createSolrQuery(query);
checkForCancellation();
final int totalCases = caseMetadata.size();
int caseCounter = 1;
progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases));
int totalSteps = 5;
progressIndicator.switchToDeterminate(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases), 0, totalCases * totalSteps);
int caseNumber = 0;
for (MultiCaseMetadata aCase : caseMetadata) {
CaseMetadata metadata = aCase.getCaseMetadata();
String caseName = metadata.getCaseDisplayName();
SleuthkitCase caseDatabase = null;
int stepsCompleted = 0;
progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_acquiringSharedLockForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps);
try (CoordinationService.Lock caseDirReadLock = CoordinationService.getInstance().tryGetSharedLock(CoordinationService.CategoryNode.CASES, aCase.getCaseMetadata().getCaseDirectory(), CASE_DIR_READ_LOCK_TIMEOUT_HOURS, TimeUnit.HOURS)) {
if (null == caseDirReadLock) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory()));
}
checkForCancellation();
++stepsCompleted;
progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_loadingSolrCoreForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps);
final HttpSolrServer solrServer = loadSolrCoreForCase(aCase);
checkForCancellation();
++stepsCompleted;
progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_openingCaseDbForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps);
caseDatabase = openCase(aCase);
checkForCancellation();
++stepsCompleted;
progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_executingSolrQueryForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps);
eventBus.post(executeQuery(solrServer, solrQuery, caseDatabase, aCase));
++stepsCompleted;
progressIndicator.progress(stepsCompleted + caseNumber * totalSteps);
++caseCounter;
} catch (CoordinationService.CoordinationServiceException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory()), ex);
} catch (MultiCaseSearcherException exception) {
logger.log(Level.INFO, "Exception encountered while performing multi-case keyword search", exception);
eventBus.post(exception);
} finally {
if (null != caseDatabase) {
closeCase(caseDatabase);
}
}
caseNumber++;
}
} catch (InterruptedException exception) {
logger.log(Level.INFO, Bundle.MultiCaseSearcher_exceptionMessage_cancelledMessage(), exception);
eventBus.post(exception);
} catch (MultiCaseSearcherException exception) {
logger.log(Level.WARNING, "Exception encountered while performing multi-case keyword search", exception);
eventBus.post(new InterruptedException("Exception encountered while performing multi-case keyword search"));
eventBus.post(exception);
} finally {
progressIndicator.finish();
eventBus.post(SEARCH_COMPLETE_MESSAGE);
}
}
/**
* Gets metadata for the cases associated with one or more with the search
*
* @param caseNames The names of the cases to search.
*
* @return The metadata for the cases.
*
* @throws MultiCaseSearcherException
* @throws InterruptedException
*/
private List<MultiCaseMetadata> getMultiCaseMetadata(final Collection<CaseNodeData> caseNodes) throws MultiCaseSearcherException, InterruptedException {
final Map<Path, String> casesToCasePaths = caseNodes.stream()
.collect(Collectors.toMap(CaseNodeData::getDirectory, CaseNodeData::getName));
checkForCancellation();
final List<MultiCaseMetadata> cases = new ArrayList<>();
for (Map.Entry<Path, String> entry : casesToCasePaths.entrySet()) {
final Path caseDirectoryPath = entry.getKey();
final CaseMetadata caseMetadata = getCaseMetadata(caseDirectoryPath);
checkForCancellation();
final TextIndexMetadata textIndexMetadata = getTextIndexMetadata(caseDirectoryPath);
checkForCancellation();
cases.add(new MultiCaseMetadata(caseMetadata, textIndexMetadata));
}
return cases;
}
/**
* Gets the metadata for a case from the case metadata file in a given case
* directory.
*
* @param caseDirectoryPath A case directory path.
*
* @return The case metadata.
*
* @throws MultiCaseSearcherException
*/
@NbBundle.Messages({
"# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToFindCaseMetadata=Failed to find case metadata file in {0}",
"# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToParseCaseMetadata=Failed to parse case file metadata in {0}"
})
private static CaseMetadata getCaseMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException {
Path metadataPath = CaseMetadata.getCaseMetadataFilePath(caseDirectoryPath);
if (metadataPath != null) {
try {
return new CaseMetadata(metadataPath);
} catch (CaseMetadata.CaseMetadataException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToParseCaseMetadata(caseDirectoryPath), ex);
}
}
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToFindCaseMetadata(caseDirectoryPath));
}
/**
* Gets the text index metadata from the Solr.properties file in a given
* case directory.
*
* @param caseDirectoryPath A case directory path.
*
* @return The text index metadata.
*
* @throws MultiCaseSearcherException
*/
@NbBundle.Messages({
"# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.missingSolrPropertiesFile=Missing {0} file in {1}",
"# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.solrPropertiesFileParseError=Error parsing {0} file in {1}",})
private static TextIndexMetadata getTextIndexMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException {
final Path solrMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), SOLR_METADATA_FILE_NAME);
final File solrMetaDataFile = solrMetaDataFilePath.toFile();
if (!solrMetaDataFile.exists() || !solrMetaDataFile.canRead()) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_missingSolrPropertiesFile(SOLR_METADATA_FILE_NAME, caseDirectoryPath));
}
try {
final DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
final Document doc = docBuilder.parse(solrMetaDataFile);
final XPath xPath = XPathFactory.newInstance().newXPath();
XPathExpression xPathExpr = xPath.compile(SOLR_CORE_NAME_XPATH);
final String solrCoreName = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
xPathExpr = xPath.compile(TEXT_INDEX_NAME_XPATH);
final String relativeTextIndexPath = (String) xPathExpr.evaluate(doc, XPathConstants.STRING);
Path textIndexPath = caseDirectoryPath.resolve(relativeTextIndexPath);
textIndexPath = textIndexPath.getParent(); // Remove "index" path component
final String textIndexUNCPath = pathUtils.convertPathToUNC(textIndexPath.toString());
return new TextIndexMetadata(caseDirectoryPath, solrCoreName, textIndexUNCPath);
} catch (ParserConfigurationException | SAXException | XPathExpressionException | IOException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrPropertiesFileParseError(SOLR_METADATA_FILE_NAME, caseDirectoryPath), ex);
}
}
/**
* Converts a keyword search query into a Solr query.
*
* @param searchQuery A keyword search query.
*
* @return A Solr query.
*/
private static SolrQuery createSolrQuery(SearchQuery searchQuery) {
final SolrQuery solrQuery = new SolrQuery();
solrQuery.setQuery(searchQuery.getSearchTerm());
solrQuery.setRows(MAX_RESULTS_PER_CURSOR_MARK);
/*
* Note that setting the sort order is necessary for cursor based paging
* to work.
*/
solrQuery.setSort(SolrQuery.SortClause.asc(SOLR_DOC_ID_FIELD));
solrQuery.setFields(SOLR_DOC_ID_FIELD, SOLR_DOC_CHUNK_SIZE_FIELD, SOLR_DOC_CONTENT_STR_FIELD);
return solrQuery;
}
/**
* Connects to the Solr server and loads the Solr core for a given case.
*
* @param aCase
*
* @return A Solr server client object that can be used for executing
* queries of the specified text index.
*
* MultiCaseSearcherException
*
* @throws InterruptedException
*/
@NbBundle.Messages({
"# {0} - connection info",
"# {1} - case name",
"# {2} - case directory",
"MultiCaseSearcher.exceptionMessage.errorLoadingCore=Error connecting to Solr server and loading core (URL: {0}) for case {1} in {2}"
})
private HttpSolrServer loadSolrCoreForCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException {
TextIndexMetadata textIndexMetadata = aCase.getTextIndexMetadata();
Server.IndexingServerProperties indexServer = Server.getMultiUserServerProperties(aCase.getCaseMetadata().getCaseDirectory());
final String serverURL = String.format(SOLR_SERVER_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort());
try {
/*
* Connect to the Solr server.
*/
final HttpSolrServer solrServer = new HttpSolrServer(serverURL);
CoreAdminRequest statusRequest = new CoreAdminRequest();
statusRequest.setCoreName(null);
statusRequest.setAction(CoreAdminParams.CoreAdminAction.STATUS);
statusRequest.setIndexInfoNeeded(false);
checkForCancellation();
statusRequest.process(solrServer);
checkForCancellation();
/*
* Load the core for the text index if it is not already loaded.
*/
CoreAdminResponse response = CoreAdminRequest.getStatus(textIndexMetadata.getSolrCoreName(), solrServer);
if (null == response.getCoreStatus(textIndexMetadata.getSolrCoreName()).get(SOLR_CORE_INSTANCE_PATH_PROPERTY)) {
CoreAdminRequest.Create loadCoreRequest = new CoreAdminRequest.Create();
loadCoreRequest.setDataDir(textIndexMetadata.getTextIndexPath());
loadCoreRequest.setCoreName(textIndexMetadata.getSolrCoreName());
loadCoreRequest.setConfigSet(SOLR_CONFIG_SET_NAME);
loadCoreRequest.setIsLoadOnStartup(false);
loadCoreRequest.setIsTransient(true);
solrServer.request(loadCoreRequest);
}
/*
* Create a server client object that can be used for executing
* queries of the specified text index.
*/
final String coreURL = String.format(SOLR_CORE_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort(), textIndexMetadata.getSolrCoreName());
final HttpSolrServer coreServer = new HttpSolrServer(coreURL);
return coreServer;
} catch (SolrServerException | IOException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorLoadingCore(serverURL, aCase.getCaseMetadata().getCaseName(), textIndexMetadata.getCaseDirectoryPath()), ex);
}
}
/**
* Opens a case database.
*
* @param caseMetadata
*
* @return A case database.
*
* @throws MultiCaseSearcherException
* @throws InterruptedException
*/
@NbBundle.Messages({
"# {0} - case_name",
"MultiCaseSearcher.exceptionMessage.failedToGetCaseDatabaseConnectionInfo=Failed to get case database connection info for case {0}",
"# {0} - PostgreSQL server host",
"# {1} - PostgreSQL server port",
"# {2} - case database name",
"# {3} - case directory",
"MultiCaseSearcher.exceptionMessage.errorOpeningCaseDatabase=Error connecting to PostgreSQL server (Host/Port: [{0}:{1}] and opening case database {2} for case at {3}"
})
private SleuthkitCase openCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException {
CaseDbConnectionInfo dbConnectionInfo;
try {
dbConnectionInfo = UserPreferences.getDatabaseConnectionInfo();
} catch (UserPreferencesException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDatabaseConnectionInfo(aCase.getCaseMetadata().getCaseName()), ex);
}
checkForCancellation();
final CaseMetadata caseMetadata = aCase.getCaseMetadata();
try {
return SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory());
} catch (UserPreferencesException | TskCoreException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorOpeningCaseDatabase(dbConnectionInfo.getHost(), dbConnectionInfo.getPort(), caseMetadata.getCaseDatabaseName(), caseMetadata.getCaseDirectory()), ex);
}
}
/**
* Closes a case database.
*
* @param aCase a case database.
*/
private static void closeCase(SleuthkitCase aCase) {
aCase.close();
}
/**
* Executes a keyword search searchTerm in the text index of a case.
*
* @param solrServer The Solr server.
* @param solrQuery The Solr searchTerm.
* @param caseDatabase The case database.
* @param aCase The case metadata.
*
* @return A list of search results, possibly empty.
*
* @throws MultiCaseSearcherException
* @throws InterruptedException
*/
@NbBundle.Messages({
"# {0} - query",
"# {1} - case_name",
"MultiCaseSearcher.exceptionMessage.solrQueryError=Failed to execute query \"{0}\" on case {1}"
})
private Collection<SearchHit> executeQuery(HttpSolrServer solrServer, SolrQuery solrQuery, SleuthkitCase caseDatabase, MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException {
final List<SearchHit> hits = new ArrayList<>();
final Set<Long> uniqueObjectIds = new HashSet<>();
String cursorMark = CursorMarkParams.CURSOR_MARK_START;
boolean allResultsProcessed = false;
while (!allResultsProcessed) {
checkForCancellation();
solrQuery.set(CursorMarkParams.CURSOR_MARK_PARAM, cursorMark);
QueryResponse response;
try {
checkForCancellation();
response = solrServer.query(solrQuery, SolrRequest.METHOD.POST);
} catch (SolrServerException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrQueryError(solrQuery.getQuery(), aCase.getCaseMetadata().getCaseName()), ex);
}
SolrDocumentList resultDocuments = response.getResults();
for (SolrDocument resultDoc : resultDocuments) {
checkForCancellation();
String solrDocumentId = resultDoc.getFieldValue(SOLR_DOC_ID_FIELD).toString();
Long solrObjectId = parseSolrObjectId(solrDocumentId);
if (!uniqueObjectIds.contains(solrObjectId)) {
uniqueObjectIds.add(solrObjectId);
checkForCancellation();
hits.add(processHit(solrObjectId, caseDatabase, aCase));
}
}
checkForCancellation();
String nextCursorMark = response.getNextCursorMark();
if (cursorMark.equals(nextCursorMark)) {
allResultsProcessed = true;
}
cursorMark = nextCursorMark;
}
return hits;
}
/**
* Parses a Solr document id to get the Solr object id.
*
* @param solrDocumentId A Solr document id.
*
* @return A Solr object id.
*/
private static Long parseSolrObjectId(String solrDocumentId) {
/**
* A Solr document id is of the form [solr_object_id] for Content object
* metadata documents and
* [solr_object_id][SOLR_DOC_ID_PARTS_SEPARATOR][chunk_id] for Content
* object text chunk documents.
*/
final String[] solrDocumentIdParts = solrDocumentId.split(SOLR_DOC_ID_PARTS_SEPARATOR);
if (1 == solrDocumentIdParts.length) {
return Long.parseLong(solrDocumentId);
} else {
return Long.parseLong(solrDocumentIdParts[0]);
}
}
/**
* Creates a keyword search hit object for a Content object identified by
* its Solr object id.
*
* @param solrObjectId The Solr object id of a Content object.
* @param caseDatabase The case database of the case that has the Content.
* @param caseInfo Metadata about the case that has the content.
*
* @return
*
* @throws MultiCaseSearcherException
*/
@NbBundle.Messages({
"# {0} - Solr document id",
"# {1} - case database name",
"# {2} - case directory",
"MultiCaseSearcher.exceptionMessage.hitProcessingError=Failed to query case database for processing of Solr object id {0} of case {1} in {2}"
})
private static SearchHit processHit(Long solrObjectId, SleuthkitCase caseDatabase, MultiCaseMetadata caseInfo) throws MultiCaseSearcherException {
try {
final long objectId = getObjectIdForSolrObjectId(solrObjectId, caseDatabase);
final CaseMetadata caseMetadata = caseInfo.getCaseMetadata();
final String caseDisplayName = caseMetadata.getCaseDisplayName();
final String caseDirectoryPath = caseMetadata.getCaseDirectory();
final Content content = caseDatabase.getContentById(objectId);
final Content dataSource = content.getDataSource();
final String dataSourceName = (dataSource == null) ? "" : dataSource.getName();
SearchHit.SourceType sourceType = SearchHit.SourceType.FILE;
String sourceName = "";
String sourcePath = "";
if (content instanceof AbstractFile) {
AbstractFile sourceFile = (AbstractFile) content;
sourceName = sourceFile.getName();
sourcePath = sourceFile.getLocalAbsPath();
if (null == sourcePath) {
sourceType = SearchHit.SourceType.FILE;
sourcePath = sourceFile.getUniquePath();
} else {
sourceType = SearchHit.SourceType.LOCAL_FILE;
sourceName = sourceFile.getName();
}
} else if (content instanceof BlackboardArtifact) {
BlackboardArtifact sourceArtifact = (BlackboardArtifact) content;
sourceType = SearchHit.SourceType.ARTIFACT;
BlackboardArtifact.Type artifactType = caseDatabase.getArtifactType(sourceArtifact.getArtifactTypeName());
sourceName = artifactType.getDisplayName();
Content source = sourceArtifact.getParent();
if (source instanceof AbstractFile) {
AbstractFile sourceFile = (AbstractFile) source;
sourcePath = sourceFile.getLocalAbsPath();
if (null == sourcePath) {
sourcePath = sourceFile.getUniquePath();
}
} else {
sourcePath = source.getUniquePath();
}
} else if (content instanceof Report) {
Report report = (Report) content;
sourceType = SearchHit.SourceType.REPORT;
sourceName = report.getReportName();
sourcePath = report.getUniquePath();
}
return new SearchHit(caseDisplayName, caseDirectoryPath, dataSourceName, sourceType, sourceName, sourcePath);
} catch (SQLException | TskCoreException ex) {
throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_hitProcessingError(solrObjectId, caseInfo.getCaseMetadata().getCaseName(), caseInfo.getCaseMetadata().getCaseDirectory()), ex);
}
}
/**
* Gets the Sleuthkit object id that corresponds to the Solr object id of
* some content.
*
* @param solrObjectId A solr object id for some content.
* @param caseDatabase The case database for the case that includes the
* content.
*
* @return The Sleuthkit object id of the content.
*
* @throws MultiCaseSearcherException
* @throws TskCoreException
* @throws SQLException
*/
private static long getObjectIdForSolrObjectId(long solrObjectId, SleuthkitCase caseDatabase) throws MultiCaseSearcherException, TskCoreException, SQLException {
if (0 < solrObjectId) {
return solrObjectId;
} else {
try (SleuthkitCase.CaseDbQuery databaseQuery = caseDatabase.executeQuery("SELECT artifact_obj_id FROM blackboard_artifacts WHERE artifact_id = " + solrObjectId)) {
final ResultSet resultSet = databaseQuery.getResultSet();
if (resultSet.next()) {
return resultSet.getLong("artifact_obj_id");
} else {
throw new TskCoreException("Empty result set getting obj_id for artifact with artifact_id =" + solrObjectId);
}
}
}
}
/**
* Checks to see if the current thread has been interrupted (i.e, the search
* has been cancelled) and throws an InterruptedException if it has been.
*
* @throws InterruptedException
*/
private void checkForCancellation() throws InterruptedException {
if (Thread.currentThread().isInterrupted() || searchStopped) {
throw new InterruptedException("Search Cancelled");
}
}
/**
* A bundle of metadata for a case.
*/
private final static class MultiCaseMetadata {
private final CaseMetadata caseMetadata;
private final TextIndexMetadata textIndexMetadata;
/**
* Contructs a bundle of metadata for a case
*
* @param caseMetadata The case metadata.
* @param textIndexMetaData The text index metadata for the case.
*/
private MultiCaseMetadata(CaseMetadata caseMetadata, TextIndexMetadata textIndexMetaData) {
this.caseMetadata = caseMetadata;
this.textIndexMetadata = textIndexMetaData;
}
/**
* Gets the case metadata.
*
* @return The case metadata.
*/
private CaseMetadata getCaseMetadata() {
return this.caseMetadata;
}
/**
* Gets the text index metadata for the case.
*
* @return The text index metadata.
*/
private TextIndexMetadata getTextIndexMetadata() {
return this.textIndexMetadata;
}
}
/**
* Bundles a case directory path, a Solr core fileName, and a text index UNC
* path.
*/
private final static class TextIndexMetadata {
private final Path caseDirectoryPath;
private final String solrCoreName;
private final String textIndexUNCPath;
/**
* Constructs an object that bundles a Solr core fileName and a text
* index UNC path.
*
* @param caseDirectoryPath The case directory path.
* @param solrCoreName The core fileName.
* @param textIndexUNCPath The text index path.
*/
private TextIndexMetadata(Path caseDirectoryPath, String solrCoreName, String textIndexUNCPath) {
this.caseDirectoryPath = caseDirectoryPath;
this.solrCoreName = solrCoreName;
this.textIndexUNCPath = textIndexUNCPath;
}
/**
* Gets the case directory path.
*
* @return The path.
*/
private Path getCaseDirectoryPath() {
return this.caseDirectoryPath;
}
/**
* Gets the Solr core fileName.
*
* @return The Solr core fileName.
*/
private String getSolrCoreName() {
return this.solrCoreName;
}
/**
*
* Gets the UNC path of the text index.
*
* @return The path.
*/
private String getTextIndexPath() {
return this.textIndexUNCPath;
}
}
/**
* Exception thrown if there is an error executing a search.
*/
static final class MultiCaseSearcherException extends Exception {
private static final long serialVersionUID = 1L;
/**
* Constructs an instance of the exception thrown if there is an error
* executing a search.
*
* @param message The exception message.
*/
private MultiCaseSearcherException(String message) {
super(message);
}
/**
* Constructs an instance of the exception thrown if there is an error
* executing a search.
*
* @param message The exception message.
* @param cause The Throwable that caused the error.
*/
private MultiCaseSearcherException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Tell the MultiCaseSearcher that it's current search can be stopped the
* next time it checks for cancellation.
*/
void stopMultiCaseSearch() {
//This is necessary because if the interrupt occurs during CoreAdminRequest.process,
//CoreAdminRequest.getStatus, or HttpSolrServer.query the interrupt gets ignored
searchStopped = true;
}
/**
* Register an object with the MultiCaseSearcher eventBus so that it's
* subscribe methods can receive results.
*
* @param object the object to register with the eventBus
*/
void registerWithEventBus(Object object) {
eventBus.register(object);
}
/**
* Unregister an object with the MultiCaseSearcher eventBus so that it's
* subscribe methods no longer receive results.
*
* @param object the object to unregister with the eventBus
*/
void unregisterWithEventBus(Object object) {
eventBus.unregister(object);
}
}

View File

@ -1,5 +1,5 @@
OpenIDE-Module-Display-Category=Ingest Module
OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web acitivity (sites visited, stored cookies, bookmarked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\n\The module currently supports Windows only disk images.\n\The plugin is also fully functional when deployed on Windows version of Autopsy.
OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy.
OpenIDE-Module-Name=RecentActivity
OpenIDE-Module-Short-Description=Recent Activity finder ingest module
Chrome.moduleName=Chrome
@ -93,7 +93,7 @@ RecentDocumentsByLnk.parentModuleName=Recent Activity
SearchEngineURLQueryAnalyzer.moduleName.text=Search Engine
SearchEngineURLQueryAnalyzer.engineName.none=NONE
SearchEngineURLQueryAnalyzer.domainSubStr.none=NONE
SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\n\count: {2}\nSplit Tokens: \n{3}
SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\nCount: {2}\nSplit Tokens: \n{3}
SearchEngineURLQueryAnalyzer.parentModuleName.noSpace=RecentActivity
SearchEngineURLQueryAnalyzer.parentModuleName=Recent Activity
UsbDeviceIdMapper.parseAndLookup.text=Product: {0}