Merge pull request #18098 from iterate-ch/bugfix/GH-17744

Load profiles from precompiled index file
This commit is contained in:
David Kocher
2026-05-21 11:39:09 +02:00
committed by GitHub
22 changed files with 708 additions and 341 deletions
@@ -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;
@@ -42,7 +44,7 @@ public class NSImageIconCache implements IconCache<NSImage> {
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 +90,7 @@ public class NSImageIconCache implements IconCache<NSImage> {
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 +100,8 @@ public class NSImageIconCache implements IconCache<NSImage> {
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 +120,9 @@ public class NSImageIconCache implements IconCache<NSImage> {
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 +134,7 @@ public class NSImageIconCache implements IconCache<NSImage> {
* @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()),
@@ -155,20 +156,25 @@ public class NSImageIconCache implements IconCache<NSImage> {
*/
@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"))) {
return this.cache(FilenameUtils.getName(name), this.convert(FilenameUtils.getName(name),
NSImage.imageWithContentsOfFile(name), width, height), width);
if(name.contains(PreferencesFactory.get().getProperty("local.delimiter"))) {
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 +191,7 @@ public class NSImageIconCache implements IconCache<NSImage> {
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 +212,7 @@ public class NSImageIconCache implements IconCache<NSImage> {
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 +279,14 @@ public class NSImageIconCache implements IconCache<NSImage> {
@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;
}
@@ -1,9 +1,11 @@
using Ch.Cyberduck.Core.Refresh.Media.Imaging;
using System;
using System.Buffers.Text;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using Ch.Cyberduck.Core.Refresh.Media.Imaging;
namespace Ch.Cyberduck.Core.Refresh.Services
{
@@ -19,32 +21,19 @@ namespace Ch.Cyberduck.Core.Refresh.Services
using (IconCache.WriteLock())
{
bool isDefault = !IconCache.TryGetIcon<Image>(key, out _, classifier);
Stream stream = default;
bool dispose = true;
try
images = GetImages(path, (c, s) => c.TryGetIcon<Image>(key, out _, classifier), (c, s, i) =>
{
stream = GetStream(path);
images = GetImages(stream, (c, s) => c.TryGetIcon<Image>(key, out _, classifier), (c, s, i) =>
if (isDefault)
{
if (isDefault)
isDefault = false;
if (returnDefault)
{
isDefault = false;
if (returnDefault)
{
image = i;
}
IconCache.CacheIcon<Image>(key, s, classifier);
image = i;
}
IconCache.CacheIcon(key, s, i, classifier);
}, out dispose);
}
finally
{
if (dispose && stream != null)
{
stream.Dispose();
IconCache.CacheIcon<Image>(key, s, classifier);
}
}
IconCache.CacheIcon(key, s, i, classifier);
});
}
}
@@ -52,58 +41,94 @@ namespace Ch.Cyberduck.Core.Refresh.Services
return images;
}
private IEnumerable<Image> GetImages(Stream stream, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out bool dispose)
private IEnumerable<Image> GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon)
{
Image source = Image.FromStream(stream);
if (!TryGetBase64Images(name, getCache, cacheIcon, out var images))
{
using var stream = GetStream(name);
_ = TryGetImages(stream, getCache, cacheIcon, out images);
}
return images;
}
private bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable<Image> images)
{
#if NETCOREAPP
if (!Base64.IsValid(name))
{
goto exit;
}
#endif
try
{
using MemoryStream imageStream = new(Convert.FromBase64String(name), false);
return TryGetImages(imageStream, getCache, cacheIcon, out images);
}
catch { /* We don't have an easy way of validating Base64 input. Let it error out. */ }
#if NETCOREAPP
exit:
#endif
images = null;
return false;
}
private bool TryGetImages(Stream stream, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable<Image> images)
{
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);
return new[] { source };
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)
{
source.Dispose();
stream.Position = 0;
GDIIcon icon = new(stream);
foreach (var item in icon.Frames)
{
cacheIcon(IconCache, item.Width, item);
}
return icon.Frames;
images = null;
return false;
}
else if (ImageFormat.Tiff.Equals(source.RawFormat))
{
List<Image> 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);
}
return frames;
}
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<Image> 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);
return new[] { copy };
}
images = frames;
}
else
{
Bitmap copy = new(source);
copy.SetResolution(96, 96);
cacheIcon(IconCache, copy.Width, copy);
images = [copy];
}
return true;
}
}
}
@@ -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
{
@@ -1,16 +1,20 @@
using System;
using System.Buffers.Text;
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<BitmapSource>
{
public WpfIconProvider(IconCache cache, IIconProviderImageSource BitmapSource) : base(cache, BitmapSource)
@@ -20,20 +24,36 @@ 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<BitmapSource> 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<BitmapSource> GetIcon(Protocol protocol)
=> Get(protocol, protocol.icon(), "Icon", false, out _);
=> Get(protocol, protocol.icon(), "Icon", false, true, out _);
public IEnumerable<BitmapSource> GetResources(string name) => Get(name, name, default, false, out var _);
public IEnumerable<BitmapSource> GetResources(string name) => Get(name, name, default, false, false, out var _);
public BitmapSource GetThumbnail(ProfileDescription profile, int size)
{
if (profile.getThumbnail() is not { } thumbnail)
{
return null;
}
var key = thumbnail.GetHashCode();
if (!IconCache.TryGetIcon(key, size, out BitmapSource image, "Thumbnail"))
{
image = Get(key, thumbnail, size, "Thumbnail", true);
}
return image;
}
protected override BitmapSource Get(IntPtr nativeIcon, CacheIconCallback cacheIcon)
{
@@ -43,10 +63,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<BitmapSource> sources, int size, CacheIconCallback cacheCallback)
{
@@ -104,21 +124,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);
@@ -127,7 +147,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services
}
}
private IEnumerable<BitmapSource> Get(object key, string path, string classifier, bool returnDefault, out BitmapSource @default)
private IEnumerable<BitmapSource> Get(object key, string name, string classifier, bool returnDefault, bool isBase64, out BitmapSource @default)
{
BitmapSource image = default;
var images = IconCache.Filter<BitmapSource>(((object key, string classifier, int) f) => Equals(key, f.key) && Equals(classifier, f.classifier));
@@ -136,8 +156,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services
using (IconCache.WriteLock())
{
bool isDefault = !IconCache.TryGetIcon<BitmapSource>(key, out _, classifier);
using Stream stream = GetStream(path);
images = GetImages(stream, (c, s) => c.TryGetIcon<BitmapSource>(key, s, out _, classifier), (c, s, i) =>
images = GetImages(name, (c, s) => c.TryGetIcon<BitmapSource>(key, s, out _, classifier), (c, s, i) =>
{
if (isDefault)
{
@@ -149,7 +168,7 @@ namespace Ch.Cyberduck.Core.Refresh.Services
IconCache.CacheIcon<BitmapSource>(key, s, classifier);
}
IconCache.CacheIcon(key, s, i, classifier);
});
}, isBase64);
}
}
@default = image;
@@ -157,6 +176,41 @@ namespace Ch.Cyberduck.Core.Refresh.Services
return images;
}
private IEnumerable<BitmapSource> GetImages(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, bool isBase64)
{
if (!isBase64 || !TryGetBase64Images(name, getCache, cacheIcon, out var images))
{
using var stream = GetStream(name);
images = GetImages(stream, getCache, cacheIcon);
}
return images;
}
private bool TryGetBase64Images(string name, GetCacheIconCallback getCache, CacheIconCallback cacheIcon, out IEnumerable<BitmapSource> images)
{
#if NETCOREAPP
if (!Base64.IsValid(name))
{
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;
}
private IEnumerable<BitmapSource> GetImages(Stream stream, GetCacheIconCallback getCache, CacheIconCallback cacheIcon)
{
var list = new List<BitmapSource>();
@@ -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.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.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));
});
@@ -1,9 +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
{
@@ -11,24 +14,25 @@ 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<Host>(BookmarkCollection.defaultCollection()).Any(x => x.getProtocol().Equals(Profile)));
ProfileDescription = description;
Installed = description.isInstalled() && description.isEnabled();
Enabled = !description.isBundled()
&& !BookmarkCollection.defaultCollection().AsEnumerable<Host>()
.Any(host => IsDefaultProfile(description, host));
OpenHelp = ReactiveCommand.Create(() =>
{
BrowserLauncherFactory.get().open(ProviderHelpServiceFactory.get().help(Profile));
});
BrowserLauncherFactory.get().open(ProfileDescription.getHelp());
}, Observable.Return(!string.IsNullOrWhiteSpace(ProfileDescription.getHelp())));
}
public bool IsEnabled { get; }
public string DefaultHostName => string.Empty;
public string DefaultHostName => Profile.getDefaultHostname();
public string Description => ProfileDescription.getDescription();
public string Description => Profile.getDescription();
public bool Enabled { get; }
public bool Installed
{
@@ -36,12 +40,19 @@ namespace Ch.Cyberduck.Core.Refresh.ViewModels.Preferences.Pages
set => this.RaiseAndSetIfChanged(ref installed, value);
}
public string Name => Profile.getName();
public string Name => ProfileDescription.getName();
public ReactiveCommand<Unit, Unit> OpenHelp { get; }
public Profile Profile { get; }
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());
}
}
}
@@ -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<ProfileViewModel>.Ascending(x => x.Profile))
.Sort(SortExpressionComparer<ProfileViewModel>.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<IEnumerable<ProfileDescription>> completionSource;
public InternalProfilesSynchronizeWorker(ProtocolFactory protocolFactory, TaskCompletionSource<IEnumerable<ProfileDescription>> completionSource) : base(protocolFactory, ProfilesFinder.Visitor.Prefetch)
public InternalProfilesSynchronizeWorker(ProtocolFactory protocols, TaskCompletionSource<IEnumerable<ProfileDescription>> completionSource) : base(protocols, ProfilesFinder.Visitor.Noop)
{
this.completionSource = completionSource;
}
@@ -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
@@ -191,17 +180,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
@@ -376,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
@@ -406,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);
@@ -806,7 +752,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();
}
@@ -222,6 +222,8 @@ public interface Protocol extends FeatureFactory, Comparable<Protocol>, Serializ
String getRegion();
/**
* Either a filename or Base64 encoded image data
*
* @return A mounted disk icon to display
*/
String disk();
@@ -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
*/
@@ -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();
}
@@ -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<Protocol> parent, final Checksum checksum, final Local local) {
this(protocols, parent, new LazyInitializer<Checksum>() {
public ProfileDescription(final ProtocolFactory protocols, final Predicate<Protocol> filter, final Checksum checksum, final Local local) {
this(protocols, filter, new LazyInitializer<Checksum>() {
@Override
protected Checksum initialize() {
return checksum;
@@ -71,7 +71,7 @@ public class ProfileDescription {
});
}
public ProfileDescription(final ProtocolFactory protocols, final Predicate<Protocol> parent,
public ProfileDescription(final ProtocolFactory protocols, final Predicate<Protocol> filter,
final LazyInitializer<Checksum> checksum, final LazyInitializer<Local> 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());
@@ -143,6 +143,36 @@ public class ProfileDescription {
}
}
public String getIdentifier() {
final Optional<Profile> profile = this.getProfile();
return profile.map(Profile::getIdentifier).orElse(null);
}
public String getProvider() {
final Optional<Profile> profile = this.getProfile();
return profile.map(Profile::getProvider).orElse(null);
}
public String getName() {
final Optional<Profile> profile = this.getProfile();
return profile.map(Profile::getName).orElse(null);
}
public String getDescription() {
final Optional<Profile> profile = this.getProfile();
return profile.map(Profile::getDescription).orElse(null);
}
public String getHelp() {
final Optional<Profile> profile = this.getProfile();
return profile.map(Profile::getHelp).orElse(null);
}
public String getThumbnail() {
final Optional<Profile> 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> profile = this.getProfile();
return profile.map(Profile::isEnabled).orElse(false);
}
public boolean isBundled() {
final Optional<Profile> profile = this.getProfile();
return profile.map(Profile::isBundled).orElse(false);
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ProfileDescription{");
@@ -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;
@@ -0,0 +1,261 @@
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.Protocol;
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.TransferStatus;
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;
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 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 = protocols;
this.session = session;
}
/**
* {
* "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<ProfileDescription> find(final Visitor visitor) throws BackgroundException {
log.info("Fetch profiles from {}", session.getHost());
final Set<ProfileDescription> 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(visitor.visit(new RemoteIndexProfileDescription(temporary, protocols, version, metadata, directory)));
}
}
}
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("description")
private String description;
@JsonProperty("help")
private String help;
@JsonProperty("thumbnail")
private String thumbnail;
@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;
}
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<Checksum>() {
@Override
protected Checksum initialize() {
return Checksum.parse(version.checksum);
}
}, new LazyInitializer<Local>() {
@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() {
final Protocol protocol = factory.forName(metadata.protocol);
if(null == protocol) {
return null;
}
return 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) {
final Protocol protocol = factory.forName(metadata.protocol);
if(null == protocol) {
return null;
}
return protocol.disk();
}
return metadata.thumbnail;
}
}
}
@@ -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();
@@ -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,36 +51,26 @@ public class RemoteProfilesFinder implements ProfilesFinder {
private final ProtocolFactory protocols;
private final Session<?> session;
private final TransferPathFilter comparison;
private final Filter<Path> filter;
private final Local temporary;
private final Local temporary = LocalFactory.get(new TemporaryApplicationResourcesFinder().find(), "profiles");
public RemoteProfilesFinder(final Session<?> session) {
this(ProtocolFactory.get(), session);
}
public RemoteProfilesFinder(final ProtocolFactory protocols, final Session<?> session) {
this(protocols, session, new CompareFilter(new DisabledDownloadSymlinkResolver(), session), new ProfileFilter());
this(protocols, session, new ProfileFilter());
}
public RemoteProfilesFinder(final Session<?> session,
final TransferPathFilter comparison, final Filter<Path> filter) {
this(ProtocolFactory.get(), session, comparison, filter);
}
public RemoteProfilesFinder(final ProtocolFactory protocols, final Session<?> session,
final TransferPathFilter comparison, final Filter<Path> filter) {
public RemoteProfilesFinder(final ProtocolFactory protocols, final Session<?> session, final Filter<Path> filter) {
this.protocols = protocols;
this.session = session;
this.comparison = comparison;
this.filter = filter;
this.temporary = LocalFactory.get(new TemporaryApplicationResourcesFinder().find(), "profiles");
}
@Override
public Set<ProfileDescription> find(final Visitor visitor) throws BackgroundException {
log.info("Fetch profiles from {}", session.getHost());
temporary.mkdir();
final AttributedList<Path> list = session.getFeature(ListService.class).list(new DelegatingHomeFeature(
new DefaultPathHomeFeature(session.getHost())).find(), new DisabledListProgressListener());
return list.filter(filter).toStream().map(file -> visitor.visit(new RemoteProfileDescription(protocols, file,
@@ -92,18 +78,16 @@ public class RemoteProfilesFinder implements ProfilesFinder {
@Override
protected Local initialize() throws ConcurrentException {
try {
temporary.mkdir();
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) {
@@ -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<ProfileDescription> {
@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;
@@ -33,7 +33,7 @@ public interface IconCache<I> {
}
/**
* @param name Icon filename with extension
* @param name Icon filename with extension or Base64 encoded image data
* @param size Requested size
* @return Cached image
*/
@@ -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<ProfileDescription> stream = finder.find();
assertFalse(stream.isEmpty());
}
@@ -33,10 +33,8 @@ 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;
@@ -46,6 +44,7 @@ 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 +57,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 +74,8 @@ public class ProfilesPreferencesController extends BundleController {
/**
* Synchronized ist of available profiles
*/
private final Map<ProfileDescription, Profile> repository
= Collections.synchronizedMap(new LinkedHashMap<>());
private final Set<ProfileDescription> repository
= Collections.synchronizedSet(new LinkedHashSet<>());
@Delegate
private ProfilesTableDataSource profilesTableDataSource;
@@ -133,12 +132,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 +145,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 +165,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<ProfileDescription> set) {
for(ProfileDescription description : set) {
if(description.getProfile().isPresent()) {
repository.put(description, description.getProfile().get());
}
}
repository.addAll(set);
reload();
progressIndicator.stopAnimation(null);
}
@@ -194,7 +189,7 @@ public class ProfilesPreferencesController extends BundleController {
}
private ProfileDescription fromChecksum(final NSObject hash) {
final Optional<ProfileDescription> found = repository.keySet().stream()
final Optional<ProfileDescription> found = repository.stream()
.filter(description -> description.getChecksum().hash.equals(hash.toString())).findFirst();
return found.orElse(null);
}
@@ -225,7 +220,7 @@ public class ProfilesPreferencesController extends BundleController {
if(null == description) {
return null;
}
return repository.get(description).getDescription();
return description.getDescription();
}
@Override
@@ -284,7 +279,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 +287,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 +374,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 +386,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 +401,14 @@ public class ProfilesPreferencesController extends BundleController {
public void setImageView(final NSImageView imageView) {
this.imageView = imageView;
this.imageView.setImage(IconCacheFactory.<NSImage>get().iconNamed(profile.icon(), 32));
this.imageView.setImage(IconCacheFactory.<NSImage>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 +416,49 @@ 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<Local> 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<Void>() {
@Override
public Void run() {
// Download profile
final Optional<Local> file = description.getFile();
// Update with last version from repository
file.ifPresent(local -> {
repository.remove(description);
repository.add(new LocalProfileDescription(protocols, ProtocolFactory.BUNDLED_PROFILE_PREDICATE,
protocols.register(local)));
});
return null;
}
});
}
else {
final Optional<Profile> 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 +475,7 @@ public class ProfilesPreferencesController extends BundleController {
*/
private static List<ProfileDescription> toSorted(final Set<ProfileDescription> profiles) {
return profiles.stream()
.filter(description -> description.getProfile().isPresent())
.sorted(Comparator.comparing(o -> o.getProfile().get()))
.sorted(Comparator.comparing(ProfileDescription::getDescription))
.collect(Collectors.toList());
}
}
@@ -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<ProfileDescription> 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(
@@ -122,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
@@ -130,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());
@@ -0,0 +1,78 @@
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.apache.commons.lang3.StringUtils;
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.*;
@Category(IntegrationTest.class)
public class RemoteIndexProfilesFinderTest {
@Test
public void testFind() throws Exception {
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(protocols, session);
final Set<ProfileDescription> set = finder.find();
assertFalse(set.isEmpty());
// Check for versions of S3 (HTTP).cyberduckprofile
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();
}
}