Creating a on-disk file cache (used primarily for resized images)
This commit is contained in:
parent
770e85dd1c
commit
7910588ce8
437
Dorkbox-Util/src/dorkbox/util/CacheUtil.java
Normal file
437
Dorkbox-Util/src/dorkbox/util/CacheUtil.java
Normal file
@ -0,0 +1,437 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 dorkbox, llc
|
||||||
|
*
|
||||||
|
* 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 dorkbox.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public
|
||||||
|
class CacheUtil {
|
||||||
|
|
||||||
|
public static final File TEMP_DIR = new File(System.getProperty("java.io.tmpdir"));
|
||||||
|
|
||||||
|
// will never be null.
|
||||||
|
private static final MessageDigest digest;
|
||||||
|
|
||||||
|
static {
|
||||||
|
@SuppressWarnings("UnusedAssignment")
|
||||||
|
MessageDigest digest_ = null;
|
||||||
|
try {
|
||||||
|
digest_ = MessageDigest.getInstance("SHA1");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException("Unable to initialize hash algorithm for images. MD5 digest doesn't exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
digest = digest_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long runtimeRandom = new SecureRandom().nextLong();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if true, we will essentially "not cache" the image. If false, we will cache the file by name+size, and reuse between runs.
|
||||||
|
*/
|
||||||
|
public static boolean setUniqueCachePerRun = false;
|
||||||
|
|
||||||
|
public static String tempDir = "";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears ALL saved files in the cache
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
void clear() {
|
||||||
|
// deletes all of the files (recursively) in the specified location. If the directory is empty (no locked files), then the
|
||||||
|
// directory is also deleted.
|
||||||
|
FileUtil.delete(new File(TEMP_DIR, tempDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the specified file is in the cache. NULL if it is not, otherwise specifies a location on disk.
|
||||||
|
* <p>
|
||||||
|
* This cache is not persisted across runs.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File check(File file) throws IOException {
|
||||||
|
if (file == null) {
|
||||||
|
throw new IOException("file cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we already have this fileName, reuse it
|
||||||
|
return check(file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the specified file is in the cache. NULL if it is not, otherwise specifies a location on disk.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File check(String fileName) throws IOException {
|
||||||
|
if (fileName == null) {
|
||||||
|
throw new IOException("fileName cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we already have this fileName, reuse it
|
||||||
|
File newFile = makeCacheFile(fileName);
|
||||||
|
// if this file already exists (via HASH), we just reuse what is saved on disk.
|
||||||
|
if (newFile.canRead() && newFile.isFile()) {
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the specified URL is in the cache. NULL if it is not, otherwise specifies a location on disk.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File check(final URL fileResource) throws IOException {
|
||||||
|
if (fileResource == null) {
|
||||||
|
throw new IOException("fileResource cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return check(fileResource.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the specified stream (based on the hash of the input stream) is in the cache. NULL if it is not, otherwise
|
||||||
|
* specifies a location on disk.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File check(final InputStream fileStream) throws IOException {
|
||||||
|
if (fileStream == null) {
|
||||||
|
throw new IOException("fileStream cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return check(null, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the specified name is in the cache. NULL if it is not, otherwise specifies a location on disk. If the
|
||||||
|
* cacheName is NULL, it will use a HASH of the fileStream
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File check(String cacheName, final InputStream fileStream) throws IOException {
|
||||||
|
if (fileStream == null) {
|
||||||
|
throw new IOException("fileStream cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheName == null) {
|
||||||
|
cacheName = createNameAsHash(fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we already have this fileName, reuse it
|
||||||
|
File newFile = makeCacheFile(cacheName);
|
||||||
|
// if this file already exists (via HASH), we just reuse what is saved on disk.
|
||||||
|
if (newFile.canRead() && newFile.isFile()) {
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the name of the file in a cache, based on the file's name.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(final File file) throws IOException {
|
||||||
|
return save(file.getAbsolutePath(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the name of the file in a cache, based on the specified name. If cacheName is NULL, it will use the file's name.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(String cacheName, final File file) throws IOException {
|
||||||
|
if (cacheName == null) {
|
||||||
|
cacheName = file.getAbsolutePath();
|
||||||
|
}
|
||||||
|
return save(cacheName, file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the name of the file in a cache, based on the specified name.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(final String fileName) throws IOException {
|
||||||
|
return save(null, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the name of the file in a cache, based on name. If cacheName is NULL, it will use the file's name.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(String cacheName, final String fileName) throws IOException {
|
||||||
|
if (cacheName == null) {
|
||||||
|
cacheName = fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we already have this fileName, reuse it
|
||||||
|
File newFile = makeCacheFile(cacheName);
|
||||||
|
// if this file already exists (via HASH), we just reuse what is saved on disk.
|
||||||
|
if (newFile.canRead() && newFile.isFile()) {
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// is file sitting on drive
|
||||||
|
File iconTest = new File(fileName);
|
||||||
|
if (iconTest.isFile() && iconTest.canRead()) {
|
||||||
|
// have to copy the resource to the cache
|
||||||
|
FileUtil.copyFile(iconTest, newFile);
|
||||||
|
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// suck it out of a URL/Resource (with debugging if necessary)
|
||||||
|
final URL systemResource = LocationResolver.getResource(fileName);
|
||||||
|
|
||||||
|
InputStream inStream = systemResource.openStream();
|
||||||
|
|
||||||
|
// saves the file into our temp location, uses HASH of cacheName
|
||||||
|
return makeFileViaStream(cacheName, inStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the name of the URL in a cache, based on it's path.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(final URL fileResource) throws IOException {
|
||||||
|
return save(null, fileResource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the name of the URL in a cache, based on the specified name. If cacheName is NULL, it will use the URL's path.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(String cacheName, final URL fileResource) throws IOException {
|
||||||
|
if (cacheName == null) {
|
||||||
|
cacheName = fileResource.getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we already have this fileName, reuse it
|
||||||
|
File newFile = makeCacheFile(cacheName);
|
||||||
|
// if this file already exists (via HASH), we just reuse what is saved on disk.
|
||||||
|
if (newFile.canRead() && newFile.isFile()) {
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream inStream = fileResource.openStream();
|
||||||
|
|
||||||
|
// saves the file into our temp location, uses HASH of cacheName
|
||||||
|
return makeFileViaStream(cacheName, inStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This caches the data based on the HASH of the input stream.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(final InputStream fileStream) throws IOException {
|
||||||
|
if (fileStream == null) {
|
||||||
|
throw new IOException("fileStream cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
return save(null, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the name of the file in a cache, based on the cacheName. If the cacheName is NULL, it will use a HASH of the fileStream
|
||||||
|
* as the name.
|
||||||
|
*/
|
||||||
|
public static synchronized
|
||||||
|
File save(String cacheName, final InputStream fileStream) throws IOException {
|
||||||
|
if (cacheName == null) {
|
||||||
|
cacheName = createNameAsHash(fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we already have this fileName, reuse it
|
||||||
|
File newFile = makeCacheFile(cacheName);
|
||||||
|
// if this file already exists (via HASH), we just reuse what is saved on disk.
|
||||||
|
if (newFile.canRead() && newFile.isFile()) {
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeFileViaStream(cacheName, fileStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cacheName needs name+extension for the resource
|
||||||
|
* @param resourceStream the resource to copy to a file on disk
|
||||||
|
*
|
||||||
|
* @return the full path of the resource copied to disk, or NULL if invalid
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
|
private static
|
||||||
|
File makeFileViaStream(String cacheName, final InputStream resourceStream) throws IOException {
|
||||||
|
if (resourceStream == null) {
|
||||||
|
throw new IOException("resourceStream is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
File newFile = makeCacheFile(cacheName);
|
||||||
|
// if this file already exists (via HASH), we just reuse what is saved on disk.
|
||||||
|
if (newFile.canRead() && newFile.isFile()) {
|
||||||
|
return newFile.getAbsoluteFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputStream outStream = null;
|
||||||
|
try {
|
||||||
|
int read;
|
||||||
|
byte[] buffer = new byte[2048];
|
||||||
|
outStream = new FileOutputStream(newFile);
|
||||||
|
|
||||||
|
while ((read = resourceStream.read(buffer)) > 0) {
|
||||||
|
outStream.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Send up exception
|
||||||
|
String message = "Unable to copy '" + cacheName + "' to temporary location: '" + newFile.getAbsolutePath() + "'";
|
||||||
|
throw new RuntimeException(message, e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
resourceStream.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (outStream != null) {
|
||||||
|
outStream.close();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (setUniqueCachePerRun) {
|
||||||
|
// KDE is unique per run, so this prevents buildup
|
||||||
|
newFile.deleteOnExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
//get the name of the new file
|
||||||
|
return newFile.getAbsoluteFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates the file that will be cached. It may, or may not already exist
|
||||||
|
// must be called from synchronized block!
|
||||||
|
// never retuns null
|
||||||
|
private static
|
||||||
|
File makeCacheFile(final String cachedName) throws IOException {
|
||||||
|
if (cachedName == null) {
|
||||||
|
throw new IOException("cachedName is null.");
|
||||||
|
}
|
||||||
|
|
||||||
|
File saveDir = new File(TEMP_DIR, tempDir);
|
||||||
|
|
||||||
|
// can be wimpy, only one at a time
|
||||||
|
String hash = hashName(cachedName);
|
||||||
|
String extension = FileUtil.getExtension(cachedName);
|
||||||
|
if (extension.length() == 0) {
|
||||||
|
extension = "cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
File newFile = new File(saveDir, hash + '.' + extension).getAbsoluteFile();
|
||||||
|
// make whatever dirs we need to.
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
newFile.getParentFile().mkdirs();
|
||||||
|
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// must be called from synchronized block!
|
||||||
|
private static
|
||||||
|
String hashName(String name) {
|
||||||
|
// figure out the fileName
|
||||||
|
byte[] bytes = name.getBytes(OS.UTF_8);
|
||||||
|
|
||||||
|
digest.reset();
|
||||||
|
digest.update(bytes);
|
||||||
|
|
||||||
|
// For KDE4, it must also be unique across runs
|
||||||
|
if (setUniqueCachePerRun) {
|
||||||
|
byte[] longBytes = new byte[8];
|
||||||
|
ByteBuffer wrap = ByteBuffer.wrap(longBytes);
|
||||||
|
wrap.putLong(runtimeRandom);
|
||||||
|
digest.update(longBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to alpha-numeric. see https://stackoverflow.com/questions/29183818/why-use-tostring32-and-not-tostring36
|
||||||
|
return new BigInteger(1, digest.digest()).toString(32).toUpperCase(Locale.US);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is if we DO NOT have a file name. We hash the resourceStream bytes to base the name on that. The extension will be ".cache"
|
||||||
|
public static synchronized
|
||||||
|
String createNameAsHash(final InputStream resourceStream) {
|
||||||
|
digest.reset();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// we have to set the cache name based on the hash of the input stream ONLY...
|
||||||
|
final ByteArrayOutputStream outStream = new ByteArrayOutputStream(4096); // will resize if necessary
|
||||||
|
|
||||||
|
int read;
|
||||||
|
byte[] buffer = new byte[2048];
|
||||||
|
|
||||||
|
while ((read = resourceStream.read(buffer)) > 0) {
|
||||||
|
digest.update(buffer, 0, read);
|
||||||
|
outStream.write(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to alpha-numeric. see https://stackoverflow.com/questions/29183818/why-use-tostring32-and-not-tostring36
|
||||||
|
return new BigInteger(1, digest.digest()).toString(32).toUpperCase(Locale.US) + ".cache";
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Send up exception
|
||||||
|
String message = "Unable to copy InputStream to memory.";
|
||||||
|
throw new RuntimeException(message, e);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
resourceStream.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user