diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java
index 227bfd0650..5ad9a606a6 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java
@@ -26,6 +26,8 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor;
import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.imagewriter.ImageWriterService;
+import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.SleuthkitJNI;
@@ -41,6 +43,7 @@ class AddImageTask implements Runnable {
private final String deviceId;
private final String imagePath;
private final String timeZone;
+ private final String imageWriterPath;
private final boolean ignoreFatOrphanFiles;
private final DataSourceProcessorProgressMonitor progressMonitor;
private final DataSourceProcessorCallback callback;
@@ -74,15 +77,19 @@ class AddImageTask implements Runnable {
* java.util.TimeZone.getID.
* @param ignoreFatOrphanFiles Whether to parse orphans if the image has a
* FAT filesystem.
+ * @param imageWriterPath Path that a copy of the image should be written to.
+ * Use empty string to disable image writing
* @param progressMonitor Progress monitor to report progress during
* processing.
* @param callback Callback to call when processing is done.
*/
- AddImageTask(String deviceId, String imagePath, String timeZone, boolean ignoreFatOrphanFiles, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
+ AddImageTask(String deviceId, String imagePath, String timeZone, boolean ignoreFatOrphanFiles, String imageWriterPath,
+ DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
this.deviceId = deviceId;
this.imagePath = imagePath;
this.timeZone = timeZone;
this.ignoreFatOrphanFiles = ignoreFatOrphanFiles;
+ this.imageWriterPath = imageWriterPath;
this.callback = callback;
this.progressMonitor = progressMonitor;
tskAddImageProcessLock = new Object();
@@ -101,7 +108,7 @@ class AddImageTask implements Runnable {
try {
currentCase.getSleuthkitCase().acquireExclusiveLock();
synchronized (tskAddImageProcessLock) {
- tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(timeZone, true, ignoreFatOrphanFiles);
+ tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(timeZone, true, ignoreFatOrphanFiles, imageWriterPath);
}
Thread progressUpdateThread = new Thread(new ProgressUpdater(progressMonitor, tskAddImageProcess));
progressUpdateThread.start();
@@ -201,6 +208,9 @@ class AddImageTask implements Runnable {
if (!verificationError.isEmpty()) {
errorMessages.add(verificationError);
}
+ if(! imageWriterPath.isEmpty()){
+ ImageWriterService.createImageWriter(imageId);
+ }
newDataSources.add(newImage);
} else {
String errorMessage = String.format("Error commiting adding image %s to the case database, no object id returned", imagePath); //NON-NLS
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
index 42c8667471..f22ada5664 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
@@ -66,6 +66,10 @@ LocalDiskPanel.timeZoneLabel.text=Please select the input timezone:
LocalDiskPanel.noFatOrphansCheckbox.toolTipText=
LocalDiskPanel.noFatOrphansCheckbox.text=Ignore orphan files in FAT file systems
LocalDiskPanel.descLabel.text=(faster results, although some data will not be searched)
+LocalDiskPanel.imageWriterDirError.text=Error - directory does not exist
+LocalDiskPanel.imageWriterEmptyPathError.text=Error - enter path for VHD
+LocalDiskPanel.imageWriterIsDirError.text=Error - VHD path is a directory
+LocalDiskPanel.imageWriterFileExistsError.text=Error - VHD path already exists
MissingImageDialog.browseButton.text=Browse
MissingImageDialog.pathNameTextField.text=
AddImageWizardAddingProgressVisual.progressTextArea.border.title=Status
@@ -222,6 +226,11 @@ LocalFilesPanel.displayNameLabel.text=Logical File Set Display Name: Default
IngestJobInfoPanel.jLabel1.text=Ingest Modules
IngestJobInfoPanel.jLabel2.text=Ingest Jobs
CaseInformationPanel.closeButton.text=Close
+LocalDiskPanel.copyImageCheckbox.text=Make a VHD image of the drive while it is being analyzed
+LocalDiskPanel.imageWriterErrorLabel.text=Error Label
+LocalDiskPanel.jLabel1.text=Note that at least one ingest module must be run to create a complete copy
+LocalDiskPanel.pathTextField.text=
+LocalDiskPanel.browseButton.text=Browse
CasePropertiesPanel.updateCaseNameButton.text=Update Name
CasePropertiesPanel.caseNameTextField.text=
CasePropertiesPanel.caseDirLabel.text=Case Directory:
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java
index f76f620413..5d976c0baa 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java
@@ -185,7 +185,7 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour
* @param callback Callback to call when processing is done.
*/
public void run(String deviceId, String imagePath, String timeZone, boolean ignoreFatOrphanFiles, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
- addImageTask = new AddImageTask(deviceId, imagePath, timeZone, ignoreFatOrphanFiles, progressMonitor, callback);
+ addImageTask = new AddImageTask(deviceId, imagePath, timeZone, ignoreFatOrphanFiles, "", progressMonitor, callback);
new Thread(addImageTask).start();
}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java
index 305bb3dd90..d5b926d197 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java
@@ -54,6 +54,7 @@ public class LocalDiskDSProcessor implements DataSourceProcessor, AutoIngestData
private String deviceId;
private String drivePath;
private String timeZone;
+ private String imageWriterPath = "";
private boolean ignoreFatOrphanFiles;
private boolean setDataSourceOptionsCalled;
@@ -137,8 +138,11 @@ public class LocalDiskDSProcessor implements DataSourceProcessor, AutoIngestData
drivePath = configPanel.getContentPaths();
timeZone = configPanel.getTimeZone();
ignoreFatOrphanFiles = configPanel.getNoFatOrphans();
+ if(configPanel.getImageWriterEnabled()){
+ imageWriterPath = configPanel.getImageWriterPath();
+ }
}
- addDiskTask = new AddImageTask(deviceId, drivePath, timeZone, ignoreFatOrphanFiles, progressMonitor, callback);
+ addDiskTask = new AddImageTask(deviceId, drivePath, timeZone, ignoreFatOrphanFiles, imageWriterPath, progressMonitor, callback);
new Thread(addDiskTask).start();
}
@@ -164,7 +168,7 @@ public class LocalDiskDSProcessor implements DataSourceProcessor, AutoIngestData
* @param callback Callback to call when processing is done.
*/
public void run(String deviceId, String drivePath, String timeZone, boolean ignoreFatOrphanFiles, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) {
- addDiskTask = new AddImageTask(deviceId, drivePath, timeZone, ignoreFatOrphanFiles, progressMonitor, callback);
+ addDiskTask = new AddImageTask(deviceId, drivePath, timeZone, ignoreFatOrphanFiles, imageWriterPath, progressMonitor, callback);
new Thread(addDiskTask).start();
}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form
index b1409546e6..89835246a9 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.form
@@ -3,10 +3,10 @@
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java
index 0d485b3392..2572c0c61d 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java
@@ -22,6 +22,8 @@ import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
+import java.io.File;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
@@ -30,6 +32,7 @@ import java.util.TimeZone;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import javax.swing.ComboBoxModel;
+import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
@@ -57,6 +60,7 @@ final class LocalDiskPanel extends JPanel {
private List disks;
private LocalDiskModel model;
private boolean enableNext = false;
+ private final JFileChooser fc = new JFileChooser();
/**
* Creates new form LocalDiskPanel
@@ -88,6 +92,10 @@ final class LocalDiskPanel extends JPanel {
errorLabel.setVisible(false);
errorLabel.setText("");
diskComboBox.setEnabled(false);
+ imageWriterErrorLabel.setOpaque(true);
+ imageWriterErrorLabel.setText("");
+ pathTextField.setEnabled(copyImageCheckbox.isSelected());
+ browseButton.setEnabled(copyImageCheckbox.isSelected());
}
/**
@@ -106,9 +114,14 @@ final class LocalDiskPanel extends JPanel {
timeZoneComboBox = new javax.swing.JComboBox<>();
noFatOrphansCheckbox = new javax.swing.JCheckBox();
descLabel = new javax.swing.JLabel();
+ copyImageCheckbox = new javax.swing.JCheckBox();
+ imageWriterErrorLabel = new javax.swing.JLabel();
+ jLabel1 = new javax.swing.JLabel();
+ pathTextField = new javax.swing.JTextField();
+ browseButton = new javax.swing.JButton();
- setMinimumSize(new java.awt.Dimension(0, 65));
- setPreferredSize(new java.awt.Dimension(485, 65));
+ setMinimumSize(new java.awt.Dimension(0, 420));
+ setPreferredSize(new java.awt.Dimension(485, 410));
diskLabel.setFont(diskLabel.getFont().deriveFont(diskLabel.getFont().getStyle() & ~java.awt.Font.BOLD, 11));
org.openide.awt.Mnemonics.setLocalizedText(diskLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.diskLabel.text")); // NOI18N
@@ -132,24 +145,66 @@ final class LocalDiskPanel extends JPanel {
descLabel.setFont(descLabel.getFont().deriveFont(descLabel.getFont().getStyle() & ~java.awt.Font.BOLD, 11));
org.openide.awt.Mnemonics.setLocalizedText(descLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.descLabel.text")); // NOI18N
+ org.openide.awt.Mnemonics.setLocalizedText(copyImageCheckbox, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.copyImageCheckbox.text")); // NOI18N
+ copyImageCheckbox.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ copyImageCheckboxActionPerformed(evt);
+ }
+ });
+
+ imageWriterErrorLabel.setFont(imageWriterErrorLabel.getFont().deriveFont(imageWriterErrorLabel.getFont().getStyle() & ~java.awt.Font.BOLD, 11));
+ imageWriterErrorLabel.setForeground(new java.awt.Color(255, 0, 0));
+ org.openide.awt.Mnemonics.setLocalizedText(imageWriterErrorLabel, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.imageWriterErrorLabel.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.jLabel1.text")); // NOI18N
+
+ pathTextField.setText(org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.pathTextField.text")); // NOI18N
+ pathTextField.addKeyListener(new java.awt.event.KeyAdapter() {
+ public void keyReleased(java.awt.event.KeyEvent evt) {
+ pathTextFieldKeyReleased(evt);
+ }
+ public void keyTyped(java.awt.event.KeyEvent evt) {
+ pathTextFieldKeyTyped(evt);
+ }
+ });
+
+ org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(LocalDiskPanel.class, "LocalDiskPanel.browseButton.text")); // NOI18N
+ browseButton.addActionListener(new java.awt.event.ActionListener() {
+ public void actionPerformed(java.awt.event.ActionEvent evt) {
+ browseButtonActionPerformed(evt);
+ }
+ });
+
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
- .addComponent(diskLabel)
- .addComponent(diskComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 345, javax.swing.GroupLayout.PREFERRED_SIZE)
- .addComponent(errorLabel)
.addGroup(layout.createSequentialGroup()
- .addComponent(timeZoneLabel)
- .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
- .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 215, javax.swing.GroupLayout.PREFERRED_SIZE))
- .addComponent(noFatOrphansCheckbox)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
+ .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 362, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(diskLabel)
+ .addComponent(diskComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 345, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addComponent(errorLabel)
+ .addGroup(layout.createSequentialGroup()
+ .addComponent(timeZoneLabel)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 215, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addComponent(noFatOrphansCheckbox)
+ .addComponent(copyImageCheckbox)
+ .addGroup(layout.createSequentialGroup()
+ .addGap(21, 21, 21)
+ .addComponent(descLabel))))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(browseButton))
.addGroup(layout.createSequentialGroup()
.addGap(21, 21, 21)
- .addComponent(descLabel)))
- .addGap(0, 102, Short.MAX_VALUE))
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 423, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addComponent(imageWriterErrorLabel))))
+ .addGap(0, 29, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
@@ -167,19 +222,76 @@ final class LocalDiskPanel extends JPanel {
.addComponent(noFatOrphansCheckbox)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(descLabel)
- .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(copyImageCheckbox)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+ .addComponent(browseButton))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(jLabel1)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(imageWriterErrorLabel)
+ .addContainerGap(170, Short.MAX_VALUE))
);
}// //GEN-END:initComponents
+
+ private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed
+ String oldText = pathTextField.getText();
+ // set the current directory of the FileChooser if the ImagePath Field is valid
+ File currentFile = new File(oldText);
+ if ((currentFile.getParentFile() != null) && (currentFile.getParentFile().exists())) {
+ fc.setCurrentDirectory(currentFile.getParentFile());
+ }
+
+ int retval = fc.showOpenDialog(this);
+ if (retval == JFileChooser.APPROVE_OPTION) {
+ String path = fc.getSelectedFile().getPath();
+ pathTextField.setText(path);
+ }
+ fireUpdateEvent();
+ }//GEN-LAST:event_browseButtonActionPerformed
+
+ private void copyImageCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_copyImageCheckboxActionPerformed
+ pathTextField.setEnabled(copyImageCheckbox.isSelected());
+ browseButton.setEnabled(copyImageCheckbox.isSelected());
+ fireUpdateEvent();
+ }//GEN-LAST:event_copyImageCheckboxActionPerformed
+
+ private void pathTextFieldKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_pathTextFieldKeyTyped
+
+ }//GEN-LAST:event_pathTextFieldKeyTyped
+
+ private void pathTextFieldKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_pathTextFieldKeyReleased
+ fireUpdateEvent();
+ }//GEN-LAST:event_pathTextFieldKeyReleased
+
// Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JButton browseButton;
+ private javax.swing.JCheckBox copyImageCheckbox;
private javax.swing.JLabel descLabel;
private javax.swing.JComboBox diskComboBox;
private javax.swing.JLabel diskLabel;
private javax.swing.JLabel errorLabel;
+ private javax.swing.JLabel imageWriterErrorLabel;
+ private javax.swing.JLabel jLabel1;
private javax.swing.JCheckBox noFatOrphansCheckbox;
+ private javax.swing.JTextField pathTextField;
private javax.swing.JComboBox timeZoneComboBox;
private javax.swing.JLabel timeZoneLabel;
// End of variables declaration//GEN-END:variables
+ private void fireUpdateEvent(){
+ try {
+ firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true);
+ } catch (Exception e) {
+ logger.log(Level.SEVERE, "LocalDiskPanel listener threw exception", e); //NON-NLS
+ MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr"),
+ NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr.msg"),
+ MessageNotifyUtil.MessageType.ERROR);
+ }
+ }
+
/**
* Return the currently selected disk path.
*
@@ -214,14 +326,69 @@ final class LocalDiskPanel extends JPanel {
boolean getNoFatOrphans() {
return noFatOrphansCheckbox.isSelected();
}
+
+ private static String getDefaultImageWriterFolder(){
+ return Paths.get(Case.getCurrentCase().getModuleDirectory(), "Image Writer").toString();
+ }
+
+ private void setPotentialImageWriterPath(LocalDisk disk){
+
+ File subDirectory = Paths.get(getDefaultImageWriterFolder()).toFile();
+ if (!subDirectory.exists()) {
+ subDirectory.mkdirs();
+ }
+
+ String path = disk.getName().replaceAll("[:]", "");
+ path += " " + System.currentTimeMillis();
+ path += ".vhd";
+ pathTextField.setText(Paths.get(getDefaultImageWriterFolder(), path).toString());
+ }
+
+ private boolean imageWriterPathIsValid(){
+ if(pathTextField.getText().isEmpty()){
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterEmptyPathError.text"));
+ return false;
+ }
+
+ File f = new File(pathTextField.getText());
+ if(((f.getParentFile() != null) && (! f.getParentFile().exists())) ||
+ (f.getParentFile() == null)) {
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterDirError.text"));
+ return false;
+ }
+ if(f.isDirectory()){
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterIsDirError.text"));
+ return false;
+ }
+ if(f.exists()){
+ imageWriterErrorLabel.setText(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.imageWriterFileExistsError.text"));
+ return false;
+ }
+
+ imageWriterErrorLabel.setText("");
+ return true;
+ }
+
+ boolean getImageWriterEnabled(){
+ return copyImageCheckbox.isSelected();
+ }
+
+ String getImageWriterPath(){
+ return pathTextField.getText();
+ }
/**
- * Should we enable the wizard's next button? Always return true because we
- * control the possible selections.
+ * Should we enable the wizard's next button? We control all the possible
+ * selections except for Image Writer.
*
- * @return true
+ * @return true if panel is valid
*/
public boolean validatePanel() {
+ if(copyImageCheckbox.isSelected() &&
+ ! imageWriterPathIsValid()){
+ return false;
+ }
+
return enableNext;
}
@@ -318,15 +485,8 @@ final class LocalDiskPanel extends JPanel {
if (ready) {
selected = (LocalDisk) anItem;
enableNext = true;
-
- try {
- firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true);
- } catch (Exception e) {
- logger.log(Level.SEVERE, "LocalDiskPanel listener threw exception", e); //NON-NLS
- MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr"),
- NbBundle.getMessage(this.getClass(), "LocalDiskPanel.moduleErr.msg"),
- MessageNotifyUtil.MessageType.ERROR);
- }
+ setPotentialImageWriterPath((LocalDisk) selected);
+ fireUpdateEvent();
}
}
diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/Bundle.properties
index 4a8de9a48b..ad62c83c2f 100755
--- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/Bundle.properties
+++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/Bundle.properties
@@ -10,3 +10,4 @@ RawDSInputPanel.jBreakFileUpLabel.text=Break image up into:
RawDSInputPanel.jNoBreakupRadioButton.text=Do not break up
RawDSInputPanel.j2GBBreakupRadioButton.text=2GB chunks
RawDSInputPanel.timeZoneLabel.text=Please select the input timezone:
+
diff --git a/Core/src/org/sleuthkit/autopsy/imagewriter/Bundle.properties b/Core/src/org/sleuthkit/autopsy/imagewriter/Bundle.properties
new file mode 100644
index 0000000000..67a92df30f
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/imagewriter/Bundle.properties
@@ -0,0 +1,8 @@
+# To change this license header, choose License Headers in Project Properties.
+# To change this template file, choose Tools | Templates
+# and open the template in the editor.
+
+ImageWriterService.serviceName=Image Writer
+ImageWriterService.waitingForVHDs=Waiting for VHD(s) to complete
+ImageWriterService.shouldWait=Wait for VHD(s) in progress to complete?
+ImageWriterService.localDisk=Local disk image copy
diff --git a/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriter.java b/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriter.java
new file mode 100644
index 0000000000..55bfa23326
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriter.java
@@ -0,0 +1,288 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011-2016 Basis Technology Corp.
+ * Contact: carrier sleuthkit org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.imagewriter;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import org.netbeans.api.progress.ProgressHandle;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.core.RuntimeProperties;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.datamodel.Image;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent;
+import org.sleuthkit.datamodel.SleuthkitJNI;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * The ImageWriter class is used to complete VHD copies created from local disks
+ * after the ingest process completes. The AddImageTask for this data source must have included
+ * a non-empty imageWriterPath parameter to enable Image Writer.
+ *
+ * Most of the cancellation/cleanup is handled through ImageWriterService
+ */
+class ImageWriter implements PropertyChangeListener{
+
+ private final Logger logger = Logger.getLogger(ImageWriter.class.getName());
+
+ private final Long dataSourceId;
+
+ private Long imageHandle = null;
+ private Future> finishTask = null;
+ private ProgressHandle progressHandle = null;
+ private ScheduledFuture> progressUpdateTask = null;
+ private boolean isCancelled = false;
+ private boolean isStarted = false;
+ private final Object currentTasksLock = new Object(); // Get this lock before accessing imageHandle, finishTask, progressHandle, progressUpdateTask,
+ // isCancelled, isStarted, or isFinished
+
+ private ScheduledThreadPoolExecutor periodicTasksExecutor = null;
+ private final boolean doUI;
+
+ /**
+ * Create the Image Writer object.
+ * After creation, startListeners() should be called.
+ * @param dataSourceId
+ */
+ ImageWriter(Long dataSourceId){
+ this.dataSourceId = dataSourceId;
+ doUI = RuntimeProperties.runningWithGUI();
+ }
+
+ /**
+ * Add this ImageWriter object as a listener to the necessary events
+ */
+ void subscribeToEvents(){
+ IngestManager.getInstance().addIngestJobEventListener(this);
+ }
+
+ /**
+ * Deregister this object from the events. This is ok to call multiple times.
+ */
+ void unsubscribeFromEvents(){
+ IngestManager.getInstance().removeIngestJobEventListener(this);
+ }
+
+ /**
+ * Handle the events:
+ * DATA_SOURCE_ANALYSIS_COMPLETED - start the finish image process and clean up after it is complete
+ */
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if(evt instanceof DataSourceAnalysisCompletedEvent){
+
+ DataSourceAnalysisCompletedEvent event = (DataSourceAnalysisCompletedEvent)evt;
+
+ if(event.getDataSource() != null){
+ long imageId = event.getDataSource().getId();
+ String name = event.getDataSource().getName();
+
+ // Check that the event corresponds to this datasource
+ if(imageId != dataSourceId){
+ return;
+ }
+ new Thread(() -> {
+ startFinishImage(name);
+ }).start();
+
+ } else {
+ logger.log(Level.SEVERE, "DataSourceAnalysisCompletedEvent did not contain a dataSource object"); //NON-NLS
+ }
+ }
+ }
+
+ private void startFinishImage(String dataSourceName){
+
+ synchronized(currentTasksLock){
+ if(isCancelled){
+ return;
+ }
+
+ // If we've already started the finish process for this datasource, return.
+ // Multiple DataSourceAnalysisCompletedEvent events can come from
+ // the same image if more ingest modules are run later
+ if(isStarted){
+ return;
+ }
+
+ Image image;
+ try{
+ image = Case.getCurrentCase().getSleuthkitCase().getImageById(dataSourceId);
+ imageHandle = image.getImageHandle();
+ } catch (IllegalStateException ex){
+ // This exception means that getCurrentCase() failed because no case was open.
+ // This can happen when the user closes the case while ingest is ongoing - canceling
+ // ingest fires off the DataSourceAnalysisCompletedEvent while the case is in the
+ // process of closing.
+ logger.log(Level.WARNING, String.format("Case closed before ImageWriter could start the finishing process for %s",
+ dataSourceName));
+ return;
+ } catch (TskCoreException ex){
+ logger.log(Level.SEVERE, "Error loading image", ex);
+ return;
+ }
+
+ logger.log(Level.INFO, String.format("Finishing VHD image for %s",
+ dataSourceName)); //NON-NLS
+
+ if(doUI){
+ periodicTasksExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("image-writer-progress-update-%d").build()); //NON-NLS
+ progressHandle = ProgressHandle.createHandle("Image writer - " + dataSourceName);
+ progressHandle.start(100);
+ progressUpdateTask = periodicTasksExecutor.scheduleAtFixedRate(
+ new ProgressUpdateTask(progressHandle, imageHandle), 0, 250, TimeUnit.MILLISECONDS);
+ }
+
+ // The added complexity here with the Future is because we absolutely need to make sure
+ // the call to finishImageWriter returns before allowing the TSK data structures to be freed
+ // during case close.
+ finishTask = Executors.newSingleThreadExecutor().submit(() -> {
+ try{
+ SleuthkitJNI.finishImageWriter(imageHandle);
+ } catch (TskCoreException ex){
+ logger.log(Level.SEVERE, "Error finishing VHD image", ex); //NON-NLS
+ }
+ });
+
+ // Setting this means that finishTask and all the UI updaters are initialized (if running UI)
+ isStarted = true;
+ }
+
+ // Wait for finishImageWriter to complete
+ try{
+ // The call to get() can happen multiple times if the user closes the case, which is ok
+ finishTask.get();
+ } catch (InterruptedException | ExecutionException ex){
+ logger.log(Level.SEVERE, "Error finishing VHD image", ex); //NON-NLS
+ }
+
+ synchronized(currentTasksLock){
+ if(doUI){
+ // Some of these may be called twice if the user closes the case
+ progressUpdateTask.cancel(true);
+ progressHandle.finish();
+ periodicTasksExecutor.shutdown();
+ }
+ }
+
+ logger.log(Level.INFO, String.format("Finished writing VHD image for %s", dataSourceName)); //NON-NLS
+ }
+
+ /**
+ * If a task hasn't been started yet, set the cancel flag so it can no longer
+ * start.
+ * This is intended to be used in case close so a job doesn't suddenly start
+ * up during cleanup.
+ */
+ void cancelIfNotStarted(){
+ synchronized(currentTasksLock){
+ if(! isStarted){
+ isCancelled = true;
+ }
+ }
+ }
+
+ /**
+ * Check if the finishTask process is running.
+ * @return true if the finish task is still going on, false if it is finished or
+ * never started
+ */
+ boolean jobIsInProgress(){
+ synchronized(currentTasksLock){
+ return((isStarted) && (! finishTask.isDone()));
+ }
+ }
+
+ /**
+ * Cancels a single job.
+ * Does not wait for the job to complete. Safe to call with Image Writer in any state.
+ */
+ void cancelJob(){
+ synchronized(currentTasksLock){
+ // All of the following is redundant but safe to call on a complete job
+ isCancelled = true;
+
+ if(isStarted){
+ SleuthkitJNI.cancelFinishImage(imageHandle);
+
+ // Stop the progress bar update task.
+ // The thread from startFinishImage will also stop it
+ // once the task completes, but we don't have a guarantee on
+ // when that happens.
+ // Since we've stopped the update task, we'll stop the associated progress
+ // bar now, too.
+ if(doUI){
+ progressUpdateTask.cancel(true);
+ progressHandle.finish();
+ }
+ }
+ }
+ }
+
+ /**
+ * Blocks while all finishImage tasks complete.
+ * Also makes sure the progressUpdateTask is canceled.
+ */
+ void waitForJobToFinish(){
+ synchronized(currentTasksLock){
+ // Wait for the finish task to end
+ if(isStarted){
+ try{
+ finishTask.get();
+ } catch (InterruptedException | ExecutionException ex){
+ Logger.getLogger(ImageWriter.class.getName()).log(Level.SEVERE, "Error finishing VHD image", ex); //NON-NLS
+ }
+ if(doUI){
+ progressUpdateTask.cancel(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Task to query the Sleuthkit processing to get the percentage done.
+ */
+ private final class ProgressUpdateTask implements Runnable {
+ final long imageHandle;
+ final ProgressHandle progressHandle;
+
+ ProgressUpdateTask(ProgressHandle progressHandle, long imageHandle){
+ this.imageHandle = imageHandle;
+ this.progressHandle = progressHandle;
+ }
+
+ @Override
+ public void run() {
+ try {
+ int progress = SleuthkitJNI.getFinishImageProgress(imageHandle);
+ progressHandle.progress(progress);
+ } catch (Exception ex) {
+ logger.log(Level.SEVERE, "Unexpected exception in ProgressUpdateTask", ex); //NON-NLS
+ }
+ }
+ }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java b/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java
new file mode 100644
index 0000000000..8f3791d53c
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/imagewriter/ImageWriterService.java
@@ -0,0 +1,123 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2011-2016 Basis Technology Corp.
+ * Contact: carrier sleuthkit org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.imagewriter;
+
+import java.util.HashSet;
+import java.util.Set;
+import org.openide.DialogDescriptor;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.util.NbBundle;
+import org.openide.util.lookup.ServiceProvider;
+import org.openide.util.lookup.ServiceProviders;
+import org.sleuthkit.autopsy.framework.AutopsyService;
+
+@ServiceProviders(value = {@ServiceProvider(service = AutopsyService.class)})
+
+/**
+ * Creates and handles closing of ImageWriter objects.
+ * Currently, ImageWriter is only enabled for local disks, and local disks can
+ * not be processed in multi user mode. If ImageWriter is ever enabled for multi user
+ * cases this code will need to be revised.
+ */
+
+public class ImageWriterService implements AutopsyService {
+
+ private static final Set imageWriters = new HashSet<>(); // Contains all Image Writer objects
+ private static final Object imageWritersLock = new Object(); // Get this lock before accessing currentImageWriters
+
+ /**
+ * Create an image writer object for the given data source ID.
+ * @param imageId ID for the image
+ */
+ public static void createImageWriter(Long imageId){
+
+ // ImageWriter objects are created during the addImageTask. They can not arrive while
+ // we're closing case resources so we don't need to worry about one showing up while
+ // doing our close/cleanup.
+ synchronized(imageWritersLock){
+ ImageWriter writer = new ImageWriter(imageId);
+ writer.subscribeToEvents();
+ imageWriters.add(writer);
+ }
+ }
+
+ @Override
+ public String getServiceName() {
+ return NbBundle.getMessage(this.getClass(), "ImageWriterService.serviceName");
+ }
+
+ @Override
+ public void closeCaseResources(CaseContext context) throws AutopsyServiceException {
+ context.getProgressIndicator().progress(NbBundle.getMessage(this.getClass(), "ImageWriterService.waitingForVHDs"));
+
+ synchronized(imageWritersLock){
+ // If any of our ImageWriter objects haven't started the finish task, set the cancel flag
+ // to make sure they don't start now. The reason they haven't started is that
+ // ingest was not complete, and the user already confirmed that they want to exit
+ // even though ingest is not complete so we will take that to mean that they
+ // also don't want to wait for Image Writer.
+ for(ImageWriter writer: imageWriters){
+ writer.cancelIfNotStarted();
+ }
+
+ // Test whether any finishImage tasks are in progress
+ boolean jobsAreInProgress = false;
+ for(ImageWriter writer: imageWriters){
+ if(writer.jobIsInProgress()){
+ jobsAreInProgress = true;
+ break;
+ }
+ }
+
+ if(jobsAreInProgress){
+ // If jobs are in progress, ask the user if they want to wait for them to complete
+ NotifyDescriptor descriptor = new NotifyDescriptor.Confirmation(
+ NbBundle.getMessage(this.getClass(), "ImageWriterService.shouldWait"),
+ NbBundle.getMessage(this.getClass(), "ImageWriterService.localDisk"),
+ NotifyDescriptor.YES_NO_OPTION,
+ NotifyDescriptor.WARNING_MESSAGE);
+ descriptor.setValue(NotifyDescriptor.NO_OPTION);
+ Object response = DialogDisplayer.getDefault().notify(descriptor);
+
+ if(response == DialogDescriptor.NO_OPTION){
+ // Cancel all the jobs
+ for(ImageWriter writer: imageWriters){
+ writer.cancelJob();
+ }
+ }
+
+ // Wait for all finishImage jobs to complete. If the jobs got cancelled
+ // this will be very fast.
+ for(ImageWriter writer: imageWriters){
+ writer.waitForJobToFinish();
+ }
+
+ }
+
+ // Stop listening for events
+ for(ImageWriter writer: imageWriters){
+ writer.unsubscribeFromEvents();
+ }
+
+ // Clear out the list of Image Writers
+ imageWriters.clear();
+ }
+ }
+}