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 @@
- + - + @@ -26,21 +26,38 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + - + @@ -61,7 +78,18 @@ - + + + + + + + + + + + + @@ -165,5 +193,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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(); + } + } +}