/*
* 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.operations.down;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.config.Config;
import org.syncany.database.FileVersion;
import org.syncany.database.FileVersion.FileStatus;
import org.syncany.database.FileVersionComparator;
import org.syncany.database.FileVersionComparator.FileChange;
import org.syncany.database.FileVersionComparator.FileVersionComparison;
import org.syncany.database.MemoryDatabase;
import org.syncany.database.PartialFileHistory;
import org.syncany.database.PartialFileHistory.FileHistoryId;
import org.syncany.database.SqlDatabase;
import org.syncany.operations.Assembler;
import org.syncany.operations.ChangeSet;
import org.syncany.operations.down.actions.ChangeFileSystemAction;
import org.syncany.operations.down.actions.DeleteFileSystemAction;
import org.syncany.operations.down.actions.FileSystemAction;
import org.syncany.operations.down.actions.NewFileSystemAction;
import org.syncany.operations.down.actions.NewSymlinkFileSystemAction;
import org.syncany.operations.down.actions.RenameFileSystemAction;
import org.syncany.operations.down.actions.SetAttributesFileSystemAction;
/**
* Implements the file synchronization algorithm in the down operation.
*
* The algorithm compares the local file on the disk with the last local
* database file version and the last winning file version and determines
* what file system action (fsa) to apply.
*
* Input variables:
* - winning version
* - winning file (= local file of winning version)
* - local version
* - local file (= local file of local version)
*
* Algorithm:
* if (has no local version) {
* compwinfwinv = compare winning file to winning version (incl. checksum!)
*
* if (compwinfwinv: winning file matches winning version) {
* // do nothing
* }
* else if (compwinfwinv: new) {
* add new fsa for winning version
* add multichunks to download list for winning version
* }
* else if (compwinfwinv: deleted) {
* add delete fsa for winning version
* }
* else if (compwinfwinv: changed link) {
* add changed link fsa for winning version
* }
* else if (compwinfwinv: changes attrs / modified date) { // does not(!) include "path"
* add changed attrs fsa for winning version
* }
* else if (compwinfwinv: changed path) {
* // Cannot be!
* }
* else { // size/checksum (path cannot be!)
* add conflict fsa for winning file
* add new fsa for winning version
* add multichunks to download list for winning version
* }
* }
*
* else { // local version exists
* complocflocv = compare local file to local version (incl. checksum!)
*
* if (complocflocv: local file matches local version) { // file as expected on disk
* complocvwinv = compare local version to winning version
*
* if (complocvwinv: local version matches winning version) { // means: local file = local version = winning version
* // Nothing to do
* }
* else if (complocvwinv: new) {
* // Cannot be!
* }
* else if (complocvwinv: deleted) {
* add delete fsa for winning version
* }
* else if (complocvwinv: changed link) {
* add changed link fsa for winning version
* }
* else if (complocvwinv: changes attrs / modified date / path) { // includes "path!"
* add changed attrs / renamed fsa for winning version
* }
* else { // size/checksum
* add changed fsa for winning version (and delete local version)
* add multichunks to download list for winning version
* }
* }
* else { // local file does NOT match local version
* if (local file exists) {
* add conflict fsa for local version
* }
*
* add new fsa for winning version
* add multichunks to download list for winning version
* }
*
*/
public class FileSystemActionReconciliator {
private static final Logger logger = Logger.getLogger(FileSystemActionReconciliator.class.getSimpleName());
private Config config;
private ChangeSet changeSet;
private SqlDatabase localDatabase;
private FileVersionComparator fileVersionComparator;
private Assembler assembler;
public FileSystemActionReconciliator(Config config, ChangeSet changeSet) {
this.config = config;
this.changeSet = changeSet;
this.localDatabase = new SqlDatabase(config);
this.fileVersionComparator = new FileVersionComparator(config.getLocalDir(), config.getChunker().getChecksumAlgorithm());
}
public List<FileSystemAction> determineFileSystemActions(MemoryDatabase winnersDatabase) throws Exception {
List<PartialFileHistory> localFileHistoriesWithLastVersion = localDatabase.getFileHistoriesWithLastVersion();
return determineFileSystemActions(winnersDatabase, false, localFileHistoriesWithLastVersion);
}
public List<FileSystemAction> determineFileSystemActions(MemoryDatabase winnersDatabase, boolean cleanupOccurred,
List<PartialFileHistory> localFileHistoriesWithLastVersion) throws Exception {
this.assembler = new Assembler(config, localDatabase, winnersDatabase);
List<FileSystemAction> fileSystemActions = new ArrayList<FileSystemAction>();
// Load file history cache
logger.log(Level.INFO, "- Loading current file tree...");
Map<FileHistoryId, FileVersion> localFileHistoryIdCache = fillFileHistoryIdCache(localFileHistoriesWithLastVersion);
logger.log(Level.INFO, "- Determine filesystem actions ...");
for (PartialFileHistory winningFileHistory : winnersDatabase.getFileHistories()) {
// Get remote file version and content
FileVersion winningLastVersion = winningFileHistory.getLastVersion();
File winningLastFile = new File(config.getLocalDir(), winningLastVersion.getPath());
// Get local file version and content
FileVersion localLastVersion = localFileHistoryIdCache.get(winningFileHistory.getFileHistoryId());
File localLastFile = (localLastVersion != null) ? new File(config.getLocalDir(), localLastVersion.getPath()) : null;
logger.log(Level.INFO, " + Comparing local version: "+localLastVersion);
logger.log(Level.INFO, " with winning version : "+winningLastVersion);
// Sync algorithm ////
// No local file version in local database
if (localLastVersion == null) {
determineActionNoLocalLastVersion(winningLastVersion, winningLastFile, winnersDatabase, fileSystemActions);
}
// Local version found in local database
else {
FileVersionComparison localFileToVersionComparison = fileVersionComparator.compare(localLastVersion, localLastFile, true);
// Local file on disk as expected
if (localFileToVersionComparison.areEqual()) {
determineActionWithLocalVersionAndLocalFileAsExpected(winningLastVersion, winningLastFile, localLastVersion, localLastFile,
winnersDatabase, fileSystemActions);
}
// Local file NOT what was expected
else {
determineActionWithLocalVersionAndLocalFileDiffers(winningLastVersion, winningLastFile, localLastVersion, localLastFile,
winnersDatabase, fileSystemActions, localFileToVersionComparison);
}
}
}
// Find file histories that are in the local database and not in the
// winner's database. They will be assumed to be deleted.
if (cleanupOccurred) {
logger.log(Level.INFO, "- Determine filesystem actions (for deleted histories in winner's branch)...");
Map<FileHistoryId, FileVersion> winnerFileHistoryIdCache = fillFileHistoryIdCache(winnersDatabase.getFileHistories());
for (PartialFileHistory localFileHistoryWithLastVersion : localFileHistoriesWithLastVersion) {
boolean localFileHistoryInWinnersDatabase = winnerFileHistoryIdCache.get(localFileHistoryWithLastVersion.getFileHistoryId()) != null;
// If the file history is also present in the winner's database, it
// has already been processed above. So we'll ignore it here.
if (!localFileHistoryInWinnersDatabase) {
FileVersion localLastVersion = localFileHistoryWithLastVersion.getLastVersion();
File localLastFile = (localLastVersion != null) ? new File(config.getLocalDir(), localLastVersion.getPath()) : null;
determineActionFileHistoryNotInWinnerBranch(localLastVersion, localLastFile, fileSystemActions);
}
}
}
return fileSystemActions;
}
private void determineActionNoLocalLastVersion(FileVersion winningLastVersion, File winningLastFile, MemoryDatabase winnersDatabase,
List<FileSystemAction> outFileSystemActions) throws Exception {
FileVersionComparison winningFileToVersionComparison = fileVersionComparator.compare(winningLastVersion, winningLastFile, true);
boolean contentChanged = winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_CHECKSUM)
|| winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_SIZE);
if (winningFileToVersionComparison.areEqual()) {
logger.log(Level.INFO, " -> (1) Equals: Nothing to do, winning version equals winning file: "+winningLastVersion+" AND "+winningLastFile);
}
else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.DELETED)) {
FileSystemAction action = new NewFileSystemAction(config, winnersDatabase, assembler, winningLastVersion);
outFileSystemActions.add(action);
logger.log(Level.INFO, " -> (2) Deleted: Local file does NOT exist, but it should, winning version not known: "+winningLastVersion+" AND "+winningLastFile);
logger.log(Level.INFO, " -> "+action);
changeSet.getNewFiles().add(winningLastVersion.getPath());
}
else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.NEW)) {
FileSystemAction action = new DeleteFileSystemAction(config, null, winningLastVersion, winnersDatabase);
outFileSystemActions.add(action);
logger.log(Level.INFO, " -> (3) New: winning version was deleted, but local exists, winning version = "+winningLastVersion+" at "+winningLastFile);
logger.log(Level.INFO, " -> "+action);
changeSet.getDeletedFiles().add(winningLastVersion.getPath());
}
else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_LINK_TARGET)) {
FileSystemAction action = new NewSymlinkFileSystemAction(config, winningLastVersion, winnersDatabase);
outFileSystemActions.add(action);
logger.log(Level.INFO, " -> (4) Changed link target: winning file has a different link target: "+winningLastVersion+" AND "+winningLastFile);
logger.log(Level.INFO, " -> "+action);
changeSet.getNewFiles().add(winningLastVersion.getPath());
}
else if (!contentChanged && (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_LAST_MOD_DATE)
|| winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_ATTRIBUTES))) {
FileSystemAction action = new SetAttributesFileSystemAction(config, winningLastVersion, winnersDatabase);
outFileSystemActions.add(action);
logger.log(Level.INFO, " -> (5) Changed file attributes: winning file has different file attributes: "+winningLastVersion+" AND "+winningLastFile);
logger.log(Level.INFO, " -> "+action);
changeSet.getNewFiles().add(winningLastVersion.getPath());
}
else if (winningFileToVersionComparison.getFileChanges().contains(FileChange.CHANGED_PATH)) {
logger.log(Level.INFO, " -> (6) Changed path: winning file has a different path: "+winningLastVersion+" AND "+winningLastFile);
throw new Exception("What happend here?");
}
else { // Content changed
FileSystemAction action = new NewFileSystemAction(config, winnersDatabase, assembler, winningLastVersion);
outFileSystemActions.add(action);
logger.log(Level.INFO, " -> (7) Content changed: Winning file differs from winning version: "+winningLastVersion+" AND "+winningLastFile);
logger.log(Level.INFO, " -> "+action);
changeSet.getNewFiles().add(winningLastVersion.getPath());
}
}
private void determineActionWithLocalVersionAndLocalFileAsExpected(FileVersion winningLastVersion, File winningLastFile,
FileVersion localLastVersion, File localLastFile, MemoryDatabase winnersDatabase, List<FileSystemAction> fileSystemActions) {
FileVersionComparison winningVersionToLocalVersionComparison = fileVersionComparator.compare(winningLastVersion, localLastVersion);
boolean contentChanged = winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_CHECKSUM)
|| winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_SIZE);
if (winningVersionToLocalVersionComparison.areEqual()) { // Local file = local version = winning version!
logger.log(Level.INFO, " -> (8) Equals: Nothing to do, local file equals local version equals winning version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
}
else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.DELETED)) {
FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (9) Content changed: Local file does not exist, but it should: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
logger.log(Level.INFO, " -> "+action);
changeSet.getChangedFiles().add(winningLastVersion.getPath());
}
else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.NEW)) {
FileSystemAction action = new DeleteFileSystemAction(config, localLastVersion, winningLastVersion, winnersDatabase);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (10) Local file exists, but should not: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
logger.log(Level.INFO, " -> "+action);
changeSet.getDeletedFiles().add(winningLastVersion.getPath());
}
else if (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_LINK_TARGET)) {
FileSystemAction action = new NewSymlinkFileSystemAction(config, winningLastVersion, winnersDatabase);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (11) Changed link target: local file has a different link target: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
logger.log(Level.INFO, " -> "+action);
changeSet.getNewFiles().add(winningLastVersion.getPath());
}
else if (!contentChanged && (winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_LAST_MOD_DATE)
|| winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_ATTRIBUTES)
|| winningVersionToLocalVersionComparison.getFileChanges().contains(FileChange.CHANGED_PATH))) {
FileSystemAction action = new RenameFileSystemAction(config, localLastVersion, winningLastVersion, winnersDatabase);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (12) Rename / Changed file attributes: Local file has different file attributes: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
logger.log(Level.INFO, " -> "+action);
changeSet.getChangedFiles().add(winningLastVersion.getPath());
}
else { // Content changed
FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (13) Content changed: Local file differs from winning version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
logger.log(Level.INFO, " -> "+action);
changeSet.getChangedFiles().add(winningLastVersion.getPath());
}
}
private void determineActionWithLocalVersionAndLocalFileDiffers(FileVersion winningLastVersion, File winningLastFile,
FileVersion localLastVersion, File localLastFile, MemoryDatabase winnersDatabase, List<FileSystemAction> fileSystemActions,
FileVersionComparison localFileToVersionComparison) {
if (localFileToVersionComparison.getFileChanges().contains(FileChange.DELETED)) {
boolean winningLastVersionDeleted = winningLastVersion.getStatus() == FileStatus.DELETED;
if (!winningLastVersionDeleted) {
FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, localLastVersion, winningLastVersion);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (14) Content changed: Local file does NOT exist, and winning version changed: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
logger.log(Level.INFO, " -> "+action);
changeSet.getChangedFiles().add(winningLastVersion.getPath());
}
else {
logger.log(Level.INFO, " -> (15) Doing nothing: Local file does NOT exist, and winning version is marked DELETED: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
}
}
else {
FileSystemAction action = new ChangeFileSystemAction(config, winnersDatabase, assembler, winningLastVersion, localLastVersion);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (16) Content changed: Local file differs from last version: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = "+winningLastVersion);
logger.log(Level.INFO, " -> "+action);
changeSet.getChangedFiles().add(winningLastVersion.getPath());
}
}
private void determineActionFileHistoryNotInWinnerBranch(FileVersion localLastVersion, File localLastFile, List<FileSystemAction> fileSystemActions) {
// No local file version in local database
if (localLastVersion == null) {
throw new RuntimeException("This should not happen.");
}
// Local version found in local database
else {
FileSystemAction action = new DeleteFileSystemAction(config, localLastVersion, localLastVersion, null);
fileSystemActions.add(action);
logger.log(Level.INFO, " -> (17) Local file exists, but not in winner branch -> File was deleted remotely: local file = "+localLastFile+", local version = "+localLastVersion+", winning version = (none)");
logger.log(Level.INFO, " -> "+action);
changeSet.getDeletedFiles().add(localLastVersion.getPath());
}
}
private Map<FileHistoryId, FileVersion> fillFileHistoryIdCache(Collection<PartialFileHistory> fileHistoriesWithLastVersion) {
Map<FileHistoryId, FileVersion> fileHistoryIdCache = new HashMap<FileHistoryId, FileVersion>();
for (PartialFileHistory fileHistory : fileHistoriesWithLastVersion) {
fileHistoryIdCache.put(fileHistory.getFileHistoryId(), fileHistory.getLastVersion());
}
return fileHistoryIdCache;
}
}