Merge pull request #4684 from rcordovano/4915-case-node-data-fixes-and-improvements

4915 case node data fixes and improvements
This commit is contained in:
Richard Cordovano 2019-04-08 12:32:44 -04:00 committed by GitHub
commit f9c71b95fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 340 additions and 281 deletions

View File

@ -81,6 +81,7 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.DataSourceNameChangedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceNameChangedEvent;
import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; 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.multiusercases.CoordinationServiceUtils;
import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.casemodule.services.Services;
import org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributeSearchAction; import org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributeSearchAction;
@ -1620,11 +1621,10 @@ public class Case {
} }
if (getCaseType() == CaseType.MULTI_USER_CASE && !oldCaseDetails.getCaseDisplayName().equals(caseDetails.getCaseDisplayName())) { if (getCaseType() == CaseType.MULTI_USER_CASE && !oldCaseDetails.getCaseDisplayName().equals(caseDetails.getCaseDisplayName())) {
try { try {
CoordinationService coordinationService = CoordinationService.getInstance(); CaseNodeData nodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory());
CaseNodeData nodeData = new CaseNodeData(coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory()));
nodeData.setDisplayName(caseDetails.getCaseDisplayName()); nodeData.setDisplayName(caseDetails.getCaseDisplayName());
coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); CaseNodeData.writeCaseNodeData(nodeData);
} catch (CoordinationServiceException | InterruptedException | IOException ex) { } catch (CaseNodeDataException | InterruptedException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex); throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex);
} }
} }
@ -2005,10 +2005,8 @@ public class Case {
if (getCaseType() == CaseType.MULTI_USER_CASE) { if (getCaseType() == CaseType.MULTI_USER_CASE) {
progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseNodeData()); progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseNodeData());
try { try {
CoordinationService coordinationService = CoordinationService.getInstance(); CaseNodeData.createCaseNodeData(metadata);
CaseNodeData nodeData = new CaseNodeData(metadata); } catch (CaseNodeDataException | InterruptedException ex) {
coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray());
} catch (CoordinationServiceException | InterruptedException | ParseException | IOException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseNodeData(ex.getLocalizedMessage()), ex); throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseNodeData(ex.getLocalizedMessage()), ex);
} }
} }
@ -2033,27 +2031,10 @@ public class Case {
if (getCaseType() == CaseType.MULTI_USER_CASE) { if (getCaseType() == CaseType.MULTI_USER_CASE) {
progressIndicator.progress(Bundle.Case_progressMessage_updatingCaseNodeData()); progressIndicator.progress(Bundle.Case_progressMessage_updatingCaseNodeData());
try { try {
CaseNodeData nodeData; CaseNodeData nodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory());
CoordinationService coordinationService = CoordinationService.getInstance(); nodeData.setLastAccessDate(new Date());
byte[] nodeBytes = coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory()); CaseNodeData.writeCaseNodeData(nodeData);
if (nodeBytes != null && nodeBytes.length > 0) { } catch (CaseNodeDataException | InterruptedException ex) {
/*
* 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) {
throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex); throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex);
} }
} }
@ -2633,9 +2614,8 @@ public class Case {
progressIndicator.progress(Bundle.Case_progressMessage_fetchingCoordSvcNodeData()); progressIndicator.progress(Bundle.Case_progressMessage_fetchingCoordSvcNodeData());
try { try {
byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, metadata.getCaseDirectory()); caseNodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory());
caseNodeData = new CaseNodeData(nodeBytes); } catch (CaseNodeDataException | InterruptedException ex) {
} catch (CoordinationServiceException | InterruptedException | IOException ex) {
logger.log(Level.SEVERE, String.format("Failed to get coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS 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())); 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 { private static void setDeletedItemFlag(CaseNodeData caseNodeData, CaseNodeData.DeletedFlags flag) throws InterruptedException {
try { try {
caseNodeData.setDeletedFlag(flag); caseNodeData.setDeletedFlag(flag);
CoordinationService coordinationService = CoordinationService.getInstance(); CaseNodeData.writeCaseNodeData(caseNodeData);
coordinationService.setNodeData(CategoryNode.CASES, caseNodeData.getDirectory().toString(), caseNodeData.toArray()); } catch (CaseNodeDataException ex) {
} catch (IOException | CoordinationServiceException ex) {
logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s (%s) in %s", flag.name(), caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); 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);
} }
} }

View File

@ -22,25 +22,33 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.ParseException; import java.text.ParseException;
import java.util.Date; import java.util.Date;
import java.util.logging.Level;
import org.sleuthkit.autopsy.casemodule.CaseMetadata; 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 * Case data stored in a case directory coordination service node.
* node to and from byte arrays.
*/ */
public final class CaseNodeData { 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; private boolean errorsOccurred;
/* /*
@ -53,28 +61,191 @@ public final class CaseNodeData {
private String displayName; private String displayName;
private short deletedItemFlags; private short deletedItemFlags;
/** /*
* Gets the current version of the case directory lock coordination service * Version 2 fields.
* node data.
*
* @return The version number.
*/ */
public static int getCurrentVersion() { private int minorVersion;
return CaseNodeData.CURRENT_VERSION;
/**
* 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 * Reads case data from a case directory coordination service node. If the
* a case directory lock coordination service node to and from byte arrays. * 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. * @param metadata The case meta data.
* *
* @throws java.text.ParseException If there is an error parsing dates from * @throws ParseException If there is an error parsing dates from string
* string representations of dates in the * representations of dates in the meta data.
* meta data.
*/ */
public CaseNodeData(CaseMetadata metadata) throws ParseException { private CaseNodeData(CaseMetadata metadata) throws ParseException {
this.version = CURRENT_VERSION; this.version = MAJOR_VERSION;
this.errorsOccurred = false; this.errorsOccurred = false;
this.directory = Paths.get(metadata.getCaseDirectory()); this.directory = Paths.get(metadata.getCaseDirectory());
this.createDate = CaseMetadata.getDateFormat().parse(metadata.getCreatedDate()); this.createDate = CaseMetadata.getDateFormat().parse(metadata.getCreatedDate());
@ -82,51 +253,64 @@ public final class CaseNodeData {
this.name = metadata.getCaseName(); this.name = metadata.getCaseName();
this.displayName = metadata.getCaseDisplayName(); this.displayName = metadata.getCaseDisplayName();
this.deletedItemFlags = 0; this.deletedItemFlags = 0;
this.minorVersion = MINOR_VERSION;
} }
/** /**
* Uses coordination service node data to construct an object that converts * Uses the raw bytes from a case directory coordination service node to
* data for a case directory lock coordination service node to and from byte * construct a case node data object.
* arrays.
* *
* @param nodeData The raw bytes received from the coordination service. * @param nodeData The raw bytes received from the coordination service.
* *
* @throws IOException If there is an error reading the node data. * @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) { if (nodeData == null || nodeData.length == 0) {
throw new IOException(null == nodeData ? "Null node data byte array" : "Zero-length node data byte array"); throw new IOException(null == nodeData ? "Null node data byte array" : "Zero-length node data byte array");
} }
DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(nodeData)); try (ByteArrayInputStream byteStream = new ByteArrayInputStream(nodeData); DataInputStream inputStream = new DataInputStream(byteStream)) {
this.version = inputStream.readInt(); this.version = inputStream.readInt();
if (this.version > 0) { if (this.version == 1) {
this.errorsOccurred = inputStream.readBoolean(); this.errorsOccurred = inputStream.readBoolean();
} else { } else {
short legacyErrorsOccurred = inputStream.readByte(); byte errorsOccurredByte = inputStream.readByte();
this.errorsOccurred = (legacyErrorsOccurred < 0); this.errorsOccurred = (errorsOccurredByte < 0);
} }
if (this.version > 0) { if (this.version > 0) {
this.directory = Paths.get(inputStream.readUTF()); this.directory = Paths.get(inputStream.readUTF());
this.createDate = new Date(inputStream.readLong()); this.createDate = new Date(inputStream.readLong());
this.lastAccessDate = new Date(inputStream.readLong()); this.lastAccessDate = new Date(inputStream.readLong());
this.name = inputStream.readUTF(); this.name = inputStream.readUTF();
this.displayName = inputStream.readUTF(); this.displayName = inputStream.readUTF();
this.deletedItemFlags = inputStream.readShort(); 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. * @return The version number.
*/ */
public int getVersion() { private int getVersion() {
return this.version; 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 * 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. * @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 * 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. * @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 * Gets the path of the case directory.
* data.
* *
* @return The case directory path. * @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 * Gets the date the case was created.
* 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.
* *
* @return The create date. * @return The create date.
*/ */
@ -174,16 +347,7 @@ public final class CaseNodeData {
} }
/** /**
* Sets the date the case represented by this node data was created. * Gets the date the case was last accessed.
*
* @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.
* *
* @return The last access date. * @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. * @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 * Gets the unique and immutable name of the case.
* represented by this node data.
* *
* @return The case name. * @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 * Gets the display 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.
* *
* @return The case display name. * @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. * @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 * Checks whether a given deleted item flag is set for the case.
* this node data.
* *
* @param flag The flag to check. * @param flag The flag to check.
* *
* @return * @return True or false.
*/ */
public boolean isDeletedFlagSet(DeletedFlags flag) { public boolean isDeletedFlagSet(DeletedFlags flag) {
return (this.deletedItemFlags & flag.getValue()) == flag.getValue(); 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. * @param flag The flag to set.
*/ */
@ -265,22 +417,24 @@ public final class CaseNodeData {
* *
* @return The node data as a byte array. * @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 { private byte[] toArray() throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); DataOutputStream outputStream = new DataOutputStream(byteStream)) {
DataOutputStream outputStream = new DataOutputStream(byteStream); outputStream.writeInt(this.version);
outputStream.writeInt(this.version); outputStream.writeByte((byte) (this.errorsOccurred ? 0x80 : 0));
outputStream.writeBoolean(this.errorsOccurred); outputStream.writeUTF(this.directory.toString());
outputStream.writeUTF(this.directory.toString()); outputStream.writeLong(this.createDate.getTime());
outputStream.writeLong(this.createDate.getTime()); outputStream.writeLong(this.lastAccessDate.getTime());
outputStream.writeLong(this.lastAccessDate.getTime()); outputStream.writeUTF(this.name);
outputStream.writeUTF(this.name); outputStream.writeUTF(this.displayName);
outputStream.writeUTF(this.displayName); outputStream.writeShort(this.deletedItemFlags);
outputStream.writeShort(this.deletedItemFlags); outputStream.writeInt(this.minorVersion);
outputStream.flush(); outputStream.flush();
byteStream.flush(); byteStream.flush();
return byteStream.toByteArray(); 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);
}
}
} }

View File

@ -18,31 +18,28 @@
*/ */
package org.sleuthkit.autopsy.casemodule.multiusercases; 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.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;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
/** /**
* Queries the coordination service to collect the multi-user case node data * Collects the multi-user case node data stored in the case directory
* stored in the case directory lock ZooKeeper nodes. * coordination service nodes.
*/ */
final public class CaseNodeDataCollector { final public class CaseNodeDataCollector {
private static final Logger logger = Logger.getLogger(CaseNodeDataCollector.class.getName()); private static final Logger logger = Logger.getLogger(CaseNodeDataCollector.class.getName());
/** /**
* Queries the coordination service to collect the multi-user case node data * Collects the multi-user case node data stored in the case directory
* stored in the case directory lock ZooKeeper nodes. * coordination service nodes.
* *
* @return The node data for the multi-user cases known to the coordination * @return The node data for the multi-user cases known to the coordination
* service. * service.
@ -54,128 +51,30 @@ final public class CaseNodeDataCollector {
* service. * service.
*/ */
public static List<CaseNodeData> getNodeData() throws CoordinationServiceException, InterruptedException { public static List<CaseNodeData> getNodeData() throws CoordinationServiceException, InterruptedException {
final List<CaseNodeData> cases = new ArrayList<>(); final List<CaseNodeData> nodeDataList = new ArrayList<>();
final CoordinationService coordinationService = CoordinationService.getInstance(); final CoordinationService coordinationService = CoordinationService.getInstance();
final List<String> nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); final List<String> nodePaths = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES);
for (String nodeName : nodeList) { for (String nodePath : nodePaths) {
if (CoordinationServiceUtils.isCaseNameNodePath(nodeName) /*
|| CoordinationServiceUtils.isCaseResourcesNodePath(nodeName) * Skip the case name, case resources, and case auto ingest log
|| CoordinationServiceUtils.isCaseAutoIngestLogNodePath(nodeName)) { * coordination service nodes. They are not used to store case data.
*/
if (isCaseNameNodePath(nodePath) || isCaseResourcesNodePath(nodePath) || isCaseAutoIngestLogNodePath(nodePath)) {
continue; continue;
} }
/* /*
* Get the data from the case directory lock node. This data may not * Get the case node data from the case directory coordination service node.
* exist or may exist only in an older version. If it is missing or
* incomplete, create or update it.
*/ */
try { try {
CaseNodeData nodeData; final CaseNodeData nodeData = CaseNodeData.readCaseNodeData(nodePath);
final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName); nodeDataList.add(nodeData);
if (nodeBytes != null && nodeBytes.length > 0) { } catch (CaseNodeDataException | InterruptedException ex) {
nodeData = new CaseNodeData(nodeBytes); logger.log(Level.WARNING, String.format("Error reading case node data from %s", nodePath), ex);
if (nodeData.getVersion() < CaseNodeData.getCurrentVersion()) {
nodeData = updateNodeData(nodeName, nodeData);
}
} else {
nodeData = updateNodeData(nodeName, null);
}
if (nodeData != null) {
cases.add(nodeData);
}
} catch (CoordinationService.CoordinationServiceException | InterruptedException | IOException | ParseException | CaseMetadata.CaseMetadataException ex) {
logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex);
} }
} }
return cases; return nodeDataList;
}
/**
* 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);
}
} }
/** /**
@ -183,5 +82,5 @@ final public class CaseNodeDataCollector {
*/ */
private CaseNodeDataCollector() { private CaseNodeDataCollector() {
} }
} }

View File

@ -62,6 +62,7 @@ import org.sleuthkit.autopsy.casemodule.CaseActionException;
import org.sleuthkit.autopsy.casemodule.CaseDetails; import org.sleuthkit.autopsy.casemodule.CaseDetails;
import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock;
@ -1016,22 +1017,19 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen
* *
* @param caseDirectoryPath The case directory path. * @param caseDirectoryPath The case directory path.
* *
* @throws CoordinationServiceException If there was an error getting the * @throws InterruptedException If the thread running the input directory
* node data from the cooordination * scan task is interrupted while blocked,
* service. * i.e., if auto ingest is shutting down.
* @throws IOException If the node data was missing or
* there was an error interpreting it.
* @throws InterruptedException If the thread running the input
* directory scan task is interrupted
* while blocked, i.e., if auto ingest
* is shutting down.
*/ */
private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws IOException, CoordinationServiceException, InterruptedException { private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws InterruptedException {
byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString()); try {
CaseNodeData caseNodeData = new CaseNodeData(rawData); CaseNodeData caseNodeData = CaseNodeData.readCaseNodeData(caseDirectoryPath.toString());
caseNodeData.setErrorsOccurred(true); caseNodeData.setErrorsOccurred(true);
rawData = caseNodeData.toArray(); CaseNodeData.writeCaseNodeData(caseNodeData);
coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString(), rawData); } 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) { if (null != caseDirectoryPath) {
job.setCaseDirectoryPath(caseDirectoryPath); job.setCaseDirectoryPath(caseDirectoryPath);
job.setErrorsOccurred(true); job.setErrorsOccurred(true);
try { setCaseNodeDataErrorsOccurred(caseDirectoryPath);
setCaseNodeDataErrorsOccurred(caseDirectoryPath);
} catch (IOException ex) {
sysLogger.log(Level.WARNING, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex);
}
} else { } else {
job.setErrorsOccurred(false); job.setErrorsOccurred(false);
} }

View File

@ -37,6 +37,7 @@ import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.CaseMetadata;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData;
import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException;
import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils; import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode;
@ -785,12 +786,14 @@ final class DeleteCaseTask implements Runnable {
* case. * case.
* *
* @param flag The flag to set. * @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 { try {
caseNodeData.setDeletedFlag(flag); caseNodeData.setDeletedFlag(flag);
coordinationService.setNodeData(CategoryNode.CASES, caseNodeData.getDirectory().toString(), caseNodeData.toArray()); CaseNodeData.writeCaseNodeData(caseNodeData);
} catch (IOException | CoordinationServiceException | InterruptedException ex) { } catch (CaseNodeDataException ex) {
logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s", flag.name(), caseNodeData.getDisplayName()), ex); logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s", flag.name(), caseNodeData.getDisplayName()), ex);
} }
} }