Interim check in of adminstrative case deletion

This commit is contained in:
Richard Cordovano 2019-03-11 11:52:00 -04:00
parent ad227a3db5
commit 486fbfe152
17 changed files with 808 additions and 646 deletions

View File

@ -11,7 +11,7 @@ Case.deleteCaseFailureMessageBox.message=Error deleting case: {0}
Case.deleteCaseFailureMessageBox.title=Failed to Delete Case Case.deleteCaseFailureMessageBox.title=Failed to Delete Case
Case.exceptionMessage.cancelledByUser=Cancelled by user. Case.exceptionMessage.cancelledByUser=Cancelled by user.
Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first. 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. 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.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. Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case.
# {0} - exception message # {0} - exception message
@ -34,7 +34,7 @@ Case.exceptionMessage.couldNotSaveDbNameToMetadataFile=Failed to save case datab
Case.exceptionMessage.couldNotUpdateCaseNodeData=Failed to update coordination service node data:\n{0}. Case.exceptionMessage.couldNotUpdateCaseNodeData=Failed to update coordination service node data:\n{0}.
Case.exceptionMessage.emptyCaseDir=Must specify a case directory path. Case.exceptionMessage.emptyCaseDir=Must specify a case directory path.
Case.exceptionMessage.emptyCaseName=Must specify a case name. 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 log for details.
# {0} - exception message # {0} - exception message
Case.exceptionMessage.execExceptionWrapperMessage={0} Case.exceptionMessage.execExceptionWrapperMessage={0}
# {0} - exception message # {0} - exception message
@ -58,36 +58,12 @@ Case.progressMessage.connectingToCoordSvc=Connecting to coordination service...
Case.progressMessage.creatingCaseDatabase=Creating case database... Case.progressMessage.creatingCaseDatabase=Creating case database...
Case.progressMessage.creatingCaseDirectory=Creating case directory... Case.progressMessage.creatingCaseDirectory=Creating case directory...
Case.progressMessage.creatingCaseNodeData=Creating coordination service node data... Case.progressMessage.creatingCaseNodeData=Creating coordination service node data...
Case.progressMessage.deletingAutoIngestLogCoordSvcNode=Deleting case auto ingest log lock coordination service node...
Case.progressMessage.deletingCaseDatabase=Deleting case database... Case.progressMessage.deletingCaseDatabase=Deleting case database...
Case.progressMessage.deletingCaseDirectory=Deleting case directory... Case.progressMessage.deletingCaseDirectory=Deleting case directory...
Case.progressMessage.deletingDirectoryCoordinationServiceNode=Deleting case directory lock coordination service node... Case.progressMessage.deletingCaseDirLockNode=Deleting case directory lock coordination service node...
Case.progressMessage.deletingResourcesCoordSvcNode=Deleting case resourceslock coordination service node... Case.progressMessage.deletingResourcesLockNode=Deleting case resources lock node...
Case.progressMessage.deletingTextIndex=Deleting text index... Case.progressMessage.deletingTextIndex=Deleting text index...
# {0} - exception message
Case.progressMessage.errorConnectingToCoordSvc=An error occurred connecting to the coordination service (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorDeletingCaseDb=An error occurred deleting the case database (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorDeletingCaseDir=An error occurred deleting the case directory (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorDeletingCaseDirLockNode=An error occurred deleting the case directory lock node (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorDeletingJobLogLockNode=An error occurred deleting the case auto ingest log lock node (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorDeletingResourcesLockNode=An error occurred deleting the case resources lock node (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorDeletingTextIndex=An error occurred deleting the text index for the case (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorGettingCoordSvcNodeData=Failed to get the coordination service case node data (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorLockingCase=An error occurred exclusively locking the case for deletion (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorLockingCaseName=An error occurred exclusively locking the case name for deletion (see log for details): {0}.
# {0} - exception message
Case.progressMessage.errorSavingDeletedItemsFlags=An error occurred saving the deleted items flags in the coordination service database (see log for details): {0}.
Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case... Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case...
Case.progressMessage.missingCoordSvcNodeData=Missing coordination service node data.
Case.progressMessage.openingApplicationServiceResources=Opening application service case resources... Case.progressMessage.openingApplicationServiceResources=Opening application service case resources...
Case.progressMessage.openingCaseDatabase=Opening case database... Case.progressMessage.openingCaseDatabase=Opening case database...
Case.progressMessage.openingCaseLevelServices=Opening case-level services... Case.progressMessage.openingCaseLevelServices=Opening case-level services...

View File

@ -702,8 +702,45 @@ public class Case {
} }
CaseMetadata metadata = currentCase.getMetadata(); CaseMetadata metadata = currentCase.getMetadata();
closeCurrentCase(); closeCurrentCase();
ProgressIndicator progressIndicator = new ModalDialogProgressIndicator(mainFrame, Bundle.Case_progressIndicatorTitle_deletingCase()); deleteCase(metadata);
deleteCase(metadata, true, progressIndicator); }
}
/**
* Deletes a case. The case to be deleted must not be the "current case."
*
* @param metadata The case metadata.
*
* @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.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first."
})
public static void deleteCase(CaseMetadata metadata) throws CaseActionException {
synchronized (caseActionSerializationLock) {
if (null != currentCase) {
throw new CaseActionException(Bundle.Case_exceptionMessage_cannotDeleteCurrentCase());
}
}
ProgressIndicator progressIndicator;
if (RuntimeProperties.runningWithGUI()) {
progressIndicator = new ModalDialogProgressIndicator(mainFrame, Bundle.Case_progressIndicatorTitle_deletingCase());
} else {
progressIndicator = new LoggingProgressIndicator();
}
progressIndicator.start(Bundle.Case_progressMessage_preparing());
try {
if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
deleteSingleUserCase(metadata, progressIndicator);
} else {
deleteMultiUserCase(metadata, progressIndicator);
}
} finally {
progressIndicator.finish();
} }
} }
@ -884,40 +921,6 @@ public class Case {
return imgPaths; return imgPaths;
} }
/**
* Deletes a case. The case to be deletd must not be the "current case."
*
* @param metadata The case metadata.
* @param deleteCaseLockNodes Whether or not to delete the coordination
* service lock nodes for the case.
* @param progressIndicator A progress indicator.
*
* @throws CaseActionException If there were one or more errors deleting the
* case.
*/
@Beta
@Messages({
"Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first."
})
public static void deleteCase(CaseMetadata metadata, boolean deleteCaseLockNodes, ProgressIndicator progressIndicator) throws CaseActionException {
synchronized (caseActionSerializationLock) {
if (null != currentCase) {
throw new CaseActionException(Bundle.Case_exceptionMessage_cannotDeleteCurrentCase());
}
}
progressIndicator.start(Bundle.Case_progressMessage_preparing());
try {
if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
deleteSingleUserCase(metadata, progressIndicator);
} else {
deleteMultiUserCase(metadata, progressIndicator);
}
} finally {
progressIndicator.finish();
}
}
/** /**
* Deletes a single-user case. * Deletes a single-user case.
* *
@ -925,12 +928,12 @@ public class Case {
* @param progressIndicator A progress indicator. * @param progressIndicator A progress indicator.
* *
* @throws CaseActionException If there were one or more errors deleting the * @throws CaseActionException If there were one or more errors deleting the
* case. * case. The exception will have a user-friendly
* message and may be a wrapper for a
* lower-level exception.
*/ */
@Messages({ @Messages({
"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 log for details."
"# {0} - exception message", "Case.progressMessage.errorDeletingTextIndex=An error occurred deleting the text index for the case (see log for details): {0}.",
"# {0} - exception message", "Case.progressMessage.errorDeletingCaseDir=An error occurred deleting the case directory (see log for details): {0}."
}) })
private static void deleteSingleUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { private static void deleteSingleUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException {
boolean errorsOccurred = false; boolean errorsOccurred = false;
@ -938,16 +941,14 @@ public class Case {
deleteTextIndex(metadata, progressIndicator); deleteTextIndex(metadata, progressIndicator);
} catch (KeywordSearchServiceException ex) { } catch (KeywordSearchServiceException ex) {
errorsOccurred = true; errorsOccurred = true;
logger.log(Level.SEVERE, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); logger.log(Level.WARNING, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingTextIndex(ex.getMessage()));
} }
try { try {
deleteCaseDirectory(metadata, progressIndicator); deleteCaseDirectory(metadata, progressIndicator);
} catch (CaseActionException ex) { } catch (CaseActionException ex) {
errorsOccurred = true; errorsOccurred = true;
logger.log(Level.SEVERE, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); logger.log(Level.WARNING, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingCaseDir(ex.getMessage()));
} }
deleteFromRecentCases(metadata, progressIndicator); deleteFromRecentCases(metadata, progressIndicator);
@ -961,61 +962,37 @@ public class Case {
* Deletes a multi-user case. * Deletes a multi-user case.
* *
* @param metadata The case metadata. * @param metadata The case metadata.
* @param deleteCaseLockNodes Whether or not to delete the coordination
* service case directory and case name lock
* nodes for the case.
* @param progressIndicator A progress indicator. * @param progressIndicator A progress indicator.
* *
* @throws CaseActionException If there were one or more errors deleting the * @throws CaseActionException If there were one or more errors deleting the
* case. * case. The exception will have a user-friendly
* message and may be a wrapper for a
* lower-level exception.
*/ */
@Messages({ @Messages({
"Case.progressMessage.connectingToCoordSvc=Connecting to coordination service...", "Case.progressMessage.connectingToCoordSvc=Connecting to coordination service...",
"# {0} - exception message", "Case.progressMessage.errorConnectingToCoordSvc=An error occurred connecting to the coordination service (see log for details): {0}.",
"Case.progressMessage.checkingForOtherUser=Checking to see if another user has the case open...", "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.", "Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host.",
"# {0} - exception message", "Case.progressMessage.errorLockingCaseName=An error occurred exclusively locking the case name for deletion (see log for details): {0}.", "Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case..."
"# {0} - exception message", "Case.progressMessage.errorLockingCase=An error occurred exclusively locking the case for deletion (see log for details): {0}.",
"Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case...",
"# {0} - exception message", "Case.progressMessage.errorGettingCoordSvcNodeData=Failed to get the coordination service case node data (see log for details): {0}.",
"Case.progressMessage.missingCoordSvcNodeData=Missing coordination service node data.",
"# {0} - exception message", "Case.progressMessage.errorDeletingCaseDb=An error occurred deleting the case database (see log for details): {0}.",
"# {0} - exception message", "Case.progressMessage.errorSavingDeletedItemsFlags=An error occurred saving the deleted items flags in the coordination service database (see log for details): {0}."
}) })
private static void deleteMultiUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { private static void deleteMultiUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException {
CaseNodeData caseNodeData;
boolean errorsOccurred = false;
progressIndicator.progress(Bundle.Case_progressMessage_connectingToCoordSvc()); progressIndicator.progress(Bundle.Case_progressMessage_connectingToCoordSvc());
CoordinationService coordinationService; CoordinationService coordinationService;
try { try {
coordinationService = CoordinationService.getInstance(); coordinationService = CoordinationService.getInstance();
} catch (CoordinationServiceException ex) { } 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); 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);
progressIndicator.progress(Bundle.Case_progressMessage_errorConnectingToCoordSvc(ex.getMessage()));
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
} }
/* CaseNodeData caseNodeData;
* Acquire an exclusive case name lock. This will prevent auto ingest boolean errorsOccurred = false;
* nodes from attempting to search for the case directory before it is
* deleted.
*/
progressIndicator.progress(Bundle.Case_progressMessage_checkingForOtherUser());
final String caseNameLockName = TimeStampUtils.removeTimeStamp(metadata.getCaseName());
try (CoordinationService.Lock nameLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, caseNameLockName)) {
if (nameLock == null) {
logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because the case name was in use by another host", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
progressIndicator.progress(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase());
throw new CaseActionException(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase());
}
/*
* Acquire an exclusive case directory lock. This ensures that no
* other node (host) currently has the case open and prevents
* another node (host) from trying to open the case as it is being
* deleted.
*/
try (CoordinationService.Lock dirLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) { try (CoordinationService.Lock dirLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) {
if (dirLock == null) { if (dirLock == null) {
logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because the case was in use by another host", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); 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()));
progressIndicator.progress(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase());
throw new CaseActionException(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase()); throw new CaseActionException(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase());
} }
@ -1025,108 +1002,143 @@ public class Case {
if (nodeBytes != null && nodeBytes.length > 0) { if (nodeBytes != null && nodeBytes.length > 0) {
caseNodeData = new CaseNodeData(nodeBytes); caseNodeData = new CaseNodeData(nodeBytes);
} else { } else {
logger.log(Level.SEVERE, String.format("Missing coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); logger.log(Level.WARNING, String.format("Missing coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()));
progressIndicator.progress(Bundle.Case_progressMessage_missingCoordSvcNodeData());
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
} }
} catch (CoordinationServiceException | InterruptedException | IOException ex) { } 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); logger.log(Level.SEVERE, String.format("Failed to get coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorGettingCoordSvcNodeData(ex.getMessage()));
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
} }
try { errorsOccurred = deleteMultiUserCase(caseNodeData, metadata, progressIndicator);
deleteCaseDatabase(metadata, progressIndicator, caseNodeData);
} catch (UserPreferencesException | ClassNotFoundException | SQLException ex) {
errorsOccurred = true;
logger.log(Level.SEVERE, String.format("Failed to delete the case database for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingCaseDb(ex.getMessage()));
}
try {
deleteTextIndex(metadata, progressIndicator, caseNodeData);
} catch (KeywordSearchServiceException ex) {
errorsOccurred = true;
logger.log(Level.SEVERE, String.format("Failed to delete the text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingTextIndex(ex.getMessage()));
}
try {
deleteCaseDirectory(metadata, progressIndicator, caseNodeData);
} catch (CaseActionException ex) {
errorsOccurred = true;
logger.log(Level.SEVERE, String.format("Failed to delete the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingCaseDir(ex.getMessage()));
}
deleteFromRecentCases(metadata, progressIndicator);
} catch (CoordinationServiceException ex) { } catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Failed to exclusively lock the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), 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);
progressIndicator.progress(Bundle.Case_progressMessage_errorLockingCase(ex.getMessage()));
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
} }
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Failed to exclusively lock the case name for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorLockingCaseName(ex.getMessage()));
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
}
deleteCaseResourcesCoordinationServiceNode(metadata, progressIndicator, coordinationService);
deleteCaseAutoIngestLogCoordinationServiceNode(metadata, progressIndicator, coordinationService);
// RJCTODO: Delete case name lock node
if (!errorsOccurred) { if (!errorsOccurred) {
deleteCaseDirectoryCoordinationServiceNode(metadata, progressIndicator, coordinationService);
} else {
/*
* Save the deleted item flags for the partially deleted case so
* that system adminstrators can try to fix whatever wnet wrong and
* then retry deleting the case.
*/
try { try {
coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), caseNodeData.toArray()); deleteCaseDirectoryLockNode(caseNodeData, progressIndicator);
} catch (IOException | CoordinationServiceException | InterruptedException ex) { } catch (CoordinationServiceException | InterruptedException ex) {
logger.log(Level.SEVERE, String.format("Failed to write the node dat with the deleted items flags for the partially deleted case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), 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);
progressIndicator.progress(Bundle.Case_progressMessage_errorSavingDeletedItemsFlags(ex.getMessage())); errorsOccurred = true;
} }
}
if (errorsOccurred) {
throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase());
} }
} }
/** /**
* Attempts to delete the case database for a multi-user case. * IMPORTANT: This is a "beta" method and is subject to change or removal
* without notice!
* *
* Attempts to delete the case database, the text index, the case directory,
* and the case resources coordination service lock code for a case and
* removes the case from the recent cases menu of the mian application
* window.
*
* @param caseNodeData The coordination service node data for the case.
* @param metadata The case metadata. * @param metadata The case metadata.
* @param progressIndicator A progress indicator. * @param progressIndicator A progress indicator.
* @param caseNodeData The coordination service node data for the case.
* *
* @throws UserPreferencesException If there is an error getting the * @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 opersation is detected
* during a wait.
*/
@Beta
public static boolean deleteMultiUserCase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator) throws InterruptedException {
boolean errorsOccurred = false;
try {
deleteCaseDatabase(caseNodeData, 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);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return errorsOccurred;
}
try {
deleteTextIndex(caseNodeData, metadata, progressIndicator);
} 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);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return errorsOccurred;
}
try {
deleteCaseDirectory(caseNodeData, metadata, progressIndicator);
} 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);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return errorsOccurred;
}
deleteFromRecentCases(metadata, progressIndicator);
if (Thread.currentThread().isInterrupted()) {
Thread.currentThread().interrupt();
return errorsOccurred;
}
try {
deleteCaseResourcesLockNode(caseNodeData, progressIndicator);
} catch (CoordinationServiceException ex) {
errorsOccurred = true;
logger.log(Level.WARNING, String.format("Error deleting the case resources lock coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return errorsOccurred;
}
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.
*
* @throws UserPreferencesException if there is an error getting the
* database server connection info. * database server connection info.
* @throws ClassNotFoundException If there is an error gettting the * @throws ClassNotFoundException if there is an error gettting the
* required JDBC driver. * required JDBC driver.
* @throws SQLException If there is an error executing the SQL * @throws SQLException if there is an error executing the SQL
* to drop the database from the database * to drop the database from the database
* server. * server.
* @throws InterruptedException If interrupted while blocked waiting for
* coordination service data to be written
* to the coordination service node
* database.
*/ */
@Messages({ @Messages({
"Case.progressMessage.deletingCaseDatabase=Deleting case database...",}) "Case.progressMessage.deletingCaseDatabase=Deleting case database..."
private static void deleteCaseDatabase(CaseMetadata metadata, ProgressIndicator progressIndicator, })
CaseNodeData caseNodeData) throws UserPreferencesException, ClassNotFoundException, SQLException { private static void deleteCaseDatabase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator) throws UserPreferencesException, ClassNotFoundException, SQLException, InterruptedException {
if (caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB)) { if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB)) {
return;
}
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase()); progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase());
CaseDbConnectionInfo db; CaseDbConnectionInfo info = UserPreferences.getDatabaseConnectionInfo();
db = UserPreferences.getDatabaseConnectionInfo(); String url = "jdbc:postgresql://" + info.getHost() + ":" + info.getPort() + "/postgres"; //NON-NLS
Class.forName("org.postgresql.Driver"); //NON-NLS 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 try (Connection connection = DriverManager.getConnection(url, info.getUserName(), info.getPassword()); Statement statement = connection.createStatement()) {
Statement statement = connection.createStatement();) {
String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS
statement.execute(deleteCommand); statement.execute(deleteCommand);
} }
caseNodeData.setDeletedFlag(CaseNodeData.DeletedFlags.CASE_DB); setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.CASE_DB);
}
} }
/** /**
@ -1151,19 +1163,21 @@ public class Case {
/** /**
* Attempts to delete the text index for a multi-user case. * 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 mnetadata. * @param metadata The case mnetadata.
* @param progressIndicator A progress indicator. * @param progressIndicator A progress indicator.
* @param caseNodeData The coordination service node data for the case
* if it is a multi-user case; for a single-user
* case pass null.
* *
* @throws KeywordSearchServiceException If there is an error deleting the * @throws KeywordSearchServiceException If there is an error deleting the
* text index. * 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 deleteTextIndex(CaseMetadata metadata, ProgressIndicator progressIndicator, CaseNodeData caseNodeData) throws KeywordSearchServiceException { private static void deleteTextIndex(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator) throws KeywordSearchServiceException, InterruptedException {
if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.TEXT_INDEX)) { if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.TEXT_INDEX)) {
deleteTextIndex(metadata, progressIndicator); deleteTextIndex(metadata, progressIndicator);
caseNodeData.setDeletedFlag(CaseNodeData.DeletedFlags.TEXT_INDEX); setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.TEXT_INDEX);
} }
} }
@ -1180,27 +1194,32 @@ public class Case {
"Case.progressMessage.deletingCaseDirectory=Deleting case directory..." "Case.progressMessage.deletingCaseDirectory=Deleting case directory..."
}) })
private static void deleteCaseDirectory(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { private static void deleteCaseDirectory(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException {
// RJCTODO: Update FileUtil.deleteDir to use robocopy on Windows
// when the path is >= 255 chars. Actually, deprecate this method and
// replace it with one that throws instead of returning a boolean value.
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirectory()); progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirectory());
// RJCTODO: Use robocopy on Windows, possibly just for longer paths
if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) { if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) {
throw new CaseActionException(String.format("Failed to delete %s", metadata.getCaseDirectory())); throw new CaseActionException(String.format("Failed to delete %s", metadata.getCaseDirectory()));
} }
} }
/** /**
* Attempts to delete the case directory for a case. * 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 mnetadata. * @param metadata The case mnetadata.
* @param progressIndicator A progress indicator. * @param progressIndicator A progress indicator.
* @param caseNodeData The coordination service node data for the case.
* *
* @throws CaseActionException If there is an error deleting the case * @throws CaseActionException if there is an error deleting the case
* directory. * directory.
* @throws InterruptedException If interrupted while blocked waiting for
* coordination service data to be written to
* the coordination service node database.
*/ */
private static void deleteCaseDirectory(CaseMetadata metadata, ProgressIndicator progressIndicator, CaseNodeData caseNodeData) throws CaseActionException { private static void deleteCaseDirectory(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException, InterruptedException {
if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR)) { if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR)) {
deleteCaseDirectory(metadata, progressIndicator); deleteCaseDirectory(metadata, progressIndicator);
caseNodeData.setDeletedFlag(CaseNodeData.DeletedFlags.CASE_DIR); setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.CASE_DIR);
} }
} }
@ -1223,71 +1242,82 @@ public class Case {
} }
} }
// RJCTODO: Copy-paste instead
/** /**
* Deletes the case resources coordination nodes for a multi-user case. * IMPORTANT: This is a "beta" method and is subject to change or removal
* without notice!
* *
* @param metadata The case metadata. * Deletes the case resources lock coordination service node for a
* @param progressIndicator A progress indicator.
*/
@Messages({
"Case.progressMessage.deletingResourcesCoordSvcNode=Deleting case resourceslock coordination service node...",
"# {0} - exception message", "Case.progressMessage.errorDeletingResourcesLockNode=An error occurred deleting the case resources lock node (see log for details): {0}."
})
static void deleteCaseResourcesCoordinationServiceNode(CaseMetadata metadata, ProgressIndicator progressIndicator, CoordinationService coordinationService) {
progressIndicator.progress(Bundle.Case_progressMessage_deletingResourcesCoordSvcNode());
String resourcesLockNodePath = metadata.getCaseDirectory() + RESOURCES_LOCK_SUFFIX;
try {
coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath);
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error deleting the case resources lock coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingResourcesLockNode(ex.getMessage()));
}
}
/**
* Deletes the case auto ingest log lock coordination service node for a
* multi-user case, if it exists.
*
* @param metadata The case metadata.
* @param progressIndicator A progress indicator.
*/
@Messages({
"Case.progressMessage.deletingAutoIngestLogCoordSvcNode=Deleting case auto ingest log lock coordination service node...",
"# {0} - exception message", "Case.progressMessage.errorDeletingJobLogLockNode=An error occurred deleting the case auto ingest log lock node (see log for details): {0}."
})
static void deleteCaseAutoIngestLogCoordinationServiceNode(CaseMetadata metadata, ProgressIndicator progressIndicator, CoordinationService coordinationService) {
progressIndicator.progress(Bundle.Case_progressMessage_deletingAutoIngestLogCoordSvcNode());
Path logFilePath = Paths.get(metadata.getCaseDirectory(), AUTO_INGEST_LOG_FILE_NAME);
try {
/*
* Does nothing if the node does not exist.
*/
coordinationService.deleteNode(CategoryNode.CASES, logFilePath.toString());
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error deleting the case auto ingest log lock coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingJobLogLockNode(ex.getMessage()));
}
}
/**
* Deletes the case (directory lock) coordination service node for a
* multi-user case. * multi-user case.
* *
* @param metadata The metadata for the case to delete. * @param caseNodeData The coordination service node data for the case.
* @param progressIndicator The progress indicator for the deletion * @param progressIndicator The progress indicator for the deletion
* operation. * operation.
*
* @throws CoordinationServiceException If there is an error deleting the
* coordination service node.
* @throws InterruptedException If a thread interrupt occurs while
* blocked waiting for the operation to
* complete.
*/ */
@Messages({ @Messages({
"Case.progressMessage.deletingDirectoryCoordinationServiceNode=Deleting case directory lock coordination service node...", "Case.progressMessage.deletingResourcesLockNode=Deleting case resources lock node..."
"# {0} - exception message", "Case.progressMessage.errorDeletingCaseDirLockNode=An error occurred deleting the case directory lock node (see log for details): {0}."
}) })
static void deleteCaseDirectoryCoordinationServiceNode(CaseMetadata metadata, ProgressIndicator progressIndicator, CoordinationService coordinationService) { @Beta
String caseDirectoryLockNodePath = metadata.getCaseDirectory(); private static void deleteCaseResourcesLockNode(CaseNodeData caseNodeData, ProgressIndicator progressIndicator) throws CoordinationServiceException, InterruptedException {
try { progressIndicator.progress(Bundle.Case_progressMessage_deletingResourcesLockNode());
String resourcesLockNodePath = caseNodeData.getDirectory().toString() + RESOURCES_LOCK_SUFFIX;//RJCTODO: Use utility
CoordinationService coordinationService = CoordinationService.getInstance();
coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath);
}
// RJCTODO: Copy-paste instead
/**
* IMPORTANT: This is a "beta" method and is subject to change or removal
* without notice!
*
* Deletes the case directory lock coordination service node for a
* multi-user case.
*
* @param caseNodeData The coordination service node data for the case.
* @param progressIndicator The progress indicator for the deletion
* operation.
*
* @throws CoordinationServiceException If there is an error deleting the
* coordination service node.
* @throws InterruptedException If a thread interrupt occurs while
* blocked waiting for the operation to
* complete.
*/
@Beta
@Messages({
"Case.progressMessage.deletingCaseDirLockNode=Deleting case directory lock coordination service node..."
})
public static void deleteCaseDirectoryLockNode(CaseNodeData caseNodeData, ProgressIndicator progressIndicator) throws CoordinationServiceException, InterruptedException {
progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirLockNode());
String caseDirectoryLockNodePath = caseNodeData.getDirectory().toString();
CoordinationService coordinationService = CoordinationService.getInstance();
coordinationService.deleteNode(CategoryNode.CASES, caseDirectoryLockNodePath); coordinationService.deleteNode(CategoryNode.CASES, caseDirectoryLockNodePath);
} catch (CoordinationServiceException ex) { }
logger.log(Level.SEVERE, String.format("Error deleting the case directory lock coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex);
progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingCaseDirLockNode(ex.getMessage())); /**
* 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);
} }
} }
@ -3310,31 +3340,4 @@ public class Case {
deleteReports(reports); deleteReports(reports);
} }
/**
* Deletes a case.
*
* @param metadata The metadata for the case to delete.
*
* @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.
* @deprecated Use deleteCase(CaseMetadata, ProgressIndicator) instead.
* instead.
*/
@Deprecated
public static void deleteCase(CaseMetadata metadata) throws CaseActionException {
/*
* 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());
} else {
progressIndicator = new LoggingProgressIndicator();
}
deleteCase(metadata, true, progressIndicator);
}
} }

View File

@ -289,7 +289,7 @@ public final class CaseNodeData {
CASE_DB(2), CASE_DB(2),
CASE_DIR(4), CASE_DIR(4),
DATA_SOURCES(8), DATA_SOURCES(8),
JOB_MANIFEST_NODES(16); MANIFEST_FILE_LOCK_NODES(16);
private final short value; private final short value;

View File

@ -29,76 +29,60 @@ import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
/** /**
* Queries the coordination service to collect the multi-user case node data * Queries the coordination service to collect the multi-user case node data
* stored in the case directory lock ZooKeeper nodes. * stored in the case directory lock ZooKeeper nodes.
*/ */
final public class MultiUserCaseNodeDataCollector { final public class MultiUserCaseNodeDataCollector { // RJCTODO: Shorten name after multi-case keyword search code is in.
private static final Logger logger = Logger.getLogger(MultiUserCaseNodeDataCollector.class.getName()); 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 * Queries the coordination service to collect the multi-user case node data
* stored in the case directory lock ZooKeeper nodes. * stored in the case directory lock ZooKeeper nodes.
* *
* @return A list of CaseNodedata objects that convert data for a case * @return The node data for the multi-user cases known to the coordination
* directory lock coordination service node to and from byte arrays. * service.
* *
* @throws CoordinationServiceException If there is an error * @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 CoordinationService.CoordinationServiceException { public static List<CaseNodeData> getNodeData() throws CoordinationServiceException, InterruptedException {
final List<CaseNodeData> cases = new ArrayList<>(); final List<CaseNodeData> cases = new ArrayList<>();
final CoordinationService coordinationService = CoordinationService.getInstance(); final CoordinationService coordinationService = CoordinationService.getInstance();
final List<String> nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); final List<String> nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES);
for (String nodeName : nodeList) { for (String nodeName : nodeList) {
/* if (CaseCoordinationServiceUtils.isCaseLockName(nodeName)
* Ignore auto ingest case name lock nodes. || CaseCoordinationServiceUtils.isCaseResourcesLockName(nodeName)
*/ || CaseCoordinationServiceUtils.isCaseAutoIngestLogLockName(nodeName)) {
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; continue;
} }
/* /*
* Get the data from the case directory lock node. This data may not * Get the data from the case directory lock node. This data may not
* exist for "legacy" nodes. If it is missing, create it. * exist or may exist only in an older version. If it is missing or
* incomplete, create or update it.
*/ */
try { try {
CaseNodeData nodeData; CaseNodeData nodeData;
byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName); final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName);
if (nodeBytes != null && nodeBytes.length > 0) { if (nodeBytes != null && nodeBytes.length > 0) {
nodeData = new CaseNodeData(nodeBytes); nodeData = new CaseNodeData(nodeBytes);
if (nodeData.getVersion() == 0) { if (nodeData.getVersion() < CaseNodeData.getCurrentVersion()) {
/* nodeData = updateNodeData(nodeName, nodeData);
* 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 { } else {
nodeData = createNodeDataFromCaseMetadata(nodeName, false); nodeData = updateNodeData(nodeName, null);
} }
if (nodeData != null) {
cases.add(nodeData); cases.add(nodeData);
}
} catch (CoordinationService.CoordinationServiceException | InterruptedException | IOException | ParseException | CaseMetadata.CaseMetadataException ex) { } 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); logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex);
@ -109,15 +93,15 @@ final public class MultiUserCaseNodeDataCollector {
} }
/** /**
* Creates and saves case directory lock coordination service node data from * Updates the case directory lock coordination service node data for a
* the metadata file for the case associated with the node. * case.
* *
* @param nodeName The coordination service node name, i.e., the case * @param nodeName The coordination service node name, i.e., the case
* directory path. * directory path.
* @param errorsOccurred Whether or not errors occurred during an auto * @param oldNodeData .
* ingest job for the case.
* *
* @return A CaseNodedata object. * @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 * @throws IOException If there is an error writing the
* node data to a byte array. * node data to a byte array.
@ -130,28 +114,67 @@ final public class MultiUserCaseNodeDataCollector {
* @throws InterruptedException If a coordination service operation * @throws InterruptedException If a coordination service operation
* is interrupted. * is interrupted.
*/ */
private static CaseNodeData createNodeDataFromCaseMetadata(String nodeName, boolean errorsOccurred) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException { private static CaseNodeData updateNodeData(String nodeName, CaseNodeData oldNodeData) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException {
CaseNodeData nodeData = null;
Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS); Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS);
File caseDirectory = caseDirectoryPath.toFile(); File caseDirectory = caseDirectoryPath.toFile();
if (caseDirectory.exists()) { 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(); File[] files = caseDirectory.listFiles();
for (File file : files) { for (File file : files) {
String name = file.getName().toLowerCase(); String name = file.getName().toLowerCase();
if (name.endsWith(CaseMetadata.getFileExtension())) { if (name.endsWith(CaseMetadata.getFileExtension())) {
CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath())); CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath()));
nodeData = new CaseNodeData(metadata); nodeData = new CaseNodeData(metadata);
nodeData.setErrorsOccurred(errorsOccurred); if (oldNodeData != null) {
/*
* Version 0 case node data was only written if errors
* occurred during an auto ingest job.
*/
nodeData.setErrorsOccurred(true);
}
break; break;
} }
} }
} }
if (nodeData != null) { if (nodeData != null) {
CoordinationService coordinationService = CoordinationService.getInstance(); CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray());
coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray()); }
return nodeData; return nodeData;
} else { }
throw new IOException(String.format("Could not find case metadata file for %s", nodeName));
/**
* 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, CaseCoordinationServiceUtils.getCaseResourcesLockName(caseDirectoryPath));
deleteCoordinationServiceNode(coordinationService, CaseCoordinationServiceUtils.getCaseAutoIngestLogLockName(caseDirectoryPath));
deleteCoordinationServiceNode(coordinationService, CaseCoordinationServiceUtils.getCaseDirectoryLockName(caseDirectoryPath));
deleteCoordinationServiceNode(coordinationService, CaseCoordinationServiceUtils.getCaseLockName(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);
} }
} }

View File

@ -1,5 +1,10 @@
MultiUserCaseBrowserCustomizer.column.caseDbDeleteStatus=Case Database Deleted
MultiUserCaseBrowserCustomizer.column.caseDirDeleteStatus=Case Directory Deleted
MultiUserCaseBrowserCustomizer.column.createTime=Create Time MultiUserCaseBrowserCustomizer.column.createTime=Create Time
MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Deleted
MultiUserCaseBrowserCustomizer.column.directory=Directory MultiUserCaseBrowserCustomizer.column.directory=Directory
MultiUserCaseBrowserCustomizer.column.displayName=Name MultiUserCaseBrowserCustomizer.column.displayName=Name
MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time
MultiUserCaseBrowserCustomizer.column.manifestCoordSvcNodesDeleteStatus=Manifest ZooKeeper Node Deleted
MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted
MultiUserCasesBrowserPanel.waitNode.message=Please Wait... MultiUserCasesBrowserPanel.waitNode.message=Please Wait...

View File

@ -141,14 +141,23 @@ public interface MultiUserCaseBrowserCustomizer {
"MultiUserCaseBrowserCustomizer.column.displayName=Name", "MultiUserCaseBrowserCustomizer.column.displayName=Name",
"MultiUserCaseBrowserCustomizer.column.createTime=Create Time", "MultiUserCaseBrowserCustomizer.column.createTime=Create Time",
"MultiUserCaseBrowserCustomizer.column.directory=Directory", "MultiUserCaseBrowserCustomizer.column.directory=Directory",
"MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time" "MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time",
"MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted",
"MultiUserCaseBrowserCustomizer.column.caseDbDeleteStatus=Case Database Deleted",
"MultiUserCaseBrowserCustomizer.column.caseDirDeleteStatus=Case Directory Deleted",
"MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Deleted",
"MultiUserCaseBrowserCustomizer.column.manifestCoordSvcNodesDeleteStatus=Manifest ZooKeeper Node Deleted"
}) })
public enum Column { public enum Column {
DISPLAY_NAME(Bundle.MultiUserCaseBrowserCustomizer_column_displayName()), DISPLAY_NAME(Bundle.MultiUserCaseBrowserCustomizer_column_displayName()),
CREATE_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_createTime()), CREATE_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_createTime()),
DIRECTORY(Bundle.MultiUserCaseBrowserCustomizer_column_directory()), DIRECTORY(Bundle.MultiUserCaseBrowserCustomizer_column_directory()),
LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime()); LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime()),
// RJCTODO: Add properties for deleted items flags TEXT_INDEX_DELETION_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_textIndexDeleteStatus()),
CASE_DB_DELETION_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_caseDbDeleteStatus()),
CASE_DIR_DELETION_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_caseDirDeleteStatus()),
DATA_SOURCES_DELETION_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_dataSourcesDeleteStatus()),
MANIFEST_FILE_LOCK_NODES_DELETION_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_manifestCoordSvcNodesDeleteStatus());
private final String displayName; private final String displayName;

View File

@ -65,7 +65,7 @@ final class MultiUserCasesRootNode extends AbstractNode {
try { try {
List<CaseNodeData> caseNodeData = MultiUserCaseNodeDataCollector.getNodeData(); List<CaseNodeData> caseNodeData = MultiUserCaseNodeDataCollector.getNodeData();
keys.addAll(caseNodeData); 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); logger.log(Level.SEVERE, "Failed to get case node data from coodination service", ex);
} }
return true; return true;

View File

@ -365,15 +365,22 @@ public final class CoordinationService {
* *
* @throws CoordinationServiceException If there is an error deleting the * @throws CoordinationServiceException If there is an error deleting the
* node. * 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); String fullNodePath = getFullyQualifiedNodePath(category, nodePath);
try { try {
curator.delete().forPath(fullNodePath); curator.delete().forPath(fullNodePath);
} catch (Exception ex) { } catch (Exception ex) {
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex); throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex);
} }
} }
}
/** /**
* Gets a list of the child nodes of a category in the namespace. * Gets a list of the child nodes of a category in the namespace.
@ -384,15 +391,22 @@ public final class CoordinationService {
* *
* @throws CoordinationServiceException If there is an error getting the * @throws CoordinationServiceException If there is an error getting the
* node list. * 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 { try {
List<String> list = curator.getChildren().forPath(categoryNodeToPath.get(category.getDisplayName())); List<String> list = curator.getChildren().forPath(categoryNodeToPath.get(category.getDisplayName()));
return list; return list;
} catch (Exception ex) { } catch (Exception ex) {
if (ex instanceof InterruptedException) {
throw (InterruptedException) ex;
} else {
throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex); throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex);
} }
} }
}
/** /**
* Creates a node path within a given category. * Creates a node path within a given category.
@ -404,9 +418,9 @@ public final class CoordinationService {
*/ */
private String getFullyQualifiedNodePath(CategoryNode category, String nodePath) { private String getFullyQualifiedNodePath(CategoryNode category, String nodePath) {
// nodePath on Unix systems starts with a "/" and ZooKeeper doesn't like two slashes in a row // 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(); return categoryNodeToPath.get(category.getDisplayName()) + nodePath.toUpperCase();
}else{ } else {
return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase(); return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase();
} }
} }

