diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index 3ec3393309..dbd5215554 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -11,7 +11,7 @@ Case.deleteCaseFailureMessageBox.message=Error deleting case: {0} Case.deleteCaseFailureMessageBox.title=Failed to Delete Case Case.exceptionMessage.cancelledByUser=Cancelled by user. Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first. -Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user. +Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host. Case.exceptionMessage.cannotLocateMainWindow=Cannot locate main application window Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case. # {0} - exception message @@ -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.emptyCaseDir=Must specify a case directory path. Case.exceptionMessage.emptyCaseName=Must specify a case name. -Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details. +Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the log for details. # {0} - exception message Case.exceptionMessage.execExceptionWrapperMessage={0} # {0} - exception message @@ -58,36 +58,12 @@ Case.progressMessage.connectingToCoordSvc=Connecting to coordination service... Case.progressMessage.creatingCaseDatabase=Creating case database... Case.progressMessage.creatingCaseDirectory=Creating case directory... Case.progressMessage.creatingCaseNodeData=Creating coordination service node data... -Case.progressMessage.deletingAutoIngestLogCoordSvcNode=Deleting case auto ingest log lock coordination service node... Case.progressMessage.deletingCaseDatabase=Deleting case database... Case.progressMessage.deletingCaseDirectory=Deleting case directory... -Case.progressMessage.deletingDirectoryCoordinationServiceNode=Deleting case directory lock coordination service node... -Case.progressMessage.deletingResourcesCoordSvcNode=Deleting case resourceslock coordination service node... +Case.progressMessage.deletingCaseDirLockNode=Deleting case directory lock coordination service node... +Case.progressMessage.deletingResourcesLockNode=Deleting case resources lock node... 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.missingCoordSvcNodeData=Missing coordination service node data. Case.progressMessage.openingApplicationServiceResources=Opening application service case resources... Case.progressMessage.openingCaseDatabase=Opening case database... Case.progressMessage.openingCaseLevelServices=Opening case-level services... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 1e0ebcde91..7a4ed43043 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -702,8 +702,45 @@ public class Case { } CaseMetadata metadata = currentCase.getMetadata(); closeCurrentCase(); - ProgressIndicator progressIndicator = new ModalDialogProgressIndicator(mainFrame, Bundle.Case_progressIndicatorTitle_deletingCase()); - deleteCase(metadata, true, progressIndicator); + deleteCase(metadata); + } + } + + /** + * 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; } - /** - * 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. * @@ -925,12 +928,12 @@ public class Case { * @param progressIndicator A progress indicator. * * @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({ - "Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application 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}." + "Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the log for details." }) private static void deleteSingleUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { boolean errorsOccurred = false; @@ -938,16 +941,14 @@ public class Case { deleteTextIndex(metadata, progressIndicator); } catch (KeywordSearchServiceException ex) { 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); - progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingTextIndex(ex.getMessage())); + logger.log(Level.WARNING, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); } try { deleteCaseDirectory(metadata, progressIndicator); } catch (CaseActionException ex) { 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); - progressIndicator.progress(Bundle.Case_progressMessage_errorDeletingCaseDir(ex.getMessage())); + logger.log(Level.WARNING, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); } deleteFromRecentCases(metadata, progressIndicator); @@ -960,173 +961,184 @@ public class Case { /** * Deletes a multi-user case. * - * @param metadata The case metadata. - * @param progressIndicator A progress indicator. + * @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. * * @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({ "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.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user.", - "# {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.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}." + "Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host.", + "Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case..." }) private static void deleteMultiUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { - CaseNodeData caseNodeData; - boolean errorsOccurred = false; progressIndicator.progress(Bundle.Case_progressMessage_connectingToCoordSvc()); CoordinationService coordinationService; try { coordinationService = CoordinationService.getInstance(); } catch (CoordinationServiceException ex) { logger.log(Level.SEVERE, String.format("Failed to connect to coordination service when attempting to delete %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); - progressIndicator.progress(Bundle.Case_progressMessage_errorConnectingToCoordSvc(ex.getMessage())); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); } - /* - * Acquire an exclusive case name lock. This will prevent auto ingest - * 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()); + CaseNodeData caseNodeData; + boolean errorsOccurred = false; + try (CoordinationService.Lock dirLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) { + if (dirLock == null) { + logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because a case directory lock was held by another host", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); 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())) { - 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())); - progressIndicator.progress(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase()); - throw new CaseActionException(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase()); - } - - progressIndicator.progress(Bundle.Case_progressMessage_fetchingCoordSvcNodeData()); - try { - byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, metadata.getCaseDirectory()); - if (nodeBytes != null && nodeBytes.length > 0) { - caseNodeData = new CaseNodeData(nodeBytes); - } else { - logger.log(Level.SEVERE, 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()); - } - } 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); - progressIndicator.progress(Bundle.Case_progressMessage_errorGettingCoordSvcNodeData(ex.getMessage())); + progressIndicator.progress(Bundle.Case_progressMessage_fetchingCoordSvcNodeData()); + try { + byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, metadata.getCaseDirectory()); + if (nodeBytes != null && nodeBytes.length > 0) { + caseNodeData = new CaseNodeData(nodeBytes); + } else { + logger.log(Level.WARNING, String.format("Missing coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); } - - try { - 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) { - 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); - progressIndicator.progress(Bundle.Case_progressMessage_errorLockingCase(ex.getMessage())); + } 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); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); } + errorsOccurred = deleteMultiUserCase(caseNodeData, metadata, progressIndicator); + } 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())); + logger.log(Level.SEVERE, String.format("Error exclusively locking the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); } - deleteCaseResourcesCoordinationServiceNode(metadata, progressIndicator, coordinationService); - deleteCaseAutoIngestLogCoordinationServiceNode(metadata, progressIndicator, coordinationService); - // RJCTODO: Delete case name lock node - 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 { - coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), caseNodeData.toArray()); - } catch (IOException | 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); - progressIndicator.progress(Bundle.Case_progressMessage_errorSavingDeletedItemsFlags(ex.getMessage())); + deleteCaseDirectoryLockNode(caseNodeData, progressIndicator); + } catch (CoordinationServiceException | InterruptedException ex) { + logger.log(Level.SEVERE, String.format("Error deleting the case directory lock node for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); + errorsOccurred = true; } + } + + if (errorsOccurred) { 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 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. - * @throws ClassNotFoundException If there is an error gettting the + * @throws ClassNotFoundException if there is an error gettting the * 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 * server. + * @throws InterruptedException If interrupted while blocked waiting for + * coordination service data to be written + * to the coordination service node + * database. */ @Messages({ - "Case.progressMessage.deletingCaseDatabase=Deleting case database...",}) - private static void deleteCaseDatabase(CaseMetadata metadata, ProgressIndicator progressIndicator, - CaseNodeData caseNodeData) throws UserPreferencesException, ClassNotFoundException, SQLException { - if (caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB)) { - return; + "Case.progressMessage.deletingCaseDatabase=Deleting case database..." + }) + private static void deleteCaseDatabase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator) throws UserPreferencesException, ClassNotFoundException, SQLException, InterruptedException { + if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB)) { + progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase()); + CaseDbConnectionInfo info = UserPreferences.getDatabaseConnectionInfo(); + String url = "jdbc:postgresql://" + info.getHost() + ":" + info.getPort() + "/postgres"; //NON-NLS + Class.forName("org.postgresql.Driver"); //NON-NLS + try (Connection connection = DriverManager.getConnection(url, info.getUserName(), info.getPassword()); Statement statement = connection.createStatement()) { + String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS + statement.execute(deleteCommand); + } + setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.CASE_DB); } - progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase()); - CaseDbConnectionInfo db; - db = UserPreferences.getDatabaseConnectionInfo(); - Class.forName("org.postgresql.Driver"); //NON-NLS - try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS - Statement statement = connection.createStatement();) { - String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS - statement.execute(deleteCommand); - } - caseNodeData.setDeletedFlag(CaseNodeData.DeletedFlags.CASE_DB); } /** @@ -1151,19 +1163,21 @@ public class 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 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 * 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)) { 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..." }) 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()); - // RJCTODO: Use robocopy on Windows, possibly just for longer paths if (!FileUtil.deleteDir(new File(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 progressIndicator A progress indicator. - * @param caseNodeData The coordination service node data for the case. * - * @throws CaseActionException If there is an error deleting the case - * directory. + * @throws CaseActionException if there is an error deleting the case + * directory. + * @throws InterruptedException If interrupted while blocked waiting for + * coordination service data to be written to + * the coordination service node database. */ - private static void 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)) { 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. - * @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 + * Deletes the case resources lock coordination service node for a * 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 * 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({ - "Case.progressMessage.deletingDirectoryCoordinationServiceNode=Deleting case directory lock coordination service node...", - "# {0} - exception message", "Case.progressMessage.errorDeletingCaseDirLockNode=An error occurred deleting the case directory lock node (see log for details): {0}." + "Case.progressMessage.deletingResourcesLockNode=Deleting case resources lock node..." }) - static void deleteCaseDirectoryCoordinationServiceNode(CaseMetadata metadata, ProgressIndicator progressIndicator, CoordinationService coordinationService) { - String caseDirectoryLockNodePath = metadata.getCaseDirectory(); + @Beta + private static void deleteCaseResourcesLockNode(CaseNodeData caseNodeData, ProgressIndicator progressIndicator) throws CoordinationServiceException, InterruptedException { + 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); + } + + /** + * 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 { - 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())); + 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); } - /** - * 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); - } - } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java index 0840fa480c..35414ed629 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java @@ -289,7 +289,7 @@ public final class CaseNodeData { CASE_DB(2), CASE_DIR(4), DATA_SOURCES(8), - JOB_MANIFEST_NODES(16); + MANIFEST_FILE_LOCK_NODES(16); private final short value; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/MultiUserCaseNodeDataCollector.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/MultiUserCaseNodeDataCollector.java index 63b11728ab..f3fa64b924 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/MultiUserCaseNodeDataCollector.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/MultiUserCaseNodeDataCollector.java @@ -29,76 +29,60 @@ import java.util.List; import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coreutils.Logger; /** * Queries the coordination service to collect the multi-user case node data * stored in the case directory lock ZooKeeper nodes. */ -final public class 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 String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS - private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS /** * Queries the coordination service to collect the multi-user case node data * stored in the case directory lock ZooKeeper nodes. * - * @return A list of CaseNodedata objects that convert data for a case - * directory lock coordination service node to and from byte arrays. + * @return The node data for the multi-user cases known to the coordination + * 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 getNodeData() throws CoordinationService.CoordinationServiceException { + public static List getNodeData() throws CoordinationServiceException, InterruptedException { final List cases = new ArrayList<>(); final CoordinationService coordinationService = CoordinationService.getInstance(); final List nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); for (String nodeName : nodeList) { - /* - * Ignore auto ingest case name lock nodes. - */ - final Path nodeNameAsPath = Paths.get(nodeName); - if (!(nodeNameAsPath.toString().contains("\\") || nodeNameAsPath.toString().contains("//"))) { - continue; - } - - /* - * Ignore case auto ingest log lock nodes and resource lock nodes. - */ - final String lastNodeNameComponent = nodeNameAsPath.getFileName().toString(); - if (lastNodeNameComponent.equals(CASE_AUTO_INGEST_LOG_NAME)) { - continue; - } - - /* - * Ignore case resources lock nodes. - */ - if (lastNodeNameComponent.endsWith(RESOURCES_LOCK_SUFFIX)) { + if (CaseCoordinationServiceUtils.isCaseLockName(nodeName) + || CaseCoordinationServiceUtils.isCaseResourcesLockName(nodeName) + || CaseCoordinationServiceUtils.isCaseAutoIngestLogLockName(nodeName)) { continue; } /* * 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 { 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) { nodeData = new CaseNodeData(nodeBytes); - if (nodeData.getVersion() == 0) { - /* - * Version 0 case node data was only written if errors - * occurred during an auto ingest job and consisted of - * only the set errors flag. - */ - nodeData = createNodeDataFromCaseMetadata(nodeName, true); + if (nodeData.getVersion() < CaseNodeData.getCurrentVersion()) { + nodeData = updateNodeData(nodeName, nodeData); } } 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) { 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 - * the metadata file for the case associated with the node. + * Updates the case directory lock coordination service node data for a + * case. * - * @param nodeName The coordination service node name, i.e., the case - * directory path. - * @param errorsOccurred Whether or not errors occurred during an auto - * ingest job for the case. + * @param nodeName The coordination service node name, i.e., the case + * directory path. + * @param oldNodeData . * - * @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 * node data to a byte array. @@ -130,28 +114,67 @@ final public class MultiUserCaseNodeDataCollector { * @throws InterruptedException If a coordination service operation * is interrupted. */ - private static CaseNodeData createNodeDataFromCaseMetadata(String nodeName, boolean errorsOccurred) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException { - CaseNodeData nodeData = null; + private static CaseNodeData updateNodeData(String nodeName, CaseNodeData oldNodeData) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException { Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS); File caseDirectory = caseDirectoryPath.toFile(); - if (caseDirectory.exists()) { + if (!caseDirectory.exists()) { + logger.log(Level.WARNING, String.format("Found orphan coordination service node %s, attempting clean up", caseDirectoryPath)); + deleteLockNodes(CoordinationService.getInstance(), caseDirectoryPath); + return null; + } + + CaseNodeData nodeData = null; + if (oldNodeData == null || oldNodeData.getVersion() == 0) { File[] files = caseDirectory.listFiles(); for (File file : files) { String name = file.getName().toLowerCase(); if (name.endsWith(CaseMetadata.getFileExtension())) { CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath())); nodeData = new CaseNodeData(metadata); - 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; } } } + if (nodeData != null) { - CoordinationService coordinationService = CoordinationService.getInstance(); - coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray()); - return nodeData; - } else { - throw new IOException(String.format("Could not find case metadata file for %s", nodeName)); + CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray()); + } + + return nodeData; + } + + /** + * Attempts to delete the coordination service lock nodes for a case, + * logging any failures. + * + * @param coordinationService The coordination service. + * @param caseDirectoryPath The case directory path. + */ + private static void deleteLockNodes(CoordinationService coordinationService, Path caseDirectoryPath) { + deleteCoordinationServiceNode(coordinationService, 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); } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED index 7ca1d937a3..6f24fde4f6 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED @@ -1,5 +1,10 @@ +MultiUserCaseBrowserCustomizer.column.caseDbDeleteStatus=Case Database Deleted +MultiUserCaseBrowserCustomizer.column.caseDirDeleteStatus=Case Directory Deleted MultiUserCaseBrowserCustomizer.column.createTime=Create Time +MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Deleted MultiUserCaseBrowserCustomizer.column.directory=Directory MultiUserCaseBrowserCustomizer.column.displayName=Name MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time +MultiUserCaseBrowserCustomizer.column.manifestCoordSvcNodesDeleteStatus=Manifest ZooKeeper Node Deleted +MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted MultiUserCasesBrowserPanel.waitNode.message=Please Wait... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java index 5cdca90ca8..3bccb63ed4 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java @@ -141,14 +141,23 @@ public interface MultiUserCaseBrowserCustomizer { "MultiUserCaseBrowserCustomizer.column.displayName=Name", "MultiUserCaseBrowserCustomizer.column.createTime=Create Time", "MultiUserCaseBrowserCustomizer.column.directory=Directory", - "MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time" + "MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time", + "MultiUserCaseBrowserCustomizer.column.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 { DISPLAY_NAME(Bundle.MultiUserCaseBrowserCustomizer_column_displayName()), CREATE_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_createTime()), DIRECTORY(Bundle.MultiUserCaseBrowserCustomizer_column_directory()), - LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime()); - // RJCTODO: Add properties for deleted items flags + LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime()), + 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; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java index 0d214e856d..3287ea53bb 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java @@ -65,7 +65,7 @@ final class MultiUserCasesRootNode extends AbstractNode { try { List caseNodeData = MultiUserCaseNodeDataCollector.getNodeData(); keys.addAll(caseNodeData); - } catch (CoordinationService.CoordinationServiceException ex) { + } catch (CoordinationService.CoordinationServiceException | InterruptedException ex) { logger.log(Level.SEVERE, "Failed to get case node data from coodination service", ex); } return true; diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java index ea087664b2..c63e517b9d 100644 --- a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java +++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java @@ -363,15 +363,22 @@ public final class CoordinationService { * @param category The desired category in the namespace. * @param nodePath The node to be deleted. * - * @throws CoordinationServiceException If there is an error deleting the - * node. + * @throws CoordinationServiceException If there is an error deleting the + * node. + * @throws java.lang.InterruptedException If a thread interrupt occurs while + * blocked waiting for the operation + * to complete. */ - public void deleteNode(CategoryNode category, String nodePath) throws CoordinationServiceException { + public void deleteNode(CategoryNode category, String nodePath) throws CoordinationServiceException, InterruptedException { String fullNodePath = getFullyQualifiedNodePath(category, nodePath); try { curator.delete().forPath(fullNodePath); } catch (Exception ex) { - throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex); + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex); + } } } @@ -382,15 +389,22 @@ public final class CoordinationService { * * @return A list of child node names. * - * @throws CoordinationServiceException If there is an error getting the - * node list. + * @throws CoordinationServiceException If there is an error getting the + * node list. + * @throws java.lang.InterruptedException If a thread interrupt occurs while + * blocked waiting for the operation + * to complete. */ - public List getNodeList(CategoryNode category) throws CoordinationServiceException { + public List getNodeList(CategoryNode category) throws CoordinationServiceException, InterruptedException { try { List list = curator.getChildren().forPath(categoryNodeToPath.get(category.getDisplayName())); return list; } catch (Exception ex) { - throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex); + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex); + } } } @@ -404,9 +418,9 @@ public final class CoordinationService { */ private String getFullyQualifiedNodePath(CategoryNode category, String nodePath) { // nodePath on Unix systems starts with a "/" and ZooKeeper doesn't like two slashes in a row - if(nodePath.startsWith("/")){ + if (nodePath.startsWith("/")) { return categoryNodeToPath.get(category.getDisplayName()) + nodePath.toUpperCase(); - }else{ + } else { return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase(); } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index 41e8218aa7..e3c678a719 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -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()}); currentJob.setProcessingStage(AutoIngestJob.Stage.OPENING_CASE, Date.from(Instant.now())); /* - * Acquire and hold a case name lock so that only one node at as - * time can scan the output directory at a time. This prevents - * making duplicate cases for the same auto ingest case. + * Acquire and hold a case name lock so that only one node at a time + * can search the output directory for an existing case. This + * prevents making duplicate cases for the same auto ingest case. */ try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseName, 30, TimeUnit.MINUTES)) { if (null != caseLock) { diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java index 36a376e3a0..402714a021 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java @@ -96,7 +96,7 @@ final class AutoIngestMetricsCollector { return newMetricsSnapshot; - } catch (CoordinationService.CoordinationServiceException ex) { + } catch (CoordinationService.CoordinationServiceException | InterruptedException ex) { LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex); return new MetricsSnapshot(); } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java index 16fc961c4b..1251155100 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java @@ -378,7 +378,7 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen return newJobsSnapshot; - } catch (CoordinationServiceException ex) { + } catch (CoordinationServiceException | InterruptedException ex) { LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex); return new JobsSnapshot(); } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED index d9bb8ec6bf..189e72954a 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED @@ -180,26 +180,31 @@ DeleteCaseInputAndOutputAction.taskName=input-and-output DeleteCaseOutputAction.menuItemText=Delete Output DeleteCaseOutputAction.progressDisplayName=Delete Output DeleteCaseOutputAction.taskName=output -DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock -DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock +DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring an exclusive case directory lock... +DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring an exclusive case name lock... DeleteCaseTask.progress.acquiringInputDirLocks=Acquiring exclusive input directory locks -DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service -DeleteCaseTask.progress.deletingCaseOutput=Deleting case output -DeleteCaseTask.progress.deletingDirLockNode=Deleting case directory lock node +DeleteCaseTask.progress.acquiringManifestFileLocks=Acquiring exclusive manifest file locks... +DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks... +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 -DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0} +DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0}... DeleteCaseTask.progress.deletingInputDirLockNodes=Deleting input directory lock nodes -DeleteCaseTask.progress.deletingInputDirs=Deleting input directory -DeleteCaseTask.progress.deletingJobLogLockNode=Deleting auto ingest job log lock node -DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock node +DeleteCaseTask.progress.deletingInputDirs=Deleting input directory... +DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest job log 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.gettingJobNodeData=Getting node data for auto ingest jobs -DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file -# {0} - input directory name -DeleteCaseTask.progress.lockingInputDir=Acquiring exclusive lock on input directory {0} +DeleteCaseTask.progress.gettingJobNodeData=Getting node data for auto ingest jobs... +DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file... +DeleteCaseTask.progress.lockingInputDir=Acquiring exclusive lock on manifest {0} +# {0} - manifest file name +DeleteCaseTask.progress.lockingManifestFile=Acquiring exclusive lock on manifest {0}... # {0} - manifest file path -DeleteCaseTask.progress.releasingManifestLock=Releasing the exclusive lock on manifest file {0} -DeleteCaseTask.progress.releasingManifestLocks=Acquiring exclusive manifest file locks +DeleteCaseTask.progress.releasingManifestLock=Releasing the exclusive lock on manifest file {0}... +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) HINT_CasesDashboardTopComponent=This is an adminstrative dashboard for multi-user cases OpenAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted. diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputTask.java index bd2c38fa14..518e92265c 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputTask.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputTask.java @@ -38,19 +38,24 @@ final class DeleteCaseInputAndOutputTask extends DeleteCaseTask { } @Override - void deleteWhileHoldingAllLocks() { + void deleteWhileHoldingAllLocks() throws InterruptedException { deleteInputDirectories(); deleteCaseOutput(); } @Override - void deleteAfterCaseLocksReleased() { - deleteCaseLockNodes(); + void deleteAfterManifestLocksReleased() throws InterruptedException { + deleteManifestFileLockNodes(); + } + + @Override + void deleteAfterCaseDirectoryLockReleased() throws InterruptedException { + this.deleteCaseDirectoryLockNode(); } @Override - void deleteAfterAllLocksReleased() { - deleteInputDirectoryLockNodes(); + void deleteAfterCaseNameLockReleased() throws InterruptedException { + this.deleteCaseNameLockNode(); } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputTask.java index 0dd1ff7837..38bb494235 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputTask.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputTask.java @@ -24,18 +24,18 @@ import org.sleuthkit.autopsy.progress.ProgressIndicator; /** * 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 - * and the rest of the case intact. The use case is freeing space while - * retaining the option to restore the input directories, effectively restoring - * the case. + * (manifest file lock nodes) and the rest of the case intact. The use case is + * freeing space while retaining the option to restore the input directories, + * effectively restoring the case. */ 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 - * coordination service nodes and the rest of the case intact. The use case - * is freeing space while retaining the option to restore the input - * directories, effectively restoring the case. + * coordination service nodes (manifest file lock nodes) and the rest of the + * case intact. The use case is freeing space while retaining the option to + * restore the input directories, effectively restoring the case. * * @param caseNodeData The case directory lock coordination service node * data for the case. @@ -46,16 +46,20 @@ final class DeleteCaseInputTask extends DeleteCaseTask { } @Override - void deleteWhileHoldingAllLocks() { + void deleteWhileHoldingAllLocks() throws InterruptedException { + } + + @Override + void deleteAfterCaseDirectoryLockReleased() throws InterruptedException { + } + + @Override + void deleteAfterCaseNameLockReleased() throws InterruptedException { deleteInputDirectories(); } @Override - void deleteAfterCaseLocksReleased() { - } - - @Override - void deleteAfterAllLocksReleased() { + void deleteAfterManifestLocksReleased() throws InterruptedException { } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java index b421e74fa5..bfcae6b39b 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java @@ -18,12 +18,7 @@ */ package org.sleuthkit.autopsy.experimental.autoingest; -import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.Collection; -import javax.swing.AbstractAction; import org.openide.util.NbBundle; -import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; import org.sleuthkit.autopsy.progress.ProgressIndicator; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputTask.java index e1d7921904..7b20dea7f8 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputTask.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputTask.java @@ -45,17 +45,22 @@ final class DeleteCaseOutputTask extends DeleteCaseTask { } @Override - void deleteWhileHoldingAllLocks() { + void deleteWhileHoldingAllLocks() throws InterruptedException { deleteCaseOutput(); } @Override - void deleteAfterCaseLocksReleased() { + void deleteAfterCaseDirectoryLockReleased() throws InterruptedException { } @Override - void deleteAfterAllLocksReleased() { - deleteInputDirectoryLockNodes(); + void deleteAfterManifestLocksReleased() throws InterruptedException { + deleteManifestFileLockNodes(); + } + + @Override + void deleteAfterCaseNameLockReleased() throws InterruptedException { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java index 815ab3e8fb..295bd0dc3e 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.experimental.autoingest; import java.io.File; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -29,138 +30,74 @@ import java.util.logging.Level; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CaseActionException; import org.sleuthkit.autopsy.casemodule.CaseMetadata; 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.CategoryNode; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.progress.ProgressIndicator; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.TimeStampUtils; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobNodeData.InvalidDataException; /** - * A base class for tasks that delete part or all of a case produced via auto - * ingest. + * A base class for tasks that delete part or all of a given case. */ abstract class DeleteCaseTask implements Runnable { - private static final String RESOURCES_LOCK_SUFFIX = "_resources"; //NON-NLS private static final Logger logger = AutoIngestDashboardLogger.getLogger(); private final CaseNodeData caseNodeData; + private final String caseDisplayName; + private final String caseUniqueName; + private final Path caseDirectoryPath; private final ProgressIndicator progress; private final List nodeDataForAutoIngestJobs; - private final Map manifestFileLocks; + private final Map manifestFileLocks; private CoordinationService coordinationService; /** * 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 - * data for the case to be deleted. + * data for the case. * @param progress A progress indicator. */ DeleteCaseTask(CaseNodeData caseNodeData, ProgressIndicator progress) { this.caseNodeData = caseNodeData; 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.manifestFileLocks = new HashMap<>(); } @Override @NbBundle.Messages({ - "DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service", - "DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock", - "DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock", - "DeleteCaseTask.progress.gettingJobNodeData=Getting node data for auto ingest jobs", - "DeleteCaseTask.progress.acquiringInputDirLocks=Acquiring exclusive input directory locks" + "DeleteCaseTask.progress.startMessage=Preparing for deletion..." }) public void run() { try { - progress.start(Bundle.DeleteCaseTask_progress_connectingToCoordSvc()); - try { - coordinationService = CoordinationService.getInstance(); - } catch (CoordinationService.CoordinationServiceException ex) { - 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(); + progress.start(Bundle.DeleteCaseTask_progress_startMessage()); + logger.log(Level.INFO, String.format("Beginning deletion of %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath)); + deleteCase(); + logger.log(Level.SEVERE, String.format("Deletion of %s (%s) in %s completed", caseDisplayName, caseUniqueName, caseDirectoryPath)); } 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, 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 { 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 - * 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. + * Deletes part or all of the given case. */ @NbBundle.Messages({ - "DeleteCaseTask.progress.deletingInputDirs=Deleting input directory", - "# {0} - input directory name", "DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0}" + "DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service...", + "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..." }) - protected void deleteInputDirectories() { - progress.progress(Bundle.DeleteCaseTask_progress_deletingInputDirs()); - logger.log(Level.INFO, String.format("Deleting input directories for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); - for (AutoIngestJobNodeData jobNodeData : nodeDataForAutoIngestJobs) { - final Path inputDirPath = jobNodeData.getManifestFilePath().getParent(); - 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())); - 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())); - // RJCTODO: Update deletion flags + 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 case directory, the case database, and the text index for the - * case. Should only be called when holding all of the exclusive locks for - * the case. + * 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({ - "DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file", - "DeleteCaseTask.progress.deletingCaseOutput=Deleting case output" + "# {0} - input directory name", "DeleteCaseTask.progress.deletingInputDir=Deleting input directory {0}..." + }) + protected void deleteInputDirectories() { + boolean allInputDirsDeleted = true; + for (AutoIngestJobNodeData jobNodeData : nodeDataForAutoIngestJobs) { + Path inputDirPath = jobNodeData.getManifestFilePath().getParent(); + File inputDir = inputDirPath.toFile(); + if (inputDir.exists()) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingInputDir(inputDirPath)); + 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()))) { + logger.log(Level.WARNING, String.format("Failed to delete the input directory %s for %s (%s) in %s", inputDirPath, caseDisplayName, caseUniqueName, caseDirectoryPath)); + allInputDirsDeleted = false; + } + } + } + if (allInputDirsDeleted) { + setDeletedItemFlag(CaseNodeData.DeletedFlags.DATA_SOURCES); + } + } + + /** + * Deletes the case database, the text index, and the case directory 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({ + "DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file...", + "DeleteCaseTask.progress.deletingCaseOutput=Deleting case database, text index, and directory...", + "DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest job log lock node..." }) protected void deleteCaseOutput() { 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())); - String metadataFilePath = null; - final File caseDirectory = caseNodeData.getDirectory().toFile(); + logger.log(Level.INFO, String.format("Locating metadata file for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath)); + CaseMetadata caseMetadata = null; + final File caseDirectory = caseDirectoryPath.toFile(); final File[] filesInDirectory = caseDirectory.listFiles(); if (filesInDirectory != null) { for (File file : filesInDirectory) { if (file.getName().toLowerCase().endsWith(CaseMetadata.getFileExtension()) && file.isFile()) { - metadataFilePath = file.getPath(); + try { + caseMetadata = new CaseMetadata(Paths.get(file.getPath())); + } catch (CaseMetadata.CaseMetadataException ex) { + logger.log(Level.WARNING, String.format("Error getting opening case metadata file for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex); + } + break; } } } - if (metadataFilePath != null) { + if (caseMetadata != 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 { - Case.deleteCase(new CaseMetadata(Paths.get(metadataFilePath)), false, progress); - } catch (CaseMetadata.CaseMetadataException | CaseActionException ex) { - // RJCTODO: Set delete flags? - logger.log(Level.WARNING, String.format("Error deleting output for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); - } + logger.log(Level.INFO, String.format("Deleting output for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath)); + Case.deleteMultiUserCase(caseNodeData, caseMetadata, progress); // RJCTODO: Make this method throw the interrupted exception. } 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 + logger.log(Level.WARNING, String.format("Failed to locate metadata file for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath)); } - } - /** - * Deletes the case name, case directory, case resources, and case auto - * ingest log for the case. Should only be called when holding the exclusive - * locks for all of the input directories for the case. - */ - @Messages({ - "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()); - 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())); - Path logFilePath = AutoIngestJobLogger.getLogPath(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(caseDirectoryPath); //RJCTODO: USe util here try { coordinationService.deleteNode(CategoryNode.CASES, logFilePath.toString()); } 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); - // RJCTODO: Set delete flags - } - - 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 + logger.log(Level.WARNING, String.format("Error deleting case auto ingest job log lock node for %s (%s) in %s", caseDisplayName, caseUniqueName, caseDirectoryPath), ex); + } catch (InterruptedException ex) { + logger.log(Level.INFO, String.format("Deletion of %s (%s) in %s cancelled", caseDisplayName, caseUniqueName, caseDirectoryPath), ex); } } /** - * Deletes the input directory lock nodes for the case. Should only be - * called after releasing all of the locks for the case. + * Deletes the manifest file lock coordination service nodes for the case. + * Intended to be called by subclasses, if required, in their customization + * of the deleteAfterManifestLocksReleased step of the case deletion + * algorithm. */ @Messages({ - "DeleteCaseTask.progress.deletingInputDirLockNodes=Deleting input directory lock nodes" + "DeleteCaseTask.progress.deletingManifestFileLockNodes=Deleting manifest file lock nodes..." }) - protected void deleteInputDirectoryLockNodes() { - progress.progress(Bundle.DeleteCaseTask_progress_deletingInputDirLockNodes()); - logger.log(Level.INFO, String.format("Deleting input directory lock nodes for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); + protected void deleteManifestFileLockNodes() throws InterruptedException { + boolean allInputDirsDeleted = true; + 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) { 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()); } catch (CoordinationServiceException ex) { - Path inputDirPath = jobNodeData.getManifestFilePath().getParent(); - 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); - // RJCTODO: Set delete flags + logger.log(Level.WARNING, String.format("Error deleting manifest file lock node %s for %s (%s) in %s", jobNodeData.getManifestFilePath(), caseDisplayName, caseUniqueName, caseDirectoryPath), ex); + allInputDirsDeleted = false; } } + 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 - * service node data for a case. + * Deletes the case name coordination service lock node for the 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. - * - * @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. + * @throws InterruptedException */ - private void getAutoIngestJobNodeData() throws CoordinationServiceException { - String caseName = caseNodeData.getName(); - final List nodes = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS); - for (String nodeName : nodes) { + @Messages({ + "DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock node..." // RJCTODO: Use consistent terminology + }) + 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 nodeNames = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS); + for (String nodeName : nodeNames) { try { byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName); 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; } AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes); if (caseName.equals(nodeData.getCaseName())) { nodeDataForAutoIngestJobs.add(nodeData); } - } catch (CoordinationService.CoordinationServiceException | InterruptedException | InvalidDataException ex) { - logger.log(Level.WARNING, String.format("Failed to get coordination service node data for %s", nodeName), ex); + } catch (CoordinationService.CoordinationServiceException | InvalidDataException 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. - * - * @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. + * Acquires either all or none of the manifest file locks for a case. */ @NbBundle.Messages({ - "# {0} - input directory name", "DeleteCaseTask.progress.lockingInputDir=Acquiring exclusive lock on input directory {0}",}) - private void getInputDirectoryLocks() { + "# {0} - manifest file name", "DeleteCaseTask.progress.lockingManifestFile=Acquiring exclusive lock on manifest {0}..." + }) + private void getManifestFileLocks() { for (AutoIngestJobNodeData autoIngestJobNodeData : nodeDataForAutoIngestJobs) { - final Path inputDirPath = autoIngestJobNodeData.getManifestFilePath().getParent(); + String manifestPath = autoIngestJobNodeData.getManifestFilePath().toString(); try { - progress.progress(Bundle.DeleteCaseTask_progress_lockingInputDir(inputDirPath)); - final CoordinationService.Lock inputDirLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, autoIngestJobNodeData.getManifestFilePath().toString()); + progress.progress(Bundle.DeleteCaseTask_progress_lockingManifestFile(manifestPath)); + 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) { - manifestFileLocks.put(autoIngestJobNodeData.getManifestFilePath(), inputDirLock); + manifestFileLocks.put(manifestPath, inputDirLock); } 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())); - releaseInputDirectoryLocks(); + logger.log(Level.INFO, String.format("Failed to exclusively lock the manifest %s for %s (%s) in %s", manifestPath, caseDisplayName, caseUniqueName, caseDirectoryPath)); + releaseManifestFileLocks(); manifestFileLocks.clear(); + break; } } 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); - releaseInputDirectoryLocks(); + logger.log(Level.SEVERE, String.format("Error exclusively locking the manifest %s for %s (%s) in %s", manifestPath, caseDisplayName, caseUniqueName, caseDirectoryPath), ex); + releaseManifestFileLocks(); manifestFileLocks.clear(); + break; } } } @@ -396,23 +501,36 @@ abstract class DeleteCaseTask implements Runnable { * for the case. */ @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()) { - progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLocks()); - for (Map.Entry entry : manifestFileLocks.entrySet()) { - final Path manifestFilePath = entry.getKey(); - final CoordinationService.Lock manifestFileLock = entry.getValue(); + for (Map.Entry entry : manifestFileLocks.entrySet()) { + String manifestFilePath = entry.getKey(); + CoordinationService.Lock manifestFileLock = entry.getValue(); try { 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(); } 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); + } + } + }