001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.config;
019
020import java.io.File;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.Comparator;
026import java.util.Date;
027import java.util.List;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030
031import org.syncany.database.MultiChunkEntry.MultiChunkId;
032
033/**
034 * The cache class represents the local disk cache. It is used for storing multichunks
035 * or other metadata files before upload, and as a download location for the same
036 * files. 
037 * 
038 * <p>The cache implements an LRU strategy based on the last modified date of the 
039 * cached files. When files are accessed using the respective getters, the last modified
040 * date is updated. Using the {@link #clear()}/{@link #clear(long)} method, the cache
041 * can be cleaned.
042 * 
043 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
044 */
045public class Cache {
046        private static final Logger logger = Logger.getLogger(Cache.class.getSimpleName());
047
048    private static long DEFAULT_CACHE_KEEP_BYTES = 500*1024*1024;
049        private static String FILE_FORMAT_MULTICHUNK_ENCRYPTED = "multichunk-%s";
050        private static String FILE_FORMAT_MULTICHUNK_DECRYPTED = "multichunk-%s-decrypted";
051    private static String FILE_FORMAT_DATABASE_FILE_ENCRYPTED = "%s";
052    
053    private long keepBytes;
054    private File cacheDir;
055    
056    public Cache(File cacheDir) {
057        this.cacheDir = cacheDir;
058        this.keepBytes = DEFAULT_CACHE_KEEP_BYTES;
059    }
060    
061    /**
062     * Returns a file path of a decrypted multichunk file, 
063     * given the identifier of a multichunk.
064     */
065    public File getDecryptedMultiChunkFile(MultiChunkId multiChunkId) {
066        return getFileInCache(FILE_FORMAT_MULTICHUNK_DECRYPTED, multiChunkId.toString());
067    }    
068
069    /**
070     * Returns a file path of a encrypted multichunk file, 
071     * given the identifier of a multichunk.
072     */
073    public File getEncryptedMultiChunkFile(MultiChunkId multiChunkId) {
074        return getFileInCache(FILE_FORMAT_MULTICHUNK_ENCRYPTED, multiChunkId.toString());
075    }    
076    
077    /**
078     * Returns a file path of a database remote file.
079     */
080        public File getDatabaseFile(String name) { // TODO [low] This should be a database file or another key
081                return getFileInCache(FILE_FORMAT_DATABASE_FILE_ENCRYPTED, name);               
082        }    
083
084        public long getKeepBytes() {
085                return keepBytes;
086        }
087
088        public void setKeepBytes(long keepBytes) {
089                this.keepBytes = keepBytes;
090        }
091
092        /**
093         * Deletes files in the the cache directory using a LRU-strategy until <code>keepBytes</code>
094         * bytes are left. This method calls {@link #clear(long)} using the <code>keepBytes</code>
095         * property.
096         * 
097         * <p>This method should not be run while an operation is executed.
098         */
099        public void clear() {
100                clear(keepBytes);
101        }
102        
103        /**
104         * Deletes files in the the cache directory using a LRU-strategy until <code>keepBytes</code>
105         * bytes are left.
106         * 
107         * <p>This method should not be run while an operation is executed.
108         */
109        public void clear(long keepBytes) {             
110                List<File> cacheFiles = getSortedFileList();
111                        
112                // Determine total size
113                long totalSize = 0;
114                
115                for (File cacheFile : cacheFiles) {
116                        totalSize += cacheFile.length();
117                }
118                
119                // Delete until total cache size <= keep size
120                if (totalSize > keepBytes) {
121                        logger.log(Level.INFO, "Cache too large (" + (totalSize/1024) + " KB), deleting until <= " + (keepBytes/1024/1024) + " MB ...");
122
123                        while (totalSize > keepBytes && cacheFiles.size() > 0) {
124                                File eldestCacheFile = cacheFiles.remove(0);
125                                
126                                long fileSize = eldestCacheFile.length();
127                                long fileLastModified = eldestCacheFile.lastModified();
128                                
129                                logger.log(Level.INFO, "- Deleting from cache (" + new Date(fileLastModified) + ", " + (fileSize/1024) + " KB): " + eldestCacheFile.getName());
130                                
131                                totalSize -= fileSize;
132                                eldestCacheFile.delete();                               
133                        }
134                }
135                else {
136                        logger.log(Level.INFO, "Cache size okay (" + (totalSize/1024) + " KB), no need to clean (keep size is " + (keepBytes/1024/1024) + " MB)");
137                }
138        }
139
140        /**
141         * Creates temporary file in the local directory cache, typically located at
142         * .syncany/cache. If not deleted by the application, the returned file is automatically
143         * deleted on exit by the JVM.
144         * 
145         * @return Temporary file in local directory cache
146         */
147    public File createTempFile(String name) throws IOException {
148       File tempFile = File.createTempFile(String.format("temp-%s-", name), ".tmp", cacheDir);
149       tempFile.deleteOnExit();
150       
151       return tempFile;
152    }
153    
154    /**
155     * Returns the file using the given format and parameters, and 
156     * updates the last modified date of the file (used for LRU strategy).
157     */
158    private File getFileInCache(String format, Object... params) {
159        File fileInCache = new File(cacheDir.getAbsoluteFile(), String.format(format, params));
160
161        if (fileInCache.exists()) {
162                touchFile(fileInCache);
163        }
164        
165        return fileInCache;
166    }
167    
168    /**
169     * Sets the last modified date of the given file to the current date/time.
170     */
171    private void touchFile(File fileInCache) {
172        fileInCache.setLastModified(System.currentTimeMillis());
173    }
174    
175    /**
176     * Returns a list of all files in the cache, sorted by the last modified
177     * date -- eldest first.
178     */
179        private List<File> getSortedFileList() {
180                File[] cacheFilesList = cacheDir.listFiles();
181                List<File> sortedCacheFiles = new ArrayList<File>();
182                
183                if (cacheFilesList != null) {
184                        sortedCacheFiles.addAll(Arrays.asList(cacheFilesList));
185                        
186                        Collections.sort(sortedCacheFiles, new Comparator<File>() {
187                                @Override
188                                public int compare(File file1, File file2) {                            
189                                        return Long.compare(file1.lastModified(), file2.lastModified());
190                                }
191                        });
192                }
193                
194                return sortedCacheFiles;
195        }
196}