View File

@ -2292,9 +2292,9 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
sysLogger.log(Level.INFO, "Opening case {0} for {1}", new Object[]{caseName, manifest.getFilePath()}); sysLogger.log(Level.INFO, "Opening case {0} for {1}", new Object[]{caseName, manifest.getFilePath()});
currentJob.setProcessingStage(AutoIngestJob.Stage.OPENING_CASE, Date.from(Instant.now())); currentJob.setProcessingStage(AutoIngestJob.Stage.OPENING_CASE, Date.from(Instant.now()));
/* /*
* Acquire and hold a case name lock so that only one node at as * Acquire and hold a case name lock so that only one node at a time
* time can scan the output directory at a time. This prevents * can search the output directory for an existing case. This
* making duplicate cases for the same auto ingest case. * prevents making duplicate cases for the same auto ingest case.
*/ */
try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseName, 30, TimeUnit.MINUTES)) { try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseName, 30, TimeUnit.MINUTES)) {
if (null != caseLock) { if (null != caseLock) {

View File

@ -96,7 +96,7 @@ final class AutoIngestMetricsCollector {
return newMetricsSnapshot; return newMetricsSnapshot;
} catch (CoordinationService.CoordinationServiceException ex) { } catch (CoordinationService.CoordinationServiceException | InterruptedException ex) {
LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex); LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex);
return new MetricsSnapshot(); return new MetricsSnapshot();
} }

