From d6b653bc1b463b7cb9ed909195483d96a4dd50c3 Mon Sep 17 00:00:00 2001 From: Kelly Kelly Date: Fri, 23 Jul 2021 14:27:33 -0400 Subject: [PATCH 1/9] Added swingworker to cvt media viewer --- .../relationships/Bundle.properties-MERGED | 2 + .../relationships/MediaViewer.java | 82 +++++++++++++++---- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED index c43740776d..def41061cd 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED @@ -17,6 +17,8 @@ ContactsViewer_columnHeader_Phone=Phone ContactsViewer_noContacts_message= ContactsViewer_tabTitle=Contacts MediaViewer_Name=Media Attachments +MediaViewer_selection_failure_msg=Failed to get media attachments for selected accounts. +MediaViewer_selection_failure_title=Selection Failed MessageNode_Node_Property_Attms=Attachment Count MessageNode_Node_Property_Date=Date MessageNode_Node_Property_From=From diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java index 63988b200d..73fd4cde2b 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java @@ -19,18 +19,24 @@ package org.sleuthkit.autopsy.communications.relationships; import java.awt.Component; +import java.awt.Cursor; import java.awt.KeyboardFocusManager; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.JOptionPane; import javax.swing.JPanel; import static javax.swing.SwingUtilities.isDescendingFrom; +import javax.swing.SwingWorker; import org.openide.explorer.ExplorerManager; import static org.openide.explorer.ExplorerUtils.createLookup; import org.openide.nodes.AbstractNode; import org.openide.nodes.Node; +import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.communications.ModifiableProxyLookup; @@ -39,6 +45,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; import org.sleuthkit.autopsy.directorytree.DataResultFilterNode; import org.sleuthkit.datamodel.AbstractContent; +import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; @@ -58,6 +65,8 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM private final MessageDataContent contentViewer; + private MediaViewerWorker worker; + @Messages({ "MediaViewer_Name=Media Attachments" }) @@ -97,24 +106,17 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM @Override public void setSelectionInfo(SelectionInfo info) { - Set relationshipSources; - Set artifactList = new HashSet<>(); contentViewer.setNode(null); - if (info != null) { - try { - relationshipSources = info.getRelationshipSources(); - - relationshipSources.stream().filter((content) -> (content instanceof BlackboardArtifact)).forEachOrdered((content) -> { - artifactList.add((BlackboardArtifact) content); - }); - - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to update selection.", ex); - } - } thumbnailViewer.resetComponent(); - thumbnailViewer.setNode(new TableFilterNode(new DataResultFilterNode(new AbstractNode(new AttachmentThumbnailsChildren(artifactList)), tableEM), true, this.getClass().getName())); + if (worker != null) { + worker.cancel(true); + } + + worker = new MediaViewerWorker(info); + + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + worker.execute(); } @Override @@ -198,6 +200,56 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM } } + /** + * Swing worker for gathering the data needed to populate the media viewer. + */ + private class MediaViewerWorker extends SwingWorker, Void> { + + private final SelectionInfo selectionInfo; + + MediaViewerWorker(SelectionInfo info) { + selectionInfo = info; + } + + @Override + protected Set doInBackground() throws Exception { + Set relationshipSources; + Set artifactList = new HashSet<>(); + + if (selectionInfo != null) { + relationshipSources = selectionInfo.getRelationshipSources(); + + relationshipSources.stream().filter((content) -> (content instanceof BlackboardArtifact)).forEachOrdered((content) -> { + artifactList.add((BlackboardArtifact) content); + }); + } + + return artifactList; + } + + @Messages({ + "MediaViewer_selection_failure_msg=Failed to get media attachments for selected accounts.", + "MediaViewer_selection_failure_title=Selection Failed" + }) + @Override + protected void done() { + try { + if (isCancelled()) { + return; + } + thumbnailViewer.setNode(new TableFilterNode(new DataResultFilterNode(new AbstractNode(new AttachmentThumbnailsChildren(get())), tableEM), true, this.getClass().getName())); + } catch (ExecutionException | InterruptedException ex) { + String accounts = selectionInfo.getAccounts().stream().map(Account::getTypeSpecificID).collect(Collectors.joining(",")); + logger.log(Level.WARNING, "Unable to update cvt media viewer for " + accounts, ex); + + JOptionPane.showMessageDialog(MediaViewer.this, Bundle.MediaViewer_selection_failure_msg(), Bundle.MediaViewer_selection_failure_title(), JOptionPane.ERROR_MESSAGE); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } + + } + /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always From f32847a2b15debe9e4fb107077f7bcb74e200087 Mon Sep 17 00:00:00 2001 From: Kelly Kelly Date: Fri, 23 Jul 2021 14:29:45 -0400 Subject: [PATCH 2/9] Removed unused import --- .../autopsy/communications/relationships/MediaViewer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java index 73fd4cde2b..50e45212e3 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java @@ -36,7 +36,6 @@ import org.openide.explorer.ExplorerManager; import static org.openide.explorer.ExplorerUtils.createLookup; import org.openide.nodes.AbstractNode; import org.openide.nodes.Node; -import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.communications.ModifiableProxyLookup; From 6c68fed2b7b873c847ba81b72a3044325fb2cb71 Mon Sep 17 00:00:00 2001 From: Kelly Kelly Date: Fri, 30 Jul 2021 12:41:33 -0400 Subject: [PATCH 3/9] Moved DB calls to actionPerformed --- .../ExplorerNodeActionVisitor.java | 9 +-- .../directorytree/ExtractUnallocAction.java | 60 ++++++++++++------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java index 674eb6bf2e..7b53d88235 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java @@ -73,13 +73,8 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final Image img) { List lst = new ArrayList<>(); - //TODO lst.add(new ExtractAction("Extract Image", img)); - try { - lst.add(new ExtractUnallocAction( - NbBundle.getMessage(this.getClass(), "ExplorerNodeActionVisitor.action.extUnallocToSingleFiles"), img)); - } catch (NoCurrentCaseException ex) { - Logger.getLogger(ExplorerNodeActionVisitor.class.getName()).log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS - } + lst.add(new ExtractUnallocAction( + NbBundle.getMessage(this.getClass(), "ExplorerNodeActionVisitor.action.extUnallocToSingleFiles"), img)); return lst; } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java index 58afb28fbe..e76389adb2 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.directorytree; import java.awt.Component; +import java.awt.Cursor; import java.awt.Frame; import java.awt.event.ActionEvent; import java.io.File; @@ -40,6 +41,7 @@ import javax.swing.SwingWorker; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.Cancellable; import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; @@ -63,6 +65,7 @@ import org.sleuthkit.datamodel.VolumeSystem; final class ExtractUnallocAction extends AbstractAction { private static final Logger logger = Logger.getLogger(ExtractUnallocAction.class.getName()); + private static final long serialVersionUID = 1L; private final List filesToExtract = new ArrayList<>(); private static final Set volumesInProgress = new HashSet<>(); @@ -70,6 +73,9 @@ final class ExtractUnallocAction extends AbstractAction { private static String userDefinedExportPath; private long currentImage = 0L; private final boolean isImage; + + private final Volume volume; + private final Image image; /** * Create an instance of ExtractUnallocAction with a volume. @@ -77,17 +83,12 @@ final class ExtractUnallocAction extends AbstractAction { * @param title The title * @param volume The volume set for extraction. */ - public ExtractUnallocAction(String title, Volume volume) { + ExtractUnallocAction(String title, Volume volume) { super(title); + + this.volume = volume; + this.image = null; isImage = false; - try { - OutputFileData outputFileData = new OutputFileData(volume); - filesToExtract.add(outputFileData); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); - setEnabled(false); - } - } /** @@ -97,19 +98,12 @@ final class ExtractUnallocAction extends AbstractAction { * @param image The image set for extraction. * @throws NoCurrentCaseException If no case is open. */ - public ExtractUnallocAction(String title, Image image) throws NoCurrentCaseException { + ExtractUnallocAction(String title, Image image) { super(title); + + this.volume = null; + this.image = image; isImage = true; - currentImage = image.getId(); - if (hasVolumeSystem(image)) { - for (Volume v : getVolumes(image)) { - OutputFileData outputFileData = new OutputFileData(v); - filesToExtract.add(outputFileData); - } - } else { - OutputFileData outputFileData = new OutputFileData(image); - filesToExtract.add(outputFileData); - } } /** @@ -126,6 +120,32 @@ final class ExtractUnallocAction extends AbstractAction { "ExtractUnallocAction.noOpenCase.errMsg=No open case available."}) @Override public void actionPerformed(ActionEvent event) { + WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try { + if (volume != null) { + OutputFileData outputFileData = new OutputFileData(volume); + filesToExtract.add(outputFileData); + + } else { + currentImage = image.getId(); + if (hasVolumeSystem(image)) { + for (Volume v : getVolumes(image)) { + OutputFileData outputFileData = new OutputFileData(v); + filesToExtract.add(outputFileData); + } + } else { + OutputFileData outputFileData = new OutputFileData(image); + filesToExtract.add(outputFileData); + } + } + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); + MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "EExtractUnallocAction.noOpenCase.errMsg")); + return; + } finally { + WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + if (filesToExtract != null && filesToExtract.isEmpty() == false) { // This check doesn't absolutely guarantee that the image won't be in progress when we make the worker, // but in general it will suffice. From 3167e6ffe3e7e1319b919305fc3095e109083007 Mon Sep 17 00:00:00 2001 From: Kelly Kelly Date: Thu, 5 Aug 2021 13:32:13 -0400 Subject: [PATCH 4/9] Modified the swing worker to return node instead of artifact list --- .../autopsy/communications/relationships/MediaViewer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java index 50e45212e3..1473e7cd67 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java @@ -202,7 +202,7 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM /** * Swing worker for gathering the data needed to populate the media viewer. */ - private class MediaViewerWorker extends SwingWorker, Void> { + private class MediaViewerWorker extends SwingWorker { private final SelectionInfo selectionInfo; @@ -211,7 +211,7 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM } @Override - protected Set doInBackground() throws Exception { + protected TableFilterNode doInBackground() throws Exception { Set relationshipSources; Set artifactList = new HashSet<>(); @@ -223,7 +223,7 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM }); } - return artifactList; + return new TableFilterNode(new DataResultFilterNode(new AbstractNode(new AttachmentThumbnailsChildren(artifactList)), tableEM), true, this.getClass().getName()); } @Messages({ @@ -236,7 +236,7 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM if (isCancelled()) { return; } - thumbnailViewer.setNode(new TableFilterNode(new DataResultFilterNode(new AbstractNode(new AttachmentThumbnailsChildren(get())), tableEM), true, this.getClass().getName())); + thumbnailViewer.setNode(get()); } catch (ExecutionException | InterruptedException ex) { String accounts = selectionInfo.getAccounts().stream().map(Account::getTypeSpecificID).collect(Collectors.joining(",")); logger.log(Level.WARNING, "Unable to update cvt media viewer for " + accounts, ex); From edf28b922cc735780a34e0639c99d2ca05a916d4 Mon Sep 17 00:00:00 2001 From: Kelly Kelly Date: Thu, 5 Aug 2021 18:21:35 -0400 Subject: [PATCH 5/9] Reorganized actionPerformed so that all DB calls are in a SwingWorker --- .../directorytree/ExtractUnallocAction.java | 427 ++++++++++-------- 1 file changed, 231 insertions(+), 196 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java index e76389adb2..9a17e100a6 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java @@ -19,13 +19,13 @@ package org.sleuthkit.autopsy.directorytree; import java.awt.Component; -import java.awt.Cursor; import java.awt.Frame; import java.awt.event.ActionEvent; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -33,15 +33,20 @@ import java.util.List; import java.util.Set; import java.util.HashSet; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.JFileChooser; +import static javax.swing.JFileChooser.SAVE_DIALOG; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.Cancellable; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; -import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; @@ -67,43 +72,48 @@ final class ExtractUnallocAction extends AbstractAction { private static final Logger logger = Logger.getLogger(ExtractUnallocAction.class.getName()); private static final long serialVersionUID = 1L; - private final List filesToExtract = new ArrayList<>(); private static final Set volumesInProgress = new HashSet<>(); private static final Set imagesInProgress = new HashSet<>(); private static String userDefinedExportPath; - private long currentImage = 0L; - private final boolean isImage; - + private final Volume volume; private final Image image; + private final FutureTask futureFileChooser = new FutureTask<>(CustomFileChooser::new); + + private JFileChooser fileChooser = null; + /** * Create an instance of ExtractUnallocAction with a volume. * - * @param title The title + * @param title The title * @param volume The volume set for extraction. */ ExtractUnallocAction(String title, Volume volume) { - super(title); - - this.volume = volume; - this.image = null; - isImage = false; + this(title, null, volume); + } /** * Create an instance of ExtractUnallocAction with an image. - * + * * @param title The title. * @param image The image set for extraction. + * * @throws NoCurrentCaseException If no case is open. */ ExtractUnallocAction(String title, Image image) { + this(title, image, null); + } + + ExtractUnallocAction(String title, Image image, Volume volume) { super(title); - + this.volume = null; this.image = image; - isImage = true; + + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.execute(futureFileChooser); } /** @@ -120,140 +130,41 @@ final class ExtractUnallocAction extends AbstractAction { "ExtractUnallocAction.noOpenCase.errMsg=No open case available."}) @Override public void actionPerformed(ActionEvent event) { - WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + Case openCase; try { - if (volume != null) { - OutputFileData outputFileData = new OutputFileData(volume); - filesToExtract.add(outputFileData); - - } else { - currentImage = image.getId(); - if (hasVolumeSystem(image)) { - for (Volume v : getVolumes(image)) { - OutputFileData outputFileData = new OutputFileData(v); - filesToExtract.add(outputFileData); - } - } else { - OutputFileData outputFileData = new OutputFileData(image); - filesToExtract.add(outputFileData); - } - } + openCase = Case.getCurrentCaseThrows(); } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); - MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "EExtractUnallocAction.noOpenCase.errMsg")); + MessageNotifyUtil.Message.info(Bundle.ExtractUnallocAction_noOpenCase_errMsg()); return; - } finally { - WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } - if (filesToExtract != null && filesToExtract.isEmpty() == false) { - // This check doesn't absolutely guarantee that the image won't be in progress when we make the worker, - // but in general it will suffice. - if (isImage && isImageInProgress(currentImage)) { - MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.notifyMsg.unallocAlreadyBeingExtr.msg")); - //JOptionPane.showMessageDialog(new Frame(), "Unallocated Space is already being extracted on this Image. Please select a different Image."); - return; - } - Case openCase; + if (fileChooser == null) { try { - openCase = Case.getCurrentCaseThrows(); - } catch (NoCurrentCaseException ex) { - MessageNotifyUtil.Message.info(Bundle.ExtractUnallocAction_noOpenCase_errMsg()); - return; - } - List copyList = new ArrayList() { - { - addAll(filesToExtract); - } - }; - - JFileChooser fileChooser = new JFileChooser() { - @Override - public void approveSelection() { - File f = getSelectedFile(); - if (!f.exists() && getDialogType() == SAVE_DIALOG || !f.canWrite()) { - JOptionPane.showMessageDialog(this, NbBundle.getMessage(this.getClass(), - "ExtractUnallocAction.msgDlg.folderDoesntExist.msg")); - return; - } - super.approveSelection(); - } - }; - - fileChooser.setCurrentDirectory(new File(getExportDirectory(openCase))); - fileChooser.setDialogTitle( - NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.dlgTitle.selectDirToSaveTo.msg")); - fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - fileChooser.setAcceptAllFileFilterUsed(false); - int returnValue = fileChooser.showSaveDialog((Component) event.getSource()); - if (returnValue == JFileChooser.APPROVE_OPTION) { - String destination = fileChooser.getSelectedFile().getPath(); - - updateExportDirectory(destination, openCase); - - for (OutputFileData outputFileData : filesToExtract) { - outputFileData.setPath(destination); - - if (outputFileData.layoutFiles != null && outputFileData.layoutFiles.size() > 0 && (!isVolumeInProgress(outputFileData.getFileName()))) { - //Format for single Unalloc File is ImgName-Unalloc-ImgObjectID-VolumeID.dat - - // Check if there is already a file with this name - if (outputFileData.fileInstance.exists()) { - int res = JOptionPane.showConfirmDialog(new Frame(), NbBundle.getMessage(this.getClass(), - "ExtractUnallocAction.confDlg.unallocFileAlreadyExist.msg", - outputFileData.getFileName())); - if (res == JOptionPane.YES_OPTION) { - // If the user wants to overwrite, delete the exising output file - outputFileData.fileInstance.delete(); - } else { - // Otherwise remove it from the list of output files - copyList.remove(outputFileData); - } - } - - if (!isImage & !copyList.isEmpty()) { - try { - ExtractUnallocWorker worker = new ExtractUnallocWorker(outputFileData); - worker.execute(); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Already extracting unallocated space into {0}", outputFileData.getFileName()); - MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.volumeInProgress", outputFileData.getFileName())); - } - } - } else { - // The output file for this volume could not be created for one of the following reasons - if (outputFileData.layoutFiles == null) { - MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.volumeError")); - logger.log(Level.SEVERE, "Tried to get unallocated content but the list of unallocated files was null"); //NON-NLS - } else if (outputFileData.layoutFiles.isEmpty()) { - MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.noFiles")); - logger.log(Level.WARNING, "No unallocated files found in volume"); //NON-NLS - copyList.remove(outputFileData); - } else { - MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.volumeInProgress", outputFileData.getFileName())); - logger.log(Level.WARNING, "Tried to get unallocated content but the volume is locked"); // NON_NLS - copyList.remove(outputFileData); - } - } - } - - // This needs refactoring. The idea seems to be that we'll take advantage of the loop above to - // check whether each output file exists but wait until this point to make a worker - // to extract everything (the worker in the above loop doesn't get created because isImage is true) - // It's also unclear to me why we need the two separate worker types. - if (isImage && !copyList.isEmpty()) { - try { - ExtractUnallocWorker worker = new ExtractUnallocWorker(copyList); - worker.execute(); - } catch (Exception ex) { - logger.log(Level.WARNING, "Error creating ExtractUnallocWorker", ex); - MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.imageError")); - } - } + fileChooser = futureFileChooser.get(); + } catch (InterruptedException | ExecutionException ex) { + fileChooser = new CustomFileChooser(); } } + + fileChooser.setCurrentDirectory(new File(getExportDirectory(openCase))); + if (JFileChooser.APPROVE_OPTION != fileChooser.showSaveDialog((Component) event.getSource())) { + return; + } + + String destination = fileChooser.getSelectedFile().getPath(); + updateExportDirectory(destination, openCase); + + if (image != null && isImageInProgress(image.getId())) { + MessageNotifyUtil.Message.info(NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.notifyMsg.unallocAlreadyBeingExtr.msg")); + JOptionPane.showMessageDialog(new Frame(), "Unallocated Space is already being extracted on this Image. Please select a different Image."); + return; + } + + ExtractUnallocWorker worker = new ExtractUnallocWorker(openCase, destination); + worker.execute(); } - + /** * Get the export directory path. * @@ -353,38 +264,14 @@ final class ExtractUnallocAction extends AbstractAction { private boolean canceled = false; private final List outputFileDataList = new ArrayList<>(); private File currentlyProcessing; - private final int totalSizeinMegs; + private int totalSizeinMegs; long totalBytes = 0; + private final String destination; + private Case openCase; - ExtractUnallocWorker(OutputFileData outputFileData) throws TskCoreException { - //Getting the total megs this worker is going to be doing - addVolumeInProgress(outputFileData.getFileName()); - outputFileDataList.add(outputFileData); - totalBytes = outputFileData.getSizeInBytes(); - totalSizeinMegs = toMb(totalBytes); - } - - ExtractUnallocWorker(List outputFileDataList) throws TskCoreException { - addImageInProgress(currentImage); - - //Getting the total megs this worker is going to be doing - for (OutputFileData outputFileData : outputFileDataList) { - try { - // If a volume is locked, skip it but continue trying to process any other requested volumes - addVolumeInProgress(outputFileData.getFileName()); - totalBytes += outputFileData.getSizeInBytes(); - this.outputFileDataList.add(outputFileData); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Already extracting data into {0}", outputFileData.getFileName()); - } - } - - // If we don't have anything to output (because of locking), throw an exception - if (this.outputFileDataList.isEmpty()) { - throw new TskCoreException("No unallocated files can be extracted"); - } - - totalSizeinMegs = toMb(totalBytes); + ExtractUnallocWorker(Case openCase, String destination) { + this.destination = destination; + this.openCase = openCase; } private int toMb(long bytes) { @@ -400,6 +287,7 @@ final class ExtractUnallocAction extends AbstractAction { @Override protected Integer doInBackground() { try { + initalizeFilesToExtract(); progress = ProgressHandle.createHandle( NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.progress.extractUnalloc.title"), new Cancellable() { @Override @@ -423,29 +311,29 @@ final class ExtractUnallocAction extends AbstractAction { for (OutputFileData outputFileData : this.outputFileDataList) { currentlyProcessing = outputFileData.getFile(); logger.log(Level.INFO, "Writing Unalloc file to {0}", currentlyProcessing.getPath()); //NON-NLS - OutputStream outputStream = new FileOutputStream(currentlyProcessing); - long bytes = 0; - int i = 0; - while (i < outputFileData.getLayouts().size() && bytes != outputFileData.getSizeInBytes()) { - LayoutFile layoutFile = outputFileData.getLayouts().get(i); - long offsetPerFile = 0L; - int bytesRead; - while (offsetPerFile != layoutFile.getSize() && !canceled) { - if (++kbs % 128 == 0) { - mbs++; - progress.progress(NbBundle.getMessage(this.getClass(), - "ExtractUnallocAction.processing.counter.msg", - mbs, totalSizeinMegs), mbs - 1); + try (OutputStream outputStream = new FileOutputStream(currentlyProcessing)) { + long bytes = 0; + int i = 0; + while (i < outputFileData.getLayouts().size() && bytes != outputFileData.getSizeInBytes()) { + LayoutFile layoutFile = outputFileData.getLayouts().get(i); + long offsetPerFile = 0L; + int bytesRead; + while (offsetPerFile != layoutFile.getSize() && !canceled) { + if (++kbs % 128 == 0) { + mbs++; + progress.progress(NbBundle.getMessage(this.getClass(), + "ExtractUnallocAction.processing.counter.msg", + mbs, totalSizeinMegs), mbs - 1); + } + bytesRead = layoutFile.read(buf, offsetPerFile, MAX_BYTES); + offsetPerFile += bytesRead; + outputStream.write(buf, 0, bytesRead); } - bytesRead = layoutFile.read(buf, offsetPerFile, MAX_BYTES); - offsetPerFile += bytesRead; - outputStream.write(buf, 0, bytesRead); + bytes += layoutFile.getSize(); + i++; } - bytes += layoutFile.getSize(); - i++; + outputStream.flush(); } - outputStream.flush(); - outputStream.close(); if (canceled) { outputFileData.getFile().delete(); @@ -468,8 +356,8 @@ final class ExtractUnallocAction extends AbstractAction { @Override protected void done() { - if (isImage) { - removeImageInProgress(currentImage); + if (image != null) { + removeImageInProgress(image.getId()); } for (OutputFileData u : outputFileDataList) { removeVolumeInProgress(u.getFileName()); @@ -488,10 +376,115 @@ final class ExtractUnallocAction extends AbstractAction { MessageNotifyUtil.Notify.error( NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.done.errMsg.title"), NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.done.errMsg.msg", ex.getMessage())); + logger.log(Level.SEVERE, "Failed to extract unallocated space", ex); } // catch and ignore if we were cancelled catch (java.util.concurrent.CancellationException ex) { } } + + private void initalizeFilesToExtract() throws TskCoreException { + List filesToExtract = new ArrayList<>(); + + if (volume != null) { + OutputFileData outputFileData = new OutputFileData(volume, openCase); + filesToExtract.add(outputFileData); + + } else { + if (hasVolumeSystem(image)) { + for (Volume v : getVolumes(image)) { + OutputFileData outputFileData = new OutputFileData(v, openCase); + filesToExtract.add(outputFileData); + } + } else { + OutputFileData outputFileData = new OutputFileData(image, openCase); + filesToExtract.add(outputFileData); + } + } + + if (filesToExtract.isEmpty() == false) { + + List copyList = new ArrayList() { + { + addAll(filesToExtract); + } + }; + + for (OutputFileData outputFileData : filesToExtract) { + outputFileData.setPath(destination); + if (outputFileData.getLayouts() != null && !outputFileData.getLayouts().isEmpty() && (!isVolumeInProgress(outputFileData.getFileName()))) { + //Format for single Unalloc File is ImgName-Unalloc-ImgObjectID-VolumeID.dat + + // Check if there is already a file with this name + if (outputFileData.getFile().exists()) { + final Result dialogResult = new Result(); + try { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + dialogResult.set(JOptionPane.showConfirmDialog(new Frame(), NbBundle.getMessage(this.getClass(), + "ExtractUnallocAction.confDlg.unallocFileAlreadyExist.msg", + outputFileData.getFileName()))); + } + }); + } catch (InterruptedException | InvocationTargetException ex) { + Exceptions.printStackTrace(ex); + } + + if (dialogResult.value == JOptionPane.YES_OPTION) { + // If the user wants to overwrite, delete the exising output file + outputFileData.getFile().delete(); + } else { + // Otherwise remove it from the list of output files + copyList.remove(outputFileData); + } + } + } else { + // The output file for this volume could not be created for one of the following reasons + if (outputFileData.getLayouts() == null) { + logger.log(Level.SEVERE, "Tried to get unallocated content but the list of unallocated files was null"); //NON-NLS + } else if (outputFileData.getLayouts().isEmpty()) { + logger.log(Level.WARNING, "No unallocated files found in volume"); //NON-NLS + copyList.remove(outputFileData); + } else { + logger.log(Level.WARNING, "Tried to get unallocated content but the volume is locked"); // NON_NLS + copyList.remove(outputFileData); + } + } + } + + if (!copyList.isEmpty()) { + + setDataFileList(copyList); + + } + } + } + + private void setDataFileList(List outputFileDataList) throws TskCoreException { + + if (image != null) { + addImageInProgress(image.getId()); + } + + //Getting the total megs this worker is going to be doing + for (OutputFileData outputFileData : outputFileDataList) { + try { + // If a volume is locked, skip it but continue trying to process any other requested volumes + addVolumeInProgress(outputFileData.getFileName()); + totalBytes += outputFileData.getSizeInBytes(); + this.outputFileDataList.add(outputFileData); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Already extracting data into {0}", outputFileData.getFileName()); + } + } + + // If we don't have anything to output (because of locking), throw an exception + if (this.outputFileDataList.isEmpty()) { + throw new TskCoreException("No unallocated files can be extracted"); + } + + totalSizeinMegs = toMb(totalBytes); + } } /** @@ -679,14 +672,14 @@ final class ExtractUnallocAction extends AbstractAction { * * @throws NoCurrentCaseException if there is no open case. */ - OutputFileData(Image img) throws NoCurrentCaseException { + OutputFileData(Image img, Case openCase) { this.layoutFiles = getUnallocFiles(img); Collections.sort(layoutFiles, new SortObjId()); this.volumeId = 0; this.imageId = img.getId(); this.imageName = img.getName(); this.fileName = this.imageName + "-Unalloc-" + this.imageId + "-" + 0 + ".dat"; //NON-NLS - this.fileInstance = new File(Case.getCurrentCaseThrows().getExportDirectory() + File.separator + this.fileName); + this.fileInstance = new File(openCase.getExportDirectory() + File.separator + this.fileName); this.sizeInBytes = calcSizeInBytes(); } @@ -697,7 +690,7 @@ final class ExtractUnallocAction extends AbstractAction { * * @throws NoCurrentCaseException if there is no open case. */ - OutputFileData(Volume volume) throws NoCurrentCaseException { + OutputFileData(Volume volume, Case openCase) { try { this.imageName = volume.getDataSource().getName(); this.imageId = volume.getDataSource().getId(); @@ -708,7 +701,7 @@ final class ExtractUnallocAction extends AbstractAction { this.imageId = 0; } this.fileName = this.imageName + "-Unalloc-" + this.imageId + "-" + volumeId + ".dat"; //NON-NLS - this.fileInstance = new File(Case.getCurrentCaseThrows().getExportDirectory() + File.separator + this.fileName); + this.fileInstance = new File(openCase.getExportDirectory() + File.separator + this.fileName); this.layoutFiles = getUnallocFiles(volume); Collections.sort(layoutFiles, new SortObjId()); this.sizeInBytes = calcSizeInBytes(); @@ -759,4 +752,46 @@ final class ExtractUnallocAction extends AbstractAction { this.fileInstance = new File(path + File.separator + this.fileName); } } + + // A Custome JFileChooser for this Action Class. + private class CustomFileChooser extends JFileChooser { + + private static final long serialVersionUID = 1L; + + CustomFileChooser() { + initalize(); + } + + private void initalize() { + setDialogTitle( + NbBundle.getMessage(this.getClass(), "ExtractUnallocAction.dlgTitle.selectDirToSaveTo.msg")); + setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + setAcceptAllFileFilterUsed(false); + } + + @Override + public void approveSelection() { + File f = getSelectedFile(); + if (!f.exists() && getDialogType() == SAVE_DIALOG || !f.canWrite()) { + JOptionPane.showMessageDialog(this, NbBundle.getMessage(this.getClass(), + "ExtractUnallocAction.msgDlg.folderDoesntExist.msg")); + return; + } + super.approveSelection(); + } + } + + // Small helper class for use with SwingUtilities involkAndWait to get + // the result from the launching of the JOptionPane. + private class Result { + private int value; + + void set(int value) { + this.value = value; + } + + int value() { + return value; + } + } } From f27b56ea7be98045bbc91ac2859b3e61a8244755 Mon Sep 17 00:00:00 2001 From: Kelly Kelly Date: Fri, 6 Aug 2021 11:22:38 -0400 Subject: [PATCH 6/9] Removed unused import --- .../sleuthkit/autopsy/directorytree/ExtractUnallocAction.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java index 9a17e100a6..dcb55e5655 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java @@ -45,7 +45,6 @@ import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.Cancellable; -import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; @@ -427,7 +426,7 @@ final class ExtractUnallocAction extends AbstractAction { } }); } catch (InterruptedException | InvocationTargetException ex) { - Exceptions.printStackTrace(ex); + logger.log(Level.SEVERE, "An error occured launching confirmation dialog for extract unalloc actions", ex); } if (dialogResult.value == JOptionPane.YES_OPTION) { From f2d79a8add0bd603db65390544caa24be0d4665e Mon Sep 17 00:00:00 2001 From: Kelly Kelly Date: Fri, 6 Aug 2021 13:45:36 -0400 Subject: [PATCH 7/9] Removed unused imports --- .../autopsy/directorytree/ExplorerNodeActionVisitor.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java index 7b53d88235..2980493e45 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java @@ -23,14 +23,12 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.Action; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; @@ -43,11 +41,10 @@ import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.LocalDirectory; import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.datamodel.Volume; -import org.sleuthkit.autopsy.coreutils.Logger; public class ExplorerNodeActionVisitor extends ContentVisitor.Default> { - private static ExplorerNodeActionVisitor instance = new ExplorerNodeActionVisitor(); + private final static ExplorerNodeActionVisitor instance = new ExplorerNodeActionVisitor(); public static List getActions(Content c) { List actions = new ArrayList<>(); From a7308d80239170ef6cc6c0579f283438e24b3238 Mon Sep 17 00:00:00 2001 From: Mark McKinnon Date: Sun, 8 Aug 2021 22:09:13 -0400 Subject: [PATCH 8/9] Update ExtractJumpLists.java Fix path where temp files and derived files are written for Multiuser case --- .../recentactivity/ExtractJumpLists.java | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractJumpLists.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractJumpLists.java index fc0ef8995e..10e0f51a47 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractJumpLists.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractJumpLists.java @@ -27,10 +27,12 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +import org.apache.poi.EmptyFileException; import org.apache.poi.poifs.filesystem.DirectoryEntry; import org.apache.poi.poifs.filesystem.DocumentEntry; import org.apache.poi.poifs.filesystem.DocumentInputStream; import org.apache.poi.poifs.filesystem.Entry; +import org.apache.poi.poifs.filesystem.NotOLE2FileException; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; @@ -62,7 +64,6 @@ final class ExtractJumpLists extends Extract { private static final String JUMPLIST_TSK_COMMENT = "Jumplist File"; private static final String RA_DIR_NAME = "RecentActivity"; //NON-NLS - private static final String MODULE_OUTPUT_DIR = "ModuleOutput"; //NON-NLS private static final String AUTOMATIC_DESTINATIONS_FILE_DIRECTORY = "%/AppData/Roaming/Microsoft/Windows/Recent/AutomaticDestinations/"; private static final String JUMPLIST_DIR_NAME = "jumplists"; //NON-NLS private static final String VERSION_NUMBER = "1.0.0"; //NON-NLS @@ -86,7 +87,8 @@ final class ExtractJumpLists extends Extract { fileManager = currentCase.getServices().getFileManager(); long ingestJobId = context.getJobId(); - List jumpListFiles = extractJumplistFiles(dataSource, ingestJobId); + String baseRaTempPath = RAImageIngestModule.getRATempPath(Case.getCurrentCase(), JUMPLIST_DIR_NAME , ingestJobId); + List jumpListFiles = extractJumplistFiles(dataSource, ingestJobId, baseRaTempPath); if (jumpListFiles.isEmpty()) { return; @@ -98,13 +100,13 @@ final class ExtractJumpLists extends Extract { List derivedFiles = new ArrayList<>(); String derivedPath = null; - String baseRaTempPath = RAImageIngestModule.getRATempPath(Case.getCurrentCase(), JUMPLIST_DIR_NAME + "_" + dataSource.getId(), ingestJobId); + String baseRaModPath = RAImageIngestModule.getRAOutputPath(Case.getCurrentCase(), JUMPLIST_DIR_NAME, ingestJobId); for (AbstractFile jumplistFile : jumpListFiles) { if (!jumplistFile.getName().toLowerCase().contains("-slack") && !jumplistFile.getName().equals("..") && !jumplistFile.getName().equals(".") && jumplistFile.getSize() > 0) { String jlFile = Paths.get(baseRaTempPath, jumplistFile.getName() + "_" + jumplistFile.getId()).toString(); - String moduleOutPath = Case.getCurrentCase().getModuleDirectory() + File.separator + RA_DIR_NAME + File.separator + JUMPLIST_DIR_NAME + "_" + dataSource.getId() + File.separator + jumplistFile.getName() + "_" + jumplistFile.getId(); - derivedPath = RA_DIR_NAME + File.separator + JUMPLIST_DIR_NAME + "_" + dataSource.getId() + File.separator + jumplistFile.getName() + "_" + jumplistFile.getId(); + String moduleOutPath = baseRaModPath + File.separator + jumplistFile.getName() + "_" + jumplistFile.getId(); + derivedPath = RA_DIR_NAME + File.separator + JUMPLIST_DIR_NAME + "_" + ingestJobId + File.separator + jumplistFile.getName() + "_" + jumplistFile.getId(); File jlDir = new File(moduleOutPath); if (jlDir.exists() == false) { boolean dirMade = jlDir.mkdirs(); @@ -129,7 +131,7 @@ final class ExtractJumpLists extends Extract { * * @return - list of jumplist abstractfiles or empty list */ - private List extractJumplistFiles(Content dataSource, Long ingestJobId) { + private List extractJumplistFiles(Content dataSource, Long ingestJobId, String baseRaTempPath) { List jumpListFiles = new ArrayList<>();; List tempJumpListFiles = new ArrayList<>();; @@ -150,11 +152,10 @@ final class ExtractJumpLists extends Extract { if (context.dataSourceIngestIsCancelled()) { return jumpListFiles; } - + if (!jumpListFile.getName().toLowerCase().contains("-slack") && !jumpListFile.getName().equals("..") && !jumpListFile.getName().equals(".") && jumpListFile.getSize() > 0) { String fileName = jumpListFile.getName() + "_" + jumpListFile.getId(); - String baseRaTempPath = RAImageIngestModule.getRATempPath(Case.getCurrentCase(), JUMPLIST_DIR_NAME+ "_" + dataSource.getId(), ingestJobId); String jlFile = Paths.get(baseRaTempPath, fileName).toString(); try { ContentUtils.writeToFile(jumpListFile, new File(jlFile)); @@ -176,7 +177,7 @@ final class ExtractJumpLists extends Extract { List derivedFiles = new ArrayList<>(); DerivedFile derivedFile; String lnkFileName = ""; - + try (POIFSFileSystem fs = new POIFSFileSystem(new File(jumpListFile))) { DirectoryEntry root = fs.getRoot(); for (Entry entry : root) { @@ -196,7 +197,8 @@ final class ExtractJumpLists extends Extract { JLNK lnk = lnkParser.parse(); lnkFileName = lnk.getBestName() + ".lnk"; File targetFile = new File(moduleOutPath + File.separator + entry.getName() + "-" + lnkFileName); - String derivedFileName = MODULE_OUTPUT_DIR + File.separator + derivedPath + File.separator + entry.getName() + "-" + lnkFileName; + String relativePath = Case.getCurrentCase().getModuleOutputDirectoryRelativePath(); + String derivedFileName = Case.getCurrentCase().getModuleOutputDirectoryRelativePath() + File.separator + derivedPath + File.separator + entry.getName() + "-" + lnkFileName; OutputStream outStream = new FileOutputStream(targetFile); outStream.write(buffer); outStream.close(); @@ -226,6 +228,8 @@ final class ExtractJumpLists extends Extract { continue; } } + } catch (NotOLE2FileException | EmptyFileException ex1) { + logger.log(Level.WARNING, String.format("Error file not a valid OLE2 Document $s", jumpListFile)); //NON-NLS } catch (IOException | TskCoreException ex) { logger.log(Level.WARNING, String.format("Error lnk parsing the file to get recent files $s", jumpListFile), ex); //NON-NLS } @@ -235,3 +239,4 @@ final class ExtractJumpLists extends Extract { } } + \ No newline at end of file From 554d5056293d80c87c19adc599166856654fc6b2 Mon Sep 17 00:00:00 2001 From: apriestman Date: Mon, 9 Aug 2021 12:56:37 -0400 Subject: [PATCH 9/9] Add link to French version --- docs/doxygen-user/main.dox | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index 8a6d99546c..c01e5ea76d 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -8,6 +8,9 @@ This is the User's Guide for the ope Note: For those users running Autopsy on Mac devices, the functionality available through the "Tools" -> "Options" dialog as described in this documentation can be accessed through the system menu bar under "Preferences" or through the Cmd + , (command-comma) shortcut. +Translated versions of this guide: +- Français (4.19.0) + Help Topics ------- The following topics are available here: