@Override public void run() { try { // check whether the own client is also in the list (should be usually, but you never know...) this.clientCounter = this.receivers.size(); for (NodeLocation location : this.receivers) { if (location.getPeerAddress().equals(this.node.getPeerAddress())) { this.clientCounter--; break; } } this.chunkCountDownLatch = new CountDownLatch(this.clientCounter); this.initReceiverLatch.countDown(); // check whether we got access to the file this.fileId = null; this.owner = null; try { PathObject pathObject = this.objectStore.getObjectManager().getObjectForPath(this.relativeFilePath); // if we are not the owner but have access to the file if (null != pathObject.getOwner() && !this.node.getUser().getUserName().equals(pathObject.getOwner()) && AccessType.WRITE.equals(pathObject.getAccessType())) { try { this.fileId = this.node.getIdentifierManager().getValue(this.relativeFilePath); this.owner = pathObject.getOwner(); } catch (InputOutputException e) { logger.error( "Failed to get file id for " + this.relativeFilePath + ". Message: " + e.getMessage()); } } // add file id also if the path is shared if (pathObject.isShared()) { for (Sharer entry : pathObject.getSharers()) { try { // ask sharer's clients to get the changes too List<NodeLocation> sharerLocations = this.nodeManager.getNodeLocations(entry.getUsername()); // only add one client of the sharer. He may propagate the change then // to his clients, and if a conflict occurs, there will be a new file if (!sharerLocations.isEmpty()) { fileId = super.node.getIdentifierManager().getValue(pathObject.getAbsolutePath()); // Note that we do not add the sharer location again since these // are assembled in FileOfferExchangeHandlerResult } } catch (InputOutputException e) { logger.error( "Could not get client locations of sharer " + entry.getUsername() + ". This sharer's clients do not get the file (change)"); } } } } catch (InputOutputException e) { logger.error( "Failed to read path object for " + this.relativeFilePath + ". Message: " + e.getMessage()); } // the owner of a file is only added on a share request for (NodeLocation location : this.receivers) { UUID uuid = UUID.randomUUID(); logger.info( "Sending first chunk as subRequest of " + this.exchangeId + " with id " + uuid + " to client " + location.getPeerAddress().inetAddress().getHostName() + ":" + location.getPeerAddress().tcpPort()); // add callback handler for sub request super.node.getObjectDataReplyHandler().addResponseCallbackHandler(uuid, this); this.sendChunk( 0, // first chunk this.fileId, this.owner, uuid, location); } } catch (Exception e) { logger.error("Failed to execute FilePushExchangeHandler. Message: " + e.getMessage(), e); } }
@BeforeClass public static void setUpChild() throws IOException, InputOutputException { // create some test files and dirs to move // -- testDir1 // mv to targetDir // | | // | |--- myFile.txt // | // - testDir2 // | | // | |--- myFile2.txt // mv to targetDir // | |--- myFile3.txt // mv to targetDir after creating conflict manually // | // - targetDir // | // - dirToDelete // | | // | |--- fileToDeleteInDir.txt // | // |--- fileToDelete.txt Files.createDirectory(ROOT_TEST_DIR1.resolve(TEST_DIR_1)); Files.createDirectory(ROOT_TEST_DIR2.resolve(TEST_DIR_1)); Files.createDirectory(ROOT_TEST_DIR1.resolve(TEST_DIR_3)); Files.createDirectory(ROOT_TEST_DIR2.resolve(TEST_DIR_3)); Files.createDirectory(ROOT_TEST_DIR1.resolve(TEST_DIR_2)); Files.createDirectory(ROOT_TEST_DIR2.resolve(TEST_DIR_2)); Files.createFile(ROOT_TEST_DIR1.resolve(TEST_FILE_1)); Files.createFile(ROOT_TEST_DIR2.resolve(TEST_FILE_1)); Files.createFile(ROOT_TEST_DIR1.resolve(TEST_FILE_2)); Files.createFile(ROOT_TEST_DIR2.resolve(TEST_FILE_2)); Files.createFile(ROOT_TEST_DIR1.resolve(TEST_FILE_3)); Files.createFile(ROOT_TEST_DIR2.resolve(TEST_FILE_3)); Files.createFile(ROOT_TEST_DIR1.resolve(TEST_FILE_4)); Files.createFile(ROOT_TEST_DIR2.resolve(TEST_FILE_4)); Files.createFile(ROOT_TEST_DIR1.resolve(TEST_FILE_5)); Files.createFile(ROOT_TEST_DIR2.resolve(TEST_FILE_5)); // create directory to where the files should be moved Files.createDirectory(ROOT_TEST_DIR1.resolve(TARGET_DIR)); Files.createDirectory(ROOT_TEST_DIR2.resolve(TARGET_DIR)); // force recreation of object store OBJECT_STORE_1.sync(); OBJECT_STORE_2.sync(); // do not start the event aggregator but manually sync the event PathObject pathObject = OBJECT_STORE_1.getObjectManager().getObjectForPath(TEST_DIR_1.toString()); moveDirEvent = new MoveEvent( TEST_DIR_1, TARGET_DIR, TARGET_DIR.getFileName().toString(), pathObject .getVersions() .get(Math.max(pathObject.getVersions().size() - 1, 0)) .getHash(), System.currentTimeMillis()); PathObject fileObject = OBJECT_STORE_1.getObjectManager().getObjectForPath(TEST_FILE_2.toString()); moveFileEvent = new MoveEvent( TEST_FILE_2, TARGET_DIR.resolve(TEST_FILE_2.getFileName().toString()), TEST_FILE_2.getFileName().toString(), fileObject .getVersions() .get(Math.max(fileObject.getVersions().size() - 1, 0)) .getHash(), System.currentTimeMillis()); moveConflictFileEvent = new MoveEvent( TEST_FILE_3, TARGET_DIR.resolve(TEST_FILE_3.getFileName().toString()), TEST_FILE_3.getFileName().toString(), fileObject .getVersions() .get(Math.max(fileObject.getVersions().size() - 1, 0)) .getHash(), System.currentTimeMillis()); }
@Override public void run() { try { UUID exchangeId = UUID.randomUUID(); logger.info("Starting non blocking background syncer (Exchange: " + exchangeId + ")"); this.eventAggregator.stop(); FetchObjectStoreExchangeHandler fetchObjectStoreExchangeHandler = new FetchObjectStoreExchangeHandler(this.node, this.nodeManager, exchangeId); int nrOfRunningResponseCallbackExchanges = this.node.getObjectDataReplyHandler().getResponseCallbackHandlers().size(); if (0 < nrOfRunningResponseCallbackExchanges || this.node.getObjectDataReplyHandler().areRequestCallbacksRunning()) { // other callbacks are currently in progress // -> postpone this run to next interval logger.info( "Skipping execution of background synchronisation since other exchanges are currently running"); return; } this.node .getObjectDataReplyHandler() .addResponseCallbackHandler(exchangeId, fetchObjectStoreExchangeHandler); Thread fetchObjectStoreExchangeHandlerThread = new Thread(fetchObjectStoreExchangeHandler); fetchObjectStoreExchangeHandlerThread.setName( "FetchObjectStoreExchangeHandler-" + exchangeId); fetchObjectStoreExchangeHandlerThread.start(); logger.info("Waiting for fetching of object stores to complete"); try { fetchObjectStoreExchangeHandler.await(); } catch (InterruptedException e) { logger.error("Got interrupted while waiting for fetching all object stores"); } this.node.getObjectDataReplyHandler().removeResponseCallbackHandler(exchangeId); if (!fetchObjectStoreExchangeHandler.isCompleted()) { logger.error( "FetchObjectStoreExchangeHandler should be completed after awaiting. Since we do not know about the other clients object store, we abort background sync for exchange " + exchangeId); return; } FetchObjectStoreExchangeHandlerResult result = fetchObjectStoreExchangeHandler.getResult(); Map<ClientDevice, IObjectStore> objectStores = Zip.unzipObjectStore(this.objectStore, result); // use a tree map for guaranteed ordering Map<String, ClientDevice> deletedPaths = new TreeMap<>(new StringLengthComparator()); Map<String, ClientDevice> updatedPaths = new TreeMap<>(new StringLengthComparator()); Map<String, ClientDevice> conflictPaths = new TreeMap<>(new StringLengthComparator()); for (Map.Entry<ClientDevice, IObjectStore> entry : objectStores.entrySet()) { HashMap<ObjectStore.MergedObjectType, Set<String>> outdatedOrDeletedPaths = this.objectStore.mergeObjectStore(entry.getValue()); outdatedOrDeletedPaths .get(ObjectStore.MergedObjectType.CHANGED) .stream() .filter(outDatedPath -> !this.isIgnored(outDatedPath)) .forEach(outDatedPath -> updatedPaths.put(outDatedPath, entry.getKey())); outdatedOrDeletedPaths .get(ObjectStore.MergedObjectType.DELETED) .stream() .filter(deletedPath -> !this.isIgnored(deletedPath)) .forEach(deletedPath -> deletedPaths.put(deletedPath, entry.getKey())); outdatedOrDeletedPaths .get(ObjectStore.MergedObjectType.CONFLICT) .stream() .filter(conflictPath -> !this.isIgnored(conflictPath)) .forEach(conflictPath -> conflictPaths.put(conflictPath, entry.getKey())); entry.getValue().getObjectManager().getStorageAdapater().delete(new TreePathElement("./")); this.objectStore .getObjectManager() .getStorageAdapater() .delete(new TreePathElement(entry.getKey().getClientDeviceId().toString())); } // delete all removed files logger.info("Removing all (" + deletedPaths.size() + ") deleted files"); for (Map.Entry<String, ClientDevice> entry : deletedPaths.entrySet()) { logger.debug("Removing deleted path " + entry.getKey()); TreePathElement elementToDelete = new TreePathElement(entry.getKey()); // only delete the file on disk if it actually exists if (this.storageAdapter.exists(StorageType.DIRECTORY, elementToDelete) || this.storageAdapter.exists(StorageType.FILE, elementToDelete)) { this.storageAdapter.delete(elementToDelete); } } logger.info("Creating all (" + conflictPaths.size() + ") conflict files"); for (Map.Entry<String, ClientDevice> entry : conflictPaths.entrySet()) { logger.debug("Creating conflict file " + entry.getKey()); Path conflictFilePath = ConflictHandler.createConflictFile( this.globalEventBus, this.node.getClientDeviceId().toString(), this.objectStore, this.storageAdapter, new TreePathElement(entry.getKey())); // we have to emit an ignore event here for the file syncer // otherwise the final difference calculation will get an updated // path (i.e. actually an create event) and will try to sync it to the other clients if (null != conflictFilePath) { this.globalEventBus.publish( new IgnoreBusEvent( new ModifyEvent( conflictFilePath, conflictFilePath.getFileName().toString(), "weIgnoreTheHash", System.currentTimeMillis()))); } // now add this to the updated paths, so that we can fetch the original again updatedPaths.put(entry.getKey(), entry.getValue()); } // fetch all missing files logger.info("Fetching all (" + updatedPaths.size() + ") missing files"); for (Map.Entry<String, ClientDevice> entry : updatedPaths.entrySet()) { UUID subExchangeId = UUID.randomUUID(); logger.debug( "Starting to fetch file " + entry.getKey() + " with subExchangeId " + subExchangeId + " (non blocking background sync " + exchangeId + ")"); // before updating, check the actual content hash on disk // to prevent data loss during sync PathObject mergedPathObject = this.objectStore.getObjectManager().getObjectForPath(entry.getKey()); Version lastVersion = mergedPathObject .getVersions() .get(Math.max(0, mergedPathObject.getVersions().size() - 1)); PathType pathType = mergedPathObject.getPathType(); StorageType storageType = pathType.equals(PathType.DIRECTORY) ? StorageType.DIRECTORY : StorageType.FILE; // only check version, if the file does exist on our disk, // if not, we have to fetch it anyway TreePathElement mergedTreeElement = new TreePathElement(mergedPathObject.getAbsolutePath()); if (this.storageAdapter.exists(storageType, mergedTreeElement)) { this.objectStore.syncFile(mergedTreeElement); PathObject modifiedPathObject = this.objectStore.getObjectManager().getObjectForPath(entry.getKey()); Version modifiedLastVersion = modifiedPathObject .getVersions() .get(Math.max(0, modifiedPathObject.getVersions().size() - 1)); if (!modifiedLastVersion.equals(lastVersion)) { // we just changed the file on this client while syncing... // therefore we use this state and do not request an outdated state from another client logger.info( "Detected file change while merging object store (from other client or end user)... Using our state"); continue; } } // add owner, access type and sharers for object store to prevent overwriting when a file // is fetched which does not exist yet this.globalEventBus.publish( new AddOwnerAndAccessTypeToObjectStoreBusEvent( mergedPathObject.getOwner(), mergedPathObject.getAccessType(), entry.getKey())); this.globalEventBus.publish( new AddSharerToObjectStoreBusEvent(entry.getKey(), mergedPathObject.getSharers())); FileDemandExchangeHandler fileDemandExchangeHandler = new FileDemandExchangeHandler( this.storageAdapter, this.node, this.nodeManager, this.globalEventBus, new NodeLocation( entry.getValue().getUserName(), entry.getValue().getClientDeviceId(), entry.getValue().getPeerAddress()), entry.getKey(), subExchangeId); this.node .getObjectDataReplyHandler() .addResponseCallbackHandler(subExchangeId, fileDemandExchangeHandler); Thread fileDemandExchangeHandlerThread = new Thread(fileDemandExchangeHandler); fileDemandExchangeHandlerThread.setName("FileDemandExchangeHandlerThread-" + subExchangeId); fileDemandExchangeHandlerThread.start(); try { fileDemandExchangeHandler.await(); } catch (InterruptedException e) { logger.error( "Got interrupted while waiting for fileDemandExchangeHandler " + subExchangeId + " to complete. Message: " + e.getMessage()); } this.node.getObjectDataReplyHandler().removeResponseCallbackHandler(subExchangeId); if (!fileDemandExchangeHandler.isCompleted()) { logger.error( "FileDemandExchangeHandler " + subExchangeId + " should be completed after wait."); } } // start event aggregator logger.info( "Starting event aggregator on client (" + this.node.getPeerAddress().inetAddress().getHostName() + ":" + this.node.getPeerAddress().tcpPort() + "): Non-blocking background sync " + exchangeId); this.eventAggregator.start(); logger.info( "Reconciling local disk changes with merged object store (non-blocking background sync " + exchangeId + ")"); // create a temporary second object store to get changes made in the mean time of syncing ITreeStorageAdapter objectStoreStorageManager = this.objectStore.getObjectManager().getStorageAdapater(); TreePathElement pathElement = new TreePathElement("nonBlockingBackgroundSyncObjectStore"); if (objectStoreStorageManager.exists(StorageType.DIRECTORY, pathElement)) { objectStoreStorageManager.delete(pathElement); } objectStoreStorageManager.persist(StorageType.DIRECTORY, pathElement, null); // create the temporary object store in the .sync folder ITreeStorageAdapter changeObjectStoreStorageManager = new LocalStorageAdapter( Paths.get(objectStoreStorageManager.getRootDir().getPath()) .resolve(pathElement.getPath())); IObjectStore changeObjectStore = new ObjectStore( this.storageAdapter, "index.json", "object", changeObjectStoreStorageManager); // build object store for differences in the mean time List<String> ignoredPaths = new ArrayList<>(); Path origSyncFolder = Paths.get(this.objectStore.getObjectManager().getStorageAdapater().getRootDir().getPath()) .getFileName(); ignoredPaths.add(origSyncFolder.toString()); changeObjectStore.sync(ignoredPaths); // get differences between disk and merged object store HashMap<ObjectStore.MergedObjectType, Set<String>> updatedOrDeletedPaths = this.objectStore.mergeObjectStore(changeObjectStore); // remove change object store again changeObjectStoreStorageManager.delete(new TreePathElement("./")); Set<String> deletedPathsInTheMeanTime = updatedOrDeletedPaths.get(ObjectStore.MergedObjectType.DELETED); Set<String> updatedPathsInTheMeanTime = updatedOrDeletedPaths.get(ObjectStore.MergedObjectType.CHANGED); logger.info( "Found " + deletedPathsInTheMeanTime.size() + " paths which have been deleted in the mean time of syncing"); for (String deletedPath : deletedPathsInTheMeanTime) { if (this.isIgnored(deletedPath)) { logger.info("Ignore deletion of " + deletedPath + " since it matches an ignore pattern"); } // publish a delete event to the SyncFileChangeListener logger.trace("Creating delete event for " + deletedPath); this.globalEventBus.publish( new CreateBusEvent( new DeleteEvent( Paths.get(deletedPath), Paths.get(deletedPath).getFileName().toString(), null, System.currentTimeMillis()))); } logger.info( "Found " + updatedPathsInTheMeanTime.size() + " paths which have changed in the mean time of syncing"); for (String updatedPath : updatedPathsInTheMeanTime) { if (this.isIgnored(updatedPath)) { logger.info("Ignore updating of " + updatedPath + " since it matches an ignore pattern"); } // publish modify events to SyncFileChangeListener logger.trace("Creating modify event for " + updatedPath); PathObject updatedPathObject = this.objectStore.getObjectManager().getObjectForPath(updatedPath); this.globalEventBus.publish( new CreateBusEvent( new ModifyEvent( Paths.get(updatedPath), Paths.get(updatedPath).getFileName().toString(), updatedPathObject .getVersions() .get(Math.max(0, updatedPathObject.getVersions().size() - 1)) .getHash(), System.currentTimeMillis()))); } logger.info("Completed non-blocking background sync " + exchangeId); } catch (Exception e) { logger.error("Got exception in NonBlockingBackgroundSyncer. Message: " + e.getMessage(), e); } }