Merge pull request #2616 from millmanorama/2404-cached-file-race-condition

2404 fix race condition when accessing cached thumbnail files
This commit is contained in:
Richard Cordovano 2017-03-21 10:03:25 -04:00 committed by GitHub
commit c066935745

View File

@ -1,7 +1,7 @@
/* /*
* Autopsy Forensic Browser * Autopsy Forensic Browser
* *
* Copyright 2012-16 Basis Technology Corp. * Copyright 2011-17 Basis Technology Corp.
* *
* Copyright 2012 42six Solutions. * Copyright 2012 42six Solutions.
* Contact: aebadirad <at> 42six <dot> com * Contact: aebadirad <at> 42six <dot> com
@ -40,6 +40,7 @@ import java.util.List;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -93,7 +94,16 @@ public class ImageUtils {
private static final List<String> SUPPORTED_IMAGE_EXTENSIONS = new ArrayList<>(); private static final List<String> SUPPORTED_IMAGE_EXTENSIONS = new ArrayList<>();
private static final SortedSet<String> SUPPORTED_IMAGE_MIME_TYPES; private static final SortedSet<String> SUPPORTED_IMAGE_MIME_TYPES;
private static final boolean openCVLoaded; private static final boolean OPEN_CV_LOADED;
/**
* Map from tsk object id to Java File object. Used to get the same File for
* different tasks related to the same object so we can then synchronize on
* the File.
*
* NOTE: Must be cleared when the case is changed.
*/
private static final ConcurrentHashMap<Long, File> cacheFileMap = new ConcurrentHashMap<>();
static { static {
ImageIO.scanForPlugins(); ImageIO.scanForPlugins();
@ -124,7 +134,7 @@ public class ImageUtils {
} }
openCVLoaded = openCVLoadedTemp; OPEN_CV_LOADED = openCVLoadedTemp;
SUPPORTED_IMAGE_EXTENSIONS.addAll(Arrays.asList(ImageIO.getReaderFileSuffixes())); SUPPORTED_IMAGE_EXTENSIONS.addAll(Arrays.asList(ImageIO.getReaderFileSuffixes()));
SUPPORTED_IMAGE_EXTENSIONS.add("tec"); // Add JFIF .tec files SUPPORTED_IMAGE_EXTENSIONS.add("tec"); // Add JFIF .tec files
SUPPORTED_IMAGE_MIME_TYPES = new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes())); SUPPORTED_IMAGE_MIME_TYPES = new TreeSet<>(Arrays.asList(ImageIO.getReaderMIMETypes()));
@ -139,6 +149,9 @@ public class ImageUtils {
"image/x-portable-bitmap", //NON-NLS "image/x-portable-bitmap", //NON-NLS
"application/x-123")); //TODO: is this correct? -jm //NON-NLS "application/x-123")); //TODO: is this correct? -jm //NON-NLS
SUPPORTED_IMAGE_MIME_TYPES.removeIf("application/octet-stream"::equals); //NON-NLS SUPPORTED_IMAGE_MIME_TYPES.removeIf("application/octet-stream"::equals); //NON-NLS
//Clear the file map when the case changes, so we don't accidentaly get images from the old case.
Case.addEventSubscriber(Case.Events.CURRENT_CASE.toString(), evt -> cacheFileMap.clear());
} }
/** /**
@ -147,7 +160,7 @@ public class ImageUtils {
private static FileTypeDetector fileTypeDetector; private static FileTypeDetector fileTypeDetector;
/** /**
* thread that saves generated thumbnails to disk in the background *Thread/Executor that saves generated thumbnails to disk in the background
*/ */
private static final Executor imageSaver private static final Executor imageSaver
= Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder() = Executors.newSingleThreadExecutor(new BasicThreadFactory.Builder()
@ -296,7 +309,7 @@ public class ImageUtils {
try { try {
return SwingFXUtils.fromFXImage(thumbnailTask.get(), null); return SwingFXUtils.fromFXImage(thumbnailTask.get(), null);
} catch (InterruptedException | ExecutionException ex) { } catch (InterruptedException | ExecutionException ex) {
LOGGER.log(Level.WARNING, "Failed to get thumbnail for {0}: " + ex.toString(), getContentPathSafe(content)); //NON-NLS LOGGER.log(Level.WARNING, "Failed to get thumbnail for " + getContentPathSafe(content), ex); //NON-NLS
return DEFAULT_THUMBNAIL; return DEFAULT_THUMBNAIL;
} }
} else { } else {
@ -305,7 +318,6 @@ public class ImageUtils {
} }
/** /**
*
* Get a thumbnail of a specified size for the given image. Generates the * Get a thumbnail of a specified size for the given image. Generates the
* thumbnail if it is not already cached. * thumbnail if it is not already cached.
* *
@ -332,13 +344,15 @@ public class ImageUtils {
* any problem getting the file, such as no case was open. * any problem getting the file, such as no case was open.
*/ */
private static File getCachedThumbnailLocation(long fileID) { private static File getCachedThumbnailLocation(long fileID) {
try { return cacheFileMap.computeIfAbsent(fileID, id -> {
String cacheDirectory = Case.getCurrentCase().getCacheDirectory(); try {
return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS String cacheDirectory = Case.getCurrentCase().getCacheDirectory();
} catch (IllegalStateException e) { return Paths.get(cacheDirectory, "thumbnails", fileID + ".png").toFile(); //NON-NLS
LOGGER.log(Level.WARNING, "Could not get cached thumbnail location. No case is open."); //NON-NLS } catch (IllegalStateException e) {
return null; LOGGER.log(Level.WARNING, "Could not get cached thumbnail location. No case is open."); //NON-NLS
} return null;
}
});
} }
/** /**
@ -489,6 +503,7 @@ public class ImageUtils {
"ImageIO could not determine height of {0}: ", //NON-NLS "ImageIO could not determine height of {0}: ", //NON-NLS
imageReader -> imageReader.getHeight(0) imageReader -> imageReader.getHeight(0)
); );
} }
/** /**
@ -576,6 +591,7 @@ public class ImageUtils {
*/ */
public static Task<javafx.scene.image.Image> newGetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) { public static Task<javafx.scene.image.Image> newGetThumbnailTask(AbstractFile file, int iconSize, boolean defaultOnFailure) {
return new GetThumbnailTask(file, iconSize, defaultOnFailure); return new GetThumbnailTask(file, iconSize, defaultOnFailure);
} }
/** /**
@ -609,16 +625,21 @@ public class ImageUtils {
if (isCancelled()) { if (isCancelled()) {
return null; return null;
} }
// If a thumbnail file is already saved locally, just read that. // If a thumbnail file is already saved locally, just read that.
if (cacheFile != null && cacheFile.exists()) { if (cacheFile != null) {
try { synchronized (cacheFile) {
BufferedImage cachedThumbnail = ImageIO.read(cacheFile); if (cacheFile.exists()) {
if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) { try {
return SwingFXUtils.toFXImage(cachedThumbnail, null); BufferedImage cachedThumbnail = ImageIO.read(cacheFile);
if (nonNull(cachedThumbnail) && cachedThumbnail.getWidth() == iconSize) {
return SwingFXUtils.toFXImage(cachedThumbnail, null);
}
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS
cacheFile.delete(); //since we can't read the file we might as well delete it.
}
} }
} catch (Exception ex) {
LOGGER.log(Level.WARNING, "ImageIO had a problem reading the cached thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS
cacheFile.delete(); //since we can't read the file we might as well delete it.
} }
} }
@ -629,7 +650,7 @@ public class ImageUtils {
//There was no correctly-sized cached thumbnail so make one. //There was no correctly-sized cached thumbnail so make one.
BufferedImage thumbnail = null; BufferedImage thumbnail = null;
if (VideoUtils.isVideoThumbnailSupported(file)) { if (VideoUtils.isVideoThumbnailSupported(file)) {
if (openCVLoaded) { if (OPEN_CV_LOADED) {
updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName())); updateMessage(Bundle.GetOrGenerateThumbnailTask_generatingPreviewFor(file.getName()));
thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize); thumbnail = VideoUtils.generateVideoThumbnail(file, iconSize);
} }
@ -698,12 +719,14 @@ public class ImageUtils {
private void saveThumbnail(BufferedImage thumbnail) { private void saveThumbnail(BufferedImage thumbnail) {
imageSaver.execute(() -> { imageSaver.execute(() -> {
try { try {
Files.createParentDirs(cacheFile); synchronized (cacheFile) {
if (cacheFile.exists()) { Files.createParentDirs(cacheFile);
cacheFile.delete(); if (cacheFile.exists()) {
cacheFile.delete();
}
ImageIO.write(thumbnail, FORMAT, cacheFile);
} }
ImageIO.write(thumbnail, FORMAT, cacheFile); } catch (Exception ex) {
} catch (IllegalArgumentException | IOException ex) {
LOGGER.log(Level.WARNING, "Could not write thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS LOGGER.log(Level.WARNING, "Could not write thumbnail for {0}: " + ex.toString(), ImageUtils.getContentPathSafe(file)); //NON-NLS
} }
}); });
@ -725,6 +748,7 @@ public class ImageUtils {
*/ */
public static Task<javafx.scene.image.Image> newReadImageTask(AbstractFile file) { public static Task<javafx.scene.image.Image> newReadImageTask(AbstractFile file) {
return new ReadImageTask(file); return new ReadImageTask(file);
} }
/** /**
@ -753,7 +777,6 @@ public class ImageUtils {
private static final String IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT = "ImageIO could not read {0}. It may be unsupported or corrupt"; //NON-NLS private static final String IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT = "ImageIO could not read {0}. It may be unsupported or corrupt"; //NON-NLS
final AbstractFile file; final AbstractFile file;
// private ImageReader reader;
ReadImageTaskBase(AbstractFile file) { ReadImageTaskBase(AbstractFile file) {
this.file = file; this.file = file;