View File

@ -378,7 +378,7 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen
return newJobsSnapshot; return newJobsSnapshot;
} catch (CoordinationServiceException ex) { } catch (CoordinationServiceException | InterruptedException ex) {
LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex); LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex);
return new JobsSnapshot(); return new JobsSnapshot();
} }

View File

@ -180,26 +180,31 @@ DeleteCaseInputAndOutputAction.taskName=input-and-output
DeleteCaseOutputAction.menuItemText=Delete Output DeleteCaseOutputAction.menuItemText=Delete Output
DeleteCaseOutputAction.progressDisplayName=Delete Output DeleteCaseOutputAction.progressDisplayName=Delete Output
DeleteCaseOutputAction.taskName=output DeleteCaseOutputAction.taskName=output
DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring an exclusive case directory lock...
DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring an exclusive case name lock...
DeleteCaseTask.progress.acquiringInputDirLocks=Acquiring exclusive input directory locks DeleteCaseTask.progress.acquiringInputDirLocks=Acquiring exclusive input directory locks
DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service DeleteCaseTask.progress.acquiringManifestFileLocks=Acquiring exclusive manifest file locks...
DeleteCaseTask.progress.deletingCaseOutput=Deleting case output DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks...
DeleteCaseTask.progress.deletingDirLockNode=Deleting case directory lock node DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service...
DeleteCaseTask.progress.deletingCaseOutput=Deleting case database, text index, and directory...
DeleteCaseTask.progress.deletingDirLockNode=Deleting case directory lock coordination service node...
# {0} - input directory name # {0} - input directory name
DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0} DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0}...
DeleteCaseTask.progress.deletingInputDirLockNodes=Deleting input directory lock nodes DeleteCaseTask.progress.deletingInputDirLockNodes=Deleting input directory lock nodes
DeleteCaseTask.progress.deletingInputDirs=Deleting input directory DeleteCaseTask.progress.deletingInputDirs=Deleting input directory...
DeleteCaseTask.progress.deletingJobLogLockNode=Deleting auto ingest job log lock node DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest job log lock node...
DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock node DeleteCaseTask.progress.deletingManifestFileLockNodes=Deleting manifest file lock nodes...
DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock node...
DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources lock node DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources lock node
DeleteCaseTask.progress.gettingJobNodeData=Getting node data for auto ingest jobs DeleteCaseTask.progress.gettingJobNodeData=Getting node data for auto ingest jobs...
DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file...
# {0} - input directory name DeleteCaseTask.progress.lockingInputDir=Acquiring exclusive lock on manifest {0}
DeleteCaseTask.progress.lockingInputDir=Acquiring exclusive lock on input directory {0} # {0} - manifest file name
DeleteCaseTask.progress.lockingManifestFile=Acquiring exclusive lock on manifest {0}...
# {0} - manifest file path # {0} - manifest file path
DeleteCaseTask.progress.releasingManifestLock=Releasing the exclusive lock on manifest file {0} DeleteCaseTask.progress.releasingManifestLock=Releasing the exclusive lock on manifest file {0}...
DeleteCaseTask.progress.releasingManifestLocks=Acquiring exclusive manifest file locks DeleteCaseTask.progress.releasingManifestLocks=Releasing exclusive manifest file locks...
DeleteCaseTask.progress.startMessage=Preparing for deletion...
GeneralFilter.archiveDesc.text=Archive Files (.zip, .rar, .arj, .7z, .7zip, .gzip, .gz, .bzip2, .tar, .tgz) GeneralFilter.archiveDesc.text=Archive Files (.zip, .rar, .arj, .7z, .7zip, .gzip, .gz, .bzip2, .tar, .tgz)
HINT_CasesDashboardTopComponent=This is an adminstrative dashboard for multi-user cases HINT_CasesDashboardTopComponent=This is an adminstrative dashboard for multi-user cases
OpenAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted. OpenAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted.

