From b295e5e916ae1f95cb73ede1a0394ea988e5223f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 14:14:48 +0200 Subject: [PATCH 01/33] Rename parameter. --- .../ch/cyberduck/core/profiles/ProfileDescription.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java b/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java index c46dfc91a5..8062b946c8 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java @@ -53,12 +53,12 @@ public class ProfileDescription { /** * @param protocols Registered protocols - * @param parent Filter to apply for parent protocol reference in registered protocols + * @param filter Filter to apply for parent protocol reference in registered protocols * @param checksum Checksum of connection profile * @param local File on disk */ - public ProfileDescription(final ProtocolFactory protocols, final Predicate parent, final Checksum checksum, final Local local) { - this(protocols, parent, new LazyInitializer() { + public ProfileDescription(final ProtocolFactory protocols, final Predicate filter, final Checksum checksum, final Local local) { + this(protocols, filter, new LazyInitializer() { @Override protected Checksum initialize() { return checksum; @@ -71,7 +71,7 @@ public class ProfileDescription { }); } - public ProfileDescription(final ProtocolFactory protocols, final Predicate parent, + public ProfileDescription(final ProtocolFactory protocols, final Predicate filter, final LazyInitializer checksum, final LazyInitializer local) { this.checksum = checksum; this.local = local; @@ -79,7 +79,7 @@ public class ProfileDescription { @Override protected Profile initialize() throws ConcurrentException { try { - return new ProfilePlistReader(protocols, parent).read(local.get()); + return new ProfilePlistReader(protocols, filter).read(local.get()); } catch(AccessDeniedException e) { log.warn("Failure {} reading profile {}", e, local.get()); From db2071563c7e3b50d306746145f3af302f444b5d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 14:15:00 +0200 Subject: [PATCH 02/33] Move initializer to field declaration. --- .../java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java index 33493e3124..f379b82040 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java @@ -57,7 +57,7 @@ public class RemoteProfilesFinder implements ProfilesFinder { private final Session session; private final TransferPathFilter comparison; private final Filter filter; - private final Local temporary; + private final Local temporary = LocalFactory.get(new TemporaryApplicationResourcesFinder().find(), "profiles"); public RemoteProfilesFinder(final Session session) { this(ProtocolFactory.get(), session); @@ -78,7 +78,6 @@ public class RemoteProfilesFinder implements ProfilesFinder { this.session = session; this.comparison = comparison; this.filter = filter; - this.temporary = LocalFactory.get(new TemporaryApplicationResourcesFinder().find(), "profiles"); } @Override From 7a6928c68cb0d78e41e1804752bd0b1cc2e0bc76 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 14:22:49 +0200 Subject: [PATCH 03/33] Add implementation to fetch available profiles from index file. --- .../profiles/RemoteIndexProfilesFinder.java | 201 ++++++++++++++++++ .../RemoteIndexProfilesFinderTest.java | 68 ++++++ 2 files changed, 269 insertions(+) create mode 100644 core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java create mode 100644 s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java new file mode 100644 index 0000000000..a664175a9f --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -0,0 +1,201 @@ +package ch.cyberduck.core.profiles; + +/* + * Copyright (c) 2002-2026 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.ConnectionCallback; +import ch.cyberduck.core.DefaultIOExceptionMappingService; +import ch.cyberduck.core.DefaultPathAttributes; +import ch.cyberduck.core.Local; +import ch.cyberduck.core.LocalFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Read; +import ch.cyberduck.core.io.Checksum; +import ch.cyberduck.core.preferences.TemporaryApplicationResourcesFinder; +import ch.cyberduck.core.shared.DefaultPathHomeFeature; +import ch.cyberduck.core.shared.DelegatingHomeFeature; +import ch.cyberduck.core.transfer.TransferPathFilter; +import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.transfer.download.CompareFilter; +import ch.cyberduck.core.transfer.symlink.DisabledDownloadSymlinkResolver; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.concurrent.ConcurrentException; +import org.apache.commons.lang3.concurrent.LazyInitializer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class RemoteIndexProfilesFinder implements ProfilesFinder { + private static final Logger log = LogManager.getLogger(RemoteIndexProfilesFinder.class); + + private final ObjectMapper mapper = new ObjectMapper(); + private final ProtocolFactory protocols; + private final Session session; + private final TransferPathFilter comparison; + private final Local temporary = LocalFactory.get(new TemporaryApplicationResourcesFinder().find(), "profiles"); + + public RemoteIndexProfilesFinder(final Session session) { + this(ProtocolFactory.get(), session); + } + + public RemoteIndexProfilesFinder(final ProtocolFactory protocols, final Session session) { + this(protocols, session, new CompareFilter(new DisabledDownloadSymlinkResolver(), session)); + } + + public RemoteIndexProfilesFinder(final ProtocolFactory protocols, final Session session, final TransferPathFilter comparison) { + this.protocols = protocols; + this.session = session; + this.comparison = comparison; + } + + /** + * { + * "filename": "AWS PrivateLink for Amazon S3 (VPC endpoint).cyberduckprofile", + * "protocol": "s3", + * "vendor": "s3-privatelink", + * "versions": [ + * { + * "checksum": "255b117667ac1f0fd1ecfe25ae99440d", + * "modified": "2025-12-02T09:09:07Z", + * "version_id": "sMABDfcz3m0pwQeU85uI2.pW.JTGGT4T", + * "latest": true + * }, + * { + * "checksum": "9a6fcc93e68a669952da5e47719af3c9", + * "modified": "2022-09-15T12:55:09Z", + * "version_id": ".iXSL9g6EWEIthW6gENMGgSileSI0XG8", + * "latest": false + * }, + * { + * "checksum": "3fbf79c16d3187f135a5fbf32e0cbaf7", + * "modified": "2021-11-30T11:54:30Z", + * "version_id": "S71y2mc.dq8BXtF11g85Z9ZLvulhZDJk", + * "latest": false + * }, + * { + * "checksum": "3fbf79c16d3187f135a5fbf32e0cbaf7", + * "modified": "2021-10-27T06:00:55Z", + * "version_id": "E.cXq21hr0xLkqtNMUZjBuZaj2HfA.n9", + * "latest": false + * }, + * { + * "checksum": "7188dafb0c649fe123b70cbe5b7bfa40", + * "modified": "2021-10-27T06:00:49Z", + * "version_id": "nosIgW.rj3jrGn9jwdf_S.8fnKMO2ZW1", + * "latest": false + * } + * ] + * } + */ + @Override + public Set find(final Visitor visitor) throws BackgroundException { + log.info("Fetch profiles from {}", session.getHost()); + final Set profiles = new HashSet<>(); + final Path directory = new DelegatingHomeFeature(new DefaultPathHomeFeature(session.getHost())).find(); + try(final InputStream in = session.getFeature(Read.class).read(new Path(directory, "index.json", EnumSet.of(Path.Type.file)), + new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop)) { + final ProfileMetadataList list = mapper.readValue(in, ProfileMetadataList.class); + for(ProfileMetadata metadata : list.profiles) { + for(ProfileMetadataVersion version : metadata.versions) { + profiles.add(new ProfileDescription(protocols, protocol -> true, + new LazyInitializer() { + @Override + protected Checksum initialize() { + return Checksum.parse(version.checksum); + } + }, + new LazyInitializer() { + @Override + protected Local initialize() throws ConcurrentException { + try { + final Local local = LocalFactory.get(temporary, metadata.filename); + final Path file = new Path(directory, metadata.filename, EnumSet.of(Path.Type.file)); + if(comparison.accept(file, local, new TransferStatus().setExists(true), ProgressListener.noop)) { + final Read read = session.getFeature(Read.class); + log.info("Download profile {}", file); + // Read latest version + try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) + .setVersionId(version.version_id)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { + IOUtils.copy(in, out); + } + } + // Skip download if previously cached + return local; + } + catch(BackgroundException | IOException e) { + throw new ConcurrentException(e); + } + } + } + ) { + @Override + public boolean isLatest() { + return version.latest; + } + }); + } + } + } + catch(IOException e) { + throw new DefaultIOExceptionMappingService().map(e); + } + return profiles; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class ProfileMetadataList { + @JsonProperty("profiles") + private ProfileMetadata[] profiles; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class ProfileMetadata { + @JsonProperty("filename") + private String filename; + @JsonProperty("protocol") + private String protocol; + @JsonProperty("vendor") + private String vendor; + @JsonProperty("versions") + private ProfileMetadataVersion[] versions; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static class ProfileMetadataVersion { + @JsonProperty("checksum") + private String checksum; + @JsonProperty("modified") + private String modified; + @JsonProperty("version_id") + private String version_id; + @JsonProperty("latest") + private Boolean latest; + } +} diff --git a/s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java b/s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java new file mode 100644 index 0000000000..f8fc06d2f8 --- /dev/null +++ b/s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java @@ -0,0 +1,68 @@ +package ch.cyberduck.core.profiles; + +/* + * Copyright (c) 2002-2021 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.Credentials; +import ch.cyberduck.core.HostKeyCallback; +import ch.cyberduck.core.HostParser; +import ch.cyberduck.core.LoginCallback; +import ch.cyberduck.core.ProtocolFactory; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.io.Checksum; +import ch.cyberduck.core.preferences.PreferencesFactory; +import ch.cyberduck.core.proxy.DisabledProxyFinder; +import ch.cyberduck.core.s3.S3Protocol; +import ch.cyberduck.core.s3.S3Session; +import ch.cyberduck.core.ssl.DefaultX509KeyManager; +import ch.cyberduck.core.ssl.DisabledX509TrustManager; +import ch.cyberduck.core.threading.CancelCallback; +import ch.cyberduck.test.IntegrationTest; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@Category(IntegrationTest.class) +public class RemoteIndexProfilesFinderTest { + + @Test + public void testFind() throws Exception { + final ProtocolFactory protocols = new ProtocolFactory(Collections.singleton(new S3Protocol() { + @Override + public boolean isEnabled() { + return true; + } + })); + final Session session = new S3Session(new HostParser(protocols).get("s3:/profiles.cyberduck.io") + .setCredentials(new Credentials(PreferencesFactory.get().getProperty("connection.login.anon.name"))), new DisabledX509TrustManager(), new DefaultX509KeyManager()); + session.open(new DisabledProxyFinder(), HostKeyCallback.noop, LoginCallback.noop, CancelCallback.noop); + final RemoteIndexProfilesFinder finder = new RemoteIndexProfilesFinder(session); + final Set stream = finder.find(); + assertFalse(stream.isEmpty()); + // Check for versions of S3 (HTTP).cyberduckprofile + assertFalse(stream.stream().filter(ProfileDescription::isLatest).collect(Collectors.toSet()).isEmpty()); + assertFalse(stream.stream().filter(description -> !description.isLatest()).collect(Collectors.toSet()).isEmpty()); + assertTrue(stream.stream().anyMatch(description -> description.getChecksum().equals(Checksum.parse("b9afd8d6da91e7b520559fa9eaac54c1")))); + assertTrue(stream.stream().anyMatch(description -> description.getChecksum().equals(Checksum.parse("19ecbfe2d8f09644197c1ef53e207792")))); + session.close(); + } +} From 70a44294fced3e413acb18779bf4d1448952773a Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 14:24:21 +0200 Subject: [PATCH 04/33] Delete duplicated test. --- .../ProfilesSynchronizeWorkerTest.java | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java b/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java index 3cdb0c2233..cd9d547208 100644 --- a/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java +++ b/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java @@ -47,55 +47,7 @@ import static org.junit.Assert.*; public class ProfilesSynchronizeWorkerTest { @Test - public void testRunCloudfrontEndpoint() throws Exception { - // Registry in temporary folder - final ProtocolFactory protocols = new ProtocolFactory(new HashSet<>(Collections.singletonList(new S3Protocol()))); - final Host host = new HostParser(protocols, new S3Protocol()).get("s3://djynunjb246r8.cloudfront.net").setCredentials( - new Credentials(PreferencesFactory.get().getProperty("connection.login.anon.name"))); - final Session session = new S3Session(host, new DisabledX509TrustManager(), new DefaultX509KeyManager()); - session.open(new DisabledProxyFinder(), HostKeyCallback.noop, LoginCallback.noop, CancelCallback.noop); - // Local directory with oudated profile - final Local conflictprofile = LocalFactory.get(this.getClass().getResource("/test-conflict.cyberduckprofile").getPath()); - final Local localonlyprofile = LocalFactory.get(this.getClass().getResource("/test-localonly.cyberduckprofile").getPath()); - // Previous checksum b9afd8d6da91e7b520559fa9eaac54c1 found on server - final Local outdatedprofile = LocalFactory.get(this.getClass().getResource("/test-outdated.cyberduckprofile").getPath()); - assertNotNull(new ProfilePlistReader(protocols).read(outdatedprofile)); - assertTrue(outdatedprofile.exists()); - final LocalProfileDescription conflictProfileDescription = new LocalProfileDescription(protocols, conflictprofile); - assertTrue(conflictProfileDescription.getProfile().isPresent()); - final LocalProfileDescription localonlyProfileDescription = new LocalProfileDescription(protocols, localonlyprofile); - assertTrue(localonlyProfileDescription.getProfile().isPresent()); - final LocalProfileDescription outdatedProfileDescription = new LocalProfileDescription(protocols, outdatedprofile); - assertTrue(outdatedProfileDescription.getProfile().isPresent()); - final Local directory = outdatedprofile.getParent(); - final ProfilesSynchronizeWorker worker = new ProfilesSynchronizeWorker(protocols, directory, ProfilesFinder.Visitor.Noop); - final Set profiles = worker.run(session); - assertFalse(profiles.isEmpty()); - profiles.forEach(d -> assertTrue(d.isLatest())); - - assertFalse(profiles.contains(outdatedProfileDescription)); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().isPresent()); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().get().isInstalled()); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().get().isLatest()); - // Assert profile updated from remote - assertNotEquals(outdatedProfileDescription, profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().get()); - assertNotEquals(outdatedProfileDescription.getChecksum(), profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().get().getChecksum()); - - assertTrue(profiles.contains(localonlyProfileDescription)); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().isPresent()); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().get().isInstalled()); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().get().isLatest()); - assertEquals(localonlyProfileDescription, profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().get()); - - assertTrue(profiles.contains(conflictProfileDescription)); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().isPresent()); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().get().isInstalled()); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().get().isLatest()); - assertEquals(conflictProfileDescription, profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().get()); - } - - @Test - public void testRunVirtualHostEndpoint() throws Exception { + public void testRun() throws Exception { // Registry in temporary folder final ProtocolFactory protocols = new ProtocolFactory(new HashSet<>(Collections.singletonList(new S3Protocol()))); final Host host = new HostParser(protocols, new S3Protocol()).get("s3:/profiles.cyberduck.io").setCredentials( From b612b1ad8cdc41292bb3b0b2ea96ead916a800d0 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 16:12:54 +0200 Subject: [PATCH 05/33] Use new implementation. --- .../ProtocolFactoryProfilesSynchronizer.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/ProtocolFactoryProfilesSynchronizer.java b/core/src/main/java/ch/cyberduck/core/profiles/ProtocolFactoryProfilesSynchronizer.java index fe6a192ea7..58629e4fef 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/ProtocolFactoryProfilesSynchronizer.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/ProtocolFactoryProfilesSynchronizer.java @@ -35,7 +35,7 @@ public class ProtocolFactoryProfilesSynchronizer implements ProfilesSynchronizer private final ProtocolFactory registry; private final LocalProfilesFinder local; - private final RemoteProfilesFinder remote; + private final RemoteIndexProfilesFinder remote; public ProtocolFactoryProfilesSynchronizer(final Session session) { this(ProtocolFactory.get(), session, LocalFactory.get(SupportDirectoryFinderFactory.get().find(), @@ -50,22 +50,22 @@ public class ProtocolFactoryProfilesSynchronizer implements ProfilesSynchronizer public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final LocalProfilesFinder local, final Session session) { this(registry, local, - // Find all profiles from repository - new RemoteProfilesFinder(registry, session)); + // Find index of all profiles from repository + new RemoteIndexProfilesFinder(registry, session)); } - public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final RemoteProfilesFinder remote) { + public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final RemoteIndexProfilesFinder 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) { + public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final Local directory, final RemoteIndexProfilesFinder 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) { + public ProtocolFactoryProfilesSynchronizer(final ProtocolFactory registry, final LocalProfilesFinder local, final RemoteIndexProfilesFinder remote) { this.registry = registry; this.local = local; this.remote = remote; From 10ff3dcba90f0e7e8a40c221b4fb5e9aafb69700 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 16:13:13 +0200 Subject: [PATCH 06/33] Review test. --- .../core/profiles/ProfilesSynchronizeWorkerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java b/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java index cd9d547208..b50f9be1bc 100644 --- a/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java +++ b/s3/src/test/java/ch/cyberduck/core/profiles/ProfilesSynchronizeWorkerTest.java @@ -74,7 +74,7 @@ public class ProfilesSynchronizeWorkerTest { profiles.forEach(d -> assertTrue(d.isLatest())); assertFalse(profiles.contains(outdatedProfileDescription)); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().isPresent()); + assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).anyMatch(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider()))); assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().get().isInstalled()); assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().get().isLatest()); // Assert profile updated from remote @@ -82,13 +82,13 @@ public class ProfilesSynchronizeWorkerTest { assertNotEquals(outdatedProfileDescription.getChecksum(), profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.getProfile().get().getProvider().equals(outdatedProfileDescription.getProfile().get().getProvider())).findFirst().get().getChecksum()); assertTrue(profiles.contains(localonlyProfileDescription)); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().isPresent()); + assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).anyMatch(d -> d.equals(localonlyProfileDescription))); assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().get().isInstalled()); assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().get().isLatest()); assertEquals(localonlyProfileDescription, profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(localonlyProfileDescription)).findFirst().get()); assertTrue(profiles.contains(conflictProfileDescription)); - assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().isPresent()); + assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).anyMatch(d -> d.equals(conflictProfileDescription))); assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().get().isInstalled()); assertTrue(profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().get().isLatest()); assertEquals(conflictProfileDescription, profiles.stream().filter(d -> d.getProfile().isPresent()).filter(d -> d.equals(conflictProfileDescription)).findFirst().get()); From 55887fd00c819c3f7fab34f10eca0548c7826bfa Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 20:32:39 +0200 Subject: [PATCH 07/33] Make utility methods static. --- .../core/resources/NSImageIconCache.java | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java b/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java index 8c5e95d45c..3a7ff0f4b7 100644 --- a/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java +++ b/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java @@ -42,7 +42,7 @@ public class NSImageIconCache implements IconCache { private final NSWorkspace workspace = NSWorkspace.sharedWorkspace(); - private NSImage cache(final String name, final NSImage image, final Integer size) { + private static NSImage cache(final String name, final NSImage image, final Integer size) { if(null == image) { log.warn("No icon named {}", name); return image; @@ -88,8 +88,7 @@ public class NSImageIconCache implements IconCache { public NSImage documentIcon(final String extension, final Integer size) { NSImage image = this.load(extension, size); if(null == image) { - return this.cache(extension, - this.convert(extension, workspace.iconForFileType(extension), size), size); + return cache(extension, convert(extension, workspace.iconForFileType(extension), size), size); } return image; } @@ -99,8 +98,8 @@ public class NSImageIconCache implements IconCache { final String name = String.format("NSDocument-%s%s", extension, badge.name()); NSImage icon = this.iconNamed(name, size); if(null == icon) { - icon = this.badge(badge, this.documentIcon(extension, size)); - this.cache(name, icon, size); + icon = badge(badge, this.documentIcon(extension, size)); + cache(name, icon, size); } return icon; } @@ -119,9 +118,9 @@ public class NSImageIconCache implements IconCache { final String name = String.format("NSFolder-%s", badge.name()); NSImage folder = this.load(name, size); if(null == folder) { - folder = this.convert(name, this.iconNamed("NSFolder", size), size); - folder = this.badge(badge, folder); - this.cache(name, folder, size); + folder = convert(name, this.iconNamed("NSFolder", size), size); + folder = badge(badge, folder); + cache(name, folder, size); } return folder; } @@ -133,7 +132,7 @@ public class NSImageIconCache implements IconCache { * @param icon Icon * @return Cached icon */ - private NSImage badge(final NSImage badge, final NSImage icon) { + private static NSImage badge(final NSImage badge, final NSImage icon) { NSImage f = NSImage.imageWithSize(icon.size()); f.lockFocus(); icon.drawInRect(new NSRect(new NSPoint(0, 0), icon.size()), @@ -163,12 +162,11 @@ public class NSImageIconCache implements IconCache { return this.iconNamed("notfound.tiff", width, height); } else if(name.contains(PreferencesFactory.get().getProperty("local.delimiter"))) { - return this.cache(FilenameUtils.getName(name), this.convert(FilenameUtils.getName(name), - NSImage.imageWithContentsOfFile(name), width, height), width); + return cache(FilenameUtils.getName(name), + convert(FilenameUtils.getName(name), NSImage.imageWithContentsOfFile(name), width, height), width); } else { - return this.cache(name, this.convert(name, - NSImage.imageNamed(name), width, height), width); + return cache(name, convert(name, NSImage.imageNamed(name), width, height), width); } } return image; @@ -185,8 +183,7 @@ public class NSImageIconCache implements IconCache { if(file.exists()) { icon = this.load(file.getAbsolute(), size); if(null == icon) { - return this.cache(file.getName(), - this.convert(file.getName(), workspace.iconForFile(file.getAbsolute()), size), size); + return cache(file.getName(), convert(file.getName(), workspace.iconForFile(file.getAbsolute()), size), size); } } if(null == icon) { @@ -207,8 +204,7 @@ public class NSImageIconCache implements IconCache { final String path = workspace.absolutePathForAppBundleWithIdentifier(app.getIdentifier()); // Null if the bundle cannot be found if(StringUtils.isNotBlank(path)) { - return this.cache(app.getIdentifier(), - this.convert(app.getIdentifier(), workspace.iconForFile(path), size), size); + return cache(app.getIdentifier(), convert(app.getIdentifier(), workspace.iconForFile(path), size), size); } } if(null == icon) { @@ -275,14 +271,14 @@ public class NSImageIconCache implements IconCache { @Override public NSImage aliasIcon(final String extension, final Integer size) { - return this.badge(this.iconNamed("aliasbadge.tiff", size), this.documentIcon(extension, size)); + return badge(this.iconNamed("aliasbadge.tiff", size), this.documentIcon(extension, size)); } - private NSImage convert(final String name, final NSImage icon, final Integer size) { - return this.convert(name, icon, size, size); + private static NSImage convert(final String name, final NSImage icon, final Integer size) { + return convert(name, icon, size, size); } - private NSImage convert(final String name, final NSImage image, final Integer width, final Integer height) { + private static NSImage convert(final String name, final NSImage image, final Integer width, final Integer height) { if(null == image) { return null; } From 1d735460a35a0ccc0ca1daf51faa03766f52801a Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 20:35:43 +0200 Subject: [PATCH 08/33] Move logic to toggle profile with property to factory. --- .../main/java/ch/cyberduck/core/Profile.java | 13 +--------- .../ch/cyberduck/core/ProtocolFactory.java | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/Profile.java b/core/src/main/java/ch/cyberduck/core/Profile.java index 1d65fca643..763972f789 100644 --- a/core/src/main/java/ch/cyberduck/core/Profile.java +++ b/core/src/main/java/ch/cyberduck/core/Profile.java @@ -191,17 +191,7 @@ public class Profile implements Protocol { if(this.isBundled()) { return true; } - final String protocol = parent.getIdentifier(); - final String vendor = this.value(VENDOR_KEY); - if(StringUtils.isNotBlank(protocol) && StringUtils.isNotBlank(vendor)) { - final String property = PreferencesFactory.get().getProperty(StringUtils.lowerCase(String.format("profiles.%s.%s.enabled", protocol, vendor))); - if(null == property) { - // Not previously configured. Assume enabled - return true; - } - return Boolean.parseBoolean(property); - } - return false; + return ProtocolFactory.get().isEnabled(this); } @Override @@ -806,7 +796,6 @@ public class Profile implements Protocol { sb.append("parent=").append(parent); sb.append(", vendor=").append(this.value(VENDOR_KEY)); sb.append(", description=").append(this.value(DESCRIPTION_KEY)); - sb.append(", image=").append(disk); sb.append('}'); return sb.toString(); } diff --git a/core/src/main/java/ch/cyberduck/core/ProtocolFactory.java b/core/src/main/java/ch/cyberduck/core/ProtocolFactory.java index 23fedecd51..8cd50bf459 100644 --- a/core/src/main/java/ch/cyberduck/core/ProtocolFactory.java +++ b/core/src/main/java/ch/cyberduck/core/ProtocolFactory.java @@ -167,15 +167,35 @@ public final class ProtocolFactory { * @param profile Connection profile */ public void unregister(final Profile profile) { - if(registered.remove(profile)) { + this.unregister(profile.getIdentifier(), profile.getProvider()); + } + + public void unregister(final String identifier, final String provider) { + if(registered.removeIf(protocol -> protocol.getIdentifier().equals(identifier) && protocol.getProvider().equals(provider))) { preferences.setProperty(StringUtils.lowerCase(String.format("profiles.%s.%s.enabled", - profile.getIdentifier(), profile.getProvider())), false); + identifier, provider)), false); } else { - log.warn("Failure removing protocol {}", profile); + log.warn("Failure removing protocol {}:{}", identifier, provider); } } + public boolean isEnabled(final Profile profile) { + return this.isEnabled(profile.getIdentifier(), profile.getProvider()); + } + + public boolean isEnabled(String protocol, String vendor) { + if(StringUtils.isNotBlank(protocol) && StringUtils.isNotBlank(vendor)) { + final String property = preferences.getProperty(StringUtils.lowerCase(String.format("profiles.%s.%s.enabled", protocol, vendor))); + if(null == property) { + // Not previously configured. Assume enabled + return true; + } + return Boolean.parseBoolean(property); + } + return false; + } + /** * @return List of enabled protocols */ From 2f183d40448bdbbfb85205feb5cfe8d9baabea51 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 20:40:07 +0200 Subject: [PATCH 09/33] Optionally load image from data instead of filename. --- .../core/resources/NSImageIconCache.java | 22 ++++--- .../main/java/ch/cyberduck/core/Profile.java | 58 +++---------------- .../main/java/ch/cyberduck/core/Protocol.java | 2 + .../cyberduck/core/resources/IconCache.java | 2 +- 4 files changed, 25 insertions(+), 59 deletions(-) diff --git a/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java b/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java index 3a7ff0f4b7..8ff8449ac8 100644 --- a/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java +++ b/core/dylib/src/main/java/ch/cyberduck/core/resources/NSImageIconCache.java @@ -20,6 +20,7 @@ package ch.cyberduck.core.resources; import ch.cyberduck.binding.application.NSGraphics; import ch.cyberduck.binding.application.NSImage; import ch.cyberduck.binding.application.NSWorkspace; +import ch.cyberduck.binding.foundation.NSData; import ch.cyberduck.core.Factory; import ch.cyberduck.core.Local; import ch.cyberduck.core.Path; @@ -27,6 +28,7 @@ import ch.cyberduck.core.Permission; import ch.cyberduck.core.local.Application; import ch.cyberduck.core.preferences.PreferencesFactory; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -154,14 +156,20 @@ public class NSImageIconCache implements IconCache { */ @Override public NSImage iconNamed(final String name, final Integer width, final Integer height) { - // Search for an object whose name was set explicitly using the setName: method and currently - // resides in the image cache - NSImage image = this.load(name, width); + if(null == name) { + return this.iconNamed("notfound.tiff", width, height); + } + NSImage image; + if(Base64.isBase64(name)) { + image = convert(name, NSImage.imageWithData(NSData.dataWithBase64EncodedString(name)), width, height); + } + else { + // Search for an object whose name was set explicitly using the setName: method and currently + // resides in the image cache + image = this.load(name, width); + } if(null == image) { - if(null == name) { - return this.iconNamed("notfound.tiff", width, height); - } - else if(name.contains(PreferencesFactory.get().getProperty("local.delimiter"))) { + if(name.contains(PreferencesFactory.get().getProperty("local.delimiter"))) { return cache(FilenameUtils.getName(name), convert(FilenameUtils.getName(name), NSImage.imageWithContentsOfFile(name), width, height), width); } diff --git a/core/src/main/java/ch/cyberduck/core/Profile.java b/core/src/main/java/ch/cyberduck/core/Profile.java index 763972f789..4b1aaeb172 100644 --- a/core/src/main/java/ch/cyberduck/core/Profile.java +++ b/core/src/main/java/ch/cyberduck/core/Profile.java @@ -19,23 +19,17 @@ package ch.cyberduck.core; * dkocher@cyberduck.ch */ -import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.features.Location; -import ch.cyberduck.core.local.TemporaryFileServiceFactory; import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.serializer.Deserializer; import ch.cyberduck.core.serializer.Serializer; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringSubstitutor; import org.apache.commons.text.lookup.StringLookupFactory; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.io.IOException; -import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -135,14 +129,9 @@ public class Profile implements Protocol { public static final String DEPRECATED_KEY = "Deprecated"; public static final String HELP_KEY = "Help"; - private Local disk; - private Local icon; - public Profile(final Protocol parent, final Deserializer dict) { this.parent = parent; this.dict = dict; - this.disk = this.write(this.value(DISK_KEY)); - this.icon = this.write(this.value(ICON_KEY)); } @Override @@ -366,29 +355,20 @@ public class Profile implements Protocol { @Override public String disk() { - if(null == disk) { + final String v = this.value(DISK_KEY); + if(StringUtils.isBlank(v)) { return parent.disk(); } - if(!disk.exists()) { - this.disk = this.write(this.value(DISK_KEY)); - } - // Temporary file - return disk.getAbsolute(); + return v; } @Override public String icon() { - if(null == icon) { - if(null == disk) { - return parent.icon(); - } - return this.disk(); + final String v = this.value(ICON_KEY); + if(StringUtils.isBlank(v)) { + return parent.icon(); } - if(!icon.exists()) { - this.icon = this.write(this.value(ICON_KEY)); - } - // Temporary file - return icon.getAbsolute(); + return v; } @Override @@ -396,30 +376,6 @@ public class Profile implements Protocol { return parent.favicon(); } - /** - * Write temporary file with data - * - * @param icon Base64 encoded image information - * @return Path to file - */ - private Local write(final String icon) { - if(StringUtils.isBlank(icon)) { - return null; - } - final byte[] favicon = Base64.decodeBase64(icon); - final Local file = TemporaryFileServiceFactory.get().create(new AlphanumericRandomStringService().random()); - try { - try (final OutputStream out = file.getOutputStream(false)) { - IOUtils.write(favicon, out); - } - return file; - } - catch(IOException | AccessDeniedException e) { - log.error("Error writing temporary file", e); - } - return null; - } - @Override public boolean validate(final Credentials credentials, final LoginOptions options) { return parent.validate(credentials, options); diff --git a/core/src/main/java/ch/cyberduck/core/Protocol.java b/core/src/main/java/ch/cyberduck/core/Protocol.java index b67947b911..3263fff05b 100644 --- a/core/src/main/java/ch/cyberduck/core/Protocol.java +++ b/core/src/main/java/ch/cyberduck/core/Protocol.java @@ -222,6 +222,8 @@ public interface Protocol extends FeatureFactory, Comparable, Serializ String getRegion(); /** + * Either a filename or Base64 encoded image data + * * @return A mounted disk icon to display */ String disk(); diff --git a/core/src/main/java/ch/cyberduck/core/resources/IconCache.java b/core/src/main/java/ch/cyberduck/core/resources/IconCache.java index 2cdb3eaa50..fecd468421 100644 --- a/core/src/main/java/ch/cyberduck/core/resources/IconCache.java +++ b/core/src/main/java/ch/cyberduck/core/resources/IconCache.java @@ -33,7 +33,7 @@ public interface IconCache { } /** - * @param name Icon filename with extension + * @param name Icon filename with extension or Base64 encoded image data * @param size Requested size * @return Cached image */ From 3181a7867101f0d030e4ccd489342cf57d78c6c0 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 20:44:41 +0200 Subject: [PATCH 10/33] Add more properties to describe profile. --- .../core/profiles/ProfileDescription.java | 40 ++++++++++++++ .../profiles/RemoteIndexProfilesFinder.java | 53 +++++++++++++++++++ .../core/profiles/SearchProfilePredicate.java | 13 ++--- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java b/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java index 8062b946c8..ddf7d045d8 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/ProfileDescription.java @@ -143,6 +143,36 @@ public class ProfileDescription { } } + public String getIdentifier() { + final Optional profile = this.getProfile(); + return profile.map(Profile::getIdentifier).orElse(null); + } + + public String getProvider() { + final Optional profile = this.getProfile(); + return profile.map(Profile::getProvider).orElse(null); + } + + public String getName() { + final Optional profile = this.getProfile(); + return profile.map(Profile::getName).orElse(null); + } + + public String getDescription() { + final Optional profile = this.getProfile(); + return profile.map(Profile::getDescription).orElse(null); + } + + public String getHelp() { + final Optional profile = this.getProfile(); + return profile.map(Profile::getHelp).orElse(null); + } + + public String getThumbnail() { + final Optional profile = this.getProfile(); + return profile.map(Profile::disk).orElse(null); + } + public boolean isLatest() { return true; } @@ -151,6 +181,16 @@ public class ProfileDescription { return false; } + public boolean isEnabled() { + final Optional profile = this.getProfile(); + return profile.map(Profile::isEnabled).orElse(false); + } + + public boolean isBundled() { + final Optional profile = this.getProfile(); + return profile.map(Profile::isBundled).orElse(false); + } + @Override public String toString() { final StringBuilder sb = new StringBuilder("ProfileDescription{"); diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index a664175a9f..6f7f568abe 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -36,6 +36,7 @@ import ch.cyberduck.core.transfer.download.CompareFilter; import ch.cyberduck.core.transfer.symlink.DisabledDownloadSymlinkResolver; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.concurrent.ConcurrentException; import org.apache.commons.lang3.concurrent.LazyInitializer; import org.apache.logging.log4j.LogManager; @@ -159,6 +160,52 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { public boolean isLatest() { return version.latest; } + + @Override + public boolean isEnabled() { + return protocols.isEnabled(metadata.protocol, metadata.vendor); + } + + @Override + public boolean isBundled() { + return false; + } + + @Override + public String getIdentifier() { + return metadata.protocol; + } + + @Override + public String getProvider() { + return metadata.vendor; + } + + @Override + public String getName() { + return ProtocolFactory.get().forName(metadata.protocol).getName(); + } + + @Override + public String getDescription() { + if(null == metadata.description) { + return StringUtils.EMPTY; + } + return metadata.description; + } + + @Override + public String getHelp() { + return metadata.help; + } + + @Override + public String getThumbnail() { + if(null == metadata.thumbnail) { + return ProtocolFactory.get().forName(metadata.protocol).disk(); + } + return metadata.thumbnail; + } }); } } @@ -183,6 +230,12 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { private String protocol; @JsonProperty("vendor") private String vendor; + @JsonProperty("description") + private String description; + @JsonProperty("help") + private String help; + @JsonProperty("thumbnail") + private String thumbnail; @JsonProperty("versions") private ProfileMetadataVersion[] versions; } diff --git a/core/src/main/java/ch/cyberduck/core/profiles/SearchProfilePredicate.java b/core/src/main/java/ch/cyberduck/core/profiles/SearchProfilePredicate.java index e0f3f93650..eaffbe0dc4 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/SearchProfilePredicate.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/SearchProfilePredicate.java @@ -15,8 +15,6 @@ package ch.cyberduck.core.profiles; * GNU General Public License for more details. */ -import ch.cyberduck.core.Protocol; - import org.apache.commons.lang3.StringUtils; import java.util.function.Predicate; @@ -31,15 +29,10 @@ public class SearchProfilePredicate implements Predicate { @Override public boolean test(final ProfileDescription entry) { - if(!entry.getProfile().isPresent()) { - return false; - } - final Protocol protocol = entry.getProfile().get(); for(String i : StringUtils.split(input, StringUtils.SPACE)) { - if(StringUtils.containsIgnoreCase(protocol.getName(), i) - || StringUtils.containsIgnoreCase(protocol.getDescription(), i) - || StringUtils.containsIgnoreCase(protocol.getDefaultHostname(), i) - || StringUtils.containsIgnoreCase(protocol.getProvider(), i)) { + if(StringUtils.containsIgnoreCase(entry.getName(), i) + || StringUtils.containsIgnoreCase(entry.getDescription(), i) + || StringUtils.containsIgnoreCase(entry.getProvider(), i)) { continue; } return false; From 7cc93c00204edcbffdd05d4d4a97d3499bc63d47 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 20:47:51 +0200 Subject: [PATCH 11/33] Drop unnecessary optimization. --- .../profiles/RemoteIndexProfilesFinder.java | 25 ++++---------- .../core/profiles/RemoteProfilesFinder.java | 33 +++++-------------- 2 files changed, 15 insertions(+), 43 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index 6f7f568abe..95f511846e 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -21,7 +21,6 @@ import ch.cyberduck.core.DefaultPathAttributes; import ch.cyberduck.core.Local; import ch.cyberduck.core.LocalFactory; import ch.cyberduck.core.Path; -import ch.cyberduck.core.ProgressListener; import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.BackgroundException; @@ -30,10 +29,7 @@ import ch.cyberduck.core.io.Checksum; import ch.cyberduck.core.preferences.TemporaryApplicationResourcesFinder; import ch.cyberduck.core.shared.DefaultPathHomeFeature; import ch.cyberduck.core.shared.DelegatingHomeFeature; -import ch.cyberduck.core.transfer.TransferPathFilter; import ch.cyberduck.core.transfer.TransferStatus; -import ch.cyberduck.core.transfer.download.CompareFilter; -import ch.cyberduck.core.transfer.symlink.DisabledDownloadSymlinkResolver; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -59,7 +55,6 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { private final ObjectMapper mapper = new ObjectMapper(); private final ProtocolFactory protocols; private final Session session; - private final TransferPathFilter comparison; private final Local temporary = LocalFactory.get(new TemporaryApplicationResourcesFinder().find(), "profiles"); public RemoteIndexProfilesFinder(final Session session) { @@ -67,13 +62,8 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { } public RemoteIndexProfilesFinder(final ProtocolFactory protocols, final Session session) { - this(protocols, session, new CompareFilter(new DisabledDownloadSymlinkResolver(), session)); - } - - public RemoteIndexProfilesFinder(final ProtocolFactory protocols, final Session session, final TransferPathFilter comparison) { this.protocols = protocols; this.session = session; - this.comparison = comparison; } /** @@ -138,16 +128,13 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { try { final Local local = LocalFactory.get(temporary, metadata.filename); final Path file = new Path(directory, metadata.filename, EnumSet.of(Path.Type.file)); - if(comparison.accept(file, local, new TransferStatus().setExists(true), ProgressListener.noop)) { - final Read read = session.getFeature(Read.class); - log.info("Download profile {}", file); - // Read latest version - try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) - .setVersionId(version.version_id)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { - IOUtils.copy(in, out); - } + final Read read = session.getFeature(Read.class); + log.info("Download profile {}", file); + // Read latest version + try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) + .setVersionId(version.version_id)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { + IOUtils.copy(in, out); } - // Skip download if previously cached return local; } catch(BackgroundException | IOException e) { diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java index f379b82040..1e3d6903fc 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java @@ -24,7 +24,6 @@ import ch.cyberduck.core.ListService; import ch.cyberduck.core.Local; import ch.cyberduck.core.LocalFactory; import ch.cyberduck.core.Path; -import ch.cyberduck.core.ProgressListener; import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.BackgroundException; @@ -32,10 +31,7 @@ import ch.cyberduck.core.features.Read; import ch.cyberduck.core.preferences.TemporaryApplicationResourcesFinder; import ch.cyberduck.core.shared.DefaultPathHomeFeature; import ch.cyberduck.core.shared.DelegatingHomeFeature; -import ch.cyberduck.core.transfer.TransferPathFilter; import ch.cyberduck.core.transfer.TransferStatus; -import ch.cyberduck.core.transfer.download.CompareFilter; -import ch.cyberduck.core.transfer.symlink.DisabledDownloadSymlinkResolver; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.concurrent.ConcurrentException; @@ -55,7 +51,6 @@ public class RemoteProfilesFinder implements ProfilesFinder { private final ProtocolFactory protocols; private final Session session; - private final TransferPathFilter comparison; private final Filter filter; private final Local temporary = LocalFactory.get(new TemporaryApplicationResourcesFinder().find(), "profiles"); @@ -64,19 +59,12 @@ public class RemoteProfilesFinder implements ProfilesFinder { } public RemoteProfilesFinder(final ProtocolFactory protocols, final Session session) { - this(protocols, session, new CompareFilter(new DisabledDownloadSymlinkResolver(), session), new ProfileFilter()); + this(protocols, session, new ProfileFilter()); } - public RemoteProfilesFinder(final Session session, - final TransferPathFilter comparison, final Filter filter) { - this(ProtocolFactory.get(), session, comparison, filter); - } - - public RemoteProfilesFinder(final ProtocolFactory protocols, final Session session, - final TransferPathFilter comparison, final Filter filter) { + public RemoteProfilesFinder(final ProtocolFactory protocols, final Session session, final Filter filter) { this.protocols = protocols; this.session = session; - this.comparison = comparison; this.filter = filter; } @@ -92,17 +80,14 @@ public class RemoteProfilesFinder implements ProfilesFinder { protected Local initialize() throws ConcurrentException { try { final Local local = LocalFactory.get(temporary, file.getName()); - if(comparison.accept(file, local, new TransferStatus().setExists(true), ProgressListener.noop)) { - final Read read = session.getFeature(Read.class); - log.info("Download profile {}", file); - // Read latest version - try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) - // Read latest version - .setVersionId(null)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { - IOUtils.copy(in, out); - } + final Read read = session.getFeature(Read.class); + log.info("Download profile {}", file); + // Read latest version + try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) + // Read latest version + .setVersionId(null)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { + IOUtils.copy(in, out); } - // Skip download if previously cached return local; } catch(BackgroundException | IOException e) { From 242f0a54d3f0be90edbf12070571c2505c3543f9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 21:23:03 +0200 Subject: [PATCH 12/33] Do not preload profiles for display in table. --- .../UserControls/ProfileElement.xaml.cs | 4 +- .../Preferences/Pages/ProfileViewModel.cs | 28 +++---- .../Preferences/Pages/ProfilesViewModel.cs | 7 +- .../ProfilesPreferencesController.java | 75 +++++++++---------- 4 files changed, 56 insertions(+), 58 deletions(-) diff --git a/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs b/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs index 6e894905f2..aae9cd16eb 100644 --- a/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs +++ b/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs @@ -18,10 +18,10 @@ namespace Ch.Cyberduck.Core.Refresh.UserControls { d(this.OneWayBind(ViewModel, vm => vm.Name, v => v.ProtocolType.Text)); d(this.OneWayBind(ViewModel, vm => vm.Description, v => v.Description.Text)); - d(this.OneWayBind(ViewModel, vm => vm.Profile, v => v.ProfileIcon.Source, p => wpfIconProvider.GetDisk(p, 32))); + d(this.OneWayBind(ViewModel, vm => vm.Thumbnail, v => v.ProfileIcon.Source, p => wpfIconProvider.GetDisk(p, 32))); d(this.OneWayBind(ViewModel, vm => vm.DefaultHostName, v => v.ToolTipEnabled, v => !string.IsNullOrWhiteSpace(v))); d(this.OneWayBind(ViewModel, vm => vm.DefaultHostName, v => v.ToolTip)); - d(this.OneWayBind(ViewModel, vm => vm.IsEnabled, v => v.Checked.IsEnabled)); + d(this.OneWayBind(ViewModel, vm => vm.Enabled, v => v.Checked.IsEnabled)); d(this.BindCommand(ViewModel, vm => vm.OpenHelp, v => v.HelpButton)); d(this.Bind(ViewModel, vm => vm.Installed, v => v.Checked.IsChecked)); }); diff --git a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs index 3518dcbbae..bd7d5e85f6 100644 --- a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs +++ b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs @@ -2,6 +2,7 @@ using ch.cyberduck.core.local; using ch.cyberduck.core.profiles; using ReactiveUI; +using System.String; using System.Linq; using System.Reactive; @@ -11,24 +12,29 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages { private bool installed; - public ProfileViewModel(ProfileDescription profile) + public ProfileViewModel(ProfileDescription description) { - ProfileDescription = profile; - Profile = (Profile)profile.getProfile().get(); - Installed = profile.isInstalled() && Profile.isEnabled(); - IsEnabled = !(Profile.isBundled() || Utils.ConvertFromJavaList(BookmarkCollection.defaultCollection()).Any(x => x.getProtocol().Equals(Profile))); + ProfileDescription = description; + Installed = description.isInstalled() && description.isEnabled(); + Enabled = !description.isBundled() && !Utils.ConvertFromJavaList(BookmarkCollection.defaultCollection()).Any(host => + host.getProtocol().getProvider().equals(description.getProvider()) + && host.getProtocol().getIdentifier().equals(description.getIdentifier())); OpenHelp = ReactiveCommand.Create(() => { - BrowserLauncherFactory.get().open(ProviderHelpServiceFactory.get().help(Profile)); + BrowserLauncherFactory.get().open(ProfileDescription.getHelp()); }); } - public bool IsEnabled { get; } + public bool Enabled { get; } - public string DefaultHostName => Profile.getDefaultHostname(); + public string Name => ProfileDescription.getName(); - public string Description => Profile.getDescription(); + public string Description => ProfileDescription.getDescription(); + + public string DefaultHostName => String.Empty; + + public string Thumbnail => ProfileDescription.getThumbnail(); public bool Installed { @@ -36,12 +42,8 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages set => this.RaiseAndSetIfChanged(ref installed, value); } - public string Name => Profile.getName(); - public ReactiveCommand OpenHelp { get; } - public Profile Profile { get; } - public ProfileDescription ProfileDescription { get; } } } diff --git a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs index 2e32f24fe4..bd1f05cb28 100644 --- a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs +++ b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs @@ -52,7 +52,6 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages LoadProfiles.SelectMany(Observable.Return(false)).Subscribe(loadActive); var profiles = LoadProfiles.Select(s => s.AsObservableChangeSet()).Switch() - .Filter(x => x.getProfile().isPresent()) .Filter(this.WhenAnyValue(v => v.FilterText) .Throttle(TimeSpan.FromMilliseconds(500)) .DistinctUntilChanged() @@ -61,7 +60,7 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages .AsObservableList(); profiles.Connect() - .Sort(SortExpressionComparer.Ascending(x => x.Profile)) + .Sort(SortExpressionComparer.Ascending(x => x.Description)) .ObserveOnDispatcher() .Bind(out this.profiles) .Subscribe(); @@ -81,7 +80,7 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages } else { - protocols.unregister(p.Sender.Profile); + protocols.unregister(p.Sender.ProfileDescription.getIdentifier(), p.Sender.ProfileDescription.getProvider()); } profileListObserver.RaiseProfilesChanged(); }); @@ -107,7 +106,7 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages { private readonly TaskCompletionSource> completionSource; - public InternalProfilesSynchronizeWorker(ProtocolFactory protocolFactory, TaskCompletionSource> completionSource) : base(protocolFactory, ProfilesFinder.Visitor.Prefetch) + public InternalProfilesSynchronizeWorker(ProtocolFactory protocols, TaskCompletionSource> completionSource) : base(protocols, ProfilesFinder.Visitor.Noop) { this.completionSource = completionSource; } diff --git a/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java b/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java index 112e23cf23..4666563fd1 100644 --- a/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java +++ b/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java @@ -33,19 +33,17 @@ import ch.cyberduck.binding.foundation.NSObject; import ch.cyberduck.binding.foundation.NSString; import ch.cyberduck.core.BookmarkCollection; import ch.cyberduck.core.Local; -import ch.cyberduck.core.Profile; import ch.cyberduck.core.Protocol; import ch.cyberduck.core.ProtocolFactory; -import ch.cyberduck.core.ProviderHelpServiceFactory; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.local.BrowserLauncherFactory; -import ch.cyberduck.core.profiles.LocalProfileDescription; import ch.cyberduck.core.profiles.ProfileDescription; import ch.cyberduck.core.profiles.ProfilesFinder; import ch.cyberduck.core.profiles.ProfilesSynchronizeWorker; import ch.cyberduck.core.profiles.ProfilesWorkerBackgroundAction; import ch.cyberduck.core.profiles.SearchProfilePredicate; import ch.cyberduck.core.resources.IconCacheFactory; +import ch.cyberduck.core.threading.AbstractBackgroundAction; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -58,7 +56,7 @@ import org.rococoa.cocoa.foundation.NSInteger; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -75,8 +73,8 @@ public class ProfilesPreferencesController extends BundleController { /** * Synchronized ist of available profiles */ - private final Map repository - = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Set repository + = Collections.synchronizedSet(new LinkedHashSet<>()); @Delegate private ProfilesTableDataSource profilesTableDataSource; @@ -133,12 +131,12 @@ public class ProfilesPreferencesController extends BundleController { private void reload() { final String input = searchField.stringValue(); if(StringUtils.isBlank(input)) { - this.profilesTableDataSource.withSource(toSorted(repository.keySet())); + this.profilesTableDataSource.withSource(toSorted(repository)); } else { // Setup search filter this.profilesTableDataSource.withSource(toSorted( - repository.keySet().stream().filter(new SearchProfilePredicate(input)).collect(Collectors.toSet()))); + repository.stream().filter(new SearchProfilePredicate(input)).collect(Collectors.toSet()))); } // Reload with current cache this.profilesTableView.reloadData(); @@ -146,7 +144,7 @@ public class ProfilesPreferencesController extends BundleController { public void setProfilesTableView(final NSOutlineView profilesTableView) { this.profilesTableView = profilesTableView; - this.profilesTableDataSource = new ProfilesTableDataSource().withSource(toSorted(repository.keySet())); + this.profilesTableDataSource = new ProfilesTableDataSource().withSource(toSorted(repository)); this.profilesTableView.setDataSource(profilesTableDataSource.id()); this.profilesTableDelegate = new ProfilesTableDelegate(profilesTableView.tableColumnWithIdentifier("Default")); this.profilesTableView.setDelegate(profilesTableDelegate.id()); @@ -166,14 +164,10 @@ public class ProfilesPreferencesController extends BundleController { try { progressIndicator.startAnimation(null); this.background(new ProfilesWorkerBackgroundAction(controller, - new ProfilesSynchronizeWorker(protocols, ProfilesFinder.Visitor.Prefetch) { + new ProfilesSynchronizeWorker(protocols, ProfilesFinder.Visitor.Noop) { @Override public void cleanup(final Set set) { - for(ProfileDescription description : set) { - if(description.getProfile().isPresent()) { - repository.put(description, description.getProfile().get()); - } - } + repository.addAll(set); reload(); progressIndicator.stopAnimation(null); } @@ -194,7 +188,7 @@ public class ProfilesPreferencesController extends BundleController { } private ProfileDescription fromChecksum(final NSObject hash) { - final Optional found = repository.keySet().stream() + final Optional found = repository.stream() .filter(description -> description.getChecksum().hash.equals(hash.toString())).findFirst(); return found.orElse(null); } @@ -225,7 +219,7 @@ public class ProfilesPreferencesController extends BundleController { if(null == description) { return null; } - return repository.get(description).getDescription(); + return description.getDescription(); } @Override @@ -284,7 +278,7 @@ public class ProfilesPreferencesController extends BundleController { if(null == description) { return false; } - return !repository.get(description).isBundled(); + return true; } public NSView outlineView_viewForTableColumn_item(final NSOutlineView outlineView, final NSTableColumn tableColumn, final NSObject item) { @@ -292,11 +286,10 @@ public class ProfilesPreferencesController extends BundleController { if(null == description) { return null; } - final Profile profile = repository.get(description); if(controllers.containsKey(description)) { return controllers.get(description).getCellView(); } - final ProfileTableViewController controller = new ProfileTableViewController(description, profile); + final ProfileTableViewController controller = new ProfileTableViewController(description); controllers.put(description, controller); return controller.getCellView(); } @@ -380,7 +373,6 @@ public class ProfilesPreferencesController extends BundleController { public final class ProfileTableViewController extends BundleController { private final ProfileDescription description; - private final Profile profile; @Outlet private NSTableCellView cellView; @@ -393,9 +385,8 @@ public class ProfilesPreferencesController extends BundleController { @Outlet private NSButton helpButton; - public ProfileTableViewController(final ProfileDescription description, final Profile profile) { + public ProfileTableViewController(final ProfileDescription description) { this.description = description; - this.profile = profile; this.loadBundle(); } @@ -409,14 +400,14 @@ public class ProfilesPreferencesController extends BundleController { public void setImageView(final NSImageView imageView) { this.imageView = imageView; - this.imageView.setImage(IconCacheFactory.get().iconNamed(profile.icon(), 32)); + this.imageView.setImage(IconCacheFactory.get().iconNamed(description.getThumbnail(), 32)); } public void setTextField(final NSTextField textField) { this.textField = textField; - final NSMutableAttributedString description = NSMutableAttributedString.create(profile.getDescription(), PRIMARY_FONT_ATTRIBUTES); - description.appendAttributedString(NSMutableAttributedString.create(String.format("\n%s", profile.getName()), SECONDARY_FONT_ATTRIBUTES)); - this.textField.setAttributedStringValue(description); + final NSMutableAttributedString s = NSMutableAttributedString.create(description.getDescription(), PRIMARY_FONT_ATTRIBUTES); + s.appendAttributedString(NSMutableAttributedString.create(String.format("\n%s", description.getName()), SECONDARY_FONT_ATTRIBUTES)); + this.textField.setAttributedStringValue(s); } public void setCheckbox(final NSButton checkbox) { @@ -424,38 +415,45 @@ public class ProfilesPreferencesController extends BundleController { this.checkbox.setState(NSCell.NSOnState); this.checkbox.setTarget(this.id()); this.checkbox.setAction(Foundation.selector("profileCheckboxClicked:")); - this.checkbox.setEnabled(!profile.isBundled()); - if(BookmarkCollection.defaultCollection().stream().filter(host -> host.getProtocol().equals(profile)).findAny().isPresent()) { + this.checkbox.setEnabled(!description.isBundled()); + if(BookmarkCollection.defaultCollection().stream().anyMatch(host -> host.getProtocol().getProvider().equals(description.getProvider()) + && host.getProtocol().getIdentifier().equals(description.getIdentifier()))) { this.checkbox.setEnabled(false); } - this.checkbox.setState(description.isInstalled() && profile.isEnabled() ? NSCell.NSOnState : NSCell.NSOffState); + this.checkbox.setState(description.isInstalled() && description.isEnabled() ? NSCell.NSOnState : NSCell.NSOffState); } public void setHelpButton(final NSButton helpButton) { this.helpButton = helpButton; this.helpButton.setTarget(this.id()); this.helpButton.setAction(Foundation.selector("helpButtonClicked:")); + this.helpButton.setEnabled(StringUtils.isNotBlank(description.getHelp())); } @Action public void profileCheckboxClicked(final NSButton sender) { boolean enabled = sender.state() == NSCell.NSOnState; if(enabled) { - final Optional file = description.getFile(); - // Update with last version from repository - file.ifPresent(local -> repository.put(new LocalProfileDescription(protocols, ProtocolFactory.BUNDLED_PROFILE_PREDICATE, - protocols.register(local)), profile)); + this.background(new AbstractBackgroundAction() { + @Override + public Void run() { + // Download profile + final Optional file = description.getFile(); + // Update with last version from repository + file.ifPresent(protocols::register); + return null; + } + }); } else { - final Optional profile = description.getProfile(); // Uninstall profile - profile.ifPresent(protocols::unregister); + protocols.unregister(description.getIdentifier(), description.getProvider()); } } @Action public void helpButtonClicked(final NSButton sender) { - BrowserLauncherFactory.get().open(ProviderHelpServiceFactory.get().help(profile)); + BrowserLauncherFactory.get().open(description.getHelp()); } @Override @@ -472,8 +470,7 @@ public class ProfilesPreferencesController extends BundleController { */ private static List toSorted(final Set profiles) { return profiles.stream() - .filter(description -> description.getProfile().isPresent()) - .sorted(Comparator.comparing(o -> o.getProfile().get())) + .sorted(Comparator.comparing(ProfileDescription::getDescription)) .collect(Collectors.toList()); } } From ca036611b84159d2f00391aabadc04023ce95396 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 21:59:05 +0200 Subject: [PATCH 13/33] Call visitor. --- .../ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index 95f511846e..090e124d49 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -115,7 +115,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { final ProfileMetadataList list = mapper.readValue(in, ProfileMetadataList.class); for(ProfileMetadata metadata : list.profiles) { for(ProfileMetadataVersion version : metadata.versions) { - profiles.add(new ProfileDescription(protocols, protocol -> true, + profiles.add(visitor.visit(new ProfileDescription(protocols, protocol -> true, new LazyInitializer() { @Override protected Checksum initialize() { @@ -193,7 +193,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { } return metadata.thumbnail; } - }); + })); } } } From 8e175c0a38e07ef5afb89a8cd9ebd49d46a2b653 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 22:02:11 +0200 Subject: [PATCH 14/33] Create directory if missing. --- .../ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java | 1 + .../java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index 090e124d49..baf6a18fe8 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -126,6 +126,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { @Override protected Local initialize() throws ConcurrentException { try { + temporary.mkdir(); final Local local = LocalFactory.get(temporary, metadata.filename); final Path file = new Path(directory, metadata.filename, EnumSet.of(Path.Type.file)); final Read read = session.getFeature(Read.class); diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java index 1e3d6903fc..5682b1572d 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfilesFinder.java @@ -71,7 +71,6 @@ public class RemoteProfilesFinder implements ProfilesFinder { @Override public Set find(final Visitor visitor) throws BackgroundException { log.info("Fetch profiles from {}", session.getHost()); - temporary.mkdir(); final AttributedList 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, @@ -79,6 +78,7 @@ public class RemoteProfilesFinder implements ProfilesFinder { @Override protected Local initialize() throws ConcurrentException { try { + temporary.mkdir(); final Local local = LocalFactory.get(temporary, file.getName()); final Read read = session.getFeature(Read.class); log.info("Download profile {}", file); From 06da8eee6e63580dba10d29425cc02b066a399ed Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 22:02:47 +0200 Subject: [PATCH 15/33] Fix null pointer. --- .../ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index baf6a18fe8..69e7de8e1d 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -146,7 +146,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { ) { @Override public boolean isLatest() { - return version.latest; + return Boolean.TRUE.equals(version.latest); } @Override From 00bf08f5cc97a0fd98ffe8cc13d8e04d496f6a79 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 22:06:33 +0200 Subject: [PATCH 16/33] Handle empty help. --- .../csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs index bd7d5e85f6..8f751f3182 100644 --- a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs +++ b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs @@ -22,7 +22,11 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages OpenHelp = ReactiveCommand.Create(() => { - BrowserLauncherFactory.get().open(ProfileDescription.getHelp()); + var help = ProfileDescription.getHelp(); + if (!string.IsNullOrWhiteSpace(help)) + { + BrowserLauncherFactory.get().open(help); + } }); } From 16be5980608e0baddffeaff057149ea3480a1ce4 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 11 May 2026 22:13:15 +0200 Subject: [PATCH 17/33] Replace model entry when installed. --- .../ui/cocoa/controller/ProfilesPreferencesController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java b/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java index 4666563fd1..dfd432a7e1 100644 --- a/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java +++ b/osx/src/main/java/ch/cyberduck/ui/cocoa/controller/ProfilesPreferencesController.java @@ -37,6 +37,7 @@ import ch.cyberduck.core.Protocol; import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.local.BrowserLauncherFactory; +import ch.cyberduck.core.profiles.LocalProfileDescription; import ch.cyberduck.core.profiles.ProfileDescription; import ch.cyberduck.core.profiles.ProfilesFinder; import ch.cyberduck.core.profiles.ProfilesSynchronizeWorker; @@ -440,7 +441,11 @@ public class ProfilesPreferencesController extends BundleController { // Download profile final Optional file = description.getFile(); // Update with last version from repository - file.ifPresent(protocols::register); + file.ifPresent(local -> { + repository.remove(description); + repository.add(new LocalProfileDescription(protocols, ProtocolFactory.BUNDLED_PROFILE_PREDICATE, + protocols.register(local))); + }); return null; } }); From 3894c56827c9c857d84170f74cedc6007c8463c2 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 12 May 2026 08:37:02 +0200 Subject: [PATCH 18/33] Filter profiles that fail parsing. --- .../java/ch/cyberduck/core/profiles/LocalProfilesFinder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/LocalProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/LocalProfilesFinder.java index 60a952cb86..e203931bd6 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/LocalProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/LocalProfilesFinder.java @@ -62,7 +62,9 @@ public class LocalProfilesFinder implements ProfilesFinder { if(directory.exists()) { log.debug("Load profiles from {}", directory); return directory.list().filter(new ProfileFilter()).toList().stream() - .map(file -> visitor.visit(new LocalProfileDescription(protocols, parent, file))).collect(Collectors.toSet()); + .map(file -> visitor.visit(new LocalProfileDescription(protocols, parent, file))) + .filter(d -> d.getProfile().isPresent()) + .collect(Collectors.toSet()); } return Collections.emptySet(); } From b5e2045226ba4c3ce05f51cf0b53d3c8ea8f05a8 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 12 May 2026 08:37:15 +0200 Subject: [PATCH 19/33] Use instance field. --- .../ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index 69e7de8e1d..9485d0c516 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -171,7 +171,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { @Override public String getName() { - return ProtocolFactory.get().forName(metadata.protocol).getName(); + return protocols.forName(metadata.protocol).getName(); } @Override @@ -190,7 +190,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { @Override public String getThumbnail() { if(null == metadata.thumbnail) { - return ProtocolFactory.get().forName(metadata.protocol).disk(); + return protocols.forName(metadata.protocol).disk(); } return metadata.thumbnail; } From 80d8501343425b771ed63e50d8c6045698dbe4b8 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 12 May 2026 08:38:29 +0200 Subject: [PATCH 20/33] Logging. --- .../ch/cyberduck/core/profiles/RemoteProfileDescription.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfileDescription.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfileDescription.java index 1a2c3f008e..2610e63e62 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfileDescription.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteProfileDescription.java @@ -23,6 +23,7 @@ import ch.cyberduck.core.io.Checksum; import org.apache.commons.lang3.concurrent.LazyInitializer; public final class RemoteProfileDescription extends ProfileDescription { + private final Path file; /** @@ -47,7 +48,7 @@ public final class RemoteProfileDescription extends ProfileDescription { @Override public String toString() { - final StringBuilder sb = new StringBuilder("PathProfileDescription{"); + final StringBuilder sb = new StringBuilder("RemoteProfileDescription{"); sb.append("file=").append(file); sb.append('}'); return sb.toString(); From 4ee890a6416927dad32e64298a3575dd1a6198f3 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 12 May 2026 09:31:05 +0200 Subject: [PATCH 21/33] Review ProfileViewModel --- .../Preferences/Pages/ProfileViewModel.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs index 8f751f3182..268173f0a4 100644 --- a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs +++ b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs @@ -2,9 +2,9 @@ using ch.cyberduck.core.local; using ch.cyberduck.core.profiles; using ReactiveUI; -using System.String; using System.Linq; using System.Reactive; +using System.Reactive.Linq; namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages { @@ -22,23 +22,15 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages OpenHelp = ReactiveCommand.Create(() => { - var help = ProfileDescription.getHelp(); - if (!string.IsNullOrWhiteSpace(help)) - { - BrowserLauncherFactory.get().open(help); - } - }); + BrowserLauncherFactory.get().open(ProfileDescription.getHelp()); + }, Observable.Return(!string.IsNullOrWhiteSpace(ProfileDescription.getHelp()))); } - public bool Enabled { get; } - - public string Name => ProfileDescription.getName(); + public string DefaultHostName => string.Empty; public string Description => ProfileDescription.getDescription(); - public string DefaultHostName => String.Empty; - - public string Thumbnail => ProfileDescription.getThumbnail(); + public bool Enabled { get; } public bool Installed { @@ -46,8 +38,12 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages set => this.RaiseAndSetIfChanged(ref installed, value); } + public string Name => ProfileDescription.getName(); + public ReactiveCommand OpenHelp { get; } public ProfileDescription ProfileDescription { get; } + + public string Thumbnail => ProfileDescription.getThumbnail(); } } From 7fb7077c90ffe228bac3d9500d49abf4b8fe4762 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 12 May 2026 09:33:55 +0200 Subject: [PATCH 22/33] Move Bundled-check to separate method --- .../Preferences/Pages/ProfileViewModel.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs index 268173f0a4..acb507af78 100644 --- a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs +++ b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfileViewModel.cs @@ -1,10 +1,12 @@ using ch.cyberduck.core; using ch.cyberduck.core.local; using ch.cyberduck.core.profiles; +using java.util; using ReactiveUI; using System.Linq; using System.Reactive; using System.Reactive.Linq; +using Observable = System.Reactive.Linq.Observable; namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages { @@ -16,9 +18,9 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages { ProfileDescription = description; Installed = description.isInstalled() && description.isEnabled(); - Enabled = !description.isBundled() && !Utils.ConvertFromJavaList(BookmarkCollection.defaultCollection()).Any(host => - host.getProtocol().getProvider().equals(description.getProvider()) - && host.getProtocol().getIdentifier().equals(description.getIdentifier())); + Enabled = !description.isBundled() + && !BookmarkCollection.defaultCollection().AsEnumerable() + .Any(host => IsDefaultProfile(description, host)); OpenHelp = ReactiveCommand.Create(() => { @@ -45,5 +47,12 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages public ProfileDescription ProfileDescription { get; } public string Thumbnail => ProfileDescription.getThumbnail(); + + private static bool IsDefaultProfile(ProfileDescription profile, Host host) + { + var protocol = host.getProtocol(); + return protocol.getProvider().Equals(profile.getProvider()) + && protocol.getIdentifier().Equals(profile.getIdentifier()); + } } } From 9f1d510a2dd2f8d7e3f0981c5aa5ac3e35dd86f9 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 19 May 2026 16:01:41 +0200 Subject: [PATCH 23/33] Add Base64 stream parsing --- .../Services/WinFormsIconProvider.Impl.cs | 103 +++++++++++++----- .../main/csharp/Services/WpfIconProvider.cs | 57 +++++++++- 2 files changed, 125 insertions(+), 35 deletions(-) diff --git a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs index ad4da14af1..7e5b5bb2ad 100644 --- a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs +++ b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs @@ -1,9 +1,13 @@ -using Ch.Cyberduck.Core.Refresh.Media.Imaging; +using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Ch.Cyberduck.Core.Refresh.Media.Imaging; namespace Ch.Cyberduck.Core.Refresh.Services { @@ -19,32 +23,19 @@ namespace Ch.Cyberduck.Core.Refresh.Services using (IconCache.WriteLock()) { bool isDefault = !IconCache.TryGetIcon(key, out _, classifier); - Stream stream = default; - bool dispose = true; - try + images = GetImages(path, (c, s) => c.TryGetIcon(key, out _, classifier), (c, s, i) => { - stream = GetStream(path); - images = GetImages(stream, (c, s) => c.TryGetIcon(key, out _, classifier), (c, s, i) => + if (isDefault) { - if (isDefault) + isDefault = false; + if (returnDefault) { - isDefault = false; - if (returnDefault) - { - image = i; - } - IconCache.CacheIcon(key, s, classifier); + image = i; } - IconCache.CacheIcon(key, s, i, classifier); - }, out dispose); - } - finally - { - if (dispose && stream != null) - { - stream.Dispose(); + IconCache.CacheIcon(key, s, classifier); } - } + IconCache.CacheIcon(key, s, i, classifier); + }, out _); } } @@ -52,24 +43,72 @@ namespace Ch.Cyberduck.Core.Refresh.Services return images; } - private IEnumerable GetImages(Stream stream, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out bool dispose) + private IEnumerable GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out bool dispose) { - Image source = Image.FromStream(stream); + if (!TryGetBase64Images(name, getCache, cacheIcon, out var images)) + { + using var stream = GetStream(name); + _ = TryGetImages(stream, getCache, cacheIcon, out images); + } + + dispose = false; + return images; + } + + private unsafe bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) + { +#if NETCOREAPP + if (System.Buffers.Text.Base64.IsValid(name)) + { + var buffer = MemoryMarshal.AsBytes(name.AsSpan()); + fixed (byte* nameLocal = buffer) + { + using UnmanagedMemoryStream bufferStream = new(nameLocal, 0, buffer.Length, FileAccess.Read); + // Instead of prefilling the buffer like in .NET Framework, just convert the Base64-buffer on-the-fly. + using var memoryStream = Encoding.CreateTranscodingStream(bufferStream, Encoding.Unicode, Encoding.UTF8); + using CryptoStream imageStream = new(memoryStream, new FromBase64Transform(), CryptoStreamMode.Read); +#else + try + { + using (MemoryStream imageStream = new(Convert.FromBase64String(name), false)) + { +#endif + return TryGetImages(imageStream, getCache, cacheIcon, out images); + } + } +#if !NETCOREAPP + catch { /* We don't have an easy way of validating Base64 input. Let it error out. */ } +#endif + + images = null; + return false; + } + + private bool TryGetImages(Stream stream, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) + { + var source = Image.FromStream(stream); if (ImageFormat.Gif.Equals(source.RawFormat)) { // Gif cannot be cached/duplicated/or anything else, really. // underlying stream must not be closed (learned the hard way). - dispose = false; + //dispose = false; cacheIcon(IconCache, source.Width, source); - return new[] { source }; + images = [source]; + return true; } - dispose = true; + //dispose = true; using (source) { if (ImageFormat.Icon.Equals(source.RawFormat)) { + if (!stream.CanSeek) + { + images = null; + return false; + } + source.Dispose(); stream.Position = 0; GDIIcon icon = new(stream); @@ -77,7 +116,8 @@ namespace Ch.Cyberduck.Core.Refresh.Services { cacheIcon(IconCache, item.Width, item); } - return icon.Frames; + images = icon.Frames; + return true; } else if (ImageFormat.Tiff.Equals(source.RawFormat)) { @@ -94,16 +134,19 @@ namespace Ch.Cyberduck.Core.Refresh.Services frames.Add(copy); cacheIcon(IconCache, copy.Width, copy); } - return frames; + images = frames; + return true; } else { Bitmap copy = new(source); copy.SetResolution(96, 96); cacheIcon(IconCache, copy.Width, copy); - return new[] { copy }; + images = [copy]; + return true; } } + } } } diff --git a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs index 1cfe6c9d80..e303505560 100644 --- a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs +++ b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs @@ -1,16 +1,19 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; using System.Windows; using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; using ch.cyberduck.core; +using ch.cyberduck.core.profiles; namespace Ch.Cyberduck.Core.Refresh.Services { - using System.IO; - public class WpfIconProvider : IconProvider { public WpfIconProvider(IconCache cache, IIconProviderImageSource BitmapSource) : base(cache, BitmapSource) @@ -35,6 +38,10 @@ namespace Ch.Cyberduck.Core.Refresh.Services public IEnumerable GetResources(string name) => Get(name, name, default, false, out var _); + public BitmapSource GetThumbnail(ProfileDescription profile, int size) + => IconCache.TryGetIcon(profile, size, out BitmapSource image, "Thumbnail") + ? image : Get(profile, profile.getThumbnail(), size, "Thumbnail"); + protected override BitmapSource Get(IntPtr nativeIcon, CacheIconCallback cacheIcon) { var source = Imaging.CreateBitmapSourceFromHIcon(nativeIcon, default, default); @@ -127,7 +134,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services } } - private IEnumerable Get(object key, string path, string classifier, bool returnDefault, out BitmapSource @default) + private IEnumerable Get(object key, string name, string classifier, bool returnDefault, out BitmapSource @default) { BitmapSource image = default; var images = IconCache.Filter(((object key, string classifier, int) f) => Equals(key, f.key) && Equals(classifier, f.classifier)); @@ -136,8 +143,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services using (IconCache.WriteLock()) { bool isDefault = !IconCache.TryGetIcon(key, out _, classifier); - using Stream stream = GetStream(path); - images = GetImages(stream, (c, s) => c.TryGetIcon(key, s, out _, classifier), (c, s, i) => + images = GetImages(name, (c, s) => c.TryGetIcon(key, s, out _, classifier), (c, s, i) => { if (isDefault) { @@ -157,6 +163,47 @@ namespace Ch.Cyberduck.Core.Refresh.Services return images; } + private IEnumerable GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon) + { + if (!TryGetBase64Images(name, getCache, cacheIcon, out var images)) + { + using var stream = GetStream(name); + images = GetImages(stream, getCache, cacheIcon); + } + + return images; + } + + private unsafe bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) + { +#if NETCOREAPP + if (System.Buffers.Text.Base64.IsValid(name)) + { + var buffer = MemoryMarshal.AsBytes(name.AsSpan()); + fixed (byte* nameLocal = buffer) + { + using UnmanagedMemoryStream bufferStream = new(nameLocal, 0, buffer.Length, FileAccess.Read); + // Instead of prefilling the buffer like in .NET Framework, just convert the Base64-buffer on-the-fly. + using var memoryStream = Encoding.CreateTranscodingStream(bufferStream, Encoding.Unicode, Encoding.UTF8); + using CryptoStream imageStream = new(memoryStream, new FromBase64Transform(), CryptoStreamMode.Read); +#else + try + { + using (MemoryStream imageStream = new(Convert.FromBase64String(name), false)) + { +#endif + images = GetImages(imageStream, getCache, cacheIcon); + return true; + } + } // Close the fixed-Block. +#if !NETCOREAPP + catch { /* We don't have an easy way of validating Base64 input. Let it error out. */ } +#endif + + images = null; + return false; + } + private IEnumerable GetImages(Stream stream, GetCacheIconCallback getCache, CacheIconCallback cacheIcon) { var list = new List(); From c84d70a16e7764a7e8044554f9f6973494c0a14b Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 19 May 2026 17:04:23 +0200 Subject: [PATCH 24/33] Extract inner class. --- .../profiles/RemoteIndexProfilesFinder.java | 170 +++++++++--------- 1 file changed, 90 insertions(+), 80 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index 9485d0c516..6e328cdc13 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -115,86 +115,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { final ProfileMetadataList list = mapper.readValue(in, ProfileMetadataList.class); for(ProfileMetadata metadata : list.profiles) { for(ProfileMetadataVersion version : metadata.versions) { - profiles.add(visitor.visit(new ProfileDescription(protocols, protocol -> true, - new LazyInitializer() { - @Override - protected Checksum initialize() { - return Checksum.parse(version.checksum); - } - }, - new LazyInitializer() { - @Override - protected Local initialize() throws ConcurrentException { - try { - temporary.mkdir(); - final Local local = LocalFactory.get(temporary, metadata.filename); - final Path file = new Path(directory, metadata.filename, EnumSet.of(Path.Type.file)); - final Read read = session.getFeature(Read.class); - log.info("Download profile {}", file); - // Read latest version - try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) - .setVersionId(version.version_id)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { - IOUtils.copy(in, out); - } - return local; - } - catch(BackgroundException | IOException e) { - throw new ConcurrentException(e); - } - } - } - ) { - @Override - public boolean isLatest() { - return Boolean.TRUE.equals(version.latest); - } - - @Override - public boolean isEnabled() { - return protocols.isEnabled(metadata.protocol, metadata.vendor); - } - - @Override - public boolean isBundled() { - return false; - } - - @Override - public String getIdentifier() { - return metadata.protocol; - } - - @Override - public String getProvider() { - return metadata.vendor; - } - - @Override - public String getName() { - return protocols.forName(metadata.protocol).getName(); - } - - @Override - public String getDescription() { - if(null == metadata.description) { - return StringUtils.EMPTY; - } - return metadata.description; - } - - @Override - public String getHelp() { - return metadata.help; - } - - @Override - public String getThumbnail() { - if(null == metadata.thumbnail) { - return protocols.forName(metadata.protocol).disk(); - } - return metadata.thumbnail; - } - })); + profiles.add(visitor.visit(new RemoteIndexProfileDescription(temporary, protocols, version, metadata, directory))); } } } @@ -239,4 +160,93 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { @JsonProperty("latest") private Boolean latest; } + + private final class RemoteIndexProfileDescription extends ProfileDescription { + private final ProtocolFactory factory; + private final ProfileMetadataVersion version; + private final ProfileMetadata metadata; + + public RemoteIndexProfileDescription(final Local temporary, final ProtocolFactory factory, final ProfileMetadataVersion version, final ProfileMetadata metadata, final Path directory) { + super(factory, protocol -> true, new LazyInitializer() { + @Override + protected Checksum initialize() { + return Checksum.parse(version.checksum); + } + }, new LazyInitializer() { + @Override + protected Local initialize() throws ConcurrentException { + try { + temporary.mkdir(); + final Local local = LocalFactory.get(temporary, metadata.filename); + final Path file = new Path(directory, metadata.filename, EnumSet.of(Path.Type.file)); + final Read read = session.getFeature(Read.class); + RemoteIndexProfilesFinder.log.info("Download profile {}", file); +// Read latest version + try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) + .setVersionId(version.version_id)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { + IOUtils.copy(in, out); + } + return local; + } + catch(BackgroundException | IOException e) { + throw new ConcurrentException(e); + } + } + }); + this.factory = factory; + this.version = version; + this.metadata = metadata; + } + + @Override + public boolean isLatest() { + return Boolean.TRUE.equals(version.latest); + } + + @Override + public boolean isEnabled() { + return factory.isEnabled(metadata.protocol, metadata.vendor); + } + + @Override + public boolean isBundled() { + return false; + } + + @Override + public String getIdentifier() { + return metadata.protocol; + } + + @Override + public String getProvider() { + return metadata.vendor; + } + + @Override + public String getName() { + return factory.forName(metadata.protocol).getName(); + } + + @Override + public String getDescription() { + if(null == metadata.description) { + return StringUtils.EMPTY; + } + return metadata.description; + } + + @Override + public String getHelp() { + return metadata.help; + } + + @Override + public String getThumbnail() { + if(null == metadata.thumbnail) { + return factory.forName(metadata.protocol).disk(); + } + return metadata.thumbnail; + } + } } From 726e4cdb666b6b76cf53a528a23031df8c5568b5 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 19 May 2026 19:11:36 +0200 Subject: [PATCH 25/33] Reuse existing BitmapSource for same Thumbnail hashcode --- .../src/main/csharp/Services/WpfIconProvider.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs index e303505560..068ba584aa 100644 --- a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs +++ b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs @@ -39,9 +39,16 @@ namespace Ch.Cyberduck.Core.Refresh.Services public IEnumerable GetResources(string name) => Get(name, name, default, false, out var _); public BitmapSource GetThumbnail(ProfileDescription profile, int size) - => IconCache.TryGetIcon(profile, size, out BitmapSource image, "Thumbnail") - ? image : Get(profile, profile.getThumbnail(), size, "Thumbnail"); + { + var thumbnail = profile.getThumbnail(); + if (!IconCache.TryGetIcon(thumbnail.GetHashCode(), size, out BitmapSource image, "Thumbnail")) + { + image = Get(thumbnail.GetHashCode(), profile.getThumbnail(), size, "Thumbnail"); + } + return image; + } + protected override BitmapSource Get(IntPtr nativeIcon, CacheIconCallback cacheIcon) { var source = Imaging.CreateBitmapSourceFromHIcon(nativeIcon, default, default); From 0d702b424deab77831706efaa46748743ed80c9c Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 19 May 2026 19:11:49 +0200 Subject: [PATCH 26/33] Use ProfileDescription when fetching Thumbnail --- .../refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs b/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs index aae9cd16eb..2c8e112d43 100644 --- a/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs +++ b/core/native/refresh/src/main/csharp/UserControls/ProfileElement.xaml.cs @@ -18,7 +18,7 @@ namespace Ch.Cyberduck.Core.Refresh.UserControls { d(this.OneWayBind(ViewModel, vm => vm.Name, v => v.ProtocolType.Text)); d(this.OneWayBind(ViewModel, vm => vm.Description, v => v.Description.Text)); - d(this.OneWayBind(ViewModel, vm => vm.Thumbnail, v => v.ProfileIcon.Source, p => wpfIconProvider.GetDisk(p, 32))); + d(this.OneWayBind(ViewModel, vm => vm.ProfileDescription, v => v.ProfileIcon.Source, p => wpfIconProvider.GetThumbnail(p, 32))); d(this.OneWayBind(ViewModel, vm => vm.DefaultHostName, v => v.ToolTipEnabled, v => !string.IsNullOrWhiteSpace(v))); d(this.OneWayBind(ViewModel, vm => vm.DefaultHostName, v => v.ToolTip)); d(this.OneWayBind(ViewModel, vm => vm.Enabled, v => v.Checked.IsEnabled)); From 19472b0e393e99872649b74130cca3dc0c1ea2c1 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 19 May 2026 20:43:55 +0200 Subject: [PATCH 27/33] Fix SearchProfilePredicate crashing the application due to NullReference exception in ProfileDescription.getName --- .../Preferences/Pages/ProfilesViewModel.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs index bd1f05cb28..57745cbd72 100644 --- a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs +++ b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs @@ -55,10 +55,24 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages .Filter(this.WhenAnyValue(v => v.FilterText) .Throttle(TimeSpan.FromMilliseconds(500)) .DistinctUntilChanged() - .Select(v => (Func)new SearchProfilePredicate(v).test)) + .Select(v => + { + SearchProfilePredicate profile = new(v); + return (Func)(p => + { + try + { + return profile.test(p); + } + catch + { + return false; + } + }); + })) .Transform(x => new ProfileViewModel(x)) .AsObservableList(); - + profiles.Connect() .Sort(SortExpressionComparer.Ascending(x => x.Description)) .ObserveOnDispatcher() From 7df6bf9cecfd0a9a8c866d2e24d3207119af8d06 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Tue, 19 May 2026 20:44:28 +0200 Subject: [PATCH 28/33] Implement Base64-parsing for Protocol Disk and Icon, and ProfileDescription Thumbnail --- .../main/csharp/Services/WpfIconProvider.cs | 75 +++++++++---------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs index 068ba584aa..a5349ba85d 100644 --- a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs +++ b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Text; using System.Collections.Generic; using System.IO; using System.Linq; @@ -23,32 +24,32 @@ namespace Ch.Cyberduck.Core.Refresh.Services public override BitmapSource GetDisk(Protocol protocol, int size) => IconCache.TryGetIcon(protocol, size, out BitmapSource image, "Disk") ? image - : Get(protocol, protocol.disk(), size, "Disk"); + : Get(protocol, protocol.disk(), size, "Disk", true); public IEnumerable GetDisk(Protocol protocol) - => Get(protocol, protocol.disk(), "Disk", false, out _); + => Get(protocol, protocol.disk(), "Disk", false, true, out _); public override BitmapSource GetIcon(Protocol protocol, int size) => IconCache.TryGetIcon(protocol, size, out BitmapSource image, "Icon") ? image - : Get(protocol, protocol.icon(), size, "Icon"); + : Get(protocol, protocol.icon(), size, "Icon", true); public IEnumerable GetIcon(Protocol protocol) - => Get(protocol, protocol.icon(), "Icon", false, out _); + => Get(protocol, protocol.icon(), "Icon", false, true, out _); - public IEnumerable GetResources(string name) => Get(name, name, default, false, out var _); + public IEnumerable GetResources(string name) => Get(name, name, default, false, false, out var _); public BitmapSource GetThumbnail(ProfileDescription profile, int size) { var thumbnail = profile.getThumbnail(); if (!IconCache.TryGetIcon(thumbnail.GetHashCode(), size, out BitmapSource image, "Thumbnail")) { - image = Get(thumbnail.GetHashCode(), profile.getThumbnail(), size, "Thumbnail"); + image = Get(thumbnail.GetHashCode(), profile.getThumbnail(), size, "Thumbnail", true); } return image; } - + protected override BitmapSource Get(IntPtr nativeIcon, CacheIconCallback cacheIcon) { var source = Imaging.CreateBitmapSourceFromHIcon(nativeIcon, default, default); @@ -57,10 +58,10 @@ namespace Ch.Cyberduck.Core.Refresh.Services } protected override BitmapSource Get(string name, int size) - => Get(name, name, size, default); + => Get(name, name, size, default, false); protected override BitmapSource Get(string name) - => Get(name, name, default); + => Get(name, name, default, false); protected override BitmapSource NearestFit(IEnumerable sources, int size, CacheIconCallback cacheCallback) { @@ -118,21 +119,21 @@ namespace Ch.Cyberduck.Core.Refresh.Services return writeableBitmap; } - private BitmapSource Get(object key, string path, string classifier) + private BitmapSource Get(object key, string path, string classifier, bool isBase64) => IconCache.TryGetIcon(key, out BitmapSource image, classifier) ? image - : Get(key, path, 0, classifier, true); + : Get(key, path, 0, classifier, true, isBase64); - private BitmapSource Get(object key, string path, int size, string classifier) + private BitmapSource Get(object key, string path, int size, string classifier, bool isBase64) => IconCache.TryGetIcon(key, size, out BitmapSource image, classifier) ? image - : Get(key, path, size, classifier, false); + : Get(key, path, size, classifier, false, isBase64); - private BitmapSource Get(object key, string path, int size, string classifier, bool returnDefault) + private BitmapSource Get(object key, string path, int size, string classifier, bool returnDefault, bool isBase64) { using (IconCache.UpgradeableReadLock()) { - var images = Get(key, path, classifier, returnDefault, out var image); + var images = Get(key, path, classifier, returnDefault, isBase64, out var image); return image ?? NearestFit(images, size, (c, s, i) => { c.CacheIcon(key, s, i, classifier); @@ -141,7 +142,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services } } - private IEnumerable Get(object key, string name, string classifier, bool returnDefault, out BitmapSource @default) + private IEnumerable Get(object key, string name, string classifier, bool returnDefault, bool isBase64, out BitmapSource @default) { BitmapSource image = default; var images = IconCache.Filter(((object key, string classifier, int) f) => Equals(key, f.key) && Equals(classifier, f.classifier)); @@ -162,7 +163,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services IconCache.CacheIcon(key, s, classifier); } IconCache.CacheIcon(key, s, i, classifier); - }); + }, isBase64); } } @default = image; @@ -170,9 +171,9 @@ namespace Ch.Cyberduck.Core.Refresh.Services return images; } - private IEnumerable GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon) + private IEnumerable GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, bool isBase64) { - if (!TryGetBase64Images(name, getCache, cacheIcon, out var images)) + if (!isBase64 || !TryGetBase64Images(name, getCache, cacheIcon, out var images)) { using var stream = GetStream(name); images = GetImages(stream, getCache, cacheIcon); @@ -181,32 +182,26 @@ namespace Ch.Cyberduck.Core.Refresh.Services return images; } - private unsafe bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) + private bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) { #if NETCOREAPP - if (System.Buffers.Text.Base64.IsValid(name)) + if (!Base64.IsValid(name)) { - var buffer = MemoryMarshal.AsBytes(name.AsSpan()); - fixed (byte* nameLocal = buffer) - { - using UnmanagedMemoryStream bufferStream = new(nameLocal, 0, buffer.Length, FileAccess.Read); - // Instead of prefilling the buffer like in .NET Framework, just convert the Base64-buffer on-the-fly. - using var memoryStream = Encoding.CreateTranscodingStream(bufferStream, Encoding.Unicode, Encoding.UTF8); - using CryptoStream imageStream = new(memoryStream, new FromBase64Transform(), CryptoStreamMode.Read); -#else - try - { - using (MemoryStream imageStream = new(Convert.FromBase64String(name), false)) - { -#endif - images = GetImages(imageStream, getCache, cacheIcon); - return true; - } - } // Close the fixed-Block. -#if !NETCOREAPP - catch { /* We don't have an easy way of validating Base64 input. Let it error out. */ } + goto exit; + } #endif + try + { + using MemoryStream imageStream = new(Convert.FromBase64String(name), false); + images = GetImages(imageStream, getCache, cacheIcon); + return true; + } + catch { /* We don't have an easy way of validating Base64 input. Let it error out. */ } + +#if NETCOREAPP + exit: +#endif images = null; return false; } From 703b25fca9a7a8d42168b339cd979064d186bd59 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 20 May 2026 13:41:30 +0200 Subject: [PATCH 29/33] Fix null pointer on unknown protocol in index. Revert "Fix SearchProfilePredicate crashing the application due to NullReference exception in ProfileDescription.getName" This reverts commit 19472b0e393e99872649b74130cca3dc0c1ea2c1. --- .../Preferences/Pages/ProfilesViewModel.cs | 18 +--- .../profiles/RemoteIndexProfilesFinder.java | 89 ++++++++++--------- .../RemoteIndexProfilesFinderTest.java | 32 ++++--- 3 files changed, 72 insertions(+), 67 deletions(-) diff --git a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs index 57745cbd72..bd1f05cb28 100644 --- a/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs +++ b/core/native/refresh/src/main/csharp/ViewModels/Preferences/Pages/ProfilesViewModel.cs @@ -55,24 +55,10 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages .Filter(this.WhenAnyValue(v => v.FilterText) .Throttle(TimeSpan.FromMilliseconds(500)) .DistinctUntilChanged() - .Select(v => - { - SearchProfilePredicate profile = new(v); - return (Func)(p => - { - try - { - return profile.test(p); - } - catch - { - return false; - } - }); - })) + .Select(v => (Func)new SearchProfilePredicate(v).test)) .Transform(x => new ProfileViewModel(x)) .AsObservableList(); - + profiles.Connect() .Sort(SortExpressionComparer.Ascending(x => x.Description)) .ObserveOnDispatcher() diff --git a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java index 6e328cdc13..1f9a4d39e3 100644 --- a/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java +++ b/core/src/main/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinder.java @@ -21,6 +21,7 @@ import ch.cyberduck.core.DefaultPathAttributes; import ch.cyberduck.core.Local; import ch.cyberduck.core.LocalFactory; import ch.cyberduck.core.Path; +import ch.cyberduck.core.Protocol; import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.BackgroundException; @@ -67,43 +68,43 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { } /** - * { - * "filename": "AWS PrivateLink for Amazon S3 (VPC endpoint).cyberduckprofile", - * "protocol": "s3", - * "vendor": "s3-privatelink", - * "versions": [ - * { - * "checksum": "255b117667ac1f0fd1ecfe25ae99440d", - * "modified": "2025-12-02T09:09:07Z", - * "version_id": "sMABDfcz3m0pwQeU85uI2.pW.JTGGT4T", - * "latest": true - * }, - * { - * "checksum": "9a6fcc93e68a669952da5e47719af3c9", - * "modified": "2022-09-15T12:55:09Z", - * "version_id": ".iXSL9g6EWEIthW6gENMGgSileSI0XG8", - * "latest": false - * }, - * { - * "checksum": "3fbf79c16d3187f135a5fbf32e0cbaf7", - * "modified": "2021-11-30T11:54:30Z", - * "version_id": "S71y2mc.dq8BXtF11g85Z9ZLvulhZDJk", - * "latest": false - * }, - * { - * "checksum": "3fbf79c16d3187f135a5fbf32e0cbaf7", - * "modified": "2021-10-27T06:00:55Z", - * "version_id": "E.cXq21hr0xLkqtNMUZjBuZaj2HfA.n9", - * "latest": false - * }, - * { - * "checksum": "7188dafb0c649fe123b70cbe5b7bfa40", - * "modified": "2021-10-27T06:00:49Z", - * "version_id": "nosIgW.rj3jrGn9jwdf_S.8fnKMO2ZW1", - * "latest": false - * } - * ] - * } + * { + * "filename": "AWS PrivateLink for Amazon S3 (VPC endpoint).cyberduckprofile", + * "protocol": "s3", + * "vendor": "s3-privatelink", + * "versions": [ + * { + * "checksum": "255b117667ac1f0fd1ecfe25ae99440d", + * "modified": "2025-12-02T09:09:07Z", + * "version_id": "sMABDfcz3m0pwQeU85uI2.pW.JTGGT4T", + * "latest": true + * }, + * { + * "checksum": "9a6fcc93e68a669952da5e47719af3c9", + * "modified": "2022-09-15T12:55:09Z", + * "version_id": ".iXSL9g6EWEIthW6gENMGgSileSI0XG8", + * "latest": false + * }, + * { + * "checksum": "3fbf79c16d3187f135a5fbf32e0cbaf7", + * "modified": "2021-11-30T11:54:30Z", + * "version_id": "S71y2mc.dq8BXtF11g85Z9ZLvulhZDJk", + * "latest": false + * }, + * { + * "checksum": "3fbf79c16d3187f135a5fbf32e0cbaf7", + * "modified": "2021-10-27T06:00:55Z", + * "version_id": "E.cXq21hr0xLkqtNMUZjBuZaj2HfA.n9", + * "latest": false + * }, + * { + * "checksum": "7188dafb0c649fe123b70cbe5b7bfa40", + * "modified": "2021-10-27T06:00:49Z", + * "version_id": "nosIgW.rj3jrGn9jwdf_S.8fnKMO2ZW1", + * "latest": false + * } + * ] + * } */ @Override public Set find(final Visitor visitor) throws BackgroundException { @@ -181,7 +182,7 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { final Path file = new Path(directory, metadata.filename, EnumSet.of(Path.Type.file)); final Read read = session.getFeature(Read.class); RemoteIndexProfilesFinder.log.info("Download profile {}", file); -// Read latest version + // Read latest version try(InputStream in = read.read(file.withAttributes(new DefaultPathAttributes(file.attributes()) .setVersionId(version.version_id)), new TransferStatus().setLength(TransferStatus.UNKNOWN_LENGTH), ConnectionCallback.noop); OutputStream out = local.getOutputStream(false)) { IOUtils.copy(in, out); @@ -225,7 +226,11 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { @Override public String getName() { - return factory.forName(metadata.protocol).getName(); + final Protocol protocol = factory.forName(metadata.protocol); + if(null == protocol) { + return null; + } + return protocol.getName(); } @Override @@ -244,7 +249,11 @@ public class RemoteIndexProfilesFinder implements ProfilesFinder { @Override public String getThumbnail() { if(null == metadata.thumbnail) { - return factory.forName(metadata.protocol).disk(); + final Protocol protocol = factory.forName(metadata.protocol); + if(null == protocol) { + return null; + } + return protocol.disk(); } return metadata.thumbnail; } diff --git a/s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java b/s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java index f8fc06d2f8..2e164242e7 100644 --- a/s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java +++ b/s3/src/test/java/ch/cyberduck/core/profiles/RemoteIndexProfilesFinderTest.java @@ -31,6 +31,7 @@ import ch.cyberduck.core.ssl.DisabledX509TrustManager; import ch.cyberduck.core.threading.CancelCallback; import ch.cyberduck.test.IntegrationTest; +import org.apache.commons.lang3.StringUtils; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -38,31 +39,40 @@ import java.util.Collections; import java.util.Set; import java.util.stream.Collectors; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; @Category(IntegrationTest.class) public class RemoteIndexProfilesFinderTest { @Test public void testFind() throws Exception { - final ProtocolFactory protocols = new ProtocolFactory(Collections.singleton(new S3Protocol() { + final S3Protocol protocol = new S3Protocol() { @Override public boolean isEnabled() { return true; } - })); + }; + final ProtocolFactory protocols = new ProtocolFactory(Collections.singleton(protocol)); final Session session = new S3Session(new HostParser(protocols).get("s3:/profiles.cyberduck.io") .setCredentials(new Credentials(PreferencesFactory.get().getProperty("connection.login.anon.name"))), new DisabledX509TrustManager(), new DefaultX509KeyManager()); session.open(new DisabledProxyFinder(), HostKeyCallback.noop, LoginCallback.noop, CancelCallback.noop); - final RemoteIndexProfilesFinder finder = new RemoteIndexProfilesFinder(session); - final Set stream = finder.find(); - assertFalse(stream.isEmpty()); + final RemoteIndexProfilesFinder finder = new RemoteIndexProfilesFinder(protocols, session); + final Set set = finder.find(); + assertFalse(set.isEmpty()); // Check for versions of S3 (HTTP).cyberduckprofile - assertFalse(stream.stream().filter(ProfileDescription::isLatest).collect(Collectors.toSet()).isEmpty()); - assertFalse(stream.stream().filter(description -> !description.isLatest()).collect(Collectors.toSet()).isEmpty()); - assertTrue(stream.stream().anyMatch(description -> description.getChecksum().equals(Checksum.parse("b9afd8d6da91e7b520559fa9eaac54c1")))); - assertTrue(stream.stream().anyMatch(description -> description.getChecksum().equals(Checksum.parse("19ecbfe2d8f09644197c1ef53e207792")))); + assertFalse(set.stream().filter(ProfileDescription::isLatest).collect(Collectors.toSet()).isEmpty()); + assertFalse(set.stream().filter(description -> !description.isLatest()).collect(Collectors.toSet()).isEmpty()); + assertTrue(set.stream().anyMatch(description -> description.getChecksum().equals(Checksum.parse("b9afd8d6da91e7b520559fa9eaac54c1")))); + assertTrue(set.stream().anyMatch(description -> description.getChecksum().equals(Checksum.parse("19ecbfe2d8f09644197c1ef53e207792")))); + set.forEach(d -> { + if(protocol.getIdentifier().equals(d.getIdentifier())) { + assertNotNull(d.getName()); + } + else { + assertNull(d.getName()); + } + assertTrue(new SearchProfilePredicate(StringUtils.EMPTY).test(d)); + }); session.close(); } } From 89fa7e561c3c2571782f4e1a1a19fde98055b52e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 20 May 2026 14:12:41 +0200 Subject: [PATCH 30/33] Fix test. --- .../core/profiles/LocalProfilesFinderTest.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/core/src/test/java/ch/cyberduck/core/profiles/LocalProfilesFinderTest.java b/core/src/test/java/ch/cyberduck/core/profiles/LocalProfilesFinderTest.java index 34a61582cd..b60eb7cb83 100644 --- a/core/src/test/java/ch/cyberduck/core/profiles/LocalProfilesFinderTest.java +++ b/core/src/test/java/ch/cyberduck/core/profiles/LocalProfilesFinderTest.java @@ -16,10 +16,8 @@ package ch.cyberduck.core.profiles; */ import ch.cyberduck.core.Local; -import ch.cyberduck.core.Profile; import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.TestProtocol; -import ch.cyberduck.core.serializer.impl.dd.ProfilePlistReader; import org.junit.Test; @@ -32,7 +30,7 @@ public class LocalProfilesFinderTest { @Test public void find() throws Exception { - final ProfilePlistReader reader = new ProfilePlistReader(new ProtocolFactory(Collections.singleton(new TestProtocol() { + final ProtocolFactory protocols = new ProtocolFactory(Collections.singleton(new TestProtocol() { @Override public Type getType() { return Type.s3; @@ -42,11 +40,8 @@ public class LocalProfilesFinderTest { public boolean isEnabled() { return false; } - }))); - final Profile profile = reader.read( - new Local("src/test/resources/Test S3 (HTTP).cyberduckprofile") - ); - final LocalProfilesFinder finder = new LocalProfilesFinder(ProtocolFactory.get(), new Local("src/test/resources/")); + })); + final LocalProfilesFinder finder = new LocalProfilesFinder(protocols, new Local("src/test/resources/")); final Set stream = finder.find(); assertFalse(stream.isEmpty()); } From 90858526a92a0e0d9a920b886f4a03dd4ab2878e Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Wed, 20 May 2026 16:02:41 +0200 Subject: [PATCH 31/33] Mirror Wpf implementation for WinForms --- .../Services/WinFormsIconProvider.Impl.cs | 115 ++++++++---------- .../csharp/Services/WinFormsIconProvider.cs | 1 - 2 files changed, 49 insertions(+), 67 deletions(-) diff --git a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs index 7e5b5bb2ad..f53a14532f 100644 --- a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs +++ b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs @@ -1,12 +1,10 @@ using System; +using System.Buffers.Text; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; -using System.Runtime.InteropServices; -using System.Security.Cryptography; -using System.Text; using Ch.Cyberduck.Core.Refresh.Media.Imaging; namespace Ch.Cyberduck.Core.Refresh.Services @@ -55,98 +53,83 @@ namespace Ch.Cyberduck.Core.Refresh.Services return images; } - private unsafe bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) + private bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) { #if NETCOREAPP - if (System.Buffers.Text.Base64.IsValid(name)) + if (!Base64.IsValid(name)) { - var buffer = MemoryMarshal.AsBytes(name.AsSpan()); - fixed (byte* nameLocal = buffer) - { - using UnmanagedMemoryStream bufferStream = new(nameLocal, 0, buffer.Length, FileAccess.Read); - // Instead of prefilling the buffer like in .NET Framework, just convert the Base64-buffer on-the-fly. - using var memoryStream = Encoding.CreateTranscodingStream(bufferStream, Encoding.Unicode, Encoding.UTF8); - using CryptoStream imageStream = new(memoryStream, new FromBase64Transform(), CryptoStreamMode.Read); -#else + goto exit; + } +#endif try { - using (MemoryStream imageStream = new(Convert.FromBase64String(name), false)) - { -#endif - return TryGetImages(imageStream, getCache, cacheIcon, out images); - } + using MemoryStream imageStream = new(Convert.FromBase64String(name), false); + return TryGetImages(imageStream, getCache, cacheIcon, out images); } -#if !NETCOREAPP catch { /* We don't have an easy way of validating Base64 input. Let it error out. */ } -#endif +#if NETCOREAPP + exit: +#endif images = null; return false; } private bool TryGetImages(Stream stream, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable images) { - var source = Image.FromStream(stream); + using var source = Image.FromStream(stream); if (ImageFormat.Gif.Equals(source.RawFormat)) { - // Gif cannot be cached/duplicated/or anything else, really. - // underlying stream must not be closed (learned the hard way). - //dispose = false; - cacheIcon(IconCache, source.Width, source); - images = [source]; - return true; + MemoryStream gifStream = new(); + source.Save(gifStream, ImageFormat.Gif); + var image = Image.FromStream(gifStream); + cacheIcon(IconCache, source.Width, image); + images = [image]; } - - //dispose = true; - using (source) + else if (ImageFormat.Icon.Equals(source.RawFormat)) { - if (ImageFormat.Icon.Equals(source.RawFormat)) + if (!stream.CanSeek) { - if (!stream.CanSeek) - { - images = null; - return false; - } - - source.Dispose(); - stream.Position = 0; - GDIIcon icon = new(stream); - foreach (var item in icon.Frames) - { - cacheIcon(IconCache, item.Width, item); - } - images = icon.Frames; - return true; + images = null; + return false; } - else if (ImageFormat.Tiff.Equals(source.RawFormat)) - { - List frames = new(); - FrameDimension frameDimension = new(source.FrameDimensionsList[0]); - var pageCount = source.GetFrameCount(FrameDimension.Page); - for (int i = 0; i < pageCount; i++) - { - source.SelectActiveFrame(frameDimension, i); - if (getCache(IconCache, source.Width)) continue; - Bitmap copy = new(source); - copy.SetResolution(96, 96); - frames.Add(copy); - cacheIcon(IconCache, copy.Width, copy); - } - images = frames; - return true; - } - else + stream.Position = 0; + GDIIcon icon = new(stream); + foreach (var item in icon.Frames) { + cacheIcon(IconCache, item.Width, item); + } + + images = icon.Frames; + } + else if (ImageFormat.Tiff.Equals(source.RawFormat)) + { + List frames = new(); + FrameDimension frameDimension = new(source.FrameDimensionsList[0]); + var pageCount = source.GetFrameCount(FrameDimension.Page); + for (int i = 0; i < pageCount; i++) + { + source.SelectActiveFrame(frameDimension, i); + if (getCache(IconCache, source.Width)) continue; + Bitmap copy = new(source); copy.SetResolution(96, 96); + frames.Add(copy); cacheIcon(IconCache, copy.Width, copy); - images = [copy]; - return true; } + images = frames; + } + else + { + Bitmap copy = new(source); + copy.SetResolution(96, 96); + cacheIcon(IconCache, copy.Width, copy); + images = [copy]; } + return true; } } } diff --git a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.cs b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.cs index 6878b2ed2b..5f076673fb 100644 --- a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.cs +++ b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; -using static Windows.Win32.CorePInvoke; namespace Ch.Cyberduck.Core.Refresh.Services { From afa0ff8969339e37b5b1196eb6b29459894b55d4 Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Wed, 20 May 2026 16:03:07 +0200 Subject: [PATCH 32/33] Handle null thumbnail --- .../src/main/csharp/Services/WpfIconProvider.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs index a5349ba85d..331eeb6a99 100644 --- a/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs +++ b/core/native/refresh/src/main/csharp/Services/WpfIconProvider.cs @@ -41,10 +41,15 @@ namespace Ch.Cyberduck.Core.Refresh.Services public BitmapSource GetThumbnail(ProfileDescription profile, int size) { - var thumbnail = profile.getThumbnail(); - if (!IconCache.TryGetIcon(thumbnail.GetHashCode(), size, out BitmapSource image, "Thumbnail")) + if (profile.getThumbnail() is not { } thumbnail) { - image = Get(thumbnail.GetHashCode(), profile.getThumbnail(), size, "Thumbnail", true); + return null; + } + + var key = thumbnail.GetHashCode(); + if (!IconCache.TryGetIcon(key, size, out BitmapSource image, "Thumbnail")) + { + image = Get(key, thumbnail, size, "Thumbnail", true); } return image; From 91408de42027cf7710697cb7b5356903ef0b72fe Mon Sep 17 00:00:00 2001 From: AliveDevil Date: Wed, 20 May 2026 16:14:00 +0200 Subject: [PATCH 33/33] Remove unused argument --- .../src/main/csharp/Services/WinFormsIconProvider.Impl.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs index f53a14532f..7f8fef7b40 100644 --- a/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs +++ b/core/native/refresh/src/main/csharp/Services/WinFormsIconProvider.Impl.cs @@ -33,7 +33,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services IconCache.CacheIcon(key, s, classifier); } IconCache.CacheIcon(key, s, i, classifier); - }, out _); + }); } } @@ -41,7 +41,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services return images; } - private IEnumerable GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out bool dispose) + private IEnumerable GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon) { if (!TryGetBase64Images(name, getCache, cacheIcon, out var images)) { @@ -49,7 +49,6 @@ namespace Ch.Cyberduck.Core.Refresh.Services _ = TryGetImages(stream, getCache, cacheIcon, out images); } - dispose = false; return images; }