jmillman a4f3377a64 refactor DrawableFile so it no longer extends AbstractFile
remove type paramater from DrawableFile since it was not adding anything usefull and cluttered up the code
add getters  that delegate to private AbastractFile instance
refactor methods on DrawableTagsManager to deal with Content or DrawableFile objects
remove unneeded  invocation of   ImageIO.scanForPlugins() in ImageFile
2016-02-17 14:35:52 -05:00

1036 lines
41 KiB
Java

/*
* Autopsy Forensic Browser
*
* Copyright 2013-16 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> 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.imagegallery;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javax.annotation.Nullable;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.StringUtils;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.api.progress.ProgressHandleFactory;
import org.openide.util.Cancellable;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.core.RuntimeProperties;
import org.sleuthkit.autopsy.coreutils.History;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ThreadConfined;
import org.sleuthkit.autopsy.imagegallery.actions.UndoRedoManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.CategoryManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableDB;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableFile;
import org.sleuthkit.autopsy.imagegallery.datamodel.DrawableTagsManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.HashSetManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupManager;
import org.sleuthkit.autopsy.imagegallery.datamodel.grouping.GroupViewState;
import org.sleuthkit.autopsy.imagegallery.gui.NoGroupsDialog;
import org.sleuthkit.autopsy.imagegallery.gui.Toolbar;
import org.sleuthkit.autopsy.ingest.IngestManager;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.FileSystem;
import org.sleuthkit.datamodel.Image;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
/**
* Connects different parts of ImageGallery together and is hub for flow of
* control.
*/
public final class ImageGalleryController implements Executor {
private final Executor execDelegate = Executors.newSingleThreadExecutor();
private Runnable showTree;
@Override
public void execute(Runnable command) {
execDelegate.execute(command);
}
private static final Logger LOGGER = Logger.getLogger(ImageGalleryController.class.getName());
private final Region infoOverLayBackground = new Region() {
{
setBackground(new Background(new BackgroundFill(Color.GREY, CornerRadii.EMPTY, Insets.EMPTY)));
setOpacity(.4);
}
};
private static ImageGalleryController instance;
public static synchronized ImageGalleryController getDefault() {
if (instance == null) {
instance = new ImageGalleryController();
}
return instance;
}
private final History<GroupViewState> historyManager = new History<>();
private final UndoRedoManager undoManager = new UndoRedoManager();
/**
* true if Image Gallery should listen to ingest events, false if it should
* not listen to speed up ingest
*/
private final SimpleBooleanProperty listeningEnabled = new SimpleBooleanProperty(false);
private final ReadOnlyIntegerWrapper queueSizeProperty = new ReadOnlyIntegerWrapper(0);
private final ReadOnlyBooleanWrapper regroupDisabled = new ReadOnlyBooleanWrapper(false);
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private final ReadOnlyBooleanWrapper stale = new ReadOnlyBooleanWrapper(false);
private final ReadOnlyBooleanWrapper metaDataCollapsed = new ReadOnlyBooleanWrapper(false);
private final FileIDSelectionModel selectionModel = new FileIDSelectionModel(this);
private DBWorkerThread dbWorkerThread;
private DrawableDB db;
private final GroupManager groupManager = new GroupManager(this);
private final HashSetManager hashSetManager = new HashSetManager();
private final CategoryManager categoryManager = new CategoryManager(this);
private final DrawableTagsManager tagsManager = new DrawableTagsManager(null);
private StackPane fullUIStackPane;
private StackPane centralStackPane;
private Node infoOverlay;
private SleuthkitCase sleuthKitCase;
// private NavPanel navPanel;
public ReadOnlyBooleanProperty getMetaDataCollapsed() {
return metaDataCollapsed.getReadOnlyProperty();
}
public void setMetaDataCollapsed(Boolean metaDataCollapsed) {
this.metaDataCollapsed.set(metaDataCollapsed);
}
private GroupViewState getViewState() {
return historyManager.getCurrentState();
}
public ReadOnlyBooleanProperty regroupDisabled() {
return regroupDisabled.getReadOnlyProperty();
}
public ReadOnlyObjectProperty<GroupViewState> viewState() {
return historyManager.currentState();
}
public synchronized FileIDSelectionModel getSelectionModel() {
return selectionModel;
}
public GroupManager getGroupManager() {
return groupManager;
}
public DrawableDB getDatabase() {
return db;
}
synchronized public void setListeningEnabled(boolean enabled) {
listeningEnabled.set(enabled);
}
synchronized boolean isListeningEnabled() {
return listeningEnabled.get();
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
void setStale(Boolean b) {
Platform.runLater(() -> {
stale.set(b);
});
if (Case.isCaseOpen()) {
new PerCaseProperties(Case.getCurrentCase()).setConfigSetting(ImageGalleryModule.getModuleName(), PerCaseProperties.STALE, b.toString());
}
}
public ReadOnlyBooleanProperty stale() {
return stale.getReadOnlyProperty();
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
boolean isStale() {
return stale.get();
}
private ImageGalleryController() {
listeningEnabled.addListener((observable, oldValue, newValue) -> {
//if we just turned on listening and a case is open and that case is not up to date
if (newValue && !oldValue && Case.existsCurrentCase() && ImageGalleryModule.isDrawableDBStale(Case.getCurrentCase())) {
//populate the db
queueDBWorkerTask(new CopyAnalyzedFiles(instance, db, sleuthKitCase));
}
});
groupManager.getAnalyzedGroups().addListener((Observable o) -> {
//analyzed groups is confined to JFX thread
if (Case.isCaseOpen()) {
checkForGroups();
}
});
groupManager.getUnSeenGroups().addListener((Observable observable) -> {
//if there are unseen groups and none being viewed
if (groupManager.getUnSeenGroups().isEmpty() == false && (getViewState() == null || getViewState().getGroup() == null)) {
advance(GroupViewState.tile(groupManager.getUnSeenGroups().get(0)), true);
}
});
viewState().addListener((Observable observable) -> {
//when the viewed group changes, clear the selection and the undo/redo history
selectionModel.clearSelection();
undoManager.clear();
});
regroupDisabled.addListener((Observable observable) -> {
checkForGroups();
});
IngestManager.getInstance().addIngestModuleEventListener((PropertyChangeEvent evt) -> {
Platform.runLater(this::updateRegroupDisabled);
});
IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {
Platform.runLater(this::updateRegroupDisabled);
});
}
public ReadOnlyBooleanProperty getCanAdvance() {
return historyManager.getCanAdvance();
}
public ReadOnlyBooleanProperty getCanRetreat() {
return historyManager.getCanRetreat();
}
@ThreadConfined(type = ThreadConfined.ThreadType.ANY)
public void advance(GroupViewState newState, boolean forceShowTree) {
if (forceShowTree && showTree != null) {
showTree.run();
}
historyManager.advance(newState);
}
public GroupViewState advance() {
return historyManager.advance();
}
public GroupViewState retreat() {
return historyManager.retreat();
}
private void updateRegroupDisabled() {
regroupDisabled.set(getFileUpdateQueueSizeProperty().get() > 0 || IngestManager.getInstance().isIngestRunning());
}
/**
* Check if there are any fully analyzed groups available from the
* GroupManager and remove blocking progress spinners if there are. If there
* aren't, add a blocking progress spinner with appropriate message.
*/
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
@NbBundle.Messages({"ImageGalleryController.noGroupsDlg.msg1=No groups are fully analyzed; but listening to ingest is disabled. " +
" No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg2=No groups are fully analyzed yet, but ingest is still ongoing. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg3=No groups are fully analyzed yet, but image / video data is still being populated. Please Wait.",
"ImageGalleryController.noGroupsDlg.msg4=There are no images/videos available from the added datasources; but listening to ingest is disabled. " +
" No groups will be available until ingest is finished and listening is re-enabled.",
"ImageGalleryController.noGroupsDlg.msg5=There are no images/videos in the added datasources.",
"ImageGalleryController.noGroupsDlg.msg6=There are no fully analyzed groups to display:" +
" the current Group By setting resulted in no groups, " +
"or no groups are fully analyzed but ingest is not running."})
public void checkForGroups() {
if (groupManager.getAnalyzedGroups().isEmpty()) {
if (IngestManager.getInstance().isIngestRunning()) {
if (listeningEnabled.get() == false) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg1()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg2(),
new ProgressIndicator()));
}
} else if (getFileUpdateQueueSizeProperty().get() > 0) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg3(),
new ProgressIndicator()));
} else if (db != null && db.countAllFiles() <= 0) { // there are no files in db
if (listeningEnabled.get() == false) {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg4()));
} else {
replaceNotification(fullUIStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg5()));
}
} else if (!groupManager.isRegrouping()) {
replaceNotification(centralStackPane,
new NoGroupsDialog(Bundle.ImageGalleryController_noGroupsDlg_msg6()));
}
} else {
clearNotification();
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void clearNotification() {
//remove the ingest spinner
if (fullUIStackPane != null) {
fullUIStackPane.getChildren().remove(infoOverlay);
}
//remove the ingest spinner
if (centralStackPane != null) {
centralStackPane.getChildren().remove(infoOverlay);
}
}
@ThreadConfined(type = ThreadConfined.ThreadType.JFX)
private void replaceNotification(StackPane stackPane, Node newNode) {
clearNotification();
infoOverlay = new StackPane(infoOverLayBackground, newNode);
if (stackPane != null) {
stackPane.getChildren().add(infoOverlay);
}
}
private void restartWorker() {
if (dbWorkerThread != null) {
// Keep using the same worker thread if one exists
return;
}
dbWorkerThread = new DBWorkerThread();
getFileUpdateQueueSizeProperty().addListener((Observable o) -> {
Platform.runLater(this::updateRegroupDisabled);
});
Thread th = new Thread(dbWorkerThread, "DB-Worker-Thread");
th.setDaemon(false); // we want it to go away when it is done
th.start();
}
/**
* configure the controller for a specific case.
*
* @param theNewCase the case to configure the controller for
*/
public synchronized void setCase(Case theNewCase) {
if (Objects.nonNull(theNewCase)) {
this.sleuthKitCase = theNewCase.getSleuthkitCase();
this.db = DrawableDB.getDrawableDB(ImageGalleryModule.getModuleOutputDir(theNewCase), this);
setListeningEnabled(ImageGalleryModule.isEnabledforCase(theNewCase));
setStale(ImageGalleryModule.isDrawableDBStale(theNewCase));
// if we add this line icons are made as files are analyzed rather than on demand.
// db.addUpdatedFileListener(IconCache.getDefault());
restartWorker();
historyManager.clear();
groupManager.setDB(db);
hashSetManager.setDb(db);
categoryManager.setDb(db);
tagsManager.setAutopsyTagsManager(theNewCase.getServices().getTagsManager());
tagsManager.registerListener(groupManager);
tagsManager.registerListener(categoryManager);
} else {
reset();
}
}
/**
* reset the state of the controller (eg if the case is closed)
*/
public synchronized void reset() {
LOGGER.info("resetting ImageGalleryControler to initial state."); //NON-NLS
selectionModel.clearSelection();
setListeningEnabled(false);
ThumbnailCache.getDefault().clearCache();
historyManager.clear();
tagsManager.clearFollowUpTagName();
tagsManager.unregisterListener(groupManager);
tagsManager.unregisterListener(categoryManager);
dbWorkerThread.cancelAllTasks();
dbWorkerThread = null;
restartWorker();
Toolbar.getDefault(this).reset();
groupManager.clear();
if (db != null) {
db.closeDBCon();
}
db = null;
}
/**
* add InnerTask to the queue that the worker thread gets its work from
*
* @param innerTask
*/
public void queueDBWorkerTask(InnerTask innerTask) {
// @@@ We could make a lock for the worker thread
if (dbWorkerThread == null) {
restartWorker();
}
dbWorkerThread.addTask(innerTask);
}
@Nullable
synchronized public DrawableFile getFileFromId(Long fileID) throws TskCoreException {
if (Objects.isNull(db)) {
LOGGER.log(Level.WARNING, "Could not get file from id, no DB set. The case is probably closed."); //NON-NLS
return null;
}
return db.getFileFromID(fileID);
}
public void setStacks(StackPane fullUIStack, StackPane centralStack) {
fullUIStackPane = fullUIStack;
this.centralStackPane = centralStack;
Platform.runLater(this::checkForGroups);
}
public ReadOnlyIntegerProperty getFileUpdateQueueSizeProperty() {
return queueSizeProperty.getReadOnlyProperty();
}
public ReadOnlyDoubleProperty regroupProgress() {
return groupManager.regroupProgress();
}
/**
* invoked by {@link OnStart} to make sure that the ImageGallery listeners
* get setup as early as possible, and do other setup stuff.
*/
void onStart() {
Platform.setImplicitExit(false);
LOGGER.info("setting up ImageGallery listeners"); //NON-NLS
//TODO can we do anything usefull in an InjestJobEventListener?
//IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> {});
IngestManager.getInstance().addIngestModuleEventListener(new IngestModuleEventListener());
Case.addPropertyChangeListener(new CaseEventListener());
}
public HashSetManager getHashSetManager() {
return hashSetManager;
}
public CategoryManager getCategoryManager() {
return categoryManager;
}
public DrawableTagsManager getTagsManager() {
return tagsManager;
}
public void setShowTree(Runnable showTree) {
this.showTree = showTree;
}
public UndoRedoManager getUndoManager() {
return undoManager;
}
// @@@ REVIEW IF THIS SHOLD BE STATIC...
//TODO: concept seems like the controller deal with how much work to do at a given time
// @@@ review this class for synchronization issues (i.e. reset and cancel being called, add, etc.)
private class DBWorkerThread implements Runnable {
// true if the process was requested to stop. Currently no way to reset it
private volatile boolean cancelled = false;
// list of tasks to run
private final BlockingQueue<InnerTask> workQueue = new LinkedBlockingQueue<>();
/**
* Cancel all of the queued up tasks and the currently scheduled task.
* Note that after you cancel, you cannot submit new jobs to this
* thread.
*/
public void cancelAllTasks() {
cancelled = true;
for (InnerTask it : workQueue) {
it.cancel();
}
workQueue.clear();
queueSizeProperty.set(workQueue.size());
}
/**
* Add a task for the worker thread to perform
*
* @param it
*/
public void addTask(InnerTask it) {
workQueue.add(it);
Platform.runLater(() -> {
queueSizeProperty.set(workQueue.size());
});
}
@Override
public void run() {
// nearly infinite loop waiting for tasks
while (true) {
if (cancelled) {
return;
}
try {
InnerTask it = workQueue.take();
if (it.isCancelled() == false) {
it.run();
}
Platform.runLater(() -> {
queueSizeProperty.set(workQueue.size());
});
} catch (InterruptedException ex) {
LOGGER.log(Level.SEVERE, "Failed to run DB worker thread", ex); //NON-NLS
}
}
}
}
public synchronized SleuthkitCase getSleuthKitCase() {
return sleuthKitCase;
}
/**
* Abstract base class for task to be done on {@link DBWorkerThread}
*/
@NbBundle.Messages({"ImageGalleryController.InnerTask.progress.name=progress",
"ImageGalleryController.InnerTask.message.name=status"})
static public abstract class InnerTask implements Runnable, Cancellable {
public double getProgress() {
return progress.get();
}
public final void updateProgress(Double workDone) {
this.progress.set(workDone);
}
public String getMessage() {
return message.get();
}
public final void updateMessage(String Status) {
this.message.set(Status);
}
SimpleObjectProperty<Worker.State> state = new SimpleObjectProperty<>(Worker.State.READY);
SimpleDoubleProperty progress = new SimpleDoubleProperty(this, Bundle.ImageGalleryController_InnerTask_progress_name());
SimpleStringProperty message = new SimpleStringProperty(this, Bundle.ImageGalleryController_InnerTask_message_name());
public SimpleDoubleProperty progressProperty() {
return progress;
}
public SimpleStringProperty messageProperty() {
return message;
}
public Worker.State getState() {
return state.get();
}
protected void updateState(Worker.State newState) {
state.set(newState);
}
public ReadOnlyObjectProperty<Worker.State> stateProperty() {
return new ReadOnlyObjectWrapper<>(state.get());
}
protected InnerTask() {
}
@Override
synchronized public boolean cancel() {
updateState(Worker.State.CANCELLED);
return true;
}
synchronized protected boolean isCancelled() {
return getState() == Worker.State.CANCELLED;
}
}
/**
* Abstract base class for tasks associated with a file in the database
*/
static public abstract class FileTask extends InnerTask {
private final AbstractFile file;
private final DrawableDB taskDB;
public DrawableDB getTaskDB() {
return taskDB;
}
public AbstractFile getFile() {
return file;
}
public FileTask(AbstractFile f, DrawableDB taskDB) {
super();
this.file = f;
this.taskDB = taskDB;
}
}
/**
* task that updates one file in database with results from ingest
*/
static private class UpdateFileTask extends FileTask {
UpdateFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB);
}
/**
* Update a file in the database
*/
@Override
public void run() {
try {
DrawableFile drawableFile = DrawableFile.create(getFile(), true, false);
getTaskDB().updateFile(drawableFile);
} catch (NullPointerException ex) {
// This is one of the places where we get many errors if the case is closed during processing.
// We don't want to print out a ton of exceptions if this is the case.
if (Case.isCaseOpen()) {
Logger.getLogger(UpdateFileTask.class.getName()).log(Level.SEVERE, "Error in UpdateFile task"); //NON-NLS
}
}
}
}
/**
* task that updates one file in database with results from ingest
*/
static private class RemoveFileTask extends FileTask {
RemoveFileTask(AbstractFile f, DrawableDB taskDB) {
super(f, taskDB);
}
/**
* Update a file in the database
*/
@Override
public void run() {
try {
getTaskDB().removeFile(getFile().getId());
} catch (NullPointerException ex) {
// This is one of the places where we get many errors if the case is closed during processing.
// We don't want to print out a ton of exceptions if this is the case.
if (Case.isCaseOpen()) {
Logger.getLogger(RemoveFileTask.class.getName()).log(Level.SEVERE, "Case was closed out from underneath RemoveFile task"); //NON-NLS
}
}
}
}
/**
* Task that runs when image gallery listening is (re) enabled.
*
* Grabs all files with supported image/video mime types or extensions, and
* adds them to the Drawable DB. Uses the presence of a mimetype as an
* approximation to 'analyzed'.
*/
@NbBundle.Messages({"CopyAnalyzedFiles.populatingDb.status=populating analyzed image/video database",
"CopyAnalyzedFiles.committingDb.status=commiting image/video database",
"CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.",
"CopyAnalyzedFiles.errPopulating.errMsg=There was an error populating Image Gallery database."})
static private class CopyAnalyzedFiles extends InnerTask {
private final ImageGalleryController controller;
private final DrawableDB taskDB;
private final SleuthkitCase tskCase;
CopyAnalyzedFiles(ImageGalleryController controller, DrawableDB taskDB, SleuthkitCase tskCase) {
this.controller = controller;
this.taskDB = taskDB;
this.tskCase = tskCase;
}
static private final String FILE_EXTENSION_CLAUSE =
"(name LIKE '%." //NON-NLS
+ StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") //NON-NLS
+ "')";
static private final String MIMETYPE_CLAUSE =
"(mime_type LIKE '" //NON-NLS
+ StringUtils.join(FileTypeUtils.getAllSupportedMimeTypes(), "' OR mime_type LIKE '") //NON-NLS
+ "') ";
static private final String DRAWABLE_QUERY =
//grab files with supported extension
FILE_EXTENSION_CLAUSE
//grab files with supported mime-types
+ " OR " + MIMETYPE_CLAUSE //NON-NLS
//grab files with image or video mime-types even if we don't officially support them
+ " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%'"; //NON-NLS
private ProgressHandle progressHandle = ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_populatingDb_status());
@Override
public void run() {
progressHandle.start();
updateMessage(Bundle.CopyAnalyzedFiles_populatingDb_status());
try {
//grab all files with supported extension or detected mime types
final List<AbstractFile> files = tskCase.findAllFilesWhere(DRAWABLE_QUERY);
progressHandle.switchToDeterminate(files.size());
updateProgress(0.0);
//do in transaction
DrawableDB.DrawableTransaction tr = taskDB.beginTransaction();
int units = 0;
for (final AbstractFile f : files) {
if (isCancelled()) {
LOGGER.log(Level.WARNING, "Task cancelled: not all contents may be transfered to drawable database."); //NON-NLS
progressHandle.finish();
break;
}
final boolean known = f.getKnown() == TskData.FileKnown.KNOWN;
if (known) {
taskDB.removeFile(f.getId(), tr); //remove known files
} else {
Optional<String> mimeType = FileTypeUtils.getMimeType(f);
if (mimeType.isPresent()) {
//mime type
if (FileTypeUtils.isDrawableMimeType(mimeType.get())) { //supported mimetype => analyzed
taskDB.updateFile(DrawableFile.create(f, true, false), tr);
} else { //unsupported mimtype => analyzed but shouldn't include
taskDB.removeFile(f.getId(), tr);
}
} else {
//no mime tyoe
if (FileTypeUtils.isDrawable(f)) {
//no mime type but supported => add as not analyzed
taskDB.insertFile(DrawableFile.create(f, false, false), tr);
} else {
//no mime type, not supported => remove ( should never get here)
taskDB.removeFile(f.getId(), tr);
}
}
}
units++;
final int prog = units;
progressHandle.progress(f.getName(), units);
updateProgress(prog - 1 / (double) files.size());
updateMessage(f.getName());
}
progressHandle.finish();
progressHandle = ProgressHandleFactory.createHandle(Bundle.CopyAnalyzedFiles_committingDb_status());
updateMessage(Bundle.CopyAnalyzedFiles_committingDb_status());
updateProgress(1.0);
progressHandle.start();
taskDB.commitTransaction(tr, true);
} catch (TskCoreException ex) {
progressHandle.progress(Bundle.CopyAnalyzedFiles_stopCopy_status());
Logger.getLogger(CopyAnalyzedFiles.class.getName()).log(Level.WARNING, "Stopping copy to drawable db task. Failed to transfer all database contents: " + ex.getMessage()); //NON-NLS
MessageNotifyUtil.Notify.warn(Bundle.CopyAnalyzedFiles_errPopulating_errMsg(), ex.getMessage());
progressHandle.finish();
updateMessage("");
updateProgress(-1.0);
controller.setStale(true);
return;
}
progressHandle.finish();
updateMessage("");
updateProgress(-1.0);
controller.setStale(false);
}
}
/**
* task that does pre-ingest copy over of files from a new datasource (uses
* fs_obj_id to identify files from new datasources)
*
* TODO: create methods to simplify progress value/text updates to both
* netbeans and ImageGallery progress/status
*/
@NbBundle.Messages({"PrePopulateDataSourceFiles.prepopulatingDb.status=prepopulating image/video database",
"PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"})
private class PrePopulateDataSourceFiles extends InnerTask {
private final Content dataSource;
/**
* here we grab by extension but in file_done listener we look at file
* type id attributes but fall back on jpeg signatures and extensions to
* check for supported images
*/
// (name like '.jpg' or name like '.png' ...)
private final String DRAWABLE_QUERY = "(name LIKE '%." + StringUtils.join(FileTypeUtils.getAllSupportedExtensions(), "' OR name LIKE '%.") + "') "; //NON-NLS
private ProgressHandle progressHandle = ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status(), this);
/**
*
* @param dataSourceId Data source object ID
*/
PrePopulateDataSourceFiles(Content dataSource) {
super();
this.dataSource = dataSource;
}
/**
* Copy files from a newly added data source into the DB. Get all
* "drawable" files, based on extension. After ingest we use file type
* id module and if necessary jpeg/png signature matching to add/remove
* files
*/
@Override
public void run() {
progressHandle.start();
updateMessage(Bundle.PrePopulateDataSourceFiles_prepopulatingDb_status());
try {
String fsQuery = "(fs_obj_id IS NULL) "; //default clause NON-NLS
/*
* NOTE: Logical files currently (Apr '15) have a null value for
* fs_obj_id in DB. for them, we will not specify a fs_obj_id,
* which means we will grab files from another data source, but
* the drawable DB is smart enough to de-dupe them. For Images
* we can do better.
*/
if (dataSource instanceof Image) {
List<FileSystem> fileSystems = ((Image) dataSource).getFileSystems();
if (fileSystems.isEmpty()) {
/*
* no filesystems, don't bother with the initial
* population, just sort things out on file_done events
*/
progressHandle.finish();
return;
}
//use this clause to only grab files from the newly added filesystems.
fsQuery = fileSystems.stream()
.map(fileSystem -> String.valueOf(fileSystem.getId()))
.collect(Collectors.joining(" OR fs_obj_id = ", "(fs_obj_id = ", ") ")); //NON-NLS
}
final List<AbstractFile> files = getSleuthKitCase().findAllFilesWhere(fsQuery + " AND " + DRAWABLE_QUERY); //NON-NLS
progressHandle.switchToDeterminate(files.size());
//do in transaction
DrawableDB.DrawableTransaction tr = db.beginTransaction();
int units = 0;
for (final AbstractFile f : files) {
if (isCancelled()) {
LOGGER.log(Level.WARNING, "task cancelled: not all contents may be transfered to database"); //NON-NLS
progressHandle.finish();
break;
}
db.insertFile(DrawableFile.create(f, false, false), tr);
units++;
progressHandle.progress(f.getName(), units);
}
progressHandle.finish();
progressHandle = ProgressHandleFactory.createHandle(Bundle.PrePopulateDataSourceFiles_committingDb_status());
progressHandle.start();
db.commitTransaction(tr, false);
} catch (TskCoreException ex) {
Logger.getLogger(PrePopulateDataSourceFiles.class.getName()).log(Level.WARNING, "failed to transfer all database contents", ex); //NON-NLS
}
progressHandle.finish();
}
}
private class IngestModuleEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.coreComponentsAreActive() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
IngestManager.getInstance().removeIngestModuleEventListener(this);
return;
}
switch (IngestManager.IngestModuleEvent.valueOf(evt.getPropertyName())) {
case CONTENT_CHANGED:
//TODO: do we need to do anything here? -jm
case DATA_ADDED:
/*
* we could listen to DATA events and progressivly update
* files, and get data from DataSource ingest modules, but
* given that most modules don't post new artifacts in the
* events and we would have to query for them, without
* knowing which are the new ones, we just ignore these
* events for now. The relevant data should all be captured
* by file done event, anyways -jm
*/
break;
case FILE_DONE:
/**
* getOldValue has fileID getNewValue has
* {@link Abstractfile}
*/
AbstractFile file = (AbstractFile) evt.getNewValue();
if (isListeningEnabled()) {
if (file.isFile()) {
try {
if (ImageGalleryModule.isDrawableAndNotKnown(file)) {
//this file should be included and we don't already know about it from hash sets (NSRL)
queueDBWorkerTask(new UpdateFileTask(file, db));
} else if (FileTypeUtils.getAllSupportedExtensions().contains(file.getNameExtension())) {
//doing this check results in fewer tasks queued up, and faster completion of db update
//this file would have gotten scooped up in initial grab, but actually we don't need it
queueDBWorkerTask(new RemoveFileTask(file, db));
}
} catch (TskCoreException ex) {
//TODO: What to do here?
LOGGER.log(Level.WARNING, "Unable to determine if file is drawable and not known. Not making any changes to DB", ex); //NON-NLS
throw new RuntimeException(ex);
}
}
} else { //TODO: keep track of what we missed for later
setStale(true);
}
break;
}
}
}
private class CaseEventListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (RuntimeProperties.coreComponentsAreActive() == false) {
/*
* Running in "headless" mode, no need to process any events.
* This cannot be done earlier because the switch to core
* components inactive may not have been made at start up.
*/
Case.removePropertyChangeListener(this);
return;
}
switch (Case.Events.valueOf(evt.getPropertyName())) {
case CURRENT_CASE:
Case newCase = (Case) evt.getNewValue();
if (newCase != null) { // case has been opened
setCase(newCase); //connect db, groupmanager, start worker thread
} else { // case is closing
//close window, reset everything
SwingUtilities.invokeLater(ImageGalleryTopComponent::closeTopComponent);
reset();
}
break;
case DATA_SOURCE_ADDED:
//copy all file data to drawable databse
Content newDataSource = (Content) evt.getNewValue();
if (isListeningEnabled()) {
queueDBWorkerTask(new PrePopulateDataSourceFiles(newDataSource));
} else {//TODO: keep track of what we missed for later
setStale(true);
}
break;
case CONTENT_TAG_ADDED:
final ContentTagAddedEvent tagAddedEvent = (ContentTagAddedEvent) evt;
if (getDatabase().isInDB(tagAddedEvent.getAddedTag().getContent().getId())) {
getTagsManager().fireTagAddedEvent(tagAddedEvent);
}
break;
case CONTENT_TAG_DELETED:
final ContentTagDeletedEvent tagDeletedEvent = (ContentTagDeletedEvent) evt;
if (getDatabase().isInDB(tagDeletedEvent.getDeletedTagInfo().getContentID())) {
getTagsManager().fireTagDeletedEvent(tagDeletedEvent);
}
break;
}
}
}
}