View File

@ -38,19 +38,24 @@ final class DeleteCaseInputAndOutputTask extends DeleteCaseTask {
} }
@Override @Override
void deleteWhileHoldingAllLocks() { void deleteWhileHoldingAllLocks() throws InterruptedException {
deleteInputDirectories(); deleteInputDirectories();
deleteCaseOutput(); deleteCaseOutput();
} }
@Override @Override
void deleteAfterCaseLocksReleased() { void deleteAfterManifestLocksReleased() throws InterruptedException {
deleteCaseLockNodes(); deleteManifestFileLockNodes();
} }
@Override @Override
void deleteAfterAllLocksReleased() { void deleteAfterCaseDirectoryLockReleased() throws InterruptedException {
deleteInputDirectoryLockNodes(); this.deleteCaseDirectoryLockNode();
}
@Override
void deleteAfterCaseNameLockReleased() throws InterruptedException {
this.deleteCaseNameLockNode();
} }
} }

View File

@ -24,18 +24,18 @@ import org.sleuthkit.autopsy.progress.ProgressIndicator;
/** /**
* A task to delete the auto ingest job input directories for a case produced * A task to delete the auto ingest job input directories for a case produced
* via auto ingest, while leaving the auto ingest job coordination service nodes * via auto ingest, while leaving the auto ingest job coordination service nodes
* and the rest of the case intact. The use case is freeing space while * (manifest file lock nodes) and the rest of the case intact. The use case is
* retaining the option to restore the input directories, effectively restoring * freeing space while retaining the option to restore the input directories,
* the case. * effectively restoring the case.
*/ */
final class DeleteCaseInputTask extends DeleteCaseTask { final class DeleteCaseInputTask extends DeleteCaseTask {
/** /**
* Constructs a task to delete the auto ingest job input directories for a * Constructs task to delete the auto ingest job input directories for a
* case produced via auto ingest, while leaving the auto ingest job * case produced via auto ingest, while leaving the auto ingest job
* coordination service nodes and the rest of the case intact. The use case * coordination service nodes (manifest file lock nodes) and the rest of the
* is freeing space while retaining the option to restore the input * case intact. The use case is freeing space while retaining the option to
* directories, effectively restoring the case. * restore the input directories, effectively restoring the case.
* *
* @param caseNodeData The case directory lock coordination service node * @param caseNodeData The case directory lock coordination service node
* data for the case. * data for the case.
@ -46,16 +46,20 @@ final class DeleteCaseInputTask extends DeleteCaseTask {
} }
@Override @Override
void deleteWhileHoldingAllLocks() { void deleteWhileHoldingAllLocks() throws InterruptedException {
}
@Override
void deleteAfterCaseDirectoryLockReleased() throws InterruptedException {
}
@Override
void deleteAfterCaseNameLockReleased() throws InterruptedException {
deleteInputDirectories(); deleteInputDirectories();
} }
@Override @Override
void deleteAfterCaseLocksReleased() { void deleteAfterManifestLocksReleased() throws InterruptedException {
}
@Override
void deleteAfterAllLocksReleased() {
} }
} }

View File

@ -18,12 +18,7 @@
*/ */
package org.sleuthkit.autopsy.experimental.autoingest; 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.NbBundle;
import org.openide.util.Utilities;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.progress.ProgressIndicator; import org.sleuthkit.autopsy.progress.ProgressIndicator;

View File

@ -45,17 +45,22 @@ final class DeleteCaseOutputTask extends DeleteCaseTask {
} }
@Override @Override
void deleteWhileHoldingAllLocks() { void deleteWhileHoldingAllLocks() throws InterruptedException {
deleteCaseOutput(); deleteCaseOutput();
} }
@Override @Override
void deleteAfterCaseLocksReleased() { void deleteAfterCaseDirectoryLockReleased() throws InterruptedException {
} }
@Override @Override
void deleteAfterAllLocksReleased() { void deleteAfterManifestLocksReleased() throws InterruptedException {
deleteInputDirectoryLockNodes(); deleteManifestFileLockNodes();
}
@Override
void deleteAfterCaseNameLockReleased() throws InterruptedException {
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
} }
} }

