Skip to content

Commit

Permalink
Merge pull request #16787 from iterate-ch/feature/profiles-registry
Browse files Browse the repository at this point in the history
Extract reusable service.
  • Loading branch information
dkocher authored Jan 16, 2025
2 parents 4b4571c + d3c526c commit 5d3312d
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,19 @@
public class ChecksumProfileMatcher implements ProfileMatcher {
private static final Logger log = LogManager.getLogger(ChecksumProfileMatcher.class.getName());

/**
* Remote list of profiles
*/
private final Set<ProfileDescription> repository;

/**
* @param repository Profiles from remote repository
*/
public ChecksumProfileMatcher(final Set<ProfileDescription> repository) {
this.repository = repository;
}

/**
* Filter locally installed profiles by matching checksum
*
* @param next Description of profile installed in application support directory
* @param repository Remote list of profiles
* @param installed Description of profile installed in application support directory
* @return Non-null if matching profile is found in remote list and not latest version
*/
@Override
public Optional<ProfileDescription> compare(final ProfileDescription next) {
public Optional<ProfileDescription> compare(final Set<ProfileDescription> repository, final ProfileDescription installed) {
// Filter out profiles with matching checksum
final Optional<ProfileDescription> found = repository.stream()
.filter(description -> Objects.equals(description.getChecksum(), next.getChecksum()))
.findFirst();
.filter(description -> Objects.equals(description.getChecksum(), installed.getChecksum()))
.findFirst();
if(found.isPresent()) {
// Found matching checksum. Determine if latest version
if(found.get().isLatest()) {
Expand All @@ -63,7 +52,7 @@ public Optional<ProfileDescription> compare(final ProfileDescription next) {
return found;
}
}
log.warn("Local only profile {}", next);
log.warn("Local only profile {}", installed);
return Optional.empty();
}
}
27 changes: 5 additions & 22 deletions core/src/main/java/ch/cyberduck/core/profiles/ProfileMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,14 @@
* GNU General Public License for more details.
*/

import ch.cyberduck.core.Protocol;

import org.apache.commons.lang3.StringUtils;

import java.util.Optional;
import java.util.function.Predicate;
import java.util.Set;

public interface ProfileMatcher {
/**
* @param next Description of profile installed
* @return Non null if profile from server has been updated. Matching profile with later version in repository.
* @param repository Description of available profiles in repository
* @param installed Description of profile installed
* @return Non-null if profile from server has been updated. Matching profile with later version from remote repository.
*/
Optional<ProfileDescription> compare(ProfileDescription next);

class IdentifierProtocolPredicate implements Predicate<Protocol> {
private final Protocol installed;

public IdentifierProtocolPredicate(final Protocol installed) {
this.installed = installed;
}

@Override
public boolean test(final Protocol protocol) {
return StringUtils.equals(installed.getIdentifier(), protocol.getIdentifier())
&& StringUtils.equals(installed.getProvider(), protocol.getProvider());
}
}
Optional<ProfileDescription> compare(Set<ProfileDescription> repository, ProfileDescription installed);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,15 @@
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.preferences.SupportDirectoryFinderFactory;
import ch.cyberduck.core.transfer.download.CompareFilter;
import ch.cyberduck.core.transfer.symlink.DisabledDownloadSymlinkResolver;
import ch.cyberduck.core.worker.Worker;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
* Merge local set with latest versions from server
*/
public class ProfilesSynchronizeWorker extends Worker<Set<ProfileDescription>> {
private static final Logger log = LogManager.getLogger(ProfilesSynchronizeWorker.class.getName());

private final ProtocolFactory registry;
private final Local directory;
Expand Down Expand Up @@ -75,57 +67,8 @@ public Set<ProfileDescription> initialize() {

@Override
public Set<ProfileDescription> run(final Session<?> session) throws BackgroundException {
final Set<ProfileDescription> returned = new HashSet<>();
// Find all locally installed profiles
final LocalProfilesFinder localProfilesFinder = new LocalProfilesFinder(registry, directory, ProtocolFactory.BUNDLED_PROFILE_PREDICATE);
final Set<ProfileDescription> installed = localProfilesFinder.find();
// Find all profiles from repository
final RemoteProfilesFinder remoteProfilesFinder = new RemoteProfilesFinder(registry, session, this.filter(session));
final Set<ProfileDescription> remote = remoteProfilesFinder.find();
final ProfileMatcher matcher = new ChecksumProfileMatcher(remote);
// Iterate over every installed profile and find match in repository
installed.forEach(local -> {
// Check for matching remote checksum and download profile if this version is not equal to latest
final Optional<ProfileDescription> match = matcher.compare(local);
if(match.isPresent()) {
// Found matching checksum for profile in remote list which is not marked as latest version
log.warn("Override {} with latest profile verison {}", local, match);
// Remove previous version
local.getProfile().ifPresent(registry::unregister);
// Register updated profile by copying temporary file to application support
match.get().getFile().ifPresent(value -> {
final Local copy = registry.register(value);
if(null != copy) {
final LocalProfileDescription d = new LocalProfileDescription(registry, copy);
log.debug("Add synched profile {}", d);
returned.add(d);
visitor.visit(d);
}
});
}
else {
log.debug("Add local only profile {}", local);
returned.add(local);
visitor.visit(local);
}
});
// Iterate over all fetched profiles and when not installed
remote.forEach(description -> {
if(description.isLatest()) {
// Check if not already added previously when syncing with local list
if(!returned.contains(description)) {
log.debug("Add remote profile {}", description);
returned.add(description);
visitor.visit(description);
}
}
});
localProfilesFinder.cleanup();
remoteProfilesFinder.cleanup();
return returned;
}

protected CompareFilter filter(final Session<?> session) {
return new CompareFilter(new DisabledDownloadSymlinkResolver(), session);
return new ProtocolFactoryProfilesSynchronizer(session).sync(
// Match profiles by ETag and MD5 checksum of profile on disk
new ChecksumProfileMatcher(), visitor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ch.cyberduck.core.profiles;

/*
* Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
* https://cyberduck.io/
*
* 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.
*/

import ch.cyberduck.core.exception.BackgroundException;

import java.util.Set;

public interface ProfilesSynchronizer {
Set<ProfileDescription> sync(ProfileMatcher matcher, ProfilesFinder.Visitor visitor) throws BackgroundException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package ch.cyberduck.core.profiles;

/*
* Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
* https://cyberduck.io/
*
* 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.
*/

import ch.cyberduck.core.Local;
import ch.cyberduck.core.LocalFactory;
import ch.cyberduck.core.ProtocolFactory;
import ch.cyberduck.core.Session;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.preferences.SupportDirectoryFinderFactory;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

public class ProtocolFactoryProfilesSynchronizer implements ProfilesSynchronizer {
private static final Logger log = LogManager.getLogger(ProtocolFactoryProfilesSynchronizer.class.getName());

private final ProtocolFactory registry;
private final LocalProfilesFinder local;
private final RemoteProfilesFinder remote;

public ProtocolFactoryProfilesSynchronizer(final Session<?> session) {
this(ProtocolFactory.get(), session, LocalFactory.get(SupportDirectoryFinderFactory.get().find(),
PreferencesFactory.get().getProperty("profiles.folder.name")));
}

public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final Session<?> session, final Local directory) {
this(ProtocolFactory.get(),
// Find all locally installed profiles
new LocalProfilesFinder(registry, directory, ProtocolFactory.BUNDLED_PROFILE_PREDICATE), session);
}

public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final LocalProfilesFinder local, final Session<?> session) {
this(registry, local,
// Find all profiles from repository
new RemoteProfilesFinder(registry, session));
}

public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final RemoteProfilesFinder remote) {
this(registry, LocalFactory.get(SupportDirectoryFinderFactory.get().find(),
PreferencesFactory.get().getProperty("profiles.folder.name")), remote);
}

public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final Local directory, final RemoteProfilesFinder remote) {
this(registry,
// Find all locally installed profiles
new LocalProfilesFinder(registry, directory, ProtocolFactory.BUNDLED_PROFILE_PREDICATE), remote);
}

public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final LocalProfilesFinder local, final RemoteProfilesFinder remote) {
this.registry = registry;
this.local = local;
this.remote = remote;
}

@Override
public Set<ProfileDescription> sync(final ProfileMatcher matcher, final ProfilesFinder.Visitor visitor) throws BackgroundException {
final Set<ProfileDescription> result = new HashSet<>();
final Set<ProfileDescription> installed = local.find();
final Set<ProfileDescription> available = remote.find();
// Iterate over every installed profile and find match in repository
installed.forEach(l -> {
// Check for matching remote checksum and download profile if this version is not equal to latest
final Optional<ProfileDescription> match = matcher.compare(available, l);
if(match.isPresent()) {
// Found matching checksum for profile in remote list which is not marked as latest version
log.warn("Override {} with latest profile verison {}", l, match);
// Remove previous version
l.getProfile().ifPresent(registry::unregister);
// Register updated profile by copying temporary file to application support
match.get().getFile().ifPresent(value -> {
final Local copy = registry.register(value);
if(null != copy) {
final LocalProfileDescription d = new LocalProfileDescription(registry, copy);
log.debug("Add synched profile {}", d);
result.add(d);
visitor.visit(d);
}
});
}
else {
log.debug("Add local only profile {}", l);
result.add(l);
visitor.visit(l);
}
});
// Iterate over all fetched profiles and when not installed
available.forEach(description -> {
if(description.isLatest()) {
// Check if not already added previously when syncing with local list
if(!result.contains(description)) {
log.debug("Add remote profile {}", description);
result.add(description);
visitor.visit(description);
}
}
});
local.cleanup();
remote.cleanup();
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,32 @@ public class RemoteProfilesFinder implements ProfilesFinder {
private final ProtocolFactory protocols;
private final Session<?> session;
private final TransferPathFilter comparison;
private final Filter<Path> filter;

public RemoteProfilesFinder(final Session<?> session) {
this(ProtocolFactory.get(), session);
}

public RemoteProfilesFinder(final ProtocolFactory protocols, final Session<?> session) {
this(protocols, session, new CompareFilter(new DisabledDownloadSymlinkResolver(), session));
this(protocols, session, new CompareFilter(new DisabledDownloadSymlinkResolver(), session), new ProfileFilter());
}

public RemoteProfilesFinder(final ProtocolFactory protocols, final Session<?> session, final TransferPathFilter comparison) {
public RemoteProfilesFinder(final Session<?> session,
final TransferPathFilter comparison, final Filter<Path> filter) {
this(ProtocolFactory.get(), session, comparison, filter);
}

public RemoteProfilesFinder(final ProtocolFactory protocols, final Session<?> session,
final TransferPathFilter comparison, final Filter<Path> filter) {
this.protocols = protocols;
this.session = session;
this.comparison = comparison;
this.filter = filter;
}

@Override
public Set<ProfileDescription> find(final Visitor visitor) throws BackgroundException {
log.info("Fetch profiles from {}", session.getHost());
final ProfileFilter filter = new ProfileFilter();
final AttributedList<Path> list = session.getFeature(ListService.class).list(new DelegatingHomeFeature(
new DefaultPathHomeFeature(session.getHost())).find(), new DisabledListProgressListener());
return list.filter(filter).toStream().map(file -> visitor.visit(new RemoteProfileDescription(protocols, file,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public void testLocalOnly() throws Exception {
// Local only profile
final ProfileDescription local = new ProfileDescription(
ProtocolFactory.get(), new Checksum(HashAlgorithm.md5, "d41d8cd98f00b204e9800998ecf8427e"), null);
assertFalse(new ChecksumProfileMatcher(Stream.<ProfileDescription>empty().collect(Collectors.toSet())).compare(local).isPresent());
assertFalse(new ChecksumProfileMatcher().compare(
Stream.<ProfileDescription>empty().collect(Collectors.toSet()), local).isPresent());
}

@Test
Expand All @@ -52,7 +53,8 @@ public boolean isLatest() {
};
final ProfileDescription local = new ProfileDescription(
ProtocolFactory.get(), new Checksum(HashAlgorithm.md5, "d41d8cd98f00b204e9800998ecf8427e"), null);
assertFalse(new ChecksumProfileMatcher(Stream.of(remote).collect(Collectors.toSet())).compare(local).isPresent());
assertFalse(new ChecksumProfileMatcher().compare(
Stream.of(remote).collect(Collectors.toSet()), local).isPresent());
}

@Test
Expand All @@ -70,6 +72,7 @@ public Optional<Local> getFile() {
return Optional.of(new NullLocal("Profile.cyberduckprofile"));
}
};
assertTrue(new ChecksumProfileMatcher(Stream.of(remote).collect(Collectors.toSet())).compare(local).isPresent());
assertTrue(new ChecksumProfileMatcher().compare(
Stream.of(remote).collect(Collectors.toSet()), local).isPresent());
}
}

0 comments on commit 5d3312d

Please sign in to comment.