/* * Syncany, www.syncany.org * Copyright (C) 2011-2016 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.database; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.syncany.database.ChunkEntry.ChunkChecksum; import org.syncany.database.FileContent.FileChecksum; import org.syncany.database.FileVersion.FileStatus; import org.syncany.database.MultiChunkEntry.MultiChunkId; import org.syncany.database.PartialFileHistory.FileHistoryId; /** * The database represents the internal file and chunk index of the application. It * can be used to reference or load a full local database (local client) or a * remote database (from a delta database file of another clients). * * <p>A database consists of a sorted list of {@link DatabaseVersion}s, i.e. it is a * collection of changes to the local file system. * * <p>For convenience, the class also offers a set of functionality to select objects * from the current accumulated database. Examples include {@link #getChunk(byte[]) getChunk()}, * {@link #getContent(byte[]) getContent()} and {@link #getMultiChunk(byte[]) getMultiChunk()}. * * <p>To allow this convenience, a few caches are kept in memory, and updated whenever a * database version is added or removed. * * @see DatabaseVersion * @author Philipp C. Heckel <philipp.heckel@gmail.com> */ public class MemoryDatabase { private List<DatabaseVersion> databaseVersions; // Caches private DatabaseVersion fullDatabaseVersionCache; private Map<String, PartialFileHistory> filenameHistoryCache; private Map<VectorClock, DatabaseVersion> databaseVersionIdCache; private Map<FileChecksum, List<PartialFileHistory>> contentChecksumFileHistoriesCache; public MemoryDatabase() { databaseVersions = new ArrayList<DatabaseVersion>(); // Caches fullDatabaseVersionCache = new DatabaseVersion(); filenameHistoryCache = new HashMap<String, PartialFileHistory>(); databaseVersionIdCache = new HashMap<VectorClock, DatabaseVersion>(); contentChecksumFileHistoriesCache = new HashMap<FileChecksum, List<PartialFileHistory>>(); } public DatabaseVersion getLastDatabaseVersion() { if (databaseVersions.size() == 0) { return null; } return databaseVersions.get(databaseVersions.size() - 1); } public List<DatabaseVersion> getDatabaseVersions() { return Collections.unmodifiableList(databaseVersions); } public DatabaseVersion getDatabaseVersion(VectorClock vectorClock) { return databaseVersionIdCache.get(vectorClock); } public FileContent getContent(FileChecksum checksum) { return (checksum != null) ? fullDatabaseVersionCache.getFileContent(checksum) : null; } public Object getChunk(ChunkChecksum checksum) { return (checksum != null) ? fullDatabaseVersionCache.getChunk(checksum) : null; } public MultiChunkEntry getMultiChunk(MultiChunkId id) { return fullDatabaseVersionCache.getMultiChunk(id); } /** * Get a multichunk that this chunk is contained in. */ public MultiChunkId getMultiChunkIdForChunk(ChunkChecksum chunk) { return fullDatabaseVersionCache.getMultiChunkId(chunk); } public PartialFileHistory getFileHistory(String relativeFilePath) { return filenameHistoryCache.get(relativeFilePath); } public List<PartialFileHistory> getFileHistories(FileChecksum fileContentChecksum) { return contentChecksumFileHistoriesCache.get(fileContentChecksum); } public PartialFileHistory getFileHistory(FileHistoryId fileId) { return fullDatabaseVersionCache.getFileHistory(fileId); } public Collection<PartialFileHistory> getFileHistories() { return fullDatabaseVersionCache.getFileHistories(); } public Collection<MultiChunkEntry> getMultiChunks() { return fullDatabaseVersionCache.getMultiChunks(); } public void addDatabaseVersion(DatabaseVersion databaseVersion) { databaseVersions.add(databaseVersion); // Populate caches // WARNING: Do NOT reorder, order important!! updateDatabaseVersionIdCache(databaseVersion); updateFullDatabaseVersionCache(databaseVersion); updateFilenameHistoryCache(); updateContentChecksumCache(); } public void removeDatabaseVersion(DatabaseVersion databaseVersion) { databaseVersions.remove(databaseVersion); // Populate caches // WARNING: Do NOT reorder, order important!! updateFullDatabaseVersionCache(); updateDatabaseVersionIdCache(); updateFilenameHistoryCache(); updateContentChecksumCache(); } // TODO [medium] Very inefficient. Always updates whole cache private void updateContentChecksumCache() { contentChecksumFileHistoriesCache.clear(); for (PartialFileHistory fullFileHistory : fullDatabaseVersionCache.getFileHistories()) { FileChecksum lastVersionChecksum = fullFileHistory.getLastVersion().getChecksum(); if (lastVersionChecksum != null) { List<PartialFileHistory> historiesWithVersionsWithSameChecksum = contentChecksumFileHistoriesCache.get(lastVersionChecksum); // Create if it does not exist if (historiesWithVersionsWithSameChecksum == null) { historiesWithVersionsWithSameChecksum = new ArrayList<PartialFileHistory>(); } // Add to cache historiesWithVersionsWithSameChecksum.add(fullFileHistory); contentChecksumFileHistoriesCache.put(lastVersionChecksum, historiesWithVersionsWithSameChecksum); } } } private void updateFilenameHistoryCache() { // TODO [medium] Performance: This throws away the unchanged entries. It should only update new database version filenameHistoryCache.clear(); for (PartialFileHistory cacheFileHistory : fullDatabaseVersionCache.getFileHistories()) { FileVersion lastVersion = cacheFileHistory.getLastVersion(); String fileName = lastVersion.getPath(); if (lastVersion.getStatus() != FileStatus.DELETED) { filenameHistoryCache.put(fileName, cacheFileHistory); } } } private void updateDatabaseVersionIdCache(DatabaseVersion newDatabaseVersion) { databaseVersionIdCache.put(newDatabaseVersion.getVectorClock(), newDatabaseVersion); } private void updateDatabaseVersionIdCache() { databaseVersionIdCache.clear(); for (DatabaseVersion databaseVersion : databaseVersions) { updateDatabaseVersionIdCache(databaseVersion); } } private void updateFullDatabaseVersionCache() { fullDatabaseVersionCache = new DatabaseVersion(); for (DatabaseVersion databaseVersion : databaseVersions) { updateFullDatabaseVersionCache(databaseVersion); } } private void updateFullDatabaseVersionCache(DatabaseVersion newDatabaseVersion) { // Chunks for (ChunkEntry sourceChunk : newDatabaseVersion.getChunks()) { if (fullDatabaseVersionCache.getChunk(sourceChunk.getChecksum()) == null) { fullDatabaseVersionCache.addChunk(sourceChunk); } } // Multichunks for (MultiChunkEntry sourceMultiChunk : newDatabaseVersion.getMultiChunks()) { if (fullDatabaseVersionCache.getMultiChunk(sourceMultiChunk.getId()) == null) { fullDatabaseVersionCache.addMultiChunk(sourceMultiChunk); } } // Contents for (FileContent sourceFileContent : newDatabaseVersion.getFileContents()) { if (fullDatabaseVersionCache.getFileContent(sourceFileContent.getChecksum()) == null) { fullDatabaseVersionCache.addFileContent(sourceFileContent); } } // Histories for (PartialFileHistory sourceFileHistory : newDatabaseVersion.getFileHistories()) { PartialFileHistory targetFileHistory = fullDatabaseVersionCache.getFileHistory(sourceFileHistory.getFileHistoryId()); if (targetFileHistory == null) { fullDatabaseVersionCache.addFileHistory(sourceFileHistory.clone()); } else { for (FileVersion sourceFileVersion : sourceFileHistory.getFileVersions().values()) { if (targetFileHistory.getFileVersion(sourceFileVersion.getVersion()) == null) { targetFileHistory.addFileVersion(sourceFileVersion); } } } } } }