/*
* Autopsy Forensic Browser
*
* Copyright 2013 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this content 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.directorytree;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.util.Cancellable;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.FileUtil;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.datamodel.ContentUtils;
import org.sleuthkit.autopsy.datamodel.ContentUtils.ExtractFscContentVisitor;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Extracts AbstractFiles to a location selected by the user.
*/
public final class ExtractAction extends AbstractAction {
private Logger logger = Logger.getLogger(ExtractAction.class.getName());
// This class is a singleton to support multi-selection of nodes, since
// org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every
// node in the array returns a reference to the same action object from Node.getActions(boolean).
private static ExtractAction instance;
public static synchronized ExtractAction getInstance() {
if (null == instance) {
instance = new ExtractAction();
}
return instance;
}
private ExtractAction() {
super(NbBundle.getMessage(ExtractAction.class, "ExtractAction.title.extractFiles.text"));
}
/**
* Asks user to choose destination, then extracts content to destination
* (recursing on directories).
*
* @param e The action event.
*/
@Override
public void actionPerformed(ActionEvent e) {
Collection<? extends AbstractFile> selectedFiles = Utilities.actionsGlobalContext().lookupAll(AbstractFile.class);
if (selectedFiles.size() > 1) {
extractFiles(e, selectedFiles);
} else if (selectedFiles.size() == 1) {
AbstractFile source = selectedFiles.iterator().next();
if (source.isDir()) {
extractFiles(e, selectedFiles);
} else {
extractFile(e, selectedFiles.iterator().next());
}
}
}
/**
* Called when user has selected a single file to extract
*
* @param e
* @param selectedFile Selected file
*/
private void extractFile(ActionEvent e, AbstractFile selectedFile) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory()));
// If there is an attribute name, change the ":". Otherwise the extracted file will be hidden
fileChooser.setSelectedFile(new File(FileUtil.escapeFileName(selectedFile.getName())));
if (fileChooser.showSaveDialog((Component) e.getSource()) == JFileChooser.APPROVE_OPTION) {
ArrayList<FileExtractionTask> fileExtractionTasks = new ArrayList<>();
fileExtractionTasks.add(new FileExtractionTask(selectedFile, fileChooser.getSelectedFile()));
runExtractionTasks(e, fileExtractionTasks);
}
}
/**
* Called when a user has selected multiple files to extract
*
* @param e
* @param selectedFiles Selected files
*/
private void extractFiles(ActionEvent e, Collection<? extends AbstractFile> selectedFiles) {
JFileChooser folderChooser = new JFileChooser();
folderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
folderChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory()));
if (folderChooser.showSaveDialog((Component) e.getSource()) == JFileChooser.APPROVE_OPTION) {
File destinationFolder = folderChooser.getSelectedFile();
if (!destinationFolder.exists()) {
try {
destinationFolder.mkdirs();
} catch (Exception ex) {
JOptionPane.showMessageDialog((Component) e.getSource(), NbBundle.getMessage(this.getClass(),
"ExtractAction.extractFiles.cantCreateFolderErr.msg"));
logger.log(Level.INFO, "Unable to create folder(s) for user " + destinationFolder.getAbsolutePath(), ex); //NON-NLS
return;
}
}
// make a task for each file
ArrayList<FileExtractionTask> fileExtractionTasks = new ArrayList<>();
for (AbstractFile source : selectedFiles) {
// If there is an attribute name, change the ":". Otherwise the extracted file will be hidden
fileExtractionTasks.add(new FileExtractionTask(source, new File(destinationFolder, source.getId() + "-" + FileUtil.escapeFileName(source.getName()))));
}
runExtractionTasks(e, fileExtractionTasks);
}
}
private void runExtractionTasks(ActionEvent e, ArrayList<FileExtractionTask> fileExtractionTasks) {
// verify all of the sources and destinations are OK
for (Iterator<FileExtractionTask> it = fileExtractionTasks.iterator(); it.hasNext();) {
FileExtractionTask task = it.next();
if (ContentUtils.isDotDirectory(task.source)) {
//JOptionPane.showMessageDialog((Component) e.getSource(), "Cannot extract virtual " + task.source.getName() + " directory.", "File is Virtual Directory", JOptionPane.WARNING_MESSAGE);
it.remove();
continue;
}
/*
* @@@ Problems with this code: - does not prevent us from having
* multiple files with the same target name in the task list (in
* which case, the first ones are overwritten) Unique Id was added
* to set of names before calling this method to deal with that.
*/
if (task.destination.exists()) {
if (JOptionPane.showConfirmDialog((Component) e.getSource(),
NbBundle.getMessage(this.getClass(), "ExtractAction.confDlg.destFileExist.msg", task.destination.getAbsolutePath()),
NbBundle.getMessage(this.getClass(), "ExtractAction.confDlg.destFileExist.title"),
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
if (!FileUtil.deleteFileDir(task.destination)) {
JOptionPane.showMessageDialog((Component) e.getSource(),
NbBundle.getMessage(this.getClass(), "ExtractAction.msgDlg.cantOverwriteFile.msg", task.destination.getAbsolutePath()));
it.remove();
}
} else {
it.remove();
}
}
}
// launch a thread to do the work
if (!fileExtractionTasks.isEmpty()) {
try {
FileExtracter extracter = new FileExtracter(fileExtractionTasks);
extracter.execute();
} catch (Exception ex) {
logger.log(Level.WARNING, "Unable to start background file extraction thread", ex); //NON-NLS
}
} else {
MessageNotifyUtil.Message.info(
NbBundle.getMessage(this.getClass(), "ExtractAction.notifyDlg.noFileToExtr.msg"));
}
}
private class FileExtractionTask {
AbstractFile source;
File destination;
FileExtractionTask(AbstractFile source, File destination) {
this.source = source;
this.destination = destination;
}
}
/**
* Thread that does the actual extraction work
*/
private class FileExtracter extends SwingWorker<Object, Void> {
private Logger logger = Logger.getLogger(FileExtracter.class.getName());
private ProgressHandle progress;
private ArrayList<FileExtractionTask> extractionTasks;
FileExtracter(ArrayList<FileExtractionTask> extractionTasks) {
this.extractionTasks = extractionTasks;
}
@Override
protected Object doInBackground() throws Exception {
if (extractionTasks.isEmpty()) {
return null;
}
// Setup progress bar.
final String displayName = NbBundle.getMessage(this.getClass(), "ExtractAction.progress.extracting");
progress = ProgressHandle.createHandle(displayName, new Cancellable() {
@Override
public boolean cancel() {
if (progress != null) {
progress.setDisplayName(
NbBundle.getMessage(this.getClass(), "ExtractAction.progress.cancellingExtraction", displayName));
}
return ExtractAction.FileExtracter.this.cancel(true);
}
});
progress.start();
progress.switchToIndeterminate();
/*
* @@@ Add back in -> Causes exceptions int workUnits = 0; for
* (FileExtractionTask task : extractionTasks) { workUnits +=
* calculateProgressBarWorkUnits(task.source); }
* progress.switchToDeterminate(workUnits);
*/
// Do the extraction tasks.
for (FileExtractionTask task : this.extractionTasks) {
// @@@ Note, we are no longer passing in progress
ExtractFscContentVisitor.extract(task.source, task.destination, null, this);
}
return null;
}
@Override
protected void done() {
boolean msgDisplayed = false;
try {
super.get();
} catch (Exception ex) {
logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS
MessageNotifyUtil.Message.info(
NbBundle.getMessage(this.getClass(), "ExtractAction.done.notifyMsg.extractErr", ex.getMessage()));
msgDisplayed = true;
} finally {
progress.finish();
if (!this.isCancelled() && !msgDisplayed) {
MessageNotifyUtil.Message.info(
NbBundle.getMessage(this.getClass(), "ExtractAction.done.notifyMsg.fileExtr.text"));
}
}
}
private int calculateProgressBarWorkUnits(AbstractFile file) {
int workUnits = 0;
if (file.isFile()) {
workUnits += file.getSize();
} else {
try {
for (Content child : file.getChildren()) {
if (child instanceof AbstractFile) {
workUnits += calculateProgressBarWorkUnits((AbstractFile) child);
}
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, "Could not get children of content", ex); //NON-NLS
}
}
return workUnits;
}
}
}