/*
* 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.operations.down;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.config.Config;
import org.syncany.database.ChunkEntry.ChunkChecksum;
import org.syncany.database.FileContent;
import org.syncany.database.FileVersion;
import org.syncany.database.MemoryDatabase;
import org.syncany.database.MultiChunkEntry.MultiChunkId;
import org.syncany.database.PartialFileHistory;
import org.syncany.database.SqlDatabase;
import org.syncany.operations.Downloader;
import org.syncany.operations.Operation;
import org.syncany.operations.OperationResult;
import org.syncany.operations.down.actions.FileCreatingFileSystemAction;
import org.syncany.operations.down.actions.FileSystemAction;
import org.syncany.plugins.transfer.TransferManager;
/**
* Applies a given winners database to the local directory.
*
* <p>Steps:
* <ul>
* <li>Determine whether the local branch needs to be updated (new database versions); if so, determine
* local {@link FileSystemAction}s</li>
* <li>Determine, download and decrypt required multi chunks from remote storage from file actions
* (implemented in {@link #determineMultiChunksToDownload(FileVersion, MemoryDatabase, MemoryDatabase) determineMultiChunksToDownload()},
* and {@link #downloadAndDecryptMultiChunks(Set) downloadAndDecryptMultiChunks()})</li>
* <li>Apply file system actions locally, creating conflict files where necessary if local file does
* not match the expected file (implemented in {@link #applyFileSystemActions(List) applyFileSystemActions()} </li>
* </ul>
*
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class ApplyChangesOperation extends Operation {
private static final Logger logger = Logger.getLogger(DownOperation.class.getSimpleName());
private SqlDatabase localDatabase;
private Downloader downloader;
private MemoryDatabase winnersDatabase;
private DownOperationResult result;
private boolean cleanupOccurred;
private List<PartialFileHistory> preDeleteFileHistoriesWithLastVersion;
public ApplyChangesOperation(Config config, SqlDatabase localDatabase, TransferManager transferManager, MemoryDatabase winnersDatabase,
DownOperationResult result, boolean cleanupOccurred, List<PartialFileHistory> preDeleteFileHistoriesWithLastVersion) {
super(config);
this.localDatabase = localDatabase;
this.downloader = new Downloader(config, transferManager);
this.winnersDatabase = winnersDatabase;
this.result = result;
this.cleanupOccurred = cleanupOccurred;
this.preDeleteFileHistoriesWithLastVersion = preDeleteFileHistoriesWithLastVersion;
}
@Override
public OperationResult execute() throws Exception {
logger.log(Level.INFO, "Determine file system actions ...");
FileSystemActionReconciliator actionReconciliator = new FileSystemActionReconciliator(config, result.getChangeSet());
List<FileSystemAction> actions;
if (cleanupOccurred) {
actions = actionReconciliator.determineFileSystemActions(winnersDatabase, true, preDeleteFileHistoriesWithLastVersion);
}
else {
actions = actionReconciliator.determineFileSystemActions(winnersDatabase);
}
Set<MultiChunkId> unknownMultiChunks = determineRequiredMultiChunks(actions, winnersDatabase);
downloader.downloadAndDecryptMultiChunks(unknownMultiChunks);
result.getDownloadedMultiChunks().addAll(unknownMultiChunks);
applyFileSystemActions(actions);
return null;
}
/**
* Finds the multichunks that need to be downloaded to apply the given file system actions.
* The method looks at all {@link FileCreatingFileSystemAction}s and returns their multichunks.
*/
private Set<MultiChunkId> determineRequiredMultiChunks(List<FileSystemAction> actions, MemoryDatabase winnersDatabase) {
Set<MultiChunkId> multiChunksToDownload = new HashSet<MultiChunkId>();
for (FileSystemAction action : actions) {
if (action instanceof FileCreatingFileSystemAction) { // TODO [low] This adds ALL multichunks even though some might be available locally
multiChunksToDownload.addAll(determineMultiChunksToDownload(action.getFile2(), winnersDatabase));
}
}
return multiChunksToDownload;
}
/**
* Finds the multichunks that need to be downloaded for the given file version -- using the local
* database and given winners database. Returns a set of multichunk identifiers.
*/
private Collection<MultiChunkId> determineMultiChunksToDownload(FileVersion fileVersion, MemoryDatabase winnersDatabase) {
Set<MultiChunkId> multiChunksToDownload = new HashSet<MultiChunkId>();
// First: Check if we know this file locally!
List<MultiChunkId> multiChunkIds = localDatabase.getMultiChunkIds(fileVersion.getChecksum());
if (multiChunkIds.size() > 0) {
multiChunksToDownload.addAll(multiChunkIds);
}
else {
// Second: We don't know it locally; must be from the winners database
FileContent winningFileContent = winnersDatabase.getContent(fileVersion.getChecksum());
boolean winningFileHasContent = winningFileContent != null;
if (winningFileHasContent) { // File can be empty!
List<ChunkChecksum> fileChunks = winningFileContent.getChunks();
// TODO [medium] Instead of just looking for multichunks to download here, we should look for chunks in local files as well
// and return the chunk positions in the local files ChunkPosition (chunk123 at file12, offset 200, size 250)
Map<ChunkChecksum, MultiChunkId> checksumsWithMultiChunkIds = localDatabase.getMultiChunkIdsByChecksums(fileChunks);
for (ChunkChecksum chunkChecksum : fileChunks) {
MultiChunkId multiChunkIdForChunk = checksumsWithMultiChunkIds.get(chunkChecksum);
if (multiChunkIdForChunk == null) {
multiChunkIdForChunk = winnersDatabase.getMultiChunkIdForChunk(chunkChecksum);
if (multiChunkIdForChunk == null) {
throw new RuntimeException("Cannot find multichunk for chunk "+chunkChecksum);
}
}
if (!multiChunksToDownload.contains(multiChunkIdForChunk)) {
logger.log(Level.INFO, " + Adding multichunk " + multiChunkIdForChunk + " to download list ...");
multiChunksToDownload.add(multiChunkIdForChunk);
}
}
}
}
return multiChunksToDownload;
}
/**
* Applies the given file system actions in a sensible order. To do that,
* the given actions are first sorted using the {@link FileSystemActionComparator} and
* then executed individually using {@link FileSystemAction#execute()}.
*/
private void applyFileSystemActions(List<FileSystemAction> actions) throws Exception {
// Sort
FileSystemActionComparator actionComparator = new FileSystemActionComparator();
actionComparator.sort(actions);
logger.log(Level.FINER, "- Applying file system actions (sorted!) ...");
// Apply
for (FileSystemAction action : actions) {
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, " + {0}", action);
}
// Execute the file system action
// Note that exceptions are not caught here, to prevent
// apply-failed-delete-on-up situations.
action.execute();
}
}
}