View File

@ -19,6 +19,7 @@
package org.sleuthkit.autopsy.experimental.autoingest; package org.sleuthkit.autopsy.experimental.autoingest;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
@ -29,138 +30,74 @@ import java.util.logging.Level;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages; import org.openide.util.NbBundle.Messages;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.CaseActionException;
import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseCoordinationServiceUtils;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException;
import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.FileUtil;
import org.sleuthkit.autopsy.progress.ProgressIndicator; import org.sleuthkit.autopsy.progress.ProgressIndicator;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.TimeStampUtils;
import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobNodeData.InvalidDataException; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobNodeData.InvalidDataException;
/** /**
* A base class for tasks that delete part or all of a case produced via auto * A base class for tasks that delete part or all of a given case.
* ingest.
*/ */
abstract class DeleteCaseTask implements Runnable { abstract class DeleteCaseTask implements Runnable {
private static final String RESOURCES_LOCK_SUFFIX = "_resources"; //NON-NLS
private static final Logger logger = AutoIngestDashboardLogger.getLogger(); private static final Logger logger = AutoIngestDashboardLogger.getLogger();
private final CaseNodeData caseNodeData; private final CaseNodeData caseNodeData;
private final String caseDisplayName;
private final String caseUniqueName;
private final Path caseDirectoryPath;
private final ProgressIndicator progress; private final ProgressIndicator progress;
private final List<AutoIngestJobNodeData> nodeDataForAutoIngestJobs; private final List<AutoIngestJobNodeData> nodeDataForAutoIngestJobs;
private final Map<Path, CoordinationService.Lock> manifestFileLocks; private final Map<String, CoordinationService.Lock> manifestFileLocks;
private CoordinationService coordinationService; private CoordinationService coordinationService;
/** /**
* Constructs the base class part of a task that deletes part or all of a * Constructs the base class part of a task that deletes part or all of a
* case produced via auto ingest. * given case.
* *
* @param caseNodeData The case directory lock coordination service node * @param caseNodeData The case directory lock coordination service node
* data for the case to be deleted. * data for the case.
* @param progress A progress indicator. * @param progress A progress indicator.
*/ */
DeleteCaseTask(CaseNodeData caseNodeData, ProgressIndicator progress) { DeleteCaseTask(CaseNodeData caseNodeData, ProgressIndicator progress) {
this.caseNodeData = caseNodeData; this.caseNodeData = caseNodeData;
this.progress = progress; this.progress = progress;
/*
* Design Decision Note: It was decided to add the following state to
* instances of this class make it easier to access given that the class
* design favors instance methods over static methods.
*/
this.caseDisplayName = caseNodeData.getDisplayName();
this.caseUniqueName = caseNodeData.getName();
this.caseDirectoryPath = caseNodeData.getDirectory();
this.nodeDataForAutoIngestJobs = new ArrayList<>(); this.nodeDataForAutoIngestJobs = new ArrayList<>();
this.manifestFileLocks = new HashMap<>(); this.manifestFileLocks = new HashMap<>();
} }
@Override @Override
@NbBundle.Messages({ @NbBundle.Messages({
"DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service", "DeleteCaseTask.progress.startMessage=Preparing for deletion..."
"DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock",
"DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock",
"DeleteCaseTask.progress.gettingJobNodeData=Getting node data for auto ingest jobs",
"DeleteCaseTask.progress.acquiringInputDirLocks=Acquiring exclusive input directory locks"
}) })
public void run() { public void run() {
try { try {
progress.start(Bundle.DeleteCaseTask_progress_connectingToCoordSvc()); progress.start(Bundle.DeleteCaseTask_progress_startMessage());
try { logger.log(Level.INFO, String.format("Beginning deletion of %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
coordinationService = CoordinationService.getInstance(); deleteCase();
} catch (CoordinationService.CoordinationServiceException ex) { logger.log(Level.SEVERE, String.format("Deletion of %s (%s) in %s completed", caseDisplayName, caseUniqueName, caseDirectoryPath));
logger.log(Level.SEVERE, String.format("Failed to connect to the coordination service to delete %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
return;
}
/*
* Acquire an exclusive case name lock. This is the lock that auto
* ingest nodes acquire when creating or opening a case specified in
* an auto ingest job manifest file. Acquiring this lock prevents
* auto ingest nodes from searching for and finding the case
* directory of the case to be deleted.
*/
progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseNameLock());
logger.log(Level.INFO, String.format("Exclusively locking the case name for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
final String caseNameLockName = TimeStampUtils.removeTimeStamp(caseNodeData.getName());
try (CoordinationService.Lock nameLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, caseNameLockName)) {
if (nameLock == null) {
logger.log(Level.WARNING, String.format("Failed to exclusively lock the case name for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
return;
}
/*
* Acquire an exclusive case directory lock. This is the lock
* that is aquired by any node (auto ingest or examiner)
* attempting to create or open a case and is held by such a
* node for as long as the case is open. Acquiring this lock
* ensures that no other node currently has the case to be
* deleted open and prevents another node from trying to open
* the case as it is being deleted.
*/
progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseDirLock());
logger.log(Level.INFO, String.format("Exclusively locking the case directory for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
try (CoordinationService.Lock caseLock = CoordinationService.getInstance().tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseNodeData.getDirectory().toString())) {
if (caseLock == null) {
logger.log(Level.WARNING, String.format("Failed to exclusively lock the case directory for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
return;
}
progress.progress(Bundle.DeleteCaseTask_progress_gettingJobNodeData());
logger.log(Level.INFO, String.format("Fetching auto ingest job node data for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
try {
getAutoIngestJobNodeData();
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Failed to fetch auto ingest job node data for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
return;
}
if (!nodeDataForAutoIngestJobs.isEmpty()) {
progress.progress(Bundle.DeleteCaseTask_progress_acquiringInputDirLocks());
logger.log(Level.INFO, String.format("Exclusively locking the case directories for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
getInputDirectoryLocks();
if (manifestFileLocks.isEmpty()) {
logger.log(Level.WARNING, String.format("Failed to exclusively lock the input directories for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
return;
}
} else {
logger.log(Level.INFO, String.format("No auto ingest job node data found for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
}
deleteWhileHoldingAllLocks();
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error acquiring exclusive case directory lock for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex);
}
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error acquiring exclusive case name lock for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex);
}
deleteAfterCaseLocksReleased();
releaseInputDirectoryLocks();
deleteAfterAllLocksReleased();
} catch (Throwable ex) { } catch (Throwable ex) {
/* /*
* Unexpected runtime exceptions firewall. * Unexpected runtime exceptions firewall. This task is designed to
* be able to be run in an executor service thread pool without
* calling get() on the task's Future<Void>, so this ensures that
* such errors do get ignored.
*/ */
logger.log(Level.SEVERE, String.format("Unexpected error deleting %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); logger.log(Level.INFO, String.format("Unexpected error deleting %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
} finally { } finally {
progress.finish(); progress.finish();
@ -169,224 +106,392 @@ abstract class DeleteCaseTask implements Runnable {
} }
/** /**
* Deletes the parts of the case that need to be deleted while holding the * Deletes part or all of the given case.
* following exclusive locks: case name lock, case directory lock, and all
* input directory locks.
*/
abstract void deleteWhileHoldingAllLocks();
/**
* Deletes the parts of the case that need to be deleted after releasing
* exclusive locks on the case name and case directory, but while still
* holding exclusive locks on all of the input directories.
*/
abstract void deleteAfterCaseLocksReleased();
/**
* Deletes the parts of the case that need to be deleted after all of the
* locks are released.
*/
abstract void deleteAfterAllLocksReleased();
/**
* Deletes the auto ingest job input directories for the case. Should only
* be called when holding all of the exclusive locks for the case.
*/ */
@NbBundle.Messages({ @NbBundle.Messages({
"DeleteCaseTask.progress.deletingInputDirs=Deleting input directory", "DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service...",
"# {0} - input directory name", "DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0}" "DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring an exclusive case name lock...",
"DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring an exclusive case directory lock...",
"DeleteCaseTask.progress.gettingJobNodeData=Getting node data for auto ingest jobs...",
"DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks..."
})
private void deleteCase() {
progress.progress(Bundle.DeleteCaseTask_progress_connectingToCoordSvc());
logger.log(Level.INFO, String.format("Connecting to coordination service for deletion of %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
try {
coordinationService = CoordinationService.getInstance();
} catch (CoordinationService.CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Failed to connect to the coordination service, cannot delete %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
return;
}
if (Thread.currentThread().isInterrupted()) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
/*
* Acquire an exclusive case name lock. This is the lock that auto
* ingest nodes acquire exclusively when creating or opening a case
* specified in an auto ingest job manifest file to ensure that only one
* auto ingest node at a time can search the auto ingest output
* directory for an existing case matching the one in the manifest file.
* Acquiring this lock effectively locks auto ingest node job processing
* tasks out of the case to be deleted.
*/
progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseNameLock());
logger.log(Level.INFO, String.format("Acquiring an exclusive case name lock for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
String caseNameLockNodeName = CaseCoordinationServiceUtils.getCaseLockName(caseDirectoryPath);
try (CoordinationService.Lock nameLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, caseNameLockNodeName)) {
if (nameLock == null) {
logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because a case name lock was held by another host", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
if (Thread.currentThread().isInterrupted()) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
/*
* Acquire an exclusive case directory lock. A shared case directory
* lock is acquired by any node (auto ingest or examiner) when it
* opens a case and is held by the node for as long as the case is
* open. Acquiring this lock exclusively ensures that no other node
* currently 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 (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
String caseDirLockNodeName = CaseCoordinationServiceUtils.getCaseDirectoryLockName(caseDirectoryPath);
try (CoordinationService.Lock caseDirLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseDirLockNodeName)) {
if (caseDirLock == null) {
logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because a case directory lock was held by another host", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
if (Thread.currentThread().isInterrupted()) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
progress.progress(Bundle.DeleteCaseTask_progress_gettingJobNodeData());
logger.log(Level.INFO, String.format("Fetching auto ingest job node data for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
try {
getAutoIngestJobNodeData();
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error fetching auto ingest job node data for %s (%s) in %s, cannot delete case", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
return;
} catch (InterruptedException ex) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
return;
}
if (Thread.currentThread().isInterrupted()) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
if (!nodeDataForAutoIngestJobs.isEmpty()) {
progress.progress(Bundle.DeleteCaseTask_progress_acquiringManifestLocks());
logger.log(Level.INFO, String.format("Acquiring exclusive manifest file locks for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
getManifestFileLocks();
if (manifestFileLocks.isEmpty()) {
logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because a case directory lock was held by another host", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
} else {
logger.log(Level.INFO, String.format("No auto ingest job node data found for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
}
if (Thread.currentThread().isInterrupted()) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath));
releaseManifestFileLocks();
return;
}
try {
deleteWhileHoldingAllLocks();
} catch (InterruptedException ex) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
return;
}
releaseManifestFileLocks();
try {
deleteAfterManifestLocksReleased();
} catch (InterruptedException ex) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
return;
}
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error acquiring exclusive case directory lock for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
}
if (Thread.currentThread().isInterrupted()) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath));
return;
}
try {
deleteAfterCaseDirectoryLockReleased();
} catch (InterruptedException ex) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
return;
}
} catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error acquiring exclusive case name lock for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
}
try {
deleteAfterCaseNameLockReleased();
} catch (InterruptedException ex) {
logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
return;
}
}
/**
* Deletes the parts of the case that need to be deleted while holding all
* of the exclusive locks: the case name lock, the case directory lock, amd
* the manifest file locks. Note that the locks are acquired in that order
* and released in the opposite order.
*/
abstract void deleteWhileHoldingAllLocks() throws InterruptedException;
/**
* Deletes the parts of the case that need to be deleted after the release
* of the exclusive manifest file locks, while still holding the exclusive
* case name and case directory locks; the manifest file locks are the first
* locks released.
*/
abstract void deleteAfterManifestLocksReleased() throws InterruptedException;
/**
* Deletes the parts of the case that need to be deleted after the release
* of the exclusive manifest file locks and case directory lock, while still
* holding the exclusive case name; the case name lock is the last lock
* released.
*/
abstract void deleteAfterCaseDirectoryLockReleased() throws InterruptedException;
/**
* Deletes the parts of the case that need to be deleted after the release
* of all of the exclusive locks; the case name lock is the last lock
* released.
*/
abstract void deleteAfterCaseNameLockReleased() throws InterruptedException;
/**
* Deletes the auto ingest job input directories for the case. Intended to
* be called by subclasses, if required, in their customization of the
* deleteWhileHoldingAllLocks step of the case deletion algorithm.
*/
@NbBundle.Messages({
"# {0} - input directory name", "DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0}..."
}) })
protected void deleteInputDirectories() { protected void deleteInputDirectories() {
progress.progress(Bundle.DeleteCaseTask_progress_deletingInputDirs()); boolean allInputDirsDeleted = true;
logger.log(Level.INFO, String.format("Deleting input directories for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
for (AutoIngestJobNodeData jobNodeData : nodeDataForAutoIngestJobs) { for (AutoIngestJobNodeData jobNodeData : nodeDataForAutoIngestJobs) {
final Path inputDirPath = jobNodeData.getManifestFilePath().getParent(); Path inputDirPath = jobNodeData.getManifestFilePath().getParent();
File inputDir = inputDirPath.toFile();
if (inputDir.exists()) {
progress.progress(Bundle.DeleteCaseTask_progress_deletingInputDir(inputDirPath)); progress.progress(Bundle.DeleteCaseTask_progress_deletingInputDir(inputDirPath));
logger.log(Level.INFO, String.format("Deleting input directory %s for %s (%s) in %s", inputDirPath, caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); logger.log(Level.INFO, String.format("Deleting input directory %s for %s (%s) in %s", inputDirPath, caseDisplayName, caseUniqueName, caseDirectoryPath));
if (FileUtil.deleteDir(new File(inputDirPath.toString()))) { if (!FileUtil.deleteDir(new File(inputDirPath.toString()))) {
logger.log(Level.WARNING, String.format("Failed to delete the input directory %s for %s (%s) in %s", inputDirPath, caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); logger.log(Level.WARNING, String.format("Failed to delete the input directory %s for %s (%s) in %s", inputDirPath, caseDisplayName, caseUniqueName, caseDirectoryPath));
// RJCTODO: Update deletion flags allInputDirsDeleted = false;
} }
} }
} }
if (allInputDirsDeleted) {
setDeletedItemFlag(CaseNodeData.DeletedFlags.DATA_SOURCES);
}
}
/** /**
* Deletes the case directory, the case database, and the text index for the * Deletes the case database, the text index, and the case directory for the
* case. Should only be called when holding all of the exclusive locks for * case. Intended to be called by subclasses, if required, in their
* the case. * customization of the deleteWhileHoldingAllLocks step of the case deletion
* algorithm.
*/ */
@NbBundle.Messages({ @NbBundle.Messages({
"DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file", "DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file...",
"DeleteCaseTask.progress.deletingCaseOutput=Deleting case output" "DeleteCaseTask.progress.deletingCaseOutput=Deleting case database, text index, and directory...",
"DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest job log lock node..."
}) })
protected void deleteCaseOutput() { protected void deleteCaseOutput() {
progress.progress(Bundle.DeleteCaseTask_progress_locatingCaseMetadataFile()); progress.progress(Bundle.DeleteCaseTask_progress_locatingCaseMetadataFile());
logger.log(Level.INFO, String.format("Locating metadata file for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); logger.log(Level.INFO, String.format("Locating metadata file for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
String metadataFilePath = null; CaseMetadata caseMetadata = null;
final File caseDirectory = caseNodeData.getDirectory().toFile(); final File caseDirectory = caseDirectoryPath.toFile();
final File[] filesInDirectory = caseDirectory.listFiles(); final File[] filesInDirectory = caseDirectory.listFiles();
if (filesInDirectory != null) { if (filesInDirectory != null) {
for (File file : filesInDirectory) { for (File file : filesInDirectory) {
if (file.getName().toLowerCase().endsWith(CaseMetadata.getFileExtension()) && file.isFile()) { if (file.getName().toLowerCase().endsWith(CaseMetadata.getFileExtension()) && file.isFile()) {
metadataFilePath = file.getPath();
}
}
}
if (metadataFilePath != null) {
progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseOutput());
logger.log(Level.INFO, String.format("Deleting output for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
try { try {
Case.deleteCase(new CaseMetadata(Paths.get(metadataFilePath)), false, progress); caseMetadata = new CaseMetadata(Paths.get(file.getPath()));
} catch (CaseMetadata.CaseMetadataException | CaseActionException ex) { } catch (CaseMetadata.CaseMetadataException ex) {
// RJCTODO: Set delete flags? logger.log(Level.WARNING, String.format("Error getting opening case metadata file for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
logger.log(Level.WARNING, String.format("Error deleting output for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); }
break;
} }
} else {
logger.log(Level.WARNING, String.format("Failed to locate metadata file for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
// RJCTODO: Set delete flags
} }
} }
/** if (caseMetadata != null) {
* Deletes the case name, case directory, case resources, and case auto progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseOutput());
* ingest log for the case. Should only be called when holding the exclusive logger.log(Level.INFO, String.format("Deleting output for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
* locks for all of the input directories for the case. Case.deleteMultiUserCase(caseNodeData, caseMetadata, progress); // RJCTODO: Make this method throw the interrupted exception.
*/ } else {
@Messages({ logger.log(Level.WARNING, String.format("Failed to locate metadata file for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
"DeleteCaseTask.progress.deletingJobLogLockNode=Deleting auto ingest job log lock node", }
"DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources lock node",
"DeleteCaseTask.progress.deletingDirLockNode=Deleting case directory lock node",
"DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock node"
})
protected void deleteCaseLockNodes() {
progress.progress(Bundle.DeleteCaseTask_progress_deletingJobLogLockNode()); progress.progress(Bundle.DeleteCaseTask_progress_deletingJobLogLockNode());
logger.log(Level.INFO, String.format("Deleting case auto ingest job log lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); logger.log(Level.INFO, String.format("Deleting case auto ingest job log lock node for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
Path logFilePath = AutoIngestJobLogger.getLogPath(caseNodeData.getDirectory()); Path logFilePath = AutoIngestJobLogger.getLogPath(caseDirectoryPath); //RJCTODO: USe util here
try { try {
coordinationService.deleteNode(CategoryNode.CASES, logFilePath.toString()); coordinationService.deleteNode(CategoryNode.CASES, logFilePath.toString());
} catch (CoordinationServiceException ex) { } catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Error deleting auto ingest job log lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); logger.log(Level.WARNING, String.format("Error deleting case auto ingest job log lock node for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
// RJCTODO: Set delete flags } catch (InterruptedException ex) {
} logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
progress.progress(Bundle.DeleteCaseTask_progress_deletingResourcesLockNode());
logger.log(Level.INFO, String.format("Deleting case resources log lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
String resourcesLockNodePath = caseNodeData.getDirectory().toString() + RESOURCES_LOCK_SUFFIX;
try {
coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath);
} catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Error deleting case resources lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex);
// RJCTODO: Set delete flags
}
progress.progress(Bundle.DeleteCaseTask_progress_deletingDirLockNode());
logger.log(Level.INFO, String.format("Deleting case directory lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
final Path caseDirectoryPath = caseNodeData.getDirectory();
try {
coordinationService.deleteNode(CategoryNode.CASES, caseDirectoryPath.toString());
} catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Error deleting case directory lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex);
// RJCTODO: Set delete flags
}
progress.progress(Bundle.DeleteCaseTask_progress_deletingNameLockNode());
logger.log(Level.INFO, String.format("Deleting case name lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()));
final String caseNameLockName = TimeStampUtils.removeTimeStamp(caseNodeData.getName());
try {
coordinationService.deleteNode(CategoryNode.CASES, caseNameLockName);
} catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Error deleting case name lock node for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex);
// RJCTODO: Set delete flags
} }
} }
/** /**
* Deletes the input directory lock nodes for the case. Should only be * Deletes the manifest file lock coordination service nodes for the case.
* called after releasing all of the locks for the case. * Intended to be called by subclasses, if required, in their customization
* of the deleteAfterManifestLocksReleased step of the case deletion
* algorithm.
*/ */
@Messages({ @Messages({
"DeleteCaseTask.progress.deletingInputDirLockNodes=Deleting input directory lock nodes" "DeleteCaseTask.progress.deletingManifestFileLockNodes=Deleting manifest file lock nodes..."
}) })
protected void deleteInputDirectoryLockNodes() { protected void deleteManifestFileLockNodes() throws InterruptedException {
progress.progress(Bundle.DeleteCaseTask_progress_deletingInputDirLockNodes()); boolean allInputDirsDeleted = true;
logger.log(Level.INFO, String.format("Deleting input directory lock nodes for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); progress.progress(Bundle.DeleteCaseTask_progress_deletingManifestFileLockNodes());
logger.log(Level.INFO, String.format("Deleting manifest file lock nodes for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath));
for (AutoIngestJobNodeData jobNodeData : nodeDataForAutoIngestJobs) { for (AutoIngestJobNodeData jobNodeData : nodeDataForAutoIngestJobs) {
try { try {
logger.log(Level.INFO, String.format("Deleting manifest file lock node for %s for %s (%s) in %s", jobNodeData.getManifestFilePath(), caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); logger.log(Level.INFO, String.format("Deleting manifest file lock node for %s for %s (%s) in %s", jobNodeData.getManifestFilePath(), caseDisplayName, caseUniqueName, caseDirectoryPath));
coordinationService.deleteNode(CoordinationService.CategoryNode.MANIFESTS, jobNodeData.getManifestFilePath().toString()); coordinationService.deleteNode(CoordinationService.CategoryNode.MANIFESTS, jobNodeData.getManifestFilePath().toString());
} catch (CoordinationServiceException ex) { } catch (CoordinationServiceException ex) {
Path inputDirPath = jobNodeData.getManifestFilePath().getParent(); logger.log(Level.WARNING, String.format("Error deleting manifest file lock node %s for %s (%s) in %s", jobNodeData.getManifestFilePath(), caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
logger.log(Level.WARNING, String.format("Error deleting input directory lock node %s for %s (%s) in %s", inputDirPath, caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); allInputDirsDeleted = false;
// RJCTODO: Set delete flags
} }
} }
if (allInputDirsDeleted) {
setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_LOCK_NODES);
}
// RJCTODO: Expand case type in case metadata to include auto ingest cases.
// Disable delete menu item for auto ingest cases, and possibly also add data source
// capability.
}
/**
* Deletes the case directory coordination service lock node for the case.
* Intended to be called by subclasses, if required, in their customization
* of the deleteAfterCaseDirectoryLockReleased step of the case deletion
* algorithm.
*/
@Messages({
"DeleteCaseTask.progress.deletingDirLockNode=Deleting case directory lock coordination service node..."
})
protected void deleteCaseDirectoryLockNode() throws InterruptedException {
progress.progress(Bundle.DeleteCaseTask_progress_deletingDirLockNode());
try {
Case.deleteCaseDirectoryLockNode(caseNodeData, progress); // RJCTODO: Case does not need to expose this?
} catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Error deleting case directory lock node for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
}
} }
/** /**
* Fetches all of the auto ingest job (manifest file) lock coordination * Deletes the case name coordination service lock node for the case.
* service node data for a case. * Intended to be called by subclasses, if required, in their customization
* of the deleteAfterCaseNameLockReleased step of the case deletion
* algorithm.
* *
* @param caseName The name of the case. * @throws InterruptedException
*
* @return A list of auto ingest job (manifest file) lock node data for the
* case.
*
* @throws CoordinationServiceException If there is an error getting a list
* of the auto ingest job (manifest
* file) lock nodes from the
* coordination service.
*/ */
private void getAutoIngestJobNodeData() throws CoordinationServiceException { @Messages({
String caseName = caseNodeData.getName(); "DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock node..." // RJCTODO: Use consistent terminology
final List<String> nodes = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS); })
for (String nodeName : nodes) { protected void deleteCaseNameLockNode() throws InterruptedException {
progress.progress(Bundle.DeleteCaseTask_progress_deletingNameLockNode());
try {
String caseNameLockNodeName = CaseCoordinationServiceUtils.getCaseLockName(caseDirectoryPath);
coordinationService.deleteNode(CategoryNode.CASES, caseNameLockNodeName);
} catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Error deleting case name lock node for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
}
}
/**
* Fetches the auto ingest job data from the manifest file lock coordination
* service nodes for a case.
*
* @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.
*/
private void getAutoIngestJobNodeData() throws CoordinationServiceException, InterruptedException {
String caseName = caseDisplayName;
final List<String> nodeNames = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS);
for (String nodeName : nodeNames) {
try { try {
byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName); byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName);
if (nodeBytes == null || nodeBytes.length <= 0) { if (nodeBytes == null || nodeBytes.length <= 0) {
logger.log(Level.WARNING, String.format("Missing auto ingest job node data for manifest file lock node %s, deleting node", nodeName));
try {
coordinationService.deleteNode(CategoryNode.MANIFESTS, nodeName);
} catch (CoordinationServiceException ex) {
logger.log(Level.WARNING, String.format("Failed to delete empty manifest file lock node %s", nodeName));
}
continue; continue;
} }
AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes); AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes);
if (caseName.equals(nodeData.getCaseName())) { if (caseName.equals(nodeData.getCaseName())) {
nodeDataForAutoIngestJobs.add(nodeData); nodeDataForAutoIngestJobs.add(nodeData);
} }
} catch (CoordinationService.CoordinationServiceException | InterruptedException | InvalidDataException ex) { } catch (CoordinationService.CoordinationServiceException | InvalidDataException ex) {
logger.log(Level.WARNING, String.format("Failed to get coordination service node data for %s", nodeName), ex); logger.log(Level.WARNING, String.format("Failed to get auto ingest job node data for %s", nodeName), ex);
} }
} }
} }
/** /**
* Acquires either all or none of the input directory locks for a case. * Acquires either all or none of the manifest file locks for a case.
*
* @param caseNodeData The case node data from the case
* directory lock node for the case.
* @param autoIngestJobNodeDataList The auto ingest job node data from the
* input directory lock nodes for the case.
*
* @return A mapping of manifest file paths to input directory lcoks for all
* input directories for the case; will be empty if all of the locks
* could not be obtained.
*/ */
@NbBundle.Messages({ @NbBundle.Messages({
"# {0} - input directory name", "DeleteCaseTask.progress.lockingInputDir=Acquiring exclusive lock on input directory {0}",}) "# {0} - manifest file name", "DeleteCaseTask.progress.lockingManifestFile=Acquiring exclusive lock on manifest {0}..."
private void getInputDirectoryLocks() { })
private void getManifestFileLocks() {
for (AutoIngestJobNodeData autoIngestJobNodeData : nodeDataForAutoIngestJobs) { for (AutoIngestJobNodeData autoIngestJobNodeData : nodeDataForAutoIngestJobs) {
final Path inputDirPath = autoIngestJobNodeData.getManifestFilePath().getParent(); String manifestPath = autoIngestJobNodeData.getManifestFilePath().toString();
try { try {
progress.progress(Bundle.DeleteCaseTask_progress_lockingInputDir(inputDirPath)); progress.progress(Bundle.DeleteCaseTask_progress_lockingManifestFile(manifestPath));
final CoordinationService.Lock inputDirLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, autoIngestJobNodeData.getManifestFilePath().toString()); logger.log(Level.INFO, String.format("Exclusively locking the manifest %s for %s (%s) in %s", manifestPath, caseDisplayName, caseUniqueName, caseDirectoryPath));
CoordinationService.Lock inputDirLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath);
if (null != inputDirLock) { if (null != inputDirLock) {
manifestFileLocks.put(autoIngestJobNodeData.getManifestFilePath(), inputDirLock); manifestFileLocks.put(manifestPath, inputDirLock);
} else { } else {
logger.log(Level.WARNING, String.format("Failed to exclusively lock the input directory %s for %s (%s) in %s", inputDirPath, caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); logger.log(Level.INFO, String.format("Failed to exclusively lock the manifest %s for %s (%s) in %s", manifestPath, caseDisplayName, caseUniqueName, caseDirectoryPath));
releaseInputDirectoryLocks(); releaseManifestFileLocks();
manifestFileLocks.clear(); manifestFileLocks.clear();
break;
} }
} catch (CoordinationService.CoordinationServiceException ex) { } catch (CoordinationService.CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Failed to exclusively lock the input directory %s for %s (%s) in %s", inputDirPath, caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); logger.log(Level.SEVERE, String.format("Error exclusively locking the manifest %s for %s (%s) in %s", manifestPath, caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
releaseInputDirectoryLocks(); releaseManifestFileLocks();
manifestFileLocks.clear(); manifestFileLocks.clear();
break;
} }
} }
} }
@ -396,23 +501,36 @@ abstract class DeleteCaseTask implements Runnable {
* for the case. * for the case.
*/ */
@NbBundle.Messages({ @NbBundle.Messages({
"DeleteCaseTask.progress.releasingManifestLocks=Acquiring exclusive manifest file locks", "# {0} - manifest file path", "DeleteCaseTask.progress.releasingManifestLock=Releasing the exclusive lock on manifest file {0}..."
"# {0} - manifest file path", "DeleteCaseTask.progress.releasingManifestLock=Releasing the exclusive lock on manifest file {0}"
}) })
private void releaseInputDirectoryLocks() { private void releaseManifestFileLocks() {
if (!manifestFileLocks.isEmpty()) { if (!manifestFileLocks.isEmpty()) {
progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLocks()); for (Map.Entry<String, CoordinationService.Lock> entry : manifestFileLocks.entrySet()) {
for (Map.Entry<Path, CoordinationService.Lock> entry : manifestFileLocks.entrySet()) { String manifestFilePath = entry.getKey();
final Path manifestFilePath = entry.getKey(); CoordinationService.Lock manifestFileLock = entry.getValue();
final CoordinationService.Lock manifestFileLock = entry.getValue();
try { try {
progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLock(manifestFilePath)); progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLock(manifestFilePath));
logger.log(Level.INFO, String.format("Releasing the exclusive lock on the manifest file %s for %s (%s) in %s", manifestFilePath, caseDisplayName, caseUniqueName, caseDirectoryPath));
manifestFileLock.release(); manifestFileLock.release();
} catch (CoordinationServiceException ex) { } catch (CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error re3leasing exclusive lock on %s for %s (%s) in %s", manifestFilePath, caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); logger.log(Level.SEVERE, String.format("Error releasing exclusive lock on the manifest file %s for %s (%s) in %s", manifestFilePath, caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
} }
} }
} }
} }
/**
* Sets a deleted item flag for the case.
*
* @param flag The flag to set.
*/
private void setDeletedItemFlag(CaseNodeData.DeletedFlags flag) {
try {
caseNodeData.setDeletedFlag(flag);
coordinationService.setNodeData(CategoryNode.CASES, caseDirectoryPath.toString(), caseNodeData.toArray());
} catch (IOException | CoordinationServiceException | InterruptedException ex) {
logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s (%s) in %s", flag.name(), caseDisplayName, caseUniqueName, caseDirectoryPath), ex);
}
}
} }