package org.peerbox.forcesync;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.hive2hive.core.events.framework.interfaces.file.IFileDeleteEvent;
import org.hive2hive.core.events.framework.interfaces.file.IFileUpdateEvent;
import org.hive2hive.core.events.implementations.FileDeleteEvent;
import org.hive2hive.core.events.implementations.FileUpdateEvent;
import org.peerbox.app.manager.file.FileInfo;
import org.peerbox.watchservice.FileEventManager;
import org.peerbox.watchservice.conflicthandling.ConflictHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
public class ListSync {
private static final Logger logger = LoggerFactory.getLogger(ListSync.class);
private Map<Path, FileInfo> localDb;
private Map<Path, FileInfo> localDisk;
private Map<Path, FileInfo> remoteDb;
private Map<Path, FileInfo> remoteNetwork;
// folders to delete locally (due to a deleted folder in the network)
private final Set<Path> foldersToDelete;
// all files that are uploaded (either the file is new or updated)
private final Set<Path> newLocalFiles;
private final FileEventManager fileEventManager;
private final Path topLevel;
@Inject
public ListSync(FileEventManager fileEventManager, Path topLevel) {
this.fileEventManager = fileEventManager;
this.topLevel = topLevel;
foldersToDelete = new HashSet<>();
newLocalFiles = new HashSet<>();
}
public void sync(
Map<Path, FileInfo> localDisk,
Map<Path, FileInfo> localDb,
Map<Path, FileInfo> remoteNetwork,
Map<Path, FileInfo> remoteDb) throws Exception {
// make sure clients set these instances
Preconditions.checkNotNull(fileEventManager);
Preconditions.checkNotNull(localDb);
Preconditions.checkNotNull(localDisk);
Preconditions.checkNotNull(remoteDb);
Preconditions.checkNotNull(remoteNetwork);
this.localDisk = localDisk;
this.localDb = localDb;
this.remoteNetwork = remoteNetwork;
this.remoteDb = remoteDb;
foldersToDelete.clear();
newLocalFiles.clear();
// perform a list sync using the maps
synchronize();
// delete folders if they do not have any descendants that are added
deleteFoldersToDelete();
cleanup();
}
private void synchronize() throws Exception {
// union of all possible files that we have to consider
SortedSet<Path> allFiles = allFiles();
allFiles = allFiles.stream().filter(file -> file.startsWith(topLevel))
.collect(Collectors.toCollection(() -> new TreeSet<Path>()));
allFiles.forEach(file -> logger.trace("Use file {}", file));
for (Path file : allFiles) {
/*
* Each file can be either [unknown, added, deleted, exists] locally or remotely
* This leads to 16 possible combinations
*/
// exists in ...?
boolean eRemoteNetwork = remoteNetwork.containsKey(file);
boolean eRemoteDb = remoteDb.containsKey(file);
boolean eLocalDb = localDb.containsKey(file);
boolean eLocalDisk = localDisk.containsKey(file);
// NOTE: may be null! Only use those where the flag is true.
// local
FileInfo fileDisk = localDisk.get(file);
FileInfo fileLocalDb = localDb.get(file);
// remote
FileInfo fileNetwork = remoteNetwork.get(file);
FileInfo fileRemoteDb = remoteDb.get(file);
if (eRemoteNetwork && eRemoteDb && eLocalDb && eLocalDisk) {
/* remote: exists - local: exists */
// file present on disk and in network - check hashes
// - if network/disk match: ok
// - if network/remote db match, but disk/local db mismatch: upload new version
// - if disk/local db match, but network/remote db mismatch: download new version
// - otherwise: conflict
if (fileNetwork.isFile()) {
if (hashesMatch(fileNetwork, fileDisk)) {
// sync
} else if (hashesMatch(fileNetwork, fileRemoteDb) && !hashesMatch(fileDisk, fileLocalDb)) {
uploadFile(file);
} else if (!hashesMatch(fileNetwork, fileRemoteDb) && hashesMatch(fileDisk, fileLocalDb)) {
downloadFile(file, fileNetwork.isFile());
} else {
conflict(file);
}
} else {
// folder - no update required
}
} else if (eRemoteNetwork && eRemoteDb && eLocalDb && !eLocalDisk) {
/* remote: exists - local: deleted */
// there was a local delete
// -> disable sync
deleteRemoteFile(file);
} else if (eRemoteNetwork && eRemoteDb && !eLocalDb && eLocalDisk) {
/* remote: exists - local: added */
// local add, but file already present in network - check hashes:
// - If network/disk match: ok.
// - Otherwise: conflict
if (fileNetwork.isFile()) {
if (hashesMatch(fileNetwork, fileDisk)) {
// match - already sync
} else {
conflict(file);
}
} else {
// folder - no content to update
}
} else if (eRemoteNetwork && eRemoteDb && !eLocalDb && !eLocalDisk) {
/* remote: exists - local: unknown */
// file was never downloaded. Note: this has nothing to do with selective sync.
// with selective sync, there would be an entry in the database.
// -> download file
downloadFile(file, fileNetwork.isFile());
} else if (eRemoteNetwork && !eRemoteDb && eLocalDb && eLocalDisk) {
/* remote: added - local: exists */
// remote add, but file already exists on disk (was not uploaded yet or DB not up to date)
// check hashes:
// - If disk/network match: ok
// - If network/local DB match: file updated locally, upload new version
// - Otherwise: conflict
if (fileNetwork.isFile()) {
if (hashesMatch(fileNetwork, fileDisk)) {
// match - already sync
} else if (hashesMatch(fileNetwork, fileLocalDb)) {
// file was updated
uploadFile(file);
} else {
conflict(file);
}
} else {
// folder - no content to update
}
} else if (eRemoteNetwork && !eRemoteDb && eLocalDb && !eLocalDisk) {
/* remote: added - local: deleted */
// local file does not exist anymore, but new remote file
// -> download file
downloadFile(file, fileNetwork.isFile());
} else if (eRemoteNetwork && !eRemoteDb && !eLocalDb && eLocalDisk) {
/* remote: added - local: added */
// new remote file and new local file - check hashes
// - If network/disk match: ok
// - Otherwise: conflict
if (fileNetwork.isFile()) {
if (hashesMatch(fileNetwork, fileDisk)) {
// match - already sync.
} else {
conflict(file);
}
} else {
// folder - no content to update
}
} else if (eRemoteNetwork && !eRemoteDb && !eLocalDb && !eLocalDisk) {
/* remote: added - local: unknown */
// new remote file
// -> download
downloadFile(file, fileNetwork.isFile());
} else if (!eRemoteNetwork && eRemoteDb && eLocalDb && eLocalDisk) {
/* remote: deleted - local: exists */
// remote file deleted
// -> delete local IFF no local update. Otherwise, add file again.
if (fileDisk.isFile()) {
if (hashesMatch(fileDisk, fileLocalDb)) {
deleteLocalFile(file, true);
} else {
uploadFile(file);
}
} else {
// folders are deleted at the end separately because we want to prevent accidental
// deletion of new files in the folders that we may not have
// processed yet.
foldersToDelete.add(file);
}
} else if (!eRemoteNetwork && eRemoteDb && eLocalDb && !eLocalDisk) {
/* remote: deleted - local: deleted */
// remote delete and local delete
// -> remove entries from both databases
removeFromRemoteDb(file);
removeFromLocalDb(file);
} else if (!eRemoteNetwork && eRemoteDb && !eLocalDb && eLocalDisk) {
/* remote: deleted - local: added */
// remote delete and local add
// -> add file (upload)
uploadFile(file);
} else if (!eRemoteNetwork && eRemoteDb && !eLocalDb && !eLocalDisk) {
/* remote: deleted - local: unknown */
// file does not exist on disk nor in network
// -> delete entry from remote DB
removeFromRemoteDb(file);
} else if (!eRemoteNetwork && !eRemoteDb && eLocalDb && eLocalDisk) {
/* remote: unknown - local: exists */
// file exists, but not known to network
// -> add file (upload)
uploadFile(file);
} else if (!eRemoteNetwork && !eRemoteDb && eLocalDb && !eLocalDisk) {
/* remote: unknown - local: deleted */
// local delete, but not known to network
// -> remove entry from local DB
removeFromLocalDb(file);
} else if (!eRemoteNetwork && !eRemoteDb && !eLocalDb && eLocalDisk) {
/* remote: unknown - local: added */
// new local file, previously not known
// -> add file (upload)
uploadFile(file);
} else if (!eRemoteNetwork && !eRemoteDb && !eLocalDb && !eLocalDisk) {
/* remote / local: unknown */
// not possible - file does not exist
// -> nothing to do
// but it would mean that we have an unexpected file in the set...
throw new Exception("Unknown combination to handle.");
} else {
// should never happen, all cases should be covered
throw new Exception("Unknown combination to handle.");
}
}
}
private SortedSet<Path> allFiles() {
SortedSet<Path> allFiles = new TreeSet<Path>();
allFiles.addAll(localDb.keySet());
allFiles.addAll(localDisk.keySet());
allFiles.addAll(remoteDb.keySet());
allFiles.addAll(remoteNetwork.keySet());
return allFiles;
}
/**
* Iterates through foldersToDelete and deletes the (local) folders iff there is no
* descendant found in newLocalFiles.
*/
private void deleteFoldersToDelete() {
for (Path folder : foldersToDelete) {
boolean hasNewDescendants = false;
for (Path file : newLocalFiles) {
if (file.startsWith(folder)) {
hasNewDescendants = true;
break;
}
}
if (!hasNewDescendants) {
deleteLocalFile(folder, false);
}
}
}
private void deleteLocalFile(Path file, boolean isFile) {
logger.trace("OPERATION: Remote delete file {}", file);
// delete the local file due to a remote delete
IFileDeleteEvent deleteEvent = new FileDeleteEvent(file.toFile(), isFile);
fileEventManager.onFileDelete(deleteEvent);
}
private void deleteRemoteFile(Path file) {
// soft delete due to a local delete
// no hard delete of files: only disable sync
logger.trace("OPERATION: Soft-delete file {}", file);
fileEventManager.onFileSoftDeleted(file);
}
private void uploadFile(Path file) {
// used to prevent accidental removal of files
newLocalFiles.add(file);
// add or update file depending on whether it already exists in network
if (remoteNetwork.containsKey(file)) {
logger.trace("OPERATION: Local Update of file {}", file);
fileEventManager.onLocalFileModified(file);
} else {
logger.trace("OPERATION: Local Create of file {}", file);
fileEventManager.onLocalFileCreated(file);
}
}
private void downloadFile(Path file, boolean isFile) {
// file update event will trigger download
IFileUpdateEvent updateEvent = new FileUpdateEvent(file.toFile(), isFile);
logger.trace("OPERATION: Download of file {}", file);
fileEventManager.onFileUpdate(updateEvent);
}
private void conflict(Path file) {
logger.trace("OPERATION: Handle conflict of file {}", file);
ConflictHandler.resolveConflict(file);
}
private void removeFromLocalDb(Path file) {
// the entry is removed with the next persistence iteration
}
private void removeFromRemoteDb(Path file) {
// the entry is removed with the next persistence iteration
}
/**
* Tests hashes for equality. Do not use it for folders where the hash is null!
*
* @param a an instance
* @param b another instance
* @return true if hashes are equals
*/
private boolean hashesMatch(FileInfo a, FileInfo b) {
boolean match = a.getContentHash().equals(b.getContentHash());
return match;
}
private void cleanup() {
localDb = null;
localDisk = null;
remoteDb = null;
remoteNetwork = null;
foldersToDelete.clear();
newLocalFiles.clear();
}
}