diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index b4fc10bf09..c9640f1a13 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.CaseNodeData.CaseNodeDataException; import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils; import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributeSearchAction; @@ -1620,11 +1621,10 @@ public class Case { } if (getCaseType() == CaseType.MULTI_USER_CASE && !oldCaseDetails.getCaseDisplayName().equals(caseDetails.getCaseDisplayName())) { try { - CoordinationService coordinationService = CoordinationService.getInstance(); - CaseNodeData nodeData = new CaseNodeData(coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory())); + CaseNodeData nodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory()); nodeData.setDisplayName(caseDetails.getCaseDisplayName()); - coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); - } catch (CoordinationServiceException | InterruptedException | IOException ex) { + CaseNodeData.writeCaseNodeData(nodeData); + } catch (CaseNodeDataException | InterruptedException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex); } } @@ -2005,10 +2005,8 @@ public class Case { if (getCaseType() == CaseType.MULTI_USER_CASE) { progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseNodeData()); try { - CoordinationService coordinationService = CoordinationService.getInstance(); - CaseNodeData nodeData = new CaseNodeData(metadata); - coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); - } catch (CoordinationServiceException | InterruptedException | ParseException | IOException ex) { + CaseNodeData.createCaseNodeData(metadata); + } catch (CaseNodeDataException | InterruptedException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseNodeData(ex.getLocalizedMessage()), ex); } } @@ -2033,27 +2031,10 @@ public class Case { if (getCaseType() == CaseType.MULTI_USER_CASE) { progressIndicator.progress(Bundle.Case_progressMessage_updatingCaseNodeData()); try { - CaseNodeData nodeData; - CoordinationService coordinationService = CoordinationService.getInstance(); - byte[] nodeBytes = coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory()); - if (nodeBytes != null && nodeBytes.length > 0) { - /* - * Update the last access date in the coordination service - * node data for the case. - */ - nodeData = new CaseNodeData(nodeBytes); - nodeData.setLastAccessDate(new Date()); - } else { - /* - * This is a "legacy" case with no data stored in its case - * directory coordination service node yet, or the node is - * empty due to some error, so create the coordination - * service node data from the case metadata. - */ - nodeData = new CaseNodeData(metadata); - } - coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); - } catch (CoordinationServiceException | InterruptedException | ParseException | IOException ex) { + CaseNodeData nodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory()); + nodeData.setLastAccessDate(new Date()); + CaseNodeData.writeCaseNodeData(nodeData); + } catch (CaseNodeDataException | InterruptedException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex); } } @@ -2633,9 +2614,8 @@ public class Case { progressIndicator.progress(Bundle.Case_progressMessage_fetchingCoordSvcNodeData()); try { - byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, metadata.getCaseDirectory()); - caseNodeData = new CaseNodeData(nodeBytes); - } catch (CoordinationServiceException | InterruptedException | IOException ex) { + caseNodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory()); + } catch (CaseNodeDataException | InterruptedException ex) { logger.log(Level.SEVERE, String.format("Failed to get coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS throw new CaseActionException(Bundle.Case_exceptionMessage_failedToFetchCoordSvcNodeData(ex.getLocalizedMessage())); } @@ -2899,9 +2879,8 @@ public class Case { private static void setDeletedItemFlag(CaseNodeData caseNodeData, CaseNodeData.DeletedFlags flag) throws InterruptedException { try { caseNodeData.setDeletedFlag(flag); - CoordinationService coordinationService = CoordinationService.getInstance(); - coordinationService.setNodeData(CategoryNode.CASES, caseNodeData.getDirectory().toString(), caseNodeData.toArray()); - } catch (IOException | CoordinationServiceException ex) { + CaseNodeData.writeCaseNodeData(caseNodeData); + } catch (CaseNodeDataException 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); } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java index d825d87ee5..7a09d737f6 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java @@ -22,25 +22,33 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.text.ParseException; import java.util.Date; +import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.casemodule.CaseMetadata.CaseMetadataException; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; +import org.sleuthkit.autopsy.coreutils.Logger; /** - * An object that converts data for a case directory lock coordination service - * node to and from byte arrays. + * Case data stored in a case directory coordination service node. */ public final class CaseNodeData { - private static final int CURRENT_VERSION = 1; + private static final int MAJOR_VERSION = 2; + private static final int MINOR_VERSION = 0; + private static final Logger logger = Logger.getLogger(CaseNodeData.class.getName()); /* - * Version 0 fields. + * Version 0 fields. Note that version 0 node data was only written to the + * coordination service node if an auto ingest job error occurred. */ - private final int version; + private int version; private boolean errorsOccurred; /* @@ -53,28 +61,191 @@ public final class CaseNodeData { private String displayName; private short deletedItemFlags; - /** - * Gets the current version of the case directory lock coordination service - * node data. - * - * @return The version number. + /* + * Version 2 fields. */ - public static int getCurrentVersion() { - return CaseNodeData.CURRENT_VERSION; + private int minorVersion; + + /** + * Creates case node data from the metadata for a case and writes it to the + * appropriate case directory coordination service node, which must already + * exist. + * + * @param metadata The case metadata. + * + * @return The case node data that was written to the coordination service + * node. + * + * @throws CaseNodeDataException If there is an error creating or writing + * the case node data. + * @throws InterruptedException If the current thread is interrupted while + * waiting for the coordination service. + */ + public static CaseNodeData createCaseNodeData(final CaseMetadata metadata) throws CaseNodeDataException, InterruptedException { + try { + final CaseNodeData nodeData = new CaseNodeData(metadata); + CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeData.getDirectory().toString(), nodeData.toArray()); + return nodeData; + + } catch (ParseException | IOException | CoordinationServiceException ex) { + throw new CaseNodeDataException(String.format("Error creating case node data for coordination service node with path %s", metadata.getCaseDirectory().toUpperCase()), ex); //NON-NLS + } } /** - * Uses a CaseMetadata object to construct an object that converts data for - * a case directory lock coordination service node to and from byte arrays. + * Reads case data from a case directory coordination service node. If the + * data is missing, corrupted, or from an older version of the software, an + * attempt is made to remedy the situation using the case metadata. + * + * @param nodePath The case directory coordination service node path. + * + * @return The case node data. + * + * @throws CaseNodeDataException If there is an error reading or writing the + * case node data. + * @throws InterruptedException If the current thread is interrupted while + * waiting for the coordination service. + */ + public static CaseNodeData readCaseNodeData(String nodePath) throws CaseNodeDataException, InterruptedException { + try { + CaseNodeData nodeData; + final byte[] nodeBytes = CoordinationService.getInstance().getNodeData(CoordinationService.CategoryNode.CASES, nodePath); + if (nodeBytes != null && nodeBytes.length > 0) { + try { + nodeData = new CaseNodeData(nodeBytes); + } catch (IOException ex) { + /* + * The existing case node data is corrupted. + */ + logger.log(Level.WARNING, String.format("Error reading node data for coordination service node with path %s, will attempt to replace it", nodePath.toUpperCase()), ex); //NON-NLS + final CaseMetadata metadata = getCaseMetadata(nodePath); + nodeData = createCaseNodeData(metadata); + logger.log(Level.INFO, String.format("Replaced corrupt node data for coordination service node with path %s", nodePath.toUpperCase())); //NON-NLS + } + } else { + /* + * The case node data is missing. Version 0 node data was only + * written to the coordination service node if an auto ingest + * job error occurred. + */ + logger.log(Level.INFO, String.format("Missing node data for coordination service node with path %s, will attempt to create it", nodePath.toUpperCase())); //NON-NLS + final CaseMetadata metadata = getCaseMetadata(nodePath); + nodeData = createCaseNodeData(metadata); + logger.log(Level.INFO, String.format("Created node data for coordination service node with path %s", nodePath.toUpperCase())); //NON-NLS + } + if (nodeData.getVersion() < CaseNodeData.MAJOR_VERSION) { + nodeData = upgradeCaseNodeData(nodePath, nodeData); + } + return nodeData; + + } catch (CaseNodeDataException | CaseMetadataException | ParseException | IOException | CoordinationServiceException ex) { + throw new CaseNodeDataException(String.format("Error reading/writing node data coordination service node with path %s", nodePath.toUpperCase()), ex); //NON-NLS + } + } + + /** + * Writes case data to a case directory coordination service node. Obtain + * the case data to be updated and written by calling createCaseNodeData() + * or readCaseNodeData(). + * + * @param nodeData The case node data. + * + * @throws CaseNodeDataException If there is an error writing the case node + * data. + * @throws InterruptedException If the current thread is interrupted while + * waiting for the coordination service. + */ + public static void writeCaseNodeData(CaseNodeData nodeData) throws CaseNodeDataException, InterruptedException { + try { + CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeData.getDirectory().toString(), nodeData.toArray()); + + } catch (IOException | CoordinationServiceException ex) { + throw new CaseNodeDataException(String.format("Error writing node data coordination service node with path %s", nodeData.getDirectory().toString().toUpperCase()), ex); //NON-NLS + } + } + + /** + * Upgrades older versions of node data to the current version and writes + * the data back to the case directory coordination service node. + * + * @param nodePath The case directory coordination service node path. + * @param oldNodeData The outdated node data. + * + * @return The updated node data. + * + * @throws CaseNodeDataException If the case meta data file or case + * directory do not exist. + * @throws CaseMetadataException If the case metadata cannot be read. + */ + private static CaseNodeData upgradeCaseNodeData(String nodePath, CaseNodeData oldNodeData) throws CaseNodeDataException, CaseMetadataException, ParseException, IOException, CoordinationServiceException, InterruptedException { + CaseNodeData nodeData; + switch (oldNodeData.getVersion()) { + case 0: + /* + * Version 0 node data consisted of only the version number and + * the errors occurred flag and was only written when an auto + * ingest job error occurred. To upgrade from version 0, the + * version 1 fields need to be set from the case metadata and + * the errors occurred flag needs to be carried forward. Note + * that the last accessed date gets advanced to now, since it is + * otherwise unknown. + */ + final CaseMetadata metadata = getCaseMetadata(nodePath); + nodeData = new CaseNodeData(metadata); + nodeData.setErrorsOccurred(oldNodeData.getErrorsOccurred()); + break; + case 1: + /* + * Version 1 node data did not have a minor version number + * field. + */ + oldNodeData.setMinorVersion(MINOR_VERSION); + nodeData = oldNodeData; + break; + default: + nodeData = oldNodeData; + break; + } + writeCaseNodeData(nodeData); + return nodeData; + } + + /** + * Gets the metadata for a case. + * + * @param nodePath The case directory coordination service node path for the + * case. + * + * @return The case metadata. + * + * @throws CaseNodeDataException If the case metadata file or the case + * directory does not exist. + * @throws CaseMetadataException If the case metadata cannot be read. + */ + private static CaseMetadata getCaseMetadata(String nodePath) throws CaseNodeDataException, CaseMetadataException { + final Path caseDirectoryPath = Paths.get(nodePath); + final File caseDirectory = caseDirectoryPath.toFile(); + if (!caseDirectory.exists()) { + throw new CaseNodeDataException("Case directory does not exist"); // NON-NLS + } + final Path metadataFilePath = CaseMetadata.getCaseMetadataFilePath(caseDirectoryPath); + if (metadataFilePath == null) { + throw new CaseNodeDataException("Case meta data file does not exist"); // NON-NLS + } + return new CaseMetadata(metadataFilePath); + } + + /** + * Uses case metadata to construct the case data to store in a case + * directory coordination service node. * * @param metadata The case meta data. * - * @throws java.text.ParseException If there is an error parsing dates from - * string representations of dates in the - * meta data. + * @throws ParseException If there is an error parsing dates from string + * representations of dates in the meta data. */ - public CaseNodeData(CaseMetadata metadata) throws ParseException { - this.version = CURRENT_VERSION; + private CaseNodeData(CaseMetadata metadata) throws ParseException { + this.version = MAJOR_VERSION; this.errorsOccurred = false; this.directory = Paths.get(metadata.getCaseDirectory()); this.createDate = CaseMetadata.getDateFormat().parse(metadata.getCreatedDate()); @@ -82,51 +253,64 @@ public final class CaseNodeData { this.name = metadata.getCaseName(); this.displayName = metadata.getCaseDisplayName(); this.deletedItemFlags = 0; + this.minorVersion = MINOR_VERSION; } /** - * Uses coordination service node data to construct an object that converts - * data for a case directory lock coordination service node to and from byte - * arrays. + * Uses the raw bytes from a case directory coordination service node to + * construct a case node data object. * * @param nodeData The raw bytes received from the coordination service. * * @throws IOException If there is an error reading the node data. */ - public CaseNodeData(byte[] nodeData) throws IOException { + private CaseNodeData(byte[] nodeData) throws IOException { if (nodeData == null || nodeData.length == 0) { throw new IOException(null == nodeData ? "Null node data byte array" : "Zero-length node data byte array"); } - DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(nodeData)); - this.version = inputStream.readInt(); - if (this.version > 0) { - this.errorsOccurred = inputStream.readBoolean(); - } else { - short legacyErrorsOccurred = inputStream.readByte(); - this.errorsOccurred = (legacyErrorsOccurred < 0); - } - if (this.version > 0) { - this.directory = Paths.get(inputStream.readUTF()); - this.createDate = new Date(inputStream.readLong()); - this.lastAccessDate = new Date(inputStream.readLong()); - this.name = inputStream.readUTF(); - this.displayName = inputStream.readUTF(); - this.deletedItemFlags = inputStream.readShort(); + try (ByteArrayInputStream byteStream = new ByteArrayInputStream(nodeData); DataInputStream inputStream = new DataInputStream(byteStream)) { + this.version = inputStream.readInt(); + if (this.version == 1) { + this.errorsOccurred = inputStream.readBoolean(); + } else { + byte errorsOccurredByte = inputStream.readByte(); + this.errorsOccurred = (errorsOccurredByte < 0); + } + if (this.version > 0) { + this.directory = Paths.get(inputStream.readUTF()); + this.createDate = new Date(inputStream.readLong()); + this.lastAccessDate = new Date(inputStream.readLong()); + this.name = inputStream.readUTF(); + this.displayName = inputStream.readUTF(); + this.deletedItemFlags = inputStream.readShort(); + } + if (this.version > 1) { + this.minorVersion = inputStream.readInt(); + } } } /** - * Gets the node data version number of this node. + * Gets the version number of this node data. * * @return The version number. */ - public int getVersion() { + private int getVersion() { return this.version; } + /** + * Sets the minor version number of this node data. + * + * @param version The version number. + */ + private void setMinorVersion(int minorVersion) { + this.minorVersion = minorVersion; + } + /** * Gets whether or not any errors occurred during the processing of any auto - * ingest job for the case represented by this node data. + * ingest job for the case. * * @return True or false. */ @@ -136,7 +320,7 @@ public final class CaseNodeData { /** * Sets whether or not any errors occurred during the processing of any auto - * ingest job for the case represented by this node data. + * ingest job for the case. * * @param errorsOccurred True or false. */ @@ -145,8 +329,7 @@ public final class CaseNodeData { } /** - * Gets the path of the case directory of the case represented by this node - * data. + * Gets the path of the case directory. * * @return The case directory path. */ @@ -155,17 +338,7 @@ public final class CaseNodeData { } /** - * Sets the path of the case directory of the case represented by this node - * data. - * - * @param caseDirectory The case directory path. - */ - public void setDirectory(Path caseDirectory) { - this.directory = caseDirectory; - } - - /** - * Gets the date the case represented by this node data was created. + * Gets the date the case was created. * * @return The create date. */ @@ -174,16 +347,7 @@ public final class CaseNodeData { } /** - * Sets the date the case represented by this node data was created. - * - * @param createDate The create date. - */ - public void setCreateDate(Date createDate) { - this.createDate = new Date(createDate.getTime()); - } - - /** - * Gets the date the case represented by this node data last accessed. + * Gets the date the case was last accessed. * * @return The last access date. */ @@ -192,7 +356,7 @@ public final class CaseNodeData { } /** - * Sets the date the case represented by this node data was last accessed. + * Sets the date the case was last accessed. * * @param lastAccessDate The last access date. */ @@ -201,8 +365,7 @@ public final class CaseNodeData { } /** - * Gets the unique and immutable (user cannot change it) name of the case - * represented by this node data. + * Gets the unique and immutable name of the case. * * @return The case name. */ @@ -211,17 +374,7 @@ public final class CaseNodeData { } /** - * Sets the unique and immutable (user cannot change it) name of the case - * represented by this node data. - * - * @param name The case name. - */ - public void setName(String name) { - this.name = name; - } - - /** - * Gets the display name of the case represented by this node data. + * Gets the display name of the case. * * @return The case display name. */ @@ -230,7 +383,7 @@ public final class CaseNodeData { } /** - * Sets the display name of the case represented by this node data. + * Sets the display name of the case. * * @param displayName The case display name. */ @@ -239,19 +392,18 @@ public final class CaseNodeData { } /** - * Checks whether a deleted item flag is set for the case represented by - * this node data. + * Checks whether a given deleted item flag is set for the case. * * @param flag The flag to check. * - * @return + * @return True or false. */ public boolean isDeletedFlagSet(DeletedFlags flag) { return (this.deletedItemFlags & flag.getValue()) == flag.getValue(); } /** - * Sets a deleted item flag for the case represented by this node data. + * Sets a given deleted item flag. * * @param flag The flag to set. */ @@ -265,22 +417,24 @@ public final class CaseNodeData { * * @return The node data as a byte array. * - * @throws IOException If there is an error writing the node data. + * @throws IOException If there is an error writing the node data to the + * array. */ - public byte[] toArray() throws IOException { - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - DataOutputStream outputStream = new DataOutputStream(byteStream); - outputStream.writeInt(this.version); - outputStream.writeBoolean(this.errorsOccurred); - outputStream.writeUTF(this.directory.toString()); - outputStream.writeLong(this.createDate.getTime()); - outputStream.writeLong(this.lastAccessDate.getTime()); - outputStream.writeUTF(this.name); - outputStream.writeUTF(this.displayName); - outputStream.writeShort(this.deletedItemFlags); - outputStream.flush(); - byteStream.flush(); - return byteStream.toByteArray(); + private byte[] toArray() throws IOException { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); DataOutputStream outputStream = new DataOutputStream(byteStream)) { + outputStream.writeInt(this.version); + outputStream.writeByte((byte) (this.errorsOccurred ? 0x80 : 0)); + outputStream.writeUTF(this.directory.toString()); + outputStream.writeLong(this.createDate.getTime()); + outputStream.writeLong(this.lastAccessDate.getTime()); + outputStream.writeUTF(this.name); + outputStream.writeUTF(this.displayName); + outputStream.writeShort(this.deletedItemFlags); + outputStream.writeInt(this.minorVersion); + outputStream.flush(); + byteStream.flush(); + return byteStream.toByteArray(); + } } /** @@ -316,4 +470,34 @@ public final class CaseNodeData { } + /** + * Exception thrown when there is an error reading or writing case node + * data. + */ + public static final class CaseNodeDataException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception to throw when there is an error reading or + * writing case node data. + * + * @param message The exception message. + */ + private CaseNodeDataException(String message) { + super(message); + } + + /** + * Constructs an exception to throw when there is an error reading or + * writing case node data. + * + * @param message The exception message. + * @param cause The cause of the exception. + */ + private CaseNodeDataException(String message, Throwable cause) { + super(message, cause); + } + } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeDataCollector.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeDataCollector.java index 5c148aa58d..cfb542d967 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeDataCollector.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeDataCollector.java @@ -18,31 +18,28 @@ */ package org.sleuthkit.autopsy.casemodule.multiusercases; -import java.io.File; -import java.io.IOException; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.text.ParseException; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; -import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; +import static org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils.isCaseAutoIngestLogNodePath; +import static org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils.isCaseNameNodePath; +import static org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils.isCaseResourcesNodePath; 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. + * Collects the multi-user case node data stored in the case directory + * coordination service nodes. */ final public class CaseNodeDataCollector { private static final Logger logger = Logger.getLogger(CaseNodeDataCollector.class.getName()); /** - * Queries the coordination service to collect the multi-user case node data - * stored in the case directory lock ZooKeeper nodes. + * Collects the multi-user case node data stored in the case directory + * coordination service nodes. * * @return The node data for the multi-user cases known to the coordination * service. @@ -54,128 +51,30 @@ final public class CaseNodeDataCollector { * service. */ public static List getNodeData() throws CoordinationServiceException, InterruptedException { - final List cases = new ArrayList<>(); + final List nodeDataList = new ArrayList<>(); final CoordinationService coordinationService = CoordinationService.getInstance(); - final List nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); - for (String nodeName : nodeList) { - if (CoordinationServiceUtils.isCaseNameNodePath(nodeName) - || CoordinationServiceUtils.isCaseResourcesNodePath(nodeName) - || CoordinationServiceUtils.isCaseAutoIngestLogNodePath(nodeName)) { + final List nodePaths = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); + for (String nodePath : nodePaths) { + /* + * Skip the case name, case resources, and case auto ingest log + * coordination service nodes. They are not used to store case data. + */ + if (isCaseNameNodePath(nodePath) || isCaseResourcesNodePath(nodePath) || isCaseAutoIngestLogNodePath(nodePath)) { continue; } /* - * Get the data from the case directory lock node. This data may not - * exist or may exist only in an older version. If it is missing or - * incomplete, create or update it. + * Get the case node data from the case directory coordination service node. */ try { - CaseNodeData nodeData; - final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName); - if (nodeBytes != null && nodeBytes.length > 0) { - nodeData = new CaseNodeData(nodeBytes); - if (nodeData.getVersion() < CaseNodeData.getCurrentVersion()) { - nodeData = updateNodeData(nodeName, nodeData); - } - } else { - nodeData = updateNodeData(nodeName, null); - } - if (nodeData != null) { - cases.add(nodeData); - } - - } catch (CoordinationService.CoordinationServiceException | InterruptedException | IOException | ParseException | CaseMetadata.CaseMetadataException ex) { - logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex); + final CaseNodeData nodeData = CaseNodeData.readCaseNodeData(nodePath); + nodeDataList.add(nodeData); + } catch (CaseNodeDataException | InterruptedException ex) { + logger.log(Level.WARNING, String.format("Error reading case node data from %s", nodePath), ex); } } - return cases; - } - - /** - * Updates the case directory lock coordination service node data for a - * case. - * - * @param nodeName The coordination service node name, i.e., the case - * directory path. - * @param oldNodeData The node data to be updated. - * - * @return A CaseNodedata object or null if the coordination service node is - * an "orphan" with no corresponding case directry. - * - * @throws IOException If there is an error writing the - * node data to a byte array. - * @throws CaseMetadataException If there is an error reading the - * case metadata file. - * @throws ParseException If there is an error parsing a date - * from the case metadata file. - * @throws CoordinationServiceException If there is an error interacting - * with the coordination service. - * @throws InterruptedException If a coordination service operation - * is interrupted. - */ - private static CaseNodeData updateNodeData(String nodeName, CaseNodeData oldNodeData) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException { - Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS); - File caseDirectory = caseDirectoryPath.toFile(); - if (!caseDirectory.exists()) { - logger.log(Level.WARNING, String.format("Found orphan coordination service node %s, attempting clean up", caseDirectoryPath)); - deleteLockNodes(CoordinationService.getInstance(), caseDirectoryPath); - return null; - } - - CaseNodeData nodeData = null; - if (oldNodeData == null || oldNodeData.getVersion() == 0) { - File[] files = caseDirectory.listFiles(); - for (File file : files) { - String name = file.getName().toLowerCase(); - if (name.endsWith(CaseMetadata.getFileExtension())) { - CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath())); - nodeData = new CaseNodeData(metadata); - if (oldNodeData != null) { - /* - * Version 0 case node data was only written if errors - * occurred during an auto ingest job. - */ - nodeData.setErrorsOccurred(true); - } - break; - } - } - } - - if (nodeData != null) { - CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray()); - } - - return nodeData; - } - - /** - * Attempts to delete the coordination service lock nodes for a case, - * logging any failures. - * - * @param coordinationService The coordination service. - * @param caseDirectoryPath The case directory path. - */ - private static void deleteLockNodes(CoordinationService coordinationService, Path caseDirectoryPath) { - deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseResourcesNodePath(caseDirectoryPath)); - deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseAutoIngestLogNodePath(caseDirectoryPath)); - deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseDirectoryNodePath(caseDirectoryPath)); - deleteCoordinationServiceNode(coordinationService, CoordinationServiceUtils.getCaseNameNodePath(caseDirectoryPath)); - } - - /** - * Attempts to delete a coordination service node, logging failure. - * - * @param coordinationService The coordination service. - * @param nodeName A node name. - */ - private static void deleteCoordinationServiceNode(CoordinationService coordinationService, String nodeName) { - try { - coordinationService.deleteNode(CoordinationService.CategoryNode.CASES, nodeName); - } catch (CoordinationService.CoordinationServiceException | InterruptedException ex) { - logger.log(Level.WARNING, String.format("Error deleting coordination service node %s", nodeName), ex); - } + return nodeDataList; } /** @@ -183,5 +82,5 @@ final public class CaseNodeDataCollector { */ private CaseNodeDataCollector() { } - + } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index a85a4993ae..e492aba3b3 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -62,6 +62,7 @@ import org.sleuthkit.autopsy.casemodule.CaseActionException; import org.sleuthkit.autopsy.casemodule.CaseDetails; import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock; @@ -1016,22 +1017,19 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen * * @param caseDirectoryPath The case directory path. * - * @throws CoordinationServiceException If there was an error getting the - * node data from the cooordination - * service. - * @throws IOException If the node data was missing or - * there was an error interpreting it. - * @throws InterruptedException If the thread running the input - * directory scan task is interrupted - * while blocked, i.e., if auto ingest - * is shutting down. + * @throws InterruptedException If the thread running the input directory + * scan task is interrupted while blocked, + * i.e., if auto ingest is shutting down. */ - private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws IOException, CoordinationServiceException, InterruptedException { - byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString()); - CaseNodeData caseNodeData = new CaseNodeData(rawData); - caseNodeData.setErrorsOccurred(true); - rawData = caseNodeData.toArray(); - coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString(), rawData); + private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws InterruptedException { + try { + CaseNodeData caseNodeData = CaseNodeData.readCaseNodeData(caseDirectoryPath.toString()); + caseNodeData.setErrorsOccurred(true); + CaseNodeData.writeCaseNodeData(caseNodeData); + } catch (CaseNodeDataException ex) { + sysLogger.log(Level.WARNING, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex); + } + } /** @@ -1381,11 +1379,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen if (null != caseDirectoryPath) { job.setCaseDirectoryPath(caseDirectoryPath); job.setErrorsOccurred(true); - try { - setCaseNodeDataErrorsOccurred(caseDirectoryPath); - } catch (IOException ex) { - sysLogger.log(Level.WARNING, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex); - } + setCaseNodeDataErrorsOccurred(caseDirectoryPath); } else { job.setErrorsOccurred(false); } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java index 31447ce2a1..54ac4f504c 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java @@ -37,6 +37,7 @@ import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode; @@ -785,12 +786,14 @@ final class DeleteCaseTask implements Runnable { * case. * * @param flag The flag to set. + * + * @throws InterruptedException If the interrupted flag is set. */ - private void setDeletedItemFlag(CaseNodeData.DeletedFlags flag) { + private void setDeletedItemFlag(CaseNodeData.DeletedFlags flag) throws InterruptedException { try { caseNodeData.setDeletedFlag(flag); - coordinationService.setNodeData(CategoryNode.CASES, caseNodeData.getDirectory().toString(), caseNodeData.toArray()); - } catch (IOException | CoordinationServiceException | InterruptedException ex) { + CaseNodeData.writeCaseNodeData(caseNodeData); + } catch (CaseNodeDataException ex) { logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s", flag.name(), caseNodeData.getDisplayName()), ex); } }