diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index dbd5215554..5033adb0ad 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -32,6 +32,8 @@ Case.exceptionMessage.couldNotSaveCaseMetadata=Failed to save case metadata:\n{0 Case.exceptionMessage.couldNotSaveDbNameToMetadataFile=Failed to save case database name to case metadata file:\n{0}. # {0} - exception message Case.exceptionMessage.couldNotUpdateCaseNodeData=Failed to update coordination service node data:\n{0}. +# {0} - case display name +Case.exceptionMessage.deletionInterrupted=Deletion of the case {0} was cancelled. Case.exceptionMessage.emptyCaseDir=Must specify a case directory path. Case.exceptionMessage.emptyCaseName=Must specify a case name. Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the log for details. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index efadae5fb7..c60d3df8b3 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -81,6 +81,7 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceNameChangedEvent; import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseCoordinationServiceUtils; import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributeSearchAction; import org.sleuthkit.autopsy.communications.OpenCommVisualizationToolAction; @@ -140,8 +141,7 @@ public class Case { private static final String MODULE_FOLDER = "ModuleOutput"; //NON-NLS private static final String CASE_ACTION_THREAD_NAME = "%s-case-action"; private static final String CASE_RESOURCES_THREAD_NAME = "%s-manage-case-resources"; - private static final String RESOURCES_LOCK_SUFFIX = "_resources"; //NON-NLS - private static final String AUTO_INGEST_LOG_FILE_NAME = "auto_ingest_log.txt"; + private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode"; private static final Logger logger = Logger.getLogger(Case.class.getName()); private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher(); private static final Object caseActionSerializationLock = new Object(); @@ -1022,13 +1022,16 @@ public class Case { try { deleteCaseResourcesLockNode(caseNodeData, progressIndicator); - } catch (CoordinationServiceException | InterruptedException ex) { - errorsOccurred = true; + } catch (CoordinationServiceException ex) { + if (!isNoNodeException(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) { 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); - } + } // RJCTODO: Is this behavior implemented correctly? - } catch (CoordinationServiceException ex) { logger.log(Level.SEVERE, String.format("Error exclusively locking the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); @@ -1071,7 +1074,8 @@ public class Case { * during a wait. */ @Beta - public static boolean deleteMultiUserCase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws InterruptedException { + public static boolean deleteMultiUserCase(CaseNodeData caseNodeData, CaseMetadata metadata, + ProgressIndicator progressIndicator, Logger logger) throws InterruptedException { boolean errorsOccurred = false; try { deleteCaseDatabase(caseNodeData, metadata, progressIndicator, logger); @@ -1201,9 +1205,6 @@ 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()); logger.log(Level.INFO, String.format("Deleting case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) { @@ -1273,7 +1274,7 @@ public class Case { @Beta public 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 + String resourcesLockNodePath = CaseCoordinationServiceUtils.getCaseResourcesLockName(caseNodeData.getDirectory()); CoordinationService coordinationService = CoordinationService.getInstance(); coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath); } @@ -1306,6 +1307,24 @@ public class Case { coordinationService.deleteNode(CategoryNode.CASES, caseDirectoryLockNodePath); } + /** + * Examines a coordination service exception to try to determine if it is a + * no node exception. + * + * @param ex A coordination service exception. + * + * @return True or false. + */ + private static boolean isNoNodeException(CoordinationServiceException ex) { + boolean isNodeNodeEx = false; + Throwable cause = ex.getCause(); + if (cause != null) { + String causeMessage = cause.getMessage(); + isNodeNodeEx = causeMessage.contains(NO_NODE_ERROR_MSG_FRAGMENT); + } + return isNodeNodeEx; + } + /** * Sets a deleted item flag in the coordination service node data for a * multi-user case. @@ -1342,7 +1361,8 @@ public class Case { }) private static CoordinationService.Lock acquireExclusiveCaseResourcesLock(String caseDir) throws CaseActionException { try { - String resourcesNodeName = caseDir + RESOURCES_LOCK_SUFFIX; + Path caseDirPath = Paths.get(caseDir); + String resourcesNodeName = CaseCoordinationServiceUtils.getCaseResourcesLockName(caseDirPath); Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, resourcesNodeName, RESOURCES_LOCK_TIMOUT_HOURS, TimeUnit.HOURS); return lock; } catch (InterruptedException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index abd2edbccf..c84fcc2cee 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -95,13 +95,13 @@ public final class CaseMetadata { private final static String EXAMINER_ELEMENT_PHONE = "ExaminerPhone"; //NON-NLS private final static String EXAMINER_ELEMENT_EMAIL = "ExaminerEmail"; //NON-NLS private final static String CASE_ELEMENT_NOTES = "CaseNotes"; //NON-NLS - + /* * Fields from schema version 5 */ private static final String SCHEMA_VERSION_FIVE = "5.0"; private final static String ORIGINAL_CASE_ELEMENT_NAME = "OriginalCase"; //NON-NLS - + /* * Unread fields, regenerated on save. */ @@ -138,16 +138,39 @@ public final class CaseMetadata { public static DateFormat getDateFormat() { return new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US); } + + /** + * Locate the case meta data file in the supplied directory. If the file + * does not exist, null is returned. + * + * @param directoryPath Directory path to search + * + * @return case meta data file path or null + */ + // RJCTODO: Perhaps this should return a CaseMetadata object + // RJCTODO: It should say get...path + public static Path getCaseMetadataFile(Path directoryPath) { + final File[] files = directoryPath.toFile().listFiles(); + if (files != null) { + for (File file : files) { + final String fileName = file.getName().toLowerCase(); + if (fileName.endsWith(CaseMetadata.getFileExtension()) && file.isFile()) { + return file.toPath(); + } + } + } + return null; + } /** * Constructs a CaseMetadata object for a new case. The metadata is not * persisted to the case metadata file until writeFile or a setX method is * called. * - * @param caseType The type of case. - * @param caseDirectory The case directory. - * @param caseName The immutable name of the case. - * @param caseDetails The details for the case + * @param caseType The type of case. + * @param caseDirectory The case directory. + * @param caseName The immutable name of the case. + * @param caseDetails The details for the case */ CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails) { this(caseType, caseDirectory, caseName, caseDetails, null); @@ -158,11 +181,11 @@ public final class CaseMetadata { * persisted to the case metadata file until writeFile or a setX method is * called. * - * @param caseType The type of case. - * @param caseDirectory The case directory. - * @param caseName The immutable name of the case. - * @param caseDetails The details for the case - * @param originalMetadata The metadata object from the original case + * @param caseType The type of case. + * @param caseDirectory The case directory. + * @param caseName The immutable name of the case. + * @param caseDetails The details for the case + * @param originalMetadata The metadata object from the original case */ CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails, CaseMetadata originalMetadata) { metadataFilePath = Paths.get(caseDirectory, caseDetails.getCaseDisplayName() + FILE_EXTENSION); @@ -190,27 +213,6 @@ public final class CaseMetadata { this.metadataFilePath = metadataFilePath; readFromFile(); } - - /** - * Locate the case meta data file in the supplied directory. If the file does - * not exist, null is returned. - * - * @param directoryPath Directory path to search - * @return case meta data file path or null - */ - public static Path getCaseMetadataFile(Path directoryPath) { - final File[] caseFiles = directoryPath.toFile().listFiles(); - if(caseFiles != null) { - for (File file : caseFiles) { - final String fileName = file.getName().toLowerCase(); - if (fileName.endsWith(CaseMetadata.getFileExtension())) { - return file.toPath(); - } - } - } - - return null; - } /** * Gets the full path to the case metadata file. @@ -460,7 +462,7 @@ public final class CaseMetadata { * Create the children of the case element. */ createCaseElements(doc, caseElement, this); - + /* * Add original case element */ @@ -472,15 +474,15 @@ public final class CaseMetadata { originalCaseElement.appendChild(originalCaseDetailsElement); createCaseElements(doc, originalCaseDetailsElement, originalMetadata); } - + } - + /** * Write the case element children for the given metadata object - * - * @param doc The document. - * @param caseElement The case element parent - * @param metadataToWrite The CaseMetadata object to read from + * + * @param doc The document. + * @param caseElement The case element parent + * @param metadataToWrite The CaseMetadata object to read from */ private void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite) { CaseDetails caseDetailsToWrite = metadataToWrite.caseDetails; @@ -572,8 +574,8 @@ public final class CaseMetadata { examinerEmail = getElementTextContent(caseElement, EXAMINER_ELEMENT_EMAIL, false); caseNotes = getElementTextContent(caseElement, CASE_ELEMENT_NOTES, false); } - - this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, + + this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, caseNotes); this.caseType = Case.CaseType.fromString(getElementTextContent(caseElement, CASE_TYPE_ELEMENT_NAME, true)); if (null == this.caseType) { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java index 35414ed629..4cffe6a0a2 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java @@ -289,8 +289,8 @@ public final class CaseNodeData { CASE_DB(2), CASE_DIR(4), DATA_SOURCES(8), - MANIFEST_FILE_LOCK_NODES(16); - + MANIFEST_FILE_NODES(16); + private final short value; private DeletedFlags(int value) { 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 6f24fde4f6..c4801e56ed 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED @@ -5,6 +5,8 @@ MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Delet MultiUserCaseBrowserCustomizer.column.directory=Directory MultiUserCaseBrowserCustomizer.column.displayName=Name MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time -MultiUserCaseBrowserCustomizer.column.manifestCoordSvcNodesDeleteStatus=Manifest ZooKeeper Node Deleted +MultiUserCaseBrowserCustomizer.column.manifestFileZNodesDeleteStatus=Manifest Znodes Deleted MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted +MultiUserCaseNode.column.createTime=False +MultiUserCaseNode.columnValue.true=True MultiUserCasesBrowserPanel.waitNode.message=Please Wait... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java index a9237aa7a1..11e73cfb12 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java @@ -78,7 +78,7 @@ final class MultiUserCaseNode extends AbstractNode { sheetSet.put(new NodeProperty<>(propName, propName, propName, caseNodeData.getLastAccessDate())); break; case MANIFEST_FILE_ZNODES_DELETE_STATUS: - sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.MANIFEST_FILE_LOCK_NODES))); + sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.MANIFEST_FILE_NODES))); break; case DATA_SOURCES_DELETE_STATUS: sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.DATA_SOURCES))); diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java b/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java index 4c7c022871..0a76b2a7fa 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2013-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,19 @@ public final class ExecUtil { } /** - * Process terminator that can be used to kill a processes after it exceeds + * A process terminator that can be used to kill a process spawned by a + * thread that has been interrupted. + */ + public static class InterruptedThreadProcessTerminator implements ProcessTerminator { + + @Override + public boolean shouldTerminateProcess() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * A process terminator that can be used to kill a process after it exceeds * a maximum allowable run time. */ public static class TimedProcessTerminator implements ProcessTerminator { @@ -212,9 +224,6 @@ public final class ExecUtil { } } - /** - * EVERYTHING FOLLOWING THIS LINE IS DEPRECATED AND SLATED FOR REMOVAL - */ private static final Logger logger = Logger.getLogger(ExecUtil.class.getName()); private Process proc = null; private ExecUtil.StreamToStringRedirect errorStringRedirect = null; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index d24fb46edf..a96e77f623 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -1211,6 +1211,18 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen */ try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString(), INPUT_SCAN_LOCKING_TIMEOUT_MINS, TimeUnit.MINUTES)) { if (null != manifestLock) { + + /* + * Now that the lock has been acquired, make sure the + * manifest is still here. This is a way to resolve the + * race condition between this task and case deletion + * tasks without resorting to a protocol using locking + * of the input directory. + */ + if (!filePath.toFile().exists()) { + return CONTINUE; + } + byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString()); if (null != rawData && rawData.length > 0) { AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(rawData); 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 189e72954a..251412f530 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED @@ -8,9 +8,6 @@ AinStatusNode.status.shuttingdown=Shutting Down AinStatusNode.status.startingup=Starting Up AinStatusNode.status.title=Status AinStatusNode.status.unknown=Unknown -ArchiveDSP.dsType.text=Archive file -ArchiveFilePanel.moduleErr=Module Error -ArchiveFilePanel.moduleErr.msg=A module caused an error listening to ArchiveFilePanel updates. See log to determine which module. Some data could be incomplete.\n AutoIngestAdminActions.cancelJobAction.title=Cancel Job AutoIngestAdminActions.cancelModuleAction.title=Cancel Module AutoIngestAdminActions.deleteCaseAction.error=Failed to delete case. @@ -169,43 +166,44 @@ CTL_AutoIngestDashboardOpenAction=Auto Ingest Dashboard CTL_AutoIngestDashboardTopComponent=Auto Ingest Jobs CTL_CasesDashboardAction=Multi-User Cases Dashboard CTL_CasesDashboardTopComponent=Cases -DataSourceOnCDriveError.noOpenCase.errMsg=Warning: Exception while getting open case. -DataSourceOnCDriveError.text=Warning: Path to multi-user data source is on "C:" drive +DeleteCaseInputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n-Manifest files\n-Data sources\n-Input directories (if empty) DeleteCaseInputAction.menuItemText=Delete Input DeleteCaseInputAction.progressDisplayName=Delete Input DeleteCaseInputAction.taskName=input +DeleteCaseInputAndOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n-Manifest files\n-Data sources\n-Input directories (if empty)\n-Manifest file znodes\n-Case database\n-Core.properties file\n-Case directory\n-Case znodes DeleteCaseInputAndOutputAction.menuItemText=Delete Input and Output DeleteCaseInputAndOutputAction.progressDisplayName=Delete Input and Output DeleteCaseInputAndOutputAction.taskName=input-and-output +DeleteCaseOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n-Manifest file znodes\n-Case database\n-Core.properties file\n-Case directory\n-Case znodes DeleteCaseOutputAction.menuItemText=Delete Output DeleteCaseOutputAction.progressDisplayName=Delete Output DeleteCaseOutputAction.taskName=output -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.acquiringManifestFileLocks=Acquiring exclusive manifest file locks... +DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock... +DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock... DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks... DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service... -DeleteCaseTask.progress.deletingCaseOutput=Deleting case database, text index, and directory... +DeleteCaseTask.progress.deletingCaseDirCoordSvcNode=Deleting case directory znode... +DeleteCaseTask.progress.deletingCaseNameCoordSvcNode=Deleting case name znode... DeleteCaseTask.progress.deletingDirLockNode=Deleting case directory lock coordination service node... -# {0} - input directory name -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 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... -DeleteCaseTask.progress.lockingInputDir=Acquiring exclusive lock on manifest {0} -# {0} - manifest file name -DeleteCaseTask.progress.lockingManifestFile=Acquiring exclusive lock on manifest {0}... +DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest log znode... # {0} - manifest file path -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) +DeleteCaseTask.progress.deletingManifest=Deleting manifest file {0}... +# {0} - manifest file path +DeleteCaseTask.progress.deletingManifestFileNode=Deleting the manifest file znode for {0}... +DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock coordination service node... +DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources znode... +DeleteCaseTask.Progress.gettingManifestPaths=Getting manifest file paths... +DeleteCaseTask.progress.gettingManifestPaths=Getting manifest file paths... +DeleteCaseTask.progress.locatingCaseMetadataFile=Opening case metadata file... +# {0} - manifest file path +DeleteCaseTask.progress.lockingManifest=Locking manifest file {0}... +DeleteCaseTask.progress.openingCaseDatabase=Opening the case database... +DeleteCaseTask.progress.openingCaseMetadataFile=Opening case metadata file... +# {0} - manifest file path +DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}... +# {0} - manifest file path +DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}... +DeleteCaseTask.progress.startMessage=Starting deletion... HINT_CasesDashboardTopComponent=This is an adminstrative dashboard for multi-user cases OpenAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted. OpenAutoIngestLogAction.logOpenFailedErrorMsg=Failed to open case auto ingest log. See application log for details. @@ -406,10 +404,6 @@ AutoIngestMetricsDialog.reportTextArea.text= AutoIngestMetricsDialog.metricsButton.text=Generate Metrics Report AutoIngestMetricsDialog.closeButton.text=Close AutoIngestMetricsDialog.datePicker.toolTipText=Choose a date -ArchiveFilePanel.pathLabel.text=Browse for an archive file: -ArchiveFilePanel.browseButton.text=Browse -ArchiveFilePanel.pathTextField.text= -ArchiveFilePanel.errorLabel.text=Error Label AutoIngestMetricsDialog.startingDataLabel.text=Starting Date: AutoIngestControlPanel.bnDeprioritizeCase.text=Deprioritize Case AutoIngestControlPanel.bnDeprioritizeJob.text=Deprioritize Job diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAction.java index 5ea4c5cc02..64a8a4c811 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAction.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAction.java @@ -18,9 +18,11 @@ */ package org.sleuthkit.autopsy.experimental.autoingest; +import java.awt.event.ActionEvent; import java.util.concurrent.ExecutorService; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions; import org.sleuthkit.autopsy.progress.ProgressIndicator; @@ -46,17 +48,27 @@ final class DeleteCaseInputAction extends DeleteCaseAction { @NbBundle.Messages({ "DeleteCaseInputAction.menuItemText=Delete Input", "DeleteCaseInputAction.progressDisplayName=Delete Input", - "DeleteCaseInputAction.taskName=input" + "DeleteCaseInputAction.taskName=input" }) DeleteCaseInputAction() { super(Bundle.DeleteCaseInputAction_menuItemText(), Bundle.DeleteCaseInputAction_progressDisplayName(), Bundle.DeleteCaseInputAction_taskName()); } + @NbBundle.Messages({ + "DeleteCaseInputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n-Manifest files\n-Data sources\n-Input directories (if empty)" + }) + @Override + public void actionPerformed(ActionEvent event) { + if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseInputAction_confirmationText())) { + super.actionPerformed(event); + } + } + @Override DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) { return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_INPUT, progress); } - + @Override public DeleteCaseInputAction clone() throws CloneNotSupportedException { super.clone(); diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputAction.java index 7a721ed947..bcfb973ff3 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputAction.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputAction.java @@ -18,8 +18,11 @@ */ package org.sleuthkit.autopsy.experimental.autoingest; +import java.awt.event.ActionEvent; +import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions; import org.sleuthkit.autopsy.progress.ProgressIndicator; @@ -46,9 +49,19 @@ final class DeleteCaseInputAndOutputAction extends DeleteCaseAction { super(Bundle.DeleteCaseInputAndOutputAction_menuItemText(), Bundle.DeleteCaseInputAndOutputAction_progressDisplayName(), Bundle.DeleteCaseInputAndOutputAction_taskName()); } + @NbBundle.Messages({ + "DeleteCaseInputAndOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n-Manifest files\n-Data sources\n-Input directories (if empty)\n-Manifest file znodes\n-Case database\n-Core.properties file\n-Case directory\n-Case znodes" + }) + @Override + public void actionPerformed(ActionEvent event) { + if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseInputAndOutputAction_confirmationText())) { + super.actionPerformed(event); + } + } + @Override DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) { - return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_OUTPUT, progress); + return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_ALL, progress); } @Override diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java index 5b09758d4f..8435addec0 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java @@ -18,8 +18,10 @@ */ package org.sleuthkit.autopsy.experimental.autoingest; +import java.awt.event.ActionEvent; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions; import org.sleuthkit.autopsy.progress.ProgressIndicator; @@ -50,6 +52,16 @@ final class DeleteCaseOutputAction extends DeleteCaseAction { super(Bundle.DeleteCaseOutputAction_menuItemText(), Bundle.DeleteCaseOutputAction_progressDisplayName(), Bundle.DeleteCaseOutputAction_taskName()); } + @NbBundle.Messages({ + "DeleteCaseOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n-Manifest file znodes\n-Case database\n-Core.properties file\n-Case directory\n-Case znodes" + }) + @Override + public void actionPerformed(ActionEvent event) { + if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseOutputAction_confirmationText())) { + super.actionPerformed(event); + } + } + @Override DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) { return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_OUTPUT, progress); diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java index 0451c906cd..d456f9d64c 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java @@ -23,14 +23,17 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Scanner; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.logging.Level; -import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; @@ -39,62 +42,53 @@ import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.progress.ProgressIndicator; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobNodeData.InvalidDataException; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.TskCoreException; /** - * A task that deletes part or all of a given case. Note that all logging done - * by this task is directed to the dedicated auto ingest dashboard log instead - * of to the general application log. + * A task that deletes part or all of a given case. Note that all logging is + * directed to the dedicated auto ingest dashboard log instead of to the general + * application log. */ -// RJCTODO: -// 1. Expand case type in case metadata to include auto ingest cases. -// Disable the delete menu item in the main app menu for auto ingest cases, -// and possibly also use this to delete the add data source capability. Could use -// this to limit the display of nodes in the in the auto ingest cases dashboard. -// 2. When an instance of this class finishes, publish an event via event bus -// so that the case browser can refresh. -// 3. Add code to file deletion utilities such that on Wimdows, for paths -// exceeding 255 chars, robocopy is invoked for the deletion. Make the new file -// deletion utility throw exceptions instead of return a boolean result code. -// 4. Make other dashbaord use the dashboard logger. -// 5. Consider moving all of the dashboard code into its own autoingest.dashboard package. -// 6. AutoIngestManager.addCompletedJob node data version updating might be out of date. -// 7. Deal with cancellation during lock releases. Look at using -// https://google.github.io/guava/releases/19.0/api/docs/com/google/common/util/concurrent/Uninterruptibles.html -// getUninterruptibly to do os. -// 8. With the removal of the auto ingest control panel, we can eliminate the -// completed jobs list and the processing list from AutoIngestManager. final class DeleteCaseTask implements Runnable { private static final int MANIFEST_FILE_LOCKING_TIMEOUT_MINS = 5; + private static final int MANIFEST_DELETE_TRIES = 3; + private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode"; private static final Logger logger = AutoIngestDashboardLogger.getLogger(); private final CaseNodeData caseNodeData; private final DeleteOptions deleteOption; private final ProgressIndicator progress; + private final List manifestFilePaths; private final List manifestFileLocks; private CoordinationService coordinationService; + private CaseMetadata caseMetadata; /* - * Options to support implementing differnet case deletion uses cases. + * Options to support implementing different case deletion use cases. */ - public enum DeleteOptions { + enum DeleteOptions { /** * Delete the auto ingest job manifests and corresponding data sources, - * if any, while leaving the manifest file coordination service nodes - * and the rest of the case intact. The use case is freeing auto ingest - * input directory space while retaining the option to restore the data + * while leaving the manifest file coordination service nodes and the + * rest of the case intact. The use case is freeing auto ingest input + * directory space while retaining the option to restore the data * sources, effectively restoring the case. */ DELETE_INPUT, /** - * Delete the auto ingest job coordination service nodes, if any, and - * the output for a case produced via auto ingest, while leaving the - * auto ingest job input directories intact. The use case is auto ingest + * Delete the manifest file coordination service nodes and the output + * for a case, while leaving the auto ingest job manifests and + * corresponding data sources intact. The use case is auto ingest * reprocessing of a case with a clean slate without having to restore - * the input directories. + * the manifests and data sources. */ DELETE_OUTPUT, /** @@ -117,7 +111,8 @@ final class DeleteCaseTask implements Runnable { this.caseNodeData = caseNodeData; this.deleteOption = deleteOption; this.progress = progress; - this.manifestFileLocks = new ArrayList<>(); + manifestFilePaths = new ArrayList<>(); + manifestFileLocks = new ArrayList<>(); } @Override @@ -127,9 +122,9 @@ final class DeleteCaseTask implements Runnable { public void run() { try { progress.start(Bundle.DeleteCaseTask_progress_startMessage()); - logger.log(Level.INFO, String.format("Starting attempt to delete %s (%s)", caseNodeData.getDisplayName(), deleteOption)); + logger.log(Level.INFO, String.format("Starting deleting %s (%s)", caseNodeData.getDisplayName(), deleteOption)); deleteCase(); - logger.log(Level.INFO, String.format("Finished attempt to delete %s (%s)", caseNodeData.getDisplayName(), deleteOption)); + logger.log(Level.INFO, String.format("Finished deleting %s (%s)", caseNodeData.getDisplayName(), deleteOption)); } catch (Throwable ex) { /* @@ -139,6 +134,7 @@ final class DeleteCaseTask implements Runnable { * the task, so this ensures that any such errors get logged. */ logger.log(Level.SEVERE, String.format("Unexpected error deleting %s", caseNodeData.getDisplayName()), ex); + throw ex; } finally { progress.finish(); @@ -152,9 +148,13 @@ final class DeleteCaseTask implements Runnable { "DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service...", "DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock...", "DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock...", + "DeleteCaseTask.progress.gettingManifestPaths=Getting manifest file paths...", "DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks...", - "DeleteCaseTask.progress.deletingDirLockNode=Deleting case directory lock coordination service node...", - "DeleteCaseTask.progress.deletingNameLockNode=Deleting case name lock coordination service node..." + "DeleteCaseTask.progress.openingCaseMetadataFile=Opening case metadata file...", + "DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources znode...", + "DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest log znode...", + "DeleteCaseTask.progress.deletingCaseDirCoordSvcNode=Deleting case directory znode...", + "DeleteCaseTask.progress.deletingCaseNameCoordSvcNode=Deleting case name znode..." }) private void deleteCase() { progress.progress(Bundle.DeleteCaseTask_progress_connectingToCoordSvc()); @@ -165,9 +165,10 @@ final class DeleteCaseTask implements Runnable { logger.log(Level.SEVERE, String.format("Could not delete %s because an error occurred connecting to the coordination service", caseNodeData.getDisplayName()), ex); return; } + logger.log(Level.INFO, String.format("Connected to the coordination service for deletion of %s", caseNodeData.getDisplayName())); if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); return; } @@ -190,9 +191,10 @@ final class DeleteCaseTask implements Runnable { logger.log(Level.INFO, String.format("Could not delete %s because a case name lock was already held by another host", caseNodeData.getDisplayName())); return; } + logger.log(Level.INFO, String.format("Acquired an exclusive case name lock for %s", caseNodeData.getDisplayName())); if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); return; } @@ -205,7 +207,6 @@ final class DeleteCaseTask implements Runnable { * deleted open and prevents another node from trying to open the * case while it is being deleted. */ - boolean success = true; // RJCTODO: Instead of having this flag, read the casenodedata instead progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseDirLock()); logger.log(Level.INFO, String.format("Acquiring an exclusive case directory lock for %s", caseNodeData.getDisplayName())); String caseDirLockName = CaseCoordinationServiceUtils.getCaseDirectoryLockName(caseNodeData.getDirectory()); @@ -214,88 +215,183 @@ final class DeleteCaseTask implements Runnable { logger.log(Level.INFO, String.format("Could not delete %s because a case directory lock was already held by another host", caseNodeData.getDisplayName())); return; } + logger.log(Level.INFO, String.format("Acquired an exclusive case directory lock for %s", caseNodeData.getDisplayName())); if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); return; } - /* - * Acquire exclusive locks for the auto ingest job manifest - * files for the case, if any. Manifest file locks are acquired - * by the auto ingest node (AIN) input directory scanning tasks - * when they look for auto ingest jobs to enqueue, and by the - * AIN job processing tasks when they execute a job. Acquiring - * these locks here ensures that the scanning tasks and job - * processing tasks cannot do anything with the auto ingest jobs - * for a case during case deletion. - */ - progress.progress(Bundle.DeleteCaseTask_progress_acquiringManifestLocks()); - logger.log(Level.INFO, String.format("Acquiring exclusive manifest file locks for %s", caseNodeData.getDisplayName())); + progress.progress(Bundle.DeleteCaseTask_progress_gettingManifestPaths()); + logger.log(Level.INFO, String.format("Getting manifest file paths for %s", caseNodeData.getDisplayName())); try { - if (!acquireManifestFileLocks()) { - logger.log(Level.INFO, String.format("Could not delete %s because a manifest file lock was already held by another host", caseNodeData.getDisplayName())); - return; - } - } catch (CoordinationServiceException ex) { - logger.log(Level.WARNING, String.format("Could not delete %s because an error occurred acquiring the manifest file locks", caseNodeData.getDisplayName()), ex); + getManifestFilePaths(); + } catch (IOException | CoordinationServiceException ex) { + logger.log(Level.SEVERE, String.format("An error occurred getting the manifest file paths", caseNodeData.getDisplayName()), ex); return; } catch (InterruptedException ex) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName()), ex); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); return; } if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); + return; + } + + if (!manifestFilePaths.isEmpty()) { + logger.log(Level.INFO, String.format("Obtained manifest file paths for %s", caseNodeData.getDisplayName())); + /* + * Acquire exclusive locks for the auto ingest job manifest + * files for the case, if any. Manifest file locks are + * acquired by the auto ingest node (AIN) input directory + * scanning tasks when they look for auto ingest jobs to + * enqueue, and by the AIN job execution tasks when they do + * a job. Acquiring these locks here ensures that the + * scanning tasks and job execution tasks cannot do anything + * with the auto ingest jobs for a case during case + * deletion. + */ + progress.progress(Bundle.DeleteCaseTask_progress_acquiringManifestLocks()); + logger.log(Level.INFO, String.format("Acquiring exclusive manifest file locks for %s", caseNodeData.getDisplayName())); + try { + if (!acquireManifestFileLocks()) { + logger.log(Level.INFO, String.format("Could not delete %s because at least one manifest file lock was already held by another host", caseNodeData.getDisplayName())); + return; + } + } catch (IOException | CoordinationServiceException ex) { + logger.log(Level.SEVERE, String.format("Could not delete %s because an error occurred acquiring the manifest file locks", caseNodeData.getDisplayName()), ex); + return; + } catch (InterruptedException ex) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); + return; + } + logger.log(Level.INFO, String.format("Acquired exclusive manifest file locks for %s", caseNodeData.getDisplayName())); + + } else { + logger.log(Level.INFO, String.format("No manifest file paths found for %s", caseNodeData.getDisplayName())); + } + + if (Thread.currentThread().isInterrupted()) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); releaseManifestFileLocks(); return; } - if (deleteOption == DeleteOptions.DELETE_INPUT || deleteOption == DeleteOptions.DELETE_ALL) { + final File caseDirectory = caseNodeData.getDirectory().toFile(); + if (caseDirectory.exists()) { + progress.progress(Bundle.DeleteCaseTask_progress_openingCaseMetadataFile()); + logger.log(Level.INFO, String.format("Locating case metadata file for %s", caseNodeData.getDisplayName())); + Path caseMetadataPath = CaseMetadata.getCaseMetadataFile(caseNodeData.getDirectory()); + if (caseMetadataPath != null) { + logger.log(Level.INFO, String.format("Found case metadata file for %s", caseNodeData.getDisplayName())); + try { + caseMetadata = new CaseMetadata(caseMetadataPath); + + if (Thread.currentThread().isInterrupted()) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); + releaseManifestFileLocks(); + return; + } + + if (!manifestFilePaths.isEmpty() && (deleteOption == DeleteOptions.DELETE_INPUT || deleteOption == DeleteOptions.DELETE_ALL)) { + try { + deleteAutoIngestInput(); + } catch (InterruptedException ex) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); + releaseManifestFileLocks(); + return; + } + } + + if (Thread.currentThread().isInterrupted()) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); + releaseManifestFileLocks(); + return; + } + + if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) { + try { + logger.log(Level.INFO, String.format("Deleting output for %s", caseNodeData.getDisplayName())); + Case.deleteMultiUserCase(caseNodeData, caseMetadata, progress, logger); + logger.log(Level.INFO, String.format("Deleted output for %s", caseNodeData.getDisplayName())); + } catch (InterruptedException ex) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); + releaseManifestFileLocks(); + return; + } + } + + } catch (CaseMetadata.CaseMetadataException ex) { + logger.log(Level.SEVERE, String.format("Error reading metadata file for %s", caseNodeData.getDisplayName()), ex); + } + + } else { + logger.log(Level.WARNING, String.format("No case metadata file found for %s", caseNodeData.getDisplayName())); + } + + } else { + setDeletedItemFlag(CaseNodeData.DeletedFlags.CASE_DIR); + logger.log(Level.INFO, String.format("No case directory found for %s", caseNodeData.getDisplayName())); + } + + if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingResourcesLockNode()); + logger.log(Level.INFO, String.format("Deleting case resources log znode for %s", caseNodeData.getDisplayName())); try { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); - deleteAutoIngestInput(); - } catch (IOException ex) { - // RJCTODO: + Case.deleteCaseResourcesLockNode(caseNodeData, progress); + logger.log(Level.INFO, String.format("Deleted case resources znode for %s", caseNodeData.getDisplayName())); + } catch (CoordinationServiceException ex) { + if (!isNoNodeException(ex)) { + logger.log(Level.SEVERE, String.format("Error deleting case resources znode for %s", caseNodeData.getDisplayName()), ex); + } } catch (InterruptedException ex) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName()), ex); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); + releaseManifestFileLocks(); + return; + } + + if (Thread.currentThread().isInterrupted()) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); + releaseManifestFileLocks(); + return; + } + + progress.progress(Bundle.DeleteCaseTask_progress_deletingJobLogLockNode()); + logger.log(Level.INFO, String.format("Deleting case auto ingest job log znode for %s", caseNodeData.getDisplayName())); + String logFilePath = CaseCoordinationServiceUtils.getCaseAutoIngestLogLockName(caseNodeData.getDirectory()); + try { + coordinationService.deleteNode(CategoryNode.CASES, logFilePath); + logger.log(Level.INFO, String.format("Deleted case auto ingest job log znode for %s", caseNodeData.getDisplayName())); + } catch (CoordinationServiceException ex) { + if (!isNoNodeException(ex)) { + logger.log(Level.SEVERE, String.format("Error deleting case auto ingest job log znode for %s", caseNodeData.getDisplayName()), ex); + } + } catch (InterruptedException ex) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); releaseManifestFileLocks(); return; } } if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); releaseManifestFileLocks(); return; } if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) { try { - success = deleteCaseOutput(); + logger.log(Level.INFO, String.format("Deleting manifest file znodes for %s", caseNodeData.getDisplayName())); + deleteManifestFileNodes(); + logger.log(Level.INFO, String.format("Deleted manifest file znodes for %s", caseNodeData.getDisplayName())); } catch (InterruptedException ex) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName()), ex); - releaseManifestFileLocks(); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); return; } } - if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); - releaseManifestFileLocks(); - return; - } - - try { - if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) { - success = deleteManifestFileNodes(); - } else { - releaseManifestFileLocks(); - } - } catch (InterruptedException ex) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName()), ex); - return; - } + releaseManifestFileLocks(); } catch (CoordinationServiceException ex) { logger.log(Level.SEVERE, String.format("Could not delete %s because an error occurred acquiring the case directory lock", caseNodeData.getDisplayName()), ex); @@ -303,7 +399,7 @@ final class DeleteCaseTask implements Runnable { } if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); return; } @@ -314,14 +410,20 @@ final class DeleteCaseTask implements Runnable { * leave the node so that what was and was not deleted can be * inspected. */ - if (success && (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL)) { - progress.progress(Bundle.DeleteCaseTask_progress_deletingDirLockNode()); + if ((deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.DATA_SOURCES) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES)) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseDirCoordSvcNode()); + logger.log(Level.INFO, String.format("Deleting case directory znode for %s", caseNodeData.getDisplayName())); try { Case.deleteCaseDirectoryLockNode(caseNodeData, progress); + logger.log(Level.INFO, String.format("Deleted case directory znode for %s", caseNodeData.getDisplayName())); } catch (CoordinationServiceException ex) { - logger.log(Level.WARNING, String.format("Error deleting case directory lock node for %s", caseNodeData.getDisplayName()), ex); + logger.log(Level.SEVERE, String.format("Error deleting case directory lock node for %s", caseNodeData.getDisplayName()), ex); } catch (InterruptedException ex) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName()), ex); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); return; } } @@ -332,7 +434,7 @@ final class DeleteCaseTask implements Runnable { } if (Thread.currentThread().isInterrupted()) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName())); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName())); return; } @@ -341,14 +443,109 @@ final class DeleteCaseTask implements Runnable { * service node for it can be deleted if the use case requires it. */ if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_ALL) { - progress.progress(Bundle.DeleteCaseTask_progress_deletingNameLockNode()); + progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseNameCoordSvcNode()); + logger.log(Level.INFO, String.format("Deleting case name znode for %s", caseNodeData.getDisplayName())); try { String caseNameLockNodeName = CaseCoordinationServiceUtils.getCaseNameLockName(caseNodeData.getDirectory()); - coordinationService.deleteNode(CategoryNode.CASES, caseNameLockNodeName); // RJCTODO: Should this be a Case method? + coordinationService.deleteNode(CategoryNode.CASES, caseNameLockNodeName); + logger.log(Level.INFO, String.format("Deleted case name znode for %s", caseNodeData.getDisplayName())); } catch (CoordinationServiceException ex) { - logger.log(Level.WARNING, String.format("Error deleting case name lock node for %s", caseNodeData.getDisplayName()), ex); + logger.log(Level.SEVERE, String.format("Error deleting case name lock node for %s", caseNodeData.getDisplayName()), ex); } catch (InterruptedException ex) { - logger.log(Level.INFO, String.format("Deletion of %s cancelled", caseNodeData.getDisplayName()), ex); + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); + } + } + } + + /** + * Gets the manifest file paths for the case, if there are any. + * + * @throws CoordinationServiceException If there is an error completing a + * coordination service operation. + * @throws InterruptedException If the thread in which this task is + * running is interrupted while blocked + * waiting for a coordination service + * operation to complete. + * @throws IOException If there is an error reading the + * manifests list file. + */ + private void getManifestFilePaths() throws IOException, CoordinationServiceException, InterruptedException { + final Path manifestsListFilePath = Paths.get(caseNodeData.getDirectory().toString(), AutoIngestManager.getCaseManifestsListFileName()); + final File manifestListsFile = manifestsListFilePath.toFile(); + if (manifestListsFile.exists()) { + getManifestPathsFromFile(manifestsListFilePath); + } else { + getManifestPathsFromNodes(); + } + if (manifestFilePaths.isEmpty()) { + setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES); + } + } + + /** + * Gets a list of the manifest file paths for the case by reading them from + * the manifests list file for the case. + * + * @param manifestsListFilePath The path of the manifests list file. + * + * @throws IOException If there is an error reading the manifests + * list file. + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private void getManifestPathsFromFile(Path manifestsListFilePath) throws IOException, InterruptedException { + try (final Scanner manifestsListFileScanner = new Scanner(manifestsListFilePath)) { + while (manifestsListFileScanner.hasNextLine()) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + final Path manifestFilePath = Paths.get(manifestsListFileScanner.nextLine()); + if (manifestFilePath.toFile().exists()) { + manifestFilePaths.add(manifestFilePath); + } + } + } + } + + /** + * Gets a list of the manifest file paths for the case by sifting through + * the node data of the manifest file coordination service nodes and + * matching on case name. + * + * @throws CoordinationServiceException If there is an error completing a + * coordination service operation. + * @throws InterruptedException If the thread in which this task is + * running is interrupted while blocked + * waiting for a coordination service + * operation to complete. + */ + private void getManifestPathsFromNodes() throws CoordinationServiceException, InterruptedException { + /* + * Get the original, undecorated case name from the case directory. This + * is necessary because the case display name can be changed and the + * original case name may have a time stamp added to make it unique, + * depending on how the case was created. An alternative aproach would + * be to strip off any time stamp from the case name in the case node + * data. + */ + String caseName = CaseCoordinationServiceUtils.getCaseNameLockName(caseNodeData.getDirectory()); + final List nodeNames = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS); + for (String manifestNodeName : nodeNames) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + try { + final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodeName); + AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes); + if (caseName.equals(nodeData.getCaseName())) { + Path manifestFilePath = nodeData.getManifestFilePath(); + if (manifestFilePath.toFile().exists()) { + manifestFilePaths.add(manifestFilePath); + } + } + } catch (CoordinationServiceException | InvalidDataException ex) { + logger.log(Level.WARNING, String.format("Error getting coordination service node data from %s", manifestNodeName), ex); } } } @@ -369,199 +566,217 @@ final class DeleteCaseTask implements Runnable { @NbBundle.Messages({ "# {0} - manifest file path", "DeleteCaseTask.progress.lockingManifest=Locking manifest file {0}..." }) - private boolean acquireManifestFileLocks() throws CoordinationServiceException, InterruptedException { + private boolean acquireManifestFileLocks() throws IOException, CoordinationServiceException, InterruptedException { /* - * Get the "original" case name that from the case directory. This is - * necessary because the case display name can be changed and the case - * name may have a time stamp added to make it unique, depending on how - * the case was created. An alternative aproach would be to strip the - * time stamp from the case name in the case node data instead, but the - * code for that is already in the utility method called here. + * When acquiring the locks, it is reasonable to block briefly, since + * the auto ingest node (AIN) input directory scanning tasks do a lot of + * short-term acquiring and releasing of the same locks. The assumption + * here is that the originator of this case deletion task is not asking + * for deletion of a case that has a job that an auto ingest node (AIN) + * job execution task is working on and that + * MANIFEST_FILE_LOCKING_TIMEOUT_MINS is not very long anyway, so + * waiting a bit should be fine. + * */ - String caseName = CaseCoordinationServiceUtils.getCaseNameLockName(caseNodeData.getDirectory()); + boolean allLocksAcquired = true; try { - boolean allLocksAcquired = true; - // RJCTODO: Read in the list of manifests for the case instead of - // inspecting the nodes this way, once the recording of the - // manifests is in place. - final List nodeNames = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS); - for (String manifestPath : nodeNames) { + for (Path manifestPath : manifestFilePaths) { if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } - byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath); - if (nodeBytes == null || nodeBytes.length <= 0) { - logger.log(Level.WARNING, String.format("Empty coordination service node data found for %s", manifestPath)); - continue; - } - - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } - - AutoIngestJobNodeData nodeData; - try { - nodeData = new AutoIngestJobNodeData(nodeBytes); - } catch (InvalidDataException ex) { - logger.log(Level.WARNING, String.format("Invalid coordination service node data found for %s", manifestPath), ex); - continue; - } - - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } - - if (caseName.equals(nodeData.getCaseName())) { - /* - * When acquiring manifest file locks, it is reasonable to - * block while acquiring this lock since the auto ingest - * node (AIN) input directory scanning tasks do a lot of - * short-term acquiring and releasing of manifest file - * locks. The assumption here is that the originator of this - * case deletion task is not asking for deletion of a case - * that has a job an auto ingest node (AIN) job processing - * task is working on and that - * MANIFEST_FILE_LOCKING_TIMEOUT_MINS is not very long, - * anyway, so we can and should wait a bit. - */ - logger.log(Level.INFO, String.format("Exclusively locking the manifest %s for %s", manifestPath, caseNodeData.getDisplayName())); - progress.progress(Bundle.DeleteCaseTask_progress_lockingManifest(manifestPath)); - CoordinationService.Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath, MANIFEST_FILE_LOCKING_TIMEOUT_MINS, TimeUnit.MINUTES); - if (null != manifestLock) { - manifestFileLocks.add(manifestLock); - } else { - allLocksAcquired = false; - logger.log(Level.INFO, String.format("Failed to exclusively lock the manifest %s because it was already held by another host", manifestPath, caseNodeData.getDisplayName())); - releaseManifestFileLocks(); - break; - } + progress.progress(Bundle.DeleteCaseTask_progress_lockingManifest(manifestPath.toString())); + logger.log(Level.INFO, String.format("Exclusively locking the manifest %s for %s", manifestPath, caseNodeData.getDisplayName())); + CoordinationService.Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString(), MANIFEST_FILE_LOCKING_TIMEOUT_MINS, TimeUnit.MINUTES); + if (null != manifestLock) { + manifestFileLocks.add(manifestLock); + } else { + logger.log(Level.INFO, String.format("Failed to exclusively lock the manifest %s because it was already held by another host", manifestPath, caseNodeData.getDisplayName())); + allLocksAcquired = false; + releaseManifestFileLocks(); + break; } } - return allLocksAcquired; - } catch (CoordinationServiceException | InterruptedException ex) { releaseManifestFileLocks(); throw ex; } + return allLocksAcquired; } /** * Deletes the auto ingest job input manifests for the case along with the * corresponding data sources. * - * @throws IOException If there is an error opening the case - * manifests list file. * @throws InterruptedException If the thread in which this task is running * is interrupted while blocked waiting for a * coordination service operation to complete. */ @NbBundle.Messages({ + "DeleteCaseTask.progress.openingCaseDatabase=Opening the case database...", + "# {0} - manifest file path", "DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}...", "# {0} - manifest file path", "DeleteCaseTask.progress.deletingManifest=Deleting manifest file {0}..." }) - private void deleteAutoIngestInput() throws IOException, InterruptedException { - boolean allInputDeleted = true; - final Path manifestsListFilePath = Paths.get(caseNodeData.getDirectory().toString(), AutoIngestManager.getCaseManifestsListFileName()); - final Scanner manifestsListFileScanner = new Scanner(manifestsListFilePath); - while (manifestsListFileScanner.hasNext()) { + private void deleteAutoIngestInput() throws InterruptedException { + SleuthkitCase caseDb = null; + try { + progress.progress(Bundle.DeleteCaseTask_progress_openingCaseDatabase()); + logger.log(Level.INFO, String.format("Opening the case database for %s", caseNodeData.getDisplayName())); + caseDb = SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory()); + List dataSources = caseDb.getDataSources(); + if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } - final String manifestFilePath = manifestsListFileScanner.next(); - final File manifestFile = new File(manifestFilePath); - if (manifestFile.exists()) { - // RJCTODO: Parse file, open case database, delete data sources - // before deleting manifest file - progress.progress(Bundle.DeleteCaseTask_progress_deletingManifest(manifestFilePath)); - logger.log(Level.INFO, String.format("Deleting manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); - if (manifestFile.delete()) { - logger.log(Level.WARNING, String.format("Failed to delete manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); - allInputDeleted = false; + + boolean allInputDeleted = true; + for (Path manifestFilePath : manifestFilePaths) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + + final File manifestFile = manifestFilePath.toFile(); + if (manifestFile.exists()) { + progress.progress(Bundle.DeleteCaseTask_progress_parsingManifest(manifestFilePath)); + logger.log(Level.INFO, String.format("Parsing manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + Manifest manifest = null; + for (ManifestFileParser parser : Lookup.getDefault().lookupAll(ManifestFileParser.class)) { + if (parser.fileIsManifest(manifestFilePath)) { + try { + manifest = parser.parse(manifestFilePath); + break; + } catch (ManifestFileParser.ManifestFileParserException ex) { + logger.log(Level.WARNING, String.format("Error parsing manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex); + } + } + } + if (manifest != null) { + if (deleteDataSources(manifest, dataSources)) { + /* + * Delete the manifest file, allowing a few retries. + * This is a way to resolve the race condition + * between this task and auto ingest node (AIN) + * input directory scanning tasks, which parse + * manifests (actually all files) before getting a + * coordination service lock, without resorting to a + * protocol using locking of the input directory. + */ + progress.progress(Bundle.DeleteCaseTask_progress_deletingManifest(manifestFilePath)); + logger.log(Level.INFO, String.format("Deleting manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + int tries = 0; + boolean deleted = false; + while (!deleted && tries < MANIFEST_DELETE_TRIES) { + deleted = manifestFile.delete(); + if (!deleted) { + ++tries; + Thread.sleep(1000); + } + } + if (deleted) { + /* + * Delete the input directory if it is empty. + */ + final Path inputDirectoryPath = manifestFilePath.getParent(); + final File inputDirectory = inputDirectoryPath.toFile(); + File[] files = inputDirectory.listFiles(); + if (files == null || files.length == 0) { + if (!inputDirectory.delete()) { + logger.log(Level.WARNING, String.format("Failed to delete empty input directory %s for %s", inputDirectoryPath, caseNodeData.getDisplayName())); + } + } + + } else { + logger.log(Level.WARNING, String.format("Failed to delete manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + allInputDeleted = false; + } + } + } else { + logger.log(Level.WARNING, String.format("Failed to parse manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + } } } if (allInputDeleted) { setDeletedItemFlag(CaseNodeData.DeletedFlags.DATA_SOURCES); } + + } catch (TskCoreException | UserPreferencesException ex) { + logger.log(Level.INFO, String.format("Failed to open the case database for %s", caseNodeData.getDisplayName()), ex); + + } finally { + if (caseDb != null) { + caseDb.close(); + } } } /** - * Deletes the case database, the text index, the case directory, and the - * case resources and auto ingest log coordination service lock nodes for - * the case. + * Locates and deletes the data source files referenced by a manifest. * - * @return If true if all of the case output that was found was deleted, - * false otherwise. + * @param manifest A manifest. + * @param dataSources The data sources in the case as obtained from the case + * database. * - * @throws InterruptedException If the thread in which this task is running - * is interrupted while blocked waiting for a - * coordination service operation to complete. + * @return True if all of the data source files werre deleted, false + * otherwise. */ - @NbBundle.Messages({ - "DeleteCaseTask.progress.locatingCaseMetadataFile=Locating case metadata file...", - "DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources coordination service node...", - "DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest job coordination service node..." - }) - private boolean deleteCaseOutput() throws InterruptedException { - boolean errorsOccurred = false; - progress.progress(Bundle.DeleteCaseTask_progress_locatingCaseMetadataFile()); - logger.log(Level.INFO, String.format("Locating metadata file for %s", caseNodeData.getDisplayName())); - CaseMetadata caseMetadata = null; - final File caseDirectory = caseNodeData.getDirectory().toFile(); - if (caseDirectory.exists()) { - final File[] filesInDirectory = caseDirectory.listFiles(); - if (filesInDirectory != null) { - for (File file : filesInDirectory) { - if (file.getName().toLowerCase().endsWith(CaseMetadata.getFileExtension()) && file.isFile()) { - 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", caseNodeData.getDisplayName()), ex); - } - break; + private boolean deleteDataSources(Manifest manifest, List dataSources) { + /* + * There are two possibilities here. The data source may be an image, + * and if so, it may be split into multiple files. In this case, all of + * the files for the image need to be deleted. Otherwise, the data + * source is a single directory or file (a logical file, logical file + * set, report file, archive file, etc.). In this case, just the file + * referenced by the manifest will be deleted. + */ + boolean allFilesDeleted = true; // RJCTODO: add progress messages + Set filesToDelete = new HashSet<>(); + final String dataSourceFileName = manifest.getDataSourceFileName(); + final String dataSourceDeviceId = manifest.getDeviceId(); + for (DataSource dataSource : dataSources) { + if (dataSource instanceof Image) { + Image image = (Image) dataSource; + if (image.getName().equals(dataSourceFileName) && image.getDeviceId().equals(dataSourceDeviceId)) { + String[] imageFilePaths = image.getPaths(); + for (String path : imageFilePaths) { + Path imageFilePath = Paths.get(path); + filesToDelete.add(imageFilePath); } + break; } } + } + if (filesToDelete.isEmpty()) { + final Path dataSourcePath = manifest.getDataSourcePath(); + filesToDelete.add(dataSourcePath); + } - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } - - if (caseMetadata != null) { - logger.log(Level.INFO, String.format("Deleting output for %s", caseNodeData.getDisplayName())); - errorsOccurred = Case.deleteMultiUserCase(caseNodeData, caseMetadata, progress, logger); // RJCTODO: CHeck for errors occurred? - } else { - logger.log(Level.WARNING, String.format("Failed to locate metadata file for %s", caseNodeData.getDisplayName())); + for (Path path : filesToDelete) { + File fileOrDir = path.toFile(); + if (fileOrDir.exists() && !FileUtil.deleteFileDir(fileOrDir)) { + allFilesDeleted = false; + logger.log(Level.INFO, String.format("Failed to delete data source file at %s for %s", path, caseNodeData.getDisplayName())); } } - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } + return allFilesDeleted; + } - progress.progress(Bundle.DeleteCaseTask_progress_deletingResourcesLockNode()); - try { - Case.deleteCaseResourcesLockNode(caseNodeData, progress); - } catch (CoordinationServiceException ex) { - logger.log(Level.WARNING, String.format("Error deleting case resources coordiation service node for %s", caseNodeData.getDisplayName()), ex); + /** + * Examines a coordination service exception to try to determine if it is a + * no node exception. + * + * @param ex A coordination service exception. + * + * @return True or false. + */ + private boolean isNoNodeException(CoordinationServiceException ex) { + boolean isNodeNodeEx = false; + Throwable cause = ex.getCause(); + if (cause != null) { + String causeMessage = cause.getMessage(); + isNodeNodeEx = causeMessage.contains(NO_NODE_ERROR_MSG_FRAGMENT); } - - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } - - // RJCTODO: Check to see if getNodeData return null if the node does not exist; - // if so, make use of it - progress.progress(Bundle.DeleteCaseTask_progress_deletingJobLogLockNode()); - logger.log(Level.INFO, String.format("Deleting case auto ingest job log coordiation service node for %s", caseNodeData.getDisplayName())); - String logFilePath = CaseCoordinationServiceUtils.getCaseAutoIngestLogLockName(caseNodeData.getDirectory()); - try { - coordinationService.deleteNode(CategoryNode.CASES, logFilePath); - } catch (CoordinationServiceException ex) { - logger.log(Level.WARNING, String.format("Error deleting case auto ingest job log coordiation service node for %s", caseNodeData.getDisplayName()), ex); - } - - return errorsOccurred; + return isNodeNodeEx; } /** @@ -569,7 +784,7 @@ final class DeleteCaseTask implements Runnable { * task. */ @NbBundle.Messages({ - "# {0} - manifest file path", "DeleteCaseTask.progress.releasingManifestLock=Releasing the exclusive coordination service lock on the manifest file {0}..." + "# {0} - manifest file path", "DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}..." }) private void releaseManifestFileLocks() { for (Lock manifestFileLock : manifestFileLocks) { @@ -597,11 +812,13 @@ final class DeleteCaseTask implements Runnable { * coordination service operation to complete. */ @Messages({ - "# {0} - manifest file path", "DeleteCaseTask.progress.deletingManifestFileNode=Deleting the manifest file coordination service node for {0}..." + "# {0} - manifest file path", "DeleteCaseTask.progress.deletingManifestFileNode=Deleting the manifest file znode for {0}..." }) - private boolean deleteManifestFileNodes() throws InterruptedException { + private void deleteManifestFileNodes() throws InterruptedException { boolean allINodesDeleted = true; - for (Lock manifestFileLock : manifestFileLocks) { + Iterator iterator = manifestFileLocks.iterator(); + while (iterator.hasNext()) { + Lock manifestFileLock = iterator.next(); String manifestFilePath = manifestFileLock.getNodePath(); try { progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLock(manifestFilePath)); @@ -614,9 +831,11 @@ final class DeleteCaseTask implements Runnable { allINodesDeleted = false; logger.log(Level.WARNING, String.format("Error deleting the manifest file coordination service node for %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex); } + iterator.remove(); + } + if (allINodesDeleted) { + setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES); } - manifestFileLocks.clear(); - return allINodesDeleted; } /**