package co.codewizards.cloudstore.core.repo.sync;
import static co.codewizards.cloudstore.core.objectfactory.ObjectFactoryUtil.*;
import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
import static co.codewizards.cloudstore.core.util.AssertUtil.*;
import static co.codewizards.cloudstore.core.util.HashUtil.*;
import static co.codewizards.cloudstore.core.util.Util.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.dto.ChangeSetDto;
import co.codewizards.cloudstore.core.dto.ConfigPropSetDto;
import co.codewizards.cloudstore.core.dto.CopyModificationDto;
import co.codewizards.cloudstore.core.dto.DeleteModificationDto;
import co.codewizards.cloudstore.core.dto.DirectoryDto;
import co.codewizards.cloudstore.core.dto.FileChunkDto;
import co.codewizards.cloudstore.core.dto.ModificationDto;
import co.codewizards.cloudstore.core.dto.NormalFileDto;
import co.codewizards.cloudstore.core.dto.RepoFileDto;
import co.codewizards.cloudstore.core.dto.RepoFileDtoTreeNode;
import co.codewizards.cloudstore.core.dto.SymlinkDto;
import co.codewizards.cloudstore.core.dto.VersionInfoDto;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.progress.ProgressMonitor;
import co.codewizards.cloudstore.core.progress.SubProgressMonitor;
import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper;
import co.codewizards.cloudstore.core.repo.local.LocalRepoManager;
import co.codewizards.cloudstore.core.repo.local.LocalRepoManagerFactory;
import co.codewizards.cloudstore.core.repo.transport.CollisionException;
import co.codewizards.cloudstore.core.repo.transport.LocalRepoTransport;
import co.codewizards.cloudstore.core.repo.transport.RepoTransport;
import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactory;
import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactoryRegistry;
import co.codewizards.cloudstore.core.repo.transport.TransferDoneMarkerType;
import co.codewizards.cloudstore.core.util.UrlUtil;
import co.codewizards.cloudstore.core.version.VersionCompatibilityValidator;
/**
* Logic for synchronising a local with a remote repository.
* @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
*/
public class RepoToRepoSync implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(RepoToRepoSync.class);
/**
* Sync in the inverse direction. This is only for testing whether the RepoTransport implementations
* are truly symmetric. It is less efficient! Therefore, this must NEVER be true in production!!!
*/
private static final boolean TEST_INVERSE = false;
protected final File localRoot;
protected final URL remoteRoot;
protected final LocalRepoManager localRepoManager;
protected final LocalRepoTransport localRepoTransport;
protected final RepoTransport remoteRepoTransport;
protected final UUID localRepositoryId;
protected final UUID remoteRepositoryId;
private ExecutorService localSyncExecutor;
private Future<Void> localSyncFuture;
/**
* Create an instance.
* @param localRoot the root of the local repository or any file/directory inside it. This is
* automatically adjusted to fit the connection-point to the remote repository (the remote
* repository might be connected to a sub-directory).
* @param remoteRoot the root of the remote repository. This must exactly match the connection point.
* If a sub-directory of the remote repository is connected to the local repository, this sub-directory
* must be referenced here.
*/
protected RepoToRepoSync(File localRoot, final URL remoteRoot) {
final File localRootWithoutPathPrefix = LocalRepoHelper.getLocalRootContainingFile(assertNotNull(localRoot, "localRoot"));
this.remoteRoot = UrlUtil.canonicalizeURL(assertNotNull(remoteRoot, "remoteRoot"));
localRepoManager = LocalRepoManagerFactory.Helper.getInstance().createLocalRepoManagerForExistingRepository(localRootWithoutPathPrefix);
this.localRoot = localRoot = createFile(localRootWithoutPathPrefix, localRepoManager.getLocalPathPrefixOrFail(remoteRoot));
localRepositoryId = localRepoManager.getRepositoryId();
if (localRepositoryId == null)
throw new IllegalStateException("localRepoManager.getRepositoryId() returned null!");
remoteRepositoryId = localRepoManager.getRemoteRepositoryIdOrFail(remoteRoot);
remoteRepoTransport = createRepoTransport(remoteRoot, localRepositoryId);
localRepoTransport = (LocalRepoTransport) createRepoTransport(localRoot, remoteRepositoryId);
}
public static RepoToRepoSync create(final File localRoot, final URL remoteRoot) {
return createObject(RepoToRepoSync.class, localRoot, remoteRoot);
}
public void sync(final ProgressMonitor monitor) {
assertNotNull(monitor, "monitor");
monitor.beginTask("Synchronising...", 201);
try {
final VersionInfoDto clientVersionInfoDto = localRepoTransport.getVersionInfoDto();
final VersionInfoDto serverVersionInfoDto = remoteRepoTransport.getVersionInfoDto();
VersionCompatibilityValidator.getInstance().validate(clientVersionInfoDto, serverVersionInfoDto);
readRemoteRepositoryIdFromRepoTransport();
monitor.worked(1);
if (localSyncExecutor != null)
throw new IllegalStateException("localSyncExecutor != null");
if (localSyncFuture != null)
throw new IllegalStateException("localSyncFuture != null");
localSyncExecutor = Executors.newFixedThreadPool(1);
localSyncFuture = localSyncExecutor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
logger.info("sync: locally syncing {} ('{}')", localRepositoryId, localRoot);
localRepoManager.localSync(new SubProgressMonitor(monitor, 50));
return null;
}
});
if (!TEST_INVERSE) { // This is the normal sync (NOT test).
syncDown(true, new SubProgressMonitor(monitor, 50));
if (localSyncExecutor != null)
throw new IllegalStateException("localSyncExecutor != null");
if (localSyncFuture != null)
throw new IllegalStateException("localSyncFuture != null");
syncUp(new SubProgressMonitor(monitor, 50));
// Immediately sync back to make sure the changes we caused don't cause problems later
// (right now there's very likely no collision and this should be very fast).
syncDown(false, new SubProgressMonitor(monitor, 50));
}
else { // THIS IS FOR TESTING ONLY!
logger.info("sync: locally syncing on *remote* side {} ('{}')", localRepositoryId, localRoot);
remoteRepoTransport.getChangeSetDto(true); // trigger the local sync on the remote side (we don't need the change set)
waitForAndCheckLocalSyncFuture();
syncUp(new SubProgressMonitor(monitor, 50));
syncDown(false, new SubProgressMonitor(monitor, 50));
syncUp(new SubProgressMonitor(monitor, 50));
}
} finally {
monitor.done();
}
}
protected void syncUp(final ProgressMonitor monitor) {
logger.info("syncUp: fromID={} from='{}' toID={} to='{}'",
localRepositoryId, localRoot, remoteRepositoryId, remoteRoot);
sync(localRepoTransport, false, remoteRepoTransport, monitor);
}
protected void syncDown(final boolean fromRepoLocalSync, final ProgressMonitor monitor) {
logger.info("syncDown: fromID={} from='{}' toID={} to='{}', fromRepoLocalSync={}",
remoteRepositoryId, remoteRoot, localRepositoryId, localRoot, fromRepoLocalSync);
sync(remoteRepoTransport, fromRepoLocalSync, localRepoTransport, monitor);
}
private void waitForAndCheckLocalSyncFutureIfExists() {
if (localSyncFuture != null)
waitForAndCheckLocalSyncFuture();
}
private void waitForAndCheckLocalSyncFuture() {
try {
assertNotNull(localSyncFuture, "localSyncFuture").get();
} catch (final RuntimeException e) {
throw e;
} catch (final Exception e) {
throw new RuntimeException(e);
}
assertNotNull(localSyncExecutor, "localSyncExecutor").shutdown();
localSyncFuture = null;
localSyncExecutor = null;
}
private void readRemoteRepositoryIdFromRepoTransport() {
final UUID repositoryId = remoteRepoTransport.getRepositoryId();
if (repositoryId == null)
throw new IllegalStateException("remoteRepoTransport.getRepositoryId() returned null!");
if (!repositoryId.equals(remoteRepositoryId))
throw new IllegalStateException(
String.format("remoteRepoTransport.getRepositoryId() does not match repositoryId in local DB! %s != %s", repositoryId, remoteRepositoryId));
}
private RepoTransport createRepoTransport(final File rootFile, final UUID clientRepositoryId) {
URL rootURL;
try {
rootURL = rootFile.toURI().toURL();
} catch (final MalformedURLException e) {
throw new RuntimeException(e);
}
return createRepoTransport(rootURL, clientRepositoryId);
}
private RepoTransport createRepoTransport(final URL remoteRoot, final UUID clientRepositoryId) {
final RepoTransportFactory repoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(remoteRoot);
return repoTransportFactory.createRepoTransport(remoteRoot, clientRepositoryId);
}
protected void sync(final RepoTransport fromRepoTransport, final boolean fromRepoLocalSync, final RepoTransport toRepoTransport, final ProgressMonitor monitor) {
monitor.beginTask("Synchronising...", 100);
try {
final ChangeSetDto changeSetDto = fromRepoTransport.getChangeSetDto(fromRepoLocalSync);
monitor.worked(8);
waitForAndCheckLocalSyncFutureIfExists();
toRepoTransport.prepareForChangeSetDto(changeSetDto);
sync(fromRepoTransport, toRepoTransport, changeSetDto, new SubProgressMonitor(monitor, 90));
fromRepoTransport.endSyncFromRepository();
toRepoTransport.endSyncToRepository(changeSetDto.getRepositoryDto().getRevision());
monitor.worked(2);
} finally {
monitor.done();
}
}
protected void sync(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final ChangeSetDto changeSetDto, final ProgressMonitor monitor) {
monitor.beginTask("Synchronising...", 1 + changeSetDto.getModificationDtos().size() + 3 * changeSetDto.getRepoFileDtos().size() + 1);
try {
syncParentConfigPropSetDto(fromRepoTransport, toRepoTransport, changeSetDto.getParentConfigPropSetDto(),
new SubProgressMonitor(monitor, 1));
final RepoFileDtoTreeNode repoFileDtoTree = RepoFileDtoTreeNode.createTree(changeSetDto.getRepoFileDtos());
if (repoFileDtoTree != null) {
sync(fromRepoTransport, toRepoTransport, repoFileDtoTree,
new Class<?>[] { DirectoryDto.class }, new Class<?>[0], false,
new SubProgressMonitor(monitor, repoFileDtoTree.size()));
}
syncModifications(fromRepoTransport, toRepoTransport, changeSetDto.getModificationDtos(),
new SubProgressMonitor(monitor, changeSetDto.getModificationDtos().size()));
if (repoFileDtoTree != null) {
sync(fromRepoTransport, toRepoTransport, repoFileDtoTree,
new Class<?>[] { RepoFileDto.class }, new Class<?>[] { DirectoryDto.class }, true,
new SubProgressMonitor(monitor, repoFileDtoTree.size()));
}
if (repoFileDtoTree != null) {
sync(fromRepoTransport, toRepoTransport, repoFileDtoTree,
new Class<?>[] { RepoFileDto.class }, new Class<?>[] { DirectoryDto.class }, false,
new SubProgressMonitor(monitor, repoFileDtoTree.size()));
}
} finally {
monitor.done();
}
}
protected void syncParentConfigPropSetDto(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final ConfigPropSetDto parentConfigPropSetDto, final ProgressMonitor monitor) {
assertNotNull(fromRepoTransport, "fromRepoTransport");
assertNotNull(toRepoTransport, "toRepoTransport");
// parentConfigPropSetDto may be null!
assertNotNull(monitor, "monitor");
monitor.beginTask("Synchronising parent-config...", 1);
try {
if (parentConfigPropSetDto == null)
return;
toRepoTransport.putParentConfigPropSetDto(parentConfigPropSetDto);
} finally {
monitor.done();
}
}
protected void sync(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final RepoFileDtoTreeNode repoFileDtoTree,
final Class<?>[] repoFileDtoClassesIncl, final Class<?>[] repoFileDtoClassesExcl, final boolean filesInProgressOnly,
final ProgressMonitor monitor) {
assertNotNull(fromRepoTransport, "fromRepoTransport");
assertNotNull(toRepoTransport, "toRepoTransport");
assertNotNull(repoFileDtoTree, "repoFileDtoTree");
assertNotNull(repoFileDtoClassesIncl, "repoFileDtoClassesIncl");
assertNotNull(repoFileDtoClassesExcl, "repoFileDtoClassesExcl");
assertNotNull(monitor, "monitor");
final Map<Class<?>, Boolean> repoFileDtoClass2Included = new HashMap<Class<?>, Boolean>();
final Map<Class<?>, Boolean> repoFileDtoClass2Excluded = new HashMap<Class<?>, Boolean>();
final Set<String> fileInProgressPaths = filesInProgressOnly
? localRepoTransport.getFileInProgressPaths(fromRepoTransport.getRepositoryId(), toRepoTransport.getRepositoryId())
: null;
monitor.beginTask("Synchronising...", repoFileDtoTree.size());
try {
for (final RepoFileDtoTreeNode repoFileDtoTreeNode : repoFileDtoTree) {
if (repoFileDtoTreeNode.getRepoFileDto().isNeededAsParent()) { // not actually modified - serves only to complete the tree structure.
monitor.worked(1);
continue;
}
if (fileInProgressPaths != null && ! fileInProgressPaths.contains(repoFileDtoTreeNode.getPath())) {
monitor.worked(1);
continue;
}
final RepoFileDto repoFileDto = repoFileDtoTreeNode.getRepoFileDto();
final Class<? extends RepoFileDto> repoFileDtoClass = repoFileDto.getClass();
Boolean included = repoFileDtoClass2Included.get(repoFileDtoClass);
if (included == null) {
included = false;
for (final Class<?> clazz : repoFileDtoClassesIncl) {
if (clazz.isAssignableFrom(repoFileDtoClass)) {
included = true;
break;
}
}
repoFileDtoClass2Included.put(repoFileDtoClass, included);
}
Boolean excluded = repoFileDtoClass2Excluded.get(repoFileDtoClass);
if (excluded == null) {
excluded = false;
for (final Class<?> clazz : repoFileDtoClassesExcl) {
if (clazz.isAssignableFrom(repoFileDtoClass)) {
excluded = true;
break;
}
}
repoFileDtoClass2Excluded.put(repoFileDtoClass, excluded);
}
if (!included || excluded) {
monitor.worked(1);
continue;
}
if (isDone(fromRepoTransport, toRepoTransport, repoFileDto)) {
logger.debug("sync: Skipping file already done in an interrupted transfer before: {}", repoFileDtoTreeNode.getPath());
monitor.worked(1);
continue;
}
if (repoFileDto instanceof DirectoryDto)
syncDirectory(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, (DirectoryDto) repoFileDto, new SubProgressMonitor(monitor, 1));
else if (repoFileDto instanceof NormalFileDto) {
syncFile(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, repoFileDto, monitor);
}
else if (repoFileDto instanceof SymlinkDto)
syncSymlink(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, (SymlinkDto) repoFileDto, new SubProgressMonitor(monitor, 1));
else
throw new IllegalStateException("Unsupported RepoFileDto type: " + repoFileDto);
markDone(fromRepoTransport, toRepoTransport, repoFileDto);
}
} finally {
monitor.done();
}
}
private boolean isDone(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport, final RepoFileDto repoFileDto) {
return localRepoTransport.isTransferDone(
fromRepoTransport.getRepositoryId(), toRepoTransport.getRepositoryId(),
TransferDoneMarkerType.REPO_FILE, repoFileDto.getId(), repoFileDto.getLocalRevision());
}
private void markDone(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport, final RepoFileDto repoFileDto) {
localRepoTransport.markTransferDone(
fromRepoTransport.getRepositoryId(), toRepoTransport.getRepositoryId(),
TransferDoneMarkerType.REPO_FILE, repoFileDto.getId(), repoFileDto.getLocalRevision());
}
private boolean isDone(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport, final ModificationDto modificationDto) {
return localRepoTransport.isTransferDone(
fromRepoTransport.getRepositoryId(), toRepoTransport.getRepositoryId(),
TransferDoneMarkerType.MODIFICATION, modificationDto.getId(), modificationDto.getLocalRevision());
}
private void markDone(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport, final ModificationDto modificationDto) {
localRepoTransport.markTransferDone(
fromRepoTransport.getRepositoryId(), toRepoTransport.getRepositoryId(),
TransferDoneMarkerType.MODIFICATION, modificationDto.getId(), modificationDto.getLocalRevision());
}
private SortedMap<Long, Collection<ModificationDto>> getLocalRevision2ModificationDtos(final Collection<ModificationDto> modificationDtos) {
final SortedMap<Long, Collection<ModificationDto>> map = new TreeMap<Long, Collection<ModificationDto>>();
for (final ModificationDto modificationDto : modificationDtos) {
final long localRevision = modificationDto.getLocalRevision();
Collection<ModificationDto> collection = map.get(localRevision);
if (collection == null) {
collection = new ArrayList<ModificationDto>();
map.put(localRevision, collection);
}
collection.add(modificationDto);
}
return map;
}
private void syncModifications(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport, final Collection<ModificationDto> modificationDtos, final ProgressMonitor monitor) {
monitor.beginTask("Synchronising...", modificationDtos.size());
try {
final SortedMap<Long,Collection<ModificationDto>> localRevision2ModificationDtos = getLocalRevision2ModificationDtos(modificationDtos);
for (final Map.Entry<Long,Collection<ModificationDto>> me : localRevision2ModificationDtos.entrySet()) {
final ModificationDtoSet modificationDtoSet = new ModificationDtoSet(me.getValue());
for (final List<CopyModificationDto> copyModificationDtos : modificationDtoSet.getFromPath2CopyModificationDtos().values()) {
for (final Iterator<CopyModificationDto> itCopyMod = copyModificationDtos.iterator(); itCopyMod.hasNext(); ) {
final CopyModificationDto copyModificationDto = itCopyMod.next();
if (isDone(fromRepoTransport, toRepoTransport, copyModificationDto)) {
logger.debug("sync: Skipping CopyModificaton already done in an interrupted transfer before: {} => {}", copyModificationDto.getFromPath(), copyModificationDto.getToPath());
monitor.worked(1);
continue;
}
final List<DeleteModificationDto> deleteModificationDtos = modificationDtoSet.getPath2DeleteModificationDtos().get(copyModificationDto.getFromPath());
boolean moveInstead = false;
if (!itCopyMod.hasNext() && deleteModificationDtos != null && !deleteModificationDtos.isEmpty())
moveInstead = true;
if (moveInstead) {
logger.info("syncModifications: Moving from '{}' to '{}'", copyModificationDto.getFromPath(), copyModificationDto.getToPath());
toRepoTransport.move(copyModificationDto.getFromPath(), copyModificationDto.getToPath());
}
else {
logger.info("syncModifications: Copying from '{}' to '{}'", copyModificationDto.getFromPath(), copyModificationDto.getToPath());
toRepoTransport.copy(copyModificationDto.getFromPath(), copyModificationDto.getToPath());
}
if (!moveInstead && deleteModificationDtos != null) {
for (final DeleteModificationDto deleteModificationDto : deleteModificationDtos) {
logger.info("syncModifications: Deleting '{}'", deleteModificationDto.getPath());
applyDeleteModification(fromRepoTransport, toRepoTransport, deleteModificationDto);
}
}
markDone(fromRepoTransport, toRepoTransport, copyModificationDto);
}
}
for (final List<DeleteModificationDto> deleteModificationDtos : modificationDtoSet.getPath2DeleteModificationDtos().values()) {
for (final DeleteModificationDto deleteModificationDto : deleteModificationDtos) {
if (isDone(fromRepoTransport, toRepoTransport, deleteModificationDto)) {
logger.debug("sync: Skipping DeleteModificaton already done in an interrupted transfer before: {}", deleteModificationDto.getPath());
monitor.worked(1);
continue;
}
logger.info("syncModifications: Deleting '{}'", deleteModificationDto.getPath());
applyDeleteModification(fromRepoTransport, toRepoTransport, deleteModificationDto);
markDone(fromRepoTransport, toRepoTransport, deleteModificationDto);
}
}
}
} finally {
monitor.done();
}
}
protected void applyDeleteModification(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport, final DeleteModificationDto deleteModificationDto) {
assertNotNull(fromRepoTransport, "fromRepoTransport");
assertNotNull(toRepoTransport, "toRepoTransport");
assertNotNull(deleteModificationDto, "deleteModificationDto");
try {
delete(fromRepoTransport, toRepoTransport, deleteModificationDto);
} catch (final CollisionException x) { // Note: This cannot happen in CloudStore! But in can happen in downstream projects with different RepoTransport implementations!
logger.info("CollisionException during delete: {}", deleteModificationDto.getPath());
if (logger.isDebugEnabled())
logger.debug(x.toString(), x);
return;
}
}
protected void delete(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport, final DeleteModificationDto deleteModificationDto) {
toRepoTransport.delete(deleteModificationDto.getPath());
}
private void syncDirectory(
final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final RepoFileDtoTreeNode repoFileDtoTreeNode, final DirectoryDto directoryDto, final ProgressMonitor monitor) {
monitor.beginTask("Synchronising...", 100);
try {
final String path = repoFileDtoTreeNode.getPath();
logger.info("syncDirectory: path='{}'", path);
try {
makeDirectory(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, path, directoryDto);
} catch (final CollisionException x) {
logger.info("CollisionException during makeDirectory: {}", path);
if (logger.isDebugEnabled())
logger.debug(x.toString(), x);
return;
}
} finally {
monitor.done();
}
}
protected void makeDirectory(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final RepoFileDtoTreeNode repoFileDtoTreeNode, final String path, final DirectoryDto directoryDto) {
toRepoTransport.makeDirectory(path, directoryDto.getLastModified());
}
private void syncSymlink(
final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final RepoFileDtoTreeNode repoFileDtoTreeNode, final SymlinkDto symlinkDto, final SubProgressMonitor monitor) {
monitor.beginTask("Synchronising...", 100);
try {
final String path = repoFileDtoTreeNode.getPath();
try {
toRepoTransport.makeSymlink(path, symlinkDto.getTarget(), symlinkDto.getLastModified());
} catch (final CollisionException x) {
logger.info("CollisionException during makeSymlink: {}", path);
if (logger.isDebugEnabled())
logger.debug(x.toString(), x);
return;
}
} finally {
monitor.done();
}
}
private void syncFile(final RepoTransport fromRepoTransport,
final RepoTransport toRepoTransport, final RepoFileDtoTreeNode repoFileDtoTreeNode,
final RepoFileDto normalFileDto, final ProgressMonitor monitor) {
monitor.beginTask("Synchronising...", 100);
try {
final String path = repoFileDtoTreeNode.getPath();
logger.info("syncFile: path='{}'", path);
final RepoFileDto fromRepoFileDto = fromRepoTransport.getRepoFileDto(path);
if (fromRepoFileDto == null) {
logger.warn("File was deleted during sync on source side: {}", path);
return;
}
if (!(fromRepoFileDto instanceof NormalFileDto)) {
logger.warn("Normal file was replaced by a directory (or another type) during sync on source side: {}", path);
return;
}
monitor.worked(10);
final NormalFileDto fromNormalFileDto = (NormalFileDto) fromRepoFileDto;
final RepoFileDto toRepoFileDto = toRepoTransport.getRepoFileDto(path);
if (areFilesExistingAndEqual(fromRepoFileDto, toRepoFileDto)) {
logger.info("File is already equal on destination side (sha1='{}'): {}", fromNormalFileDto.getSha1(), path);
return;
}
monitor.worked(10);
logger.info("Beginning to copy file (from.sha1='{}' to.sha1='{}'): {}", fromNormalFileDto.getSha1(),
toRepoFileDto instanceof NormalFileDto ? ((NormalFileDto)toRepoFileDto).getSha1() : "<NoInstanceOf_NormalFileDto>",
path);
final NormalFileDto toNormalFileDto;
if (toRepoFileDto instanceof NormalFileDto)
toNormalFileDto = (NormalFileDto) toRepoFileDto;
else
toNormalFileDto = createObject(NormalFileDto.class); // dummy (null-object pattern)
try {
beginPutFile(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, path, fromNormalFileDto);
} catch (final CollisionException x) {
logger.info("CollisionException during beginPutFile: {}", path);
if (logger.isDebugEnabled())
logger.debug(x.toString(), x);
return;
}
localRepoTransport.markFileInProgress(fromRepoTransport.getRepositoryId(), toRepoTransport.getRepositoryId(), path, true);
monitor.worked(1);
final Map<Long, FileChunkDto> offset2ToTempFileChunkDto = new HashMap<>(toNormalFileDto.getTempFileChunkDtos().size());
for (final FileChunkDto toTempFileChunkDto : toNormalFileDto.getTempFileChunkDtos())
offset2ToTempFileChunkDto.put(toTempFileChunkDto.getOffset(), toTempFileChunkDto);
logger.debug("Comparing {} FileChunkDtos. path='{}'", fromNormalFileDto.getFileChunkDtos().size(), path);
final List<FileChunkDto> fromFileChunkDtosDirty = new ArrayList<FileChunkDto>();
final Iterator<FileChunkDto> toFileChunkDtoIterator = toNormalFileDto.getFileChunkDtos().iterator();
int fileChunkIndex = -1;
for (final FileChunkDto fromFileChunkDto : fromNormalFileDto.getFileChunkDtos()) {
final FileChunkDto toFileChunkDto = toFileChunkDtoIterator.hasNext() ? toFileChunkDtoIterator.next() : null;
++fileChunkIndex;
final FileChunkDto toTempFileChunkDto = offset2ToTempFileChunkDto.get(fromFileChunkDto.getOffset());
if (toTempFileChunkDto == null) {
if (toFileChunkDto != null
&& equal(fromFileChunkDto.getOffset(), toFileChunkDto.getOffset())
&& equal(fromFileChunkDto.getLength(), toFileChunkDto.getLength())
&& equal(fromFileChunkDto.getSha1(), toFileChunkDto.getSha1())) {
if (logger.isTraceEnabled()) {
logger.trace("Skipping clean FileChunkDto. index={} offset={} sha1='{}'",
fileChunkIndex, fromFileChunkDto.getOffset(), fromFileChunkDto.getSha1());
}
continue;
}
}
else {
if (equal(fromFileChunkDto.getOffset(), toTempFileChunkDto.getOffset())
&& equal(fromFileChunkDto.getLength(), toTempFileChunkDto.getLength())
&& equal(fromFileChunkDto.getSha1(), toTempFileChunkDto.getSha1())) {
if (logger.isTraceEnabled()) {
logger.trace("Skipping clean temporary FileChunkDto. index={} offset={} sha1='{}'",
fileChunkIndex, fromFileChunkDto.getOffset(), fromFileChunkDto.getSha1());
}
continue;
}
}
if (logger.isTraceEnabled()) {
logger.trace("Enlisting dirty FileChunkDto. index={} fromOffset={} toOffset={} fromSha1='{}' toSha1='{}'",
fileChunkIndex, fromFileChunkDto.getOffset(),
(toFileChunkDto == null ? "null" : toFileChunkDto.getOffset()),
fromFileChunkDto.getSha1(),
(toFileChunkDto == null ? "null" : toFileChunkDto.getSha1()));
}
fromFileChunkDtosDirty.add(fromFileChunkDto);
}
logger.info("Need to copy {} dirty file-chunks (of {} total). path='{}'",
fromFileChunkDtosDirty.size(), fromNormalFileDto.getFileChunkDtos().size(), path);
final ProgressMonitor subMonitor = new SubProgressMonitor(monitor, 73);
subMonitor.beginTask("Synchronising...", fromFileChunkDtosDirty.size());
fileChunkIndex = -1;
long bytesCopied = 0;
final long copyChunksBeginTimestamp = System.currentTimeMillis();
for (final FileChunkDto fileChunkDto : fromFileChunkDtosDirty) {
++fileChunkIndex;
if (logger.isTraceEnabled()) {
logger.trace("Reading data for dirty FileChunkDto (index {} of {}). path='{}' offset={}",
fileChunkIndex, fromFileChunkDtosDirty.size(), path, fileChunkDto.getOffset());
}
final byte[] fileData = getFileData(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, path, fileChunkDto);
if (fileData == null) {
logger.warn("Source file was modified or deleted during sync: {}", path);
// The file is left in state 'inProgress'. Thus it should definitely not be synced back in the opposite
// direction. The file should be synced again in the correct direction in the next run (after the source
// repo did a local sync, too).
return;
}
if (logger.isTraceEnabled()) {
logger.trace("Writing data for dirty FileChunkDto ({} of {}). path='{}' offset={}",
fileChunkIndex + 1, fromFileChunkDtosDirty.size(), path, fileChunkDto.getOffset());
}
try {
putFileData(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, path, fileChunkDto, fileData);
} catch (final CollisionException x) { // Never happens in CloudStore, but in down-stream-projects. Important: They must handle this properly themselves!
logger.info("CollisionException during putFileData: {}", path);
if (logger.isDebugEnabled())
logger.debug(x.toString(), x);
return;
}
bytesCopied += fileData.length;
subMonitor.worked(1);
}
subMonitor.done();
logger.info("Copied {} dirty file-chunks with together {} bytes in {} ms. path='{}'",
fromFileChunkDtosDirty.size(), bytesCopied, System.currentTimeMillis() - copyChunksBeginTimestamp, path);
endPutFile(fromRepoTransport, toRepoTransport, repoFileDtoTreeNode, path, fromNormalFileDto);
localRepoTransport.markFileInProgress(fromRepoTransport.getRepositoryId(), toRepoTransport.getRepositoryId(), path, false);
monitor.worked(6);
} finally {
monitor.done();
}
}
protected byte[] getFileData(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final RepoFileDtoTreeNode repoFileDtoTreeNode,
final String path, final FileChunkDto fileChunkDto) {
final byte[] fileData = fromRepoTransport.getFileData(path, fileChunkDto.getOffset(), fileChunkDto.getLength());
if (fileData == null)
return null; // file was deleted
if (fileData.length != fileChunkDto.getLength() || !sha1(fileData).equals(fileChunkDto.getSha1()))
return null; // file was modified
return fileData;
}
protected void putFileData(final RepoTransport fromRepoTransport, final RepoTransport toRepoTransport,
final RepoFileDtoTreeNode repoFileDtoTreeNode,
final String path, final FileChunkDto fileChunkDto,
final byte[] fileData) {
toRepoTransport.putFileData(path, fileChunkDto.getOffset(), fileData);
}
protected void beginPutFile(final RepoTransport fromRepoTransport,
final RepoTransport toRepoTransport, final RepoFileDtoTreeNode repoFileDtoTreeNode,
final String path, final NormalFileDto fromNormalFileDto) throws CollisionException {
toRepoTransport.beginPutFile(path);
}
protected void endPutFile(final RepoTransport fromRepoTransport,
final RepoTransport toRepoTransport, final RepoFileDtoTreeNode repoFileDtoTreeNode,
final String path, final NormalFileDto fromNormalFileDto) {
toRepoTransport.endPutFile(
path, fromNormalFileDto.getLastModified(),
fromNormalFileDto.getLength(), fromNormalFileDto.getSha1());
}
private boolean areFilesExistingAndEqual(final RepoFileDto fromRepoFileDto, final RepoFileDto toRepoFileDto) {
if (!(fromRepoFileDto instanceof NormalFileDto))
return false;
if (!(toRepoFileDto instanceof NormalFileDto))
return false;
final NormalFileDto fromNormalFileDto = (NormalFileDto) fromRepoFileDto;
final NormalFileDto toNormalFileDto = (NormalFileDto) toRepoFileDto;
return equal(fromNormalFileDto.getLength(), toNormalFileDto.getLength())
&& equal(fromNormalFileDto.getLastModified(), toNormalFileDto.getLastModified())
&& equal(fromNormalFileDto.getSha1(), toNormalFileDto.getSha1());
}
@Override
public void close() {
localRepoManager.close();
localRepoTransport.close();
remoteRepoTransport.close();
}
}