/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2015 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.config;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.database.MultiChunkEntry.MultiChunkId;
/**
* The cache class represents the local disk cache. It is used for storing multichunks
* or other metadata files before upload, and as a download location for the same
* files.
*
* <p>The cache implements an LRU strategy based on the last modified date of the
* cached files. When files are accessed using the respective getters, the last modified
* date is updated. Using the {@link #clear()}/{@link #clear(long)} method, the cache
* can be cleaned.
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class Cache {
private static final Logger logger = Logger.getLogger(Cache.class.getSimpleName());
private static long DEFAULT_CACHE_KEEP_BYTES = 500*1024*1024;
private static String FILE_FORMAT_MULTICHUNK_ENCRYPTED = "multichunk-%s";
private static String FILE_FORMAT_MULTICHUNK_DECRYPTED = "multichunk-%s-decrypted";
private static String FILE_FORMAT_DATABASE_FILE_ENCRYPTED = "%s";
private long keepBytes;
private File cacheDir;
public Cache(File cacheDir) {
this.cacheDir = cacheDir;
this.keepBytes = DEFAULT_CACHE_KEEP_BYTES;
}
/**
* Returns a file path of a decrypted multichunk file,
* given the identifier of a multichunk.
*/
public File getDecryptedMultiChunkFile(MultiChunkId multiChunkId) {
return getFileInCache(FILE_FORMAT_MULTICHUNK_DECRYPTED, multiChunkId.toString());
}
/**
* Returns a file path of a encrypted multichunk file,
* given the identifier of a multichunk.
*/
public File getEncryptedMultiChunkFile(MultiChunkId multiChunkId) {
return getFileInCache(FILE_FORMAT_MULTICHUNK_ENCRYPTED, multiChunkId.toString());
}
/**
* Returns a file path of a database remote file.
*/
public File getDatabaseFile(String name) { // TODO [low] This shoule be a database file or another key
return getFileInCache(FILE_FORMAT_DATABASE_FILE_ENCRYPTED, name);
}
public long getKeepBytes() {
return keepBytes;
}
public void setKeepBytes(long keepBytes) {
this.keepBytes = keepBytes;
}
/**
* Deletes files in the the cache directory using a LRU-strategy until <tt>keepBytes</tt>
* bytes are left. This method calls {@link #clear(long)} using the <tt>keepBytes</tt>
* property.
*
* <p>This method should not be run while an operation is executed.
*/
public void clear() {
clear(keepBytes);
}
/**
* Deletes files in the the cache directory using a LRU-strategy until <tt>keepBytes</tt>
* bytes are left.
*
* <p>This method should not be run while an operation is executed.
*/
public void clear(long keepBytes) {
List<File> cacheFiles = getSortedFileList();
// Determine total size
long totalSize = 0;
for (File cacheFile : cacheFiles) {
totalSize += cacheFile.length();
}
// Delete until total cache size <= keep size
if (totalSize > keepBytes) {
logger.log(Level.INFO, "Cache too large (" + (totalSize/1024) + " KB), deleting until <= " + (keepBytes/1024/1024) + " MB ...");
while (totalSize > keepBytes && cacheFiles.size() > 0) {
File eldestCacheFile = cacheFiles.remove(0);
long fileSize = eldestCacheFile.length();
long fileLastModified = eldestCacheFile.lastModified();
logger.log(Level.INFO, "- Deleting from cache (" + new Date(fileLastModified) + ", " + (fileSize/1024) + " KB): " + eldestCacheFile.getName());
totalSize -= fileSize;
eldestCacheFile.delete();
}
}
else {
logger.log(Level.INFO, "Cache size okay (" + (totalSize/1024) + " KB), no need to clean (keep size is " + (keepBytes/1024/1024) + " MB)");
}
}
/**
* Creates temporary file in the local directory cache, typically located at
* .syncany/cache. If not deleted by the application, the returned file is automatically
* deleted on exit by the JVM.
*
* @return Temporary file in local directory cache
*/
public File createTempFile(String name) throws IOException {
File tempFile = File.createTempFile(String.format("temp-%s-", name), ".tmp", cacheDir);
tempFile.deleteOnExit();
return tempFile;
}
/**
* Returns the file using the given format and parameters, and
* updates the last modified date of the file (used for LRU strategy).
*/
private File getFileInCache(String format, Object... params) {
File fileInCache = new File(cacheDir.getAbsoluteFile(), String.format(format, params));
if (fileInCache.exists()) {
touchFile(fileInCache);
}
return fileInCache;
}
/**
* Sets the last modified date of the given file to the current date/time.
*/
private void touchFile(File fileInCache) {
fileInCache.setLastModified(System.currentTimeMillis());
}
/**
* Returns a list of all files in the cache, sorted by the last modified
* date -- eldest first.
*/
private List<File> getSortedFileList() {
File[] cacheFilesList = cacheDir.listFiles();
List<File> sortedCacheFiles = new ArrayList<File>();
if (cacheFilesList != null) {
sortedCacheFiles.addAll(Arrays.asList(cacheFilesList));
Collections.sort(sortedCacheFiles, new Comparator<File>() {
@Override
public int compare(File file1, File file2) {
return Long.compare(file1.lastModified(), file2.lastModified());
}
});
}
return sortedCacheFiles;
}
}