Merge remote-tracking branch 'origin/master' into feature/10-ikvmnet-cli

This commit is contained in:
AliveDevil
2026-03-27 13:58:23 +01:00
83 changed files with 733 additions and 683 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
if: runner.os == 'windows' || runner.os == 'macos'
- run: msiexec /i setup\wix\Bonjour64.msi /Quiet /Passive /NoRestart
if: runner.os == 'windows'
- uses: microsoft/setup-msbuild@v2
- uses: microsoft/setup-msbuild@v3
if: runner.os == 'windows'
with:
msbuild-architecture: x64
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
cache: maven
- name: Add msbuild to PATH
if: ${{ runner.os == 'Windows' }}
uses: microsoft/setup-msbuild@v2
uses: microsoft/setup-msbuild@v3
with:
msbuild-architecture: x64
- name: "NuGet"
@@ -78,7 +78,7 @@ public class B2WriteFeature extends AbstractHttpWriteFeature<BaseB2Response> imp
try {
final Checksum checksum = status.getChecksum();
if(status.isSegment()) {
final B2GetUploadPartUrlResponse uploadUrl = session.getClient().getUploadPartUrl(status.getParameters().get("fileId"));
final B2GetUploadPartUrlResponse uploadUrl = session.getClient().getUploadPartUrl(status.getParameters().get("fileId").toString());
return session.getClient().uploadLargeFilePart(uploadUrl, status.getPart(), entity, checksum.hash);
}
else {
@@ -30,6 +30,7 @@ import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.ProviderHelpServiceFactory;
import ch.cyberduck.core.local.BrowserLauncherFactory;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.rococoa.Foundation;
@@ -86,17 +87,20 @@ public abstract class WindowController extends BundleController implements NSWin
public void setWindow(final NSWindow window) {
this.window = window;
this.window.setFrameAutosaveName(StringUtils.EMPTY);
this.window.recalculateKeyViewLoop();
this.window.setReleasedWhenClosed(true);
this.window.setDelegate(this.id());
this.window.setCollectionBehavior(window.collectionBehavior()
| NSWindow.NSWindowCollectionBehavior.NSWindowCollectionBehaviorTransient);
}
public NSWindow window() {
return window;
}
public String windowFrameName() {
return window.title();
}
@Override
public NSView view() {
return window.contentView();
@@ -106,16 +110,22 @@ public abstract class WindowController extends BundleController implements NSWin
* Order front window
*/
public void display() {
this.display(true);
this.display(true, this.isVisible() ? null : this.windowFrameName());
}
/**
* Order front window
*
* @param key Make key window
* @param key Make key window
* @param frameName Property name
*/
public void display(final boolean key) {
public void display(final boolean key, final String frameName) {
this.loadBundle();
if(frameName != null) {
if(window.setFrameUsingName(frameName)) {
log.debug("Restored frame {} for window {}", window.frame(), frameName);
}
}
if(key) {
window.makeKeyAndOrderFront(null);
}
@@ -202,6 +212,14 @@ public abstract class WindowController extends BundleController implements NSWin
@Delegate
public void windowWillClose(final NSNotification notification) {
window.endEditingFor(null);
// Save frame rectangle
final String frameName = this.windowFrameName();
if(frameName != null) {
log.debug("Save frame {} for window {}", window.frame(), frameName);
window.saveFrameUsingName(frameName);
}
// Workaround for macOS 26.x bug where closed windows leave an invisible rect that intercepts mouse events
window.setFrame_display_animate(new NSRect(new NSPoint(0, 0), new NSSize(0, 0)), false, false);
log.debug("Window will close {}", notification);
for(WindowListener listener : listeners.toArray(new WindowListener[listeners.size()])) {
listener.windowWillClose();
@@ -935,8 +935,7 @@ public abstract class NSWindow extends NSResponder {
public abstract boolean isDocumentEdited();
/**
* Original signature : <code>BOOL isVisible()</code><br>
* <i>native declaration : :361</i>
* @return A Boolean value that indicates whether the window is visible onscreen (even when its obscured by other windows).
*/
public abstract boolean isVisible();
@@ -1335,21 +1334,18 @@ public abstract class NSWindow extends NSResponder {
public abstract void setFrameFromString(String string);
/**
* Original signature : <code>void saveFrameUsingName(NSString*)</code><br>
* <i>native declaration : :467</i>
* Saves the windows frame rectangle in the user defaults system under a given name.
* With the companion method setFrameUsingName(_:), you can save and reset an NSWindow objects frame over various launches of an application.
*
* @param name The name under which the frame is to be saved.
*/
public abstract void saveFrameUsingName(String name);
/**
* Set force=YES to use setFrameUsingName on a non-resizable window<br> Original signature : <code>BOOL
* setFrameUsingName(NSString*, BOOL)</code><br>
* <i>native declaration : :469</i>
*/
public abstract boolean setFrameUsingName_force(String name, boolean force);
/**
* Original signature : <code>BOOL setFrameUsingName(NSString*)</code><br>
* <i>native declaration : :470</i>
* Sets the windows frame rectangle by reading the rectangle data stored under a given name from the defaults system.
* The frame is constrained according to the windows minimum and maximum size settings. This method causes a windowWillResize(_:to:) message to be sent to the delegate
*
* @param name The name of the frame to read.
*/
public abstract boolean setFrameUsingName(String name);
@@ -1418,6 +1414,7 @@ public abstract class NSWindow extends NSResponder {
/**
* Sets the size of the windows content view to a given size, which is expressed in the windows base coordinate system.
*
* @param size The new size of the windows content view in the windows base coordinate system.
*/
public abstract void setContentSize(NSSize size);
@@ -185,8 +185,8 @@ public class BoxWriteFeature extends AbstractHttpWriteFeature<File> {
final HttpRange range = HttpRange.withStatus(new TransferStatus()
.setLength(status.getLength())
.setOffset(status.getOffset()));
final String uploadSessionId = status.getParameters().get(BoxLargeUploadService.UPLOAD_SESSION_ID);
final String overall_length = status.getParameters().get(BoxLargeUploadService.OVERALL_LENGTH);
final String uploadSessionId = status.getParameters().get(BoxLargeUploadService.UPLOAD_SESSION_ID).toString();
final String overall_length = status.getParameters().get(BoxLargeUploadService.OVERALL_LENGTH).toString();
log.debug("Send range {} for file {}", range, file);
final HttpPut request = new HttpPut(String.format("%s/files/upload_sessions/%s", client.getBasePath(), uploadSessionId));
// Must not overlap with the range of a part already uploaded this session.
@@ -18,7 +18,6 @@ package ch.cyberduck.cli;
* feedback@cyberduck.io
*/
import ch.cyberduck.core.DeserializerFactory;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.Profile;
import ch.cyberduck.core.ProtocolFactory;
@@ -50,7 +49,7 @@ public class CommandLinePathParserTest {
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Arrays.asList(new FTPTLSProtocol(), new S3Protocol())));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/FTP.cyberduckprofile")));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/FTPS.cyberduckprofile")));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile")));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile")));
assertEquals(new Path("/", EnumSet.of(Path.Type.directory)),
new CommandLinePathParser(input, factory).parse("ftps://u@test.cyberduck.ch/"));
@@ -51,7 +51,7 @@ public class CommandLineUriParserTest {
final ProtocolFactory factory = new ProtocolFactory(new LinkedHashSet<>(Arrays.asList(new FTPTLSProtocol(), new S3Protocol())));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/FTP.cyberduckprofile")));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/FTPS.cyberduckprofile")));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile")));
factory.register(new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile")));
assertEquals(0, new Host(new S3Protocol(), "s3.amazonaws.com", 443, "/cyberduck-test", new Credentials("AWS456", null))
.compareTo(new CommandLineUriParser(input, factory).parse("s3:AWS456@cyberduck-test/key")));
assertEquals(0, new Host(new S3Protocol(), "s3.amazonaws.com", 443, "/cyberduck-test", new Credentials("AWS456", null))
@@ -158,7 +158,7 @@ public class FinderLocal extends Local {
}
}
if(null == bookmark) {
log.warn("No security scoped bookmark for {}", path);
log.debug("No security scoped bookmark for {}", path);
return null;
}
log.debug("Lock with bookmark {}", bookmark);
@@ -15,11 +15,7 @@ package ch.cyberduck.core.exception;
* GNU General Public License for more details.
*/
public class SSLNegotiateException extends InteroperabilityException {
public SSLNegotiateException(final String detail) {
super(detail);
}
public class SSLNegotiateException extends ConnectionRefusedException {
public SSLNegotiateException(final String detail, final Throwable cause) {
super(detail, cause);
}
@@ -327,10 +327,18 @@ public abstract class Preferences implements Locales, PreferencesReader {
this.configureLogging(level);
}
/**
* Reset logging configuration to default level from configuration
*/
public void resetLogging() {
this.deleteProperty("logging");
this.configureLogging(null);
}
/**
* Reconfigure logging configuration
*
* @param level Log level
* @param level Log level or null to use default level from configuration
*/
protected void configureLogging(final String level) {
// Call only once during initialization time of your application
@@ -346,10 +354,13 @@ public abstract class Preferences implements Locales, PreferencesReader {
}
catch(IOException e) {
log.error("Failure configuring log4j", e);
Configurator.setRootLevel(Level.ERROR);
}
}
// Allow to override default logging level
Configurator.setRootLevel(Level.toLevel(level, Level.ERROR));
if(StringUtils.isNotEmpty(level)) {
// Allow to override default logging level
Configurator.setRootLevel(Level.toLevel(level, Level.ERROR));
}
// Map logging level to pass through bridge
final ImmutableMap<Level, java.util.logging.Level> map = new ImmutableMap.Builder<Level, java.util.logging.Level>()
.put(Level.ALL, java.util.logging.Level.ALL)
@@ -369,7 +380,7 @@ public abstract class Preferences implements Locales, PreferencesReader {
java.util.logging.Logger.getLogger(loggerConfig.getName()).setLevel(map.get(loggerConfig.getLevel()));
}
}
this.configureAppenders(level);
this.configureAppenders(LogManager.getRootLogger().getLevel().name());
}
private InputStream getLogConfiguration() {
@@ -22,7 +22,7 @@ import ch.cyberduck.core.AbstractExceptionMappingService;
import ch.cyberduck.core.DefaultSocketExceptionMappingService;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.exception.InteroperabilityException;
import ch.cyberduck.core.exception.ConnectionRefusedException;
import ch.cyberduck.core.exception.SSLNegotiateException;
import org.apache.commons.lang3.StringUtils;
@@ -98,10 +98,10 @@ public class SSLExceptionMappingService extends AbstractExceptionMappingService<
}
if(ExceptionUtils.getRootCause(failure) instanceof GeneralSecurityException) {
this.append(buffer, ExceptionUtils.getRootCause(failure).getMessage());
return new InteroperabilityException(buffer.toString(), failure);
return new ConnectionRefusedException(buffer.toString(), failure);
}
this.append(buffer, message);
return new InteroperabilityException(buffer.toString(), failure);
return new ConnectionRefusedException(buffer.toString(), failure);
}
private enum Alert {
@@ -29,7 +29,6 @@ import ch.cyberduck.core.exception.LocalNotfoundException;
import ch.cyberduck.core.exception.LoginFailureException;
import ch.cyberduck.core.exception.QuotaException;
import ch.cyberduck.core.exception.ResolveFailedException;
import ch.cyberduck.core.exception.SSLNegotiateException;
import ch.cyberduck.core.exception.TransferCanceledException;
import ch.cyberduck.core.exception.UnsupportedException;
import ch.cyberduck.core.io.IOResumeException;
@@ -78,9 +77,6 @@ public final class DefaultFailureDiagnostics implements FailureDiagnostics<Backg
if(cause instanceof ConnectionRefusedException) {
return Type.network;
}
if(cause instanceof SSLNegotiateException) {
return Type.application;
}
if(cause instanceof SSLHandshakeException) {
return Type.application;
}
@@ -160,7 +160,7 @@ public class TransferStatus implements TransferResponse, StreamCancelation, Stre
private Long modified;
private Long created;
private Map<String, String> parameters
private Map<String, ?> parameters
= Collections.emptyMap();
private Map<String, String> metadata
@@ -522,11 +522,11 @@ public class TransferStatus implements TransferResponse, StreamCancelation, Stre
return this;
}
public Map<String, String> getParameters() {
public Map<String, ?> getParameters() {
return parameters;
}
public TransferStatus setParameters(final Map<String, String> parameters) {
public TransferStatus setParameters(final Map<String, ?> parameters) {
this.parameters = parameters;
return this;
}
@@ -37,7 +37,6 @@ public class VaultRegistryCopyFeature implements Copy {
private final Copy proxy;
private final VaultRegistry registry;
public VaultRegistryCopyFeature(final Session<?> session, final Copy proxy, final VaultRegistry registry) {
this.session = session;
this.destination = session;
@@ -31,7 +31,6 @@ public class VaultRegistryDirectoryFeature<Reply> implements Directory<Reply> {
private final VaultRegistry registry;
public VaultRegistryDirectoryFeature(final Session<?> session, final Directory<Reply> proxy, final VaultRegistry registry) {
this.session = session;
this.proxy = proxy;
this.registry = registry;
@@ -19,23 +19,34 @@ package ch.cyberduck.core.ssl;
*/
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionRefusedException;
import ch.cyberduck.core.exception.SSLNegotiateException;
import org.junit.Test;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.net.SocketException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
public class SSLExceptionMappingServiceTest {
@Test
public void testMap() {
public void testMapSocketException() {
final BackgroundException f = new SSLExceptionMappingService().map(new SSLException(
"Connection has been shutdown: javax.net.ssl.SSLException: java.net.SocketException: Broken pipe",
new SSLException("javax.net.ssl.SSLException: java.net.SocketException: Broken pipe",
new SocketException("Broken pipe"))));
assertSame(ConnectionRefusedException.class, f.getClass());
assertEquals("Connection failed", f.getMessage());
assertEquals("Broken pipe. The connection attempt was rejected. The server may be down, or your network may not be properly configured.", f.getDetail());
}
@Test
public void testMapSSLFailure() {
final BackgroundException f = new SSLExceptionMappingService().map(new SSLHandshakeException("r"));
assertSame(SSLNegotiateException.class, f.getClass());
}
}
@@ -1,7 +1,7 @@
package ch.cyberduck.core.ctera;
/*
* Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
* Copyright (c) 2002-2026 iterate GmbH. All rights reserved.
* https://cyberduck.io/
*
* This program is free software; you can redistribute it and/or modify
@@ -21,6 +21,7 @@ import ch.cyberduck.core.Path;
import ch.cyberduck.core.ctera.model.DirectIO;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.VersionIdProvider;
import ch.cyberduck.core.http.HttpExceptionMappingService;
import ch.cyberduck.core.shared.DisabledBulkFeature;
import ch.cyberduck.core.transfer.Transfer;
import ch.cyberduck.core.transfer.TransferItem;
@@ -33,8 +34,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -42,6 +42,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
public class CteraBulkFeature extends DisabledBulkFeature {
private static final Logger log = LogManager.getLogger(CteraBulkFeature.class);
public static final String DIRECTIO_PARAMETER = "directio";
private final CteraSession session;
private final VersionIdProvider versionid;
@@ -57,46 +59,11 @@ public class CteraBulkFeature extends DisabledBulkFeature {
break;
case download:
for(Map.Entry<TransferItem, TransferStatus> file : files.entrySet()) {
final DirectIO metadata;
try {
metadata = this.getMetadata(file.getKey().remote);
}
catch(IOException e) {
log.warn("Ignore DirectIO download failure {} for {}", e, file.getKey().remote);
continue;
}
final DirectIO metadata = this.getMetadata(file.getKey().remote);
log.debug("DirectIO metadata {} retrieved for {}", metadata, file.getKey().remote);
final TransferStatus status = file.getValue();
if(status.isSegmented()) {
final List<TransferStatus> segments = status.getSegments();
if(segments.size() <= metadata.chunks.size()) {
for(int i = 0; i < segments.size(); i++) {
final TransferStatus segment = segments.get(i);
if(i == 0) {
if(segment.getOffset() > 0) {
log.warn("DirectIO download for {} with an initial offset is not supported", file.getKey().remote);
continue;
}
}
segment.setUrl(metadata.chunks.get(i).url);
final Map<String, String> parameters = new HashMap<>(segment.getParameters());
parameters.put(CteraDirectIOReadFeature.CTERA_WRAPPEDKEY, metadata.encrypt_info.wrapped_key);
segment.setParameters(parameters);
}
}
else {
log.error("Mismatch between number of segments ({}) and chunks ({}) for {}",
segments.size(), metadata.chunks.size(), file.getKey().remote);
}
}
else {
if(metadata.actual_blocks_range.file_size == 0) {
final Map<String, String> parameters = new HashMap<>(status.getParameters());
parameters.put(CteraDirectIOReadFeature.CTERA_WRAPPEDKEY, metadata.encrypt_info.wrapped_key);
status.setParameters(parameters);
}
else {
log.warn("DirectIO download for {} with an initial offset is not supported", file.getKey().remote);
}
for(TransferStatus segment : status.getSegments()) {
segment.setParameters(Collections.singletonMap(DIRECTIO_PARAMETER, metadata));
}
}
break;
@@ -104,17 +71,20 @@ public class CteraBulkFeature extends DisabledBulkFeature {
return files;
}
private DirectIO getMetadata(final Path file) throws IOException, BackgroundException {
private DirectIO getMetadata(final Path file) throws BackgroundException {
final HttpGet request = new HttpGet(String.format("%s%s%s", new HostUrlProvider().withUsername(false).withPath(false)
.get(session.getHost()), CteraDirectIOInterceptor.DIRECTIO_PATH, versionid.getVersionId(file)));
final DirectIO metadata = session.getClient().getClient().execute(request, new AbstractResponseHandler<DirectIO>() {
@Override
public DirectIO handleEntity(final HttpEntity entity) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(entity.getContent(), DirectIO.class);
}
});
log.debug("DirectIO metadata {} retrieved for {}", metadata, file);
return metadata;
try {
return session.getClient().getClient().execute(request, new AbstractResponseHandler<DirectIO>() {
@Override
public DirectIO handleEntity(final HttpEntity entity) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(entity.getContent(), DirectIO.class);
}
});
}
catch(IOException e) {
throw new HttpExceptionMappingService().map("Download {0} failed", e, file);
}
}
}
@@ -19,9 +19,10 @@ import ch.cyberduck.core.ConnectionCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Read;
import ch.cyberduck.core.features.VersionIdProvider;
import ch.cyberduck.core.preferences.HostPreferencesFactory;
import ch.cyberduck.core.transfer.TransferStatus;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -32,32 +33,50 @@ public class CteraDelegatingReadFeature implements Read {
private static final Logger log = LogManager.getLogger(CteraDelegatingReadFeature.class);
private final CteraSession session;
private final VersionIdProvider versionid;
private final boolean directio;
public CteraDelegatingReadFeature(final CteraSession session) {
public CteraDelegatingReadFeature(final CteraSession session, final VersionIdProvider versionid) {
this.session = session;
this.versionid = versionid;
this.directio = HostPreferencesFactory.get(session.getHost()).getBoolean("ctera.download.directio.enable");
}
@Override
public InputStream read(final Path file, final TransferStatus status, final ConnectionCallback callback) throws BackgroundException {
if(StringUtils.isNotBlank(status.getParameters().get(CteraDirectIOReadFeature.CTERA_WRAPPEDKEY))) {
return new CteraDirectIOReadFeature(session).read(file, status, callback);
if(directio) {
try {
return new CteraDirectIOReadFeature(session).read(file, status, callback);
}
catch(BackgroundException e) {
log.warn("Ignore DirectIO retrieval failure {} for {}", e, file);
}
}
log.warn("No key material found in status {} for {}", status, file);
return new CteraReadFeature(session).read(file, status, callback);
}
@Override
public boolean offset(final Path file) throws BackgroundException {
if(directio) {
return new CteraDirectIOReadFeature(session).offset(file);
}
return new CteraReadFeature(session).offset(file);
}
@Override
public void preflight(final Path file) throws BackgroundException {
if(directio) {
new CteraDirectIOReadFeature(session).preflight(file);
return;
}
new CteraReadFeature(session).preflight(file);
}
@Override
public EnumSet<Flags> features(final Path file) {
if(directio) {
return new CteraDirectIOReadFeature(session).features(file);
}
return new CteraReadFeature(session).features(file);
}
}
@@ -18,7 +18,6 @@ package ch.cyberduck.core.ctera;
import ch.cyberduck.core.ConnectionCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.ctera.directio.DirectIOInputStream;
import ch.cyberduck.core.ctera.directio.EncryptInfo;
import ch.cyberduck.core.ctera.model.DirectIO;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.features.Read;
@@ -27,7 +26,6 @@ import ch.cyberduck.core.http.HttpMethodReleaseInputStream;
import ch.cyberduck.core.transfer.TransferStatus;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -35,15 +33,15 @@ import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import static ch.cyberduck.core.ctera.CteraAttributesFinderFeature.READPERMISSION;
import static ch.cyberduck.core.ctera.CteraAttributesFinderFeature.assumeRole;
public class CteraDirectIOReadFeature implements Read {
private static final Logger log = LogManager.getLogger(CteraDirectIOReadFeature.class);
public static final String CTERA_WRAPPEDKEY = "wrapped_key";
private final CteraSession session;
public CteraDirectIOReadFeature(final CteraSession session) {
@@ -53,15 +51,13 @@ public class CteraDirectIOReadFeature implements Read {
@Override
public InputStream read(final Path file, final TransferStatus status, final ConnectionCallback callback) throws BackgroundException {
try {
final EncryptInfo key = new EncryptInfo(status.getParameters().get(CTERA_WRAPPEDKEY), session.getOrCreateAPIKeys().secretKey);
final DirectIO metadata = (DirectIO) status.getParameters().get(CteraBulkFeature.DIRECTIO_PARAMETER);
log.debug("DirectIO metadata {} retrieved for {}", metadata, file);
final String secretKey = session.getOrCreateAPIKeys().secretKey;
if(status.getLength() == 0) {
return new ChunkSequenceInputStream(Collections.emptyList(), key);
return new ChunkSequenceInputStream(Collections.emptyList(), metadata.encrypt_info, secretKey, status.getOffset());
}
final DirectIO.Chunk chunk = new DirectIO.Chunk();
chunk.url = status.getUrl();
chunk.len = status.getLength();
log.debug("Return chunk {} for file {}", chunk, file);
return new ChunkSequenceInputStream(Collections.singletonList(chunk), key);
return new ChunkSequenceInputStream(metadata.chunks, metadata.encrypt_info, secretKey, status.getOffset());
}
catch(IOException e) {
throw new HttpExceptionMappingService().map("Download {0} failed", e, file);
@@ -69,43 +65,63 @@ public class CteraDirectIOReadFeature implements Read {
}
@Override
public EnumSet<Flags> features(final Path file) {
return EnumSet.noneOf(Flags.class);
public void preflight(final Path file) throws BackgroundException {
assumeRole(file, READPERMISSION);
}
private final class ChunkSequenceInputStream extends InputStream {
private final Enumeration<DirectIO.Chunk> chunks;
private final EncryptInfo key;
private InputStream in;
private final DirectIO.EncryptInfo key;
private final String secretKey;
private final long offset;
public ChunkSequenceInputStream(final List<DirectIO.Chunk> chunks, final EncryptInfo key) throws IOException {
private InputStream in;
private long currentPosition = 0L;
public ChunkSequenceInputStream(final List<DirectIO.Chunk> chunks, final DirectIO.EncryptInfo key, final String secretKey, final long offset) throws IOException {
this.chunks = Collections.enumeration(chunks);
this.key = key;
this.peekNextStream();
this.secretKey = secretKey;
this.offset = offset;
this.peek();
}
private void nextStream() throws IOException {
if(in != null) {
in.close();
}
this.peekNextStream();
this.peek();
}
private void peekNextStream() throws IOException {
if(chunks.hasMoreElements()) {
in = getStream(chunks.nextElement());
/**
* Peek at the next chunk in the sequence
*/
private void peek() throws IOException {
while(chunks.hasMoreElements()) {
final DirectIO.Chunk chunk = chunks.nextElement();
final long chunkStart = currentPosition;
final long chunkEnd = currentPosition + chunk.len;
// Skip chunks that are entirely before the offset
if(chunkEnd <= offset) {
log.debug("Skipping chunk {} entirely before offset {}", chunk, offset);
currentPosition = chunkEnd;
continue;
}
log.debug("Request chunk {}", chunk);
// Open the stream for this chunk
in = new DirectIOInputStream(new HttpMethodReleaseInputStream(
session.getClient().getClient().execute(new HttpGet(chunk.url)),
new TransferStatus().setOffset(0L).setLength(chunk.len)), key, secretKey);
// If this chunk contains the offset, skip bytes before the offset
if(chunkStart < offset) {
final long bytesToSkip = offset - chunkStart;
log.debug("Skipping {} bytes in chunk {} to reach offset {}", bytesToSkip, chunk, offset);
IOUtils.skip(in, bytesToSkip);
}
currentPosition = chunkEnd;
return;
}
else {
in = null;
}
}
private InputStream getStream(final DirectIO.Chunk chunk) throws IOException {
log.debug("Request chunk {}", chunk);
final HttpGet chunkRequest = new HttpGet(chunk.url);
final HttpResponse chunkResponse = session.getClient().getClient().execute(chunkRequest);
return new DirectIOInputStream(new HttpMethodReleaseInputStream(chunkResponse, new TransferStatus()), key);
in = null;
}
@Override
@@ -26,9 +26,6 @@ import ch.cyberduck.core.synchronization.ComparisonService;
import ch.cyberduck.core.synchronization.DefaultComparisonService;
import ch.cyberduck.core.synchronization.ETagComparisonService;
import java.util.HashMap;
import java.util.Map;
import com.google.auto.service.AutoService;
@AutoService(Protocol.class)
@@ -36,7 +33,6 @@ public class CteraProtocol extends AbstractProtocol {
public static final String CTERA_REDIRECT_URI = String.format("%s:websso",
PreferencesFactory.get().getProperty("oauth.handler.scheme"));
private static final int DIRECTIO_CHUNKSIZE = 4194304;
@Override
public Type getType() {
@@ -102,17 +98,6 @@ public class CteraProtocol extends AbstractProtocol {
return "CTERA Token";
}
@Override
public Map<String, String> getProperties() {
final Map<String, String> properties = new HashMap<>();
if(PreferencesFactory.get().getBoolean("ctera.download.directio.enable")) {
properties.put("queue.download.segments.size.dynamic", String.valueOf(false));
properties.put("queue.download.segments.size", String.valueOf(DIRECTIO_CHUNKSIZE));
properties.put("queue.download.segments.threshold", String.valueOf(0));
}
return properties;
}
@Override
@SuppressWarnings("unchecked")
public <T> T getFeature(final Class<T> type) {
@@ -181,10 +181,8 @@ public class CteraSession extends DAVSession {
final HttpPost post = new HttpPost(API_PATH);
try {
final String userId = this.getPortalSession().getUserIdFromUserRef();
post.setEntity(
new StringEntity(String.format("<obj><att id=\"type\"><val>user-defined</val></att><att id=\"name\"><val>createApiKey</val></att><att id=\"param\"><val>%s</val></att></obj>",
userId), ContentType.TEXT_XML
)
post.setEntity(new StringEntity(String.format("<obj><att id=\"type\"><val>user-defined</val></att><att id=\"name\"><val>createApiKey</val></att><att id=\"param\"><val>%s</val></att></obj>", userId),
ContentType.TEXT_XML)
);
final APICredentials credentials = this.getClient().execute(post, new AbstractResponseHandler<APICredentials>() {
@Override
@@ -279,10 +277,7 @@ public class CteraSession extends DAVSession {
return (T) new CteraListService(this);
}
if(type == Read.class) {
if(preferences.getBoolean("ctera.download.directio.enable")) {
return (T) new CteraDelegatingReadFeature(this);
}
return (T) new CteraReadFeature(this);
return (T) new CteraDelegatingReadFeature(this, versionid);
}
if(type == Write.class) {
return (T) new CteraWriteFeature(this);
@@ -15,6 +15,8 @@ package ch.cyberduck.core.ctera.directio;
* GNU General Public License for more details.
*/
import ch.cyberduck.core.ctera.model.DirectIO;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
@@ -44,10 +46,10 @@ public class Decryptor {
private static final byte[] GZIP_MAGIC = {0x1F, (byte) 0x8B};
private static final byte[] SNAPPY_MAGIC = {-126, 83, 78, 65, 80, 80, 89, 0};
public InputStream decryptData(final InputStream blockData, final EncryptInfo encryptInfo) throws IOException {
public InputStream decryptData(final InputStream blockData, final DirectIO.EncryptInfo encryptInfo, final String secretKey) throws IOException {
try {
final DecryptKey decryptKey = new DecryptKey(encryptInfo.getWrappedKey());
decryptKey.decrypt(encryptInfo.getWrappingKey());
final DecryptKey decryptKey = new DecryptKey(encryptInfo.wrapped_key);
decryptKey.decrypt(secretKey);
final SecretKeySpec key = new SecretKeySpec(Base64.decodeBase64(decryptKey.getDecryptedKey()), ENCRYPTION_KEY_ALGORITHM);
blockData.read();
final byte[] iv = new byte[16];
@@ -15,6 +15,8 @@ package ch.cyberduck.core.ctera.directio;
* GNU General Public License for more details.
*/
import ch.cyberduck.core.ctera.model.DirectIO;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ProxyInputStream;
@@ -23,14 +25,12 @@ import java.io.InputStream;
public class DirectIOInputStream extends ProxyInputStream {
private InputStream decryptedInputStream;
private final Decryptor decryptor;
private final EncryptInfo encryptInfo;
private final InputStream decryptedInputStream;
public DirectIOInputStream(final InputStream proxy, final EncryptInfo encryptInfo) {
public DirectIOInputStream(final InputStream proxy, final DirectIO.EncryptInfo encryptInfo, final String secretKey) throws IOException {
super(proxy);
this.decryptor = new Decryptor();
this.encryptInfo = encryptInfo;
final Decryptor decryptor = new Decryptor();
this.decryptedInputStream = decryptor.decryptData(in, encryptInfo, secretKey);
}
@Override
@@ -46,23 +46,12 @@ public class DirectIOInputStream extends ProxyInputStream {
@Override
public int read(final byte[] dst, final int off, final int len) throws IOException {
this.initStream();
return decryptedInputStream.read(dst, off, len);
}
private void initStream() throws IOException {
if(decryptedInputStream == null) {
this.readNextChunk();
}
}
@Override
public long skip(final long len) throws IOException {
return IOUtils.skip(this, len);
}
private void readNextChunk() throws IOException {
decryptedInputStream = decryptor.decryptData(this.in, encryptInfo);
}
}
@@ -1,35 +0,0 @@
package ch.cyberduck.core.ctera.directio;
/*
* Copyright (c) 2002-2025 iterate GmbH. All rights reserved.
* https://cyberduck.io/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
public class EncryptInfo {
public EncryptInfo(final String wrappedKey, final String wrappingKey) {
this.wrappedKey = wrappedKey;
this.wrappingKey = wrappingKey;
}
private String wrappedKey;
private String wrappingKey;
public String getWrappedKey() {
return wrappedKey;
}
public String getWrappingKey() {
return wrappingKey;
}
}
@@ -29,6 +29,5 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
public final class APICredentials {
public String accessKey;
public String secretKey;
}
@@ -41,7 +41,6 @@ public final class DirectIO {
}
public static class EncryptInfo {
public String wrapped_key;
public boolean data_encrypted;
@@ -56,7 +55,6 @@ public final class DirectIO {
}
public static class ActualBlocksRange {
public long file_size;
public String range;
@@ -21,7 +21,6 @@ import ch.cyberduck.core.HostKeyCallback;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginConnectionService;
import ch.cyberduck.core.ProgressListener;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.proxy.DisabledProxyFinder;
import ch.cyberduck.core.ssl.DefaultX509KeyManager;
import ch.cyberduck.core.ssl.DisabledX509TrustManager;
@@ -34,10 +33,12 @@ import org.junit.Before;
public class AbstractCteraDirectIOTest extends VaultTest {
protected CteraSession session;
private TestPasswordStore keychain;
@After
public void disconnect() throws Exception {
session.close();
keychain.save(session.getHost());
}
@Before
@@ -52,10 +53,10 @@ public class AbstractCteraDirectIOTest extends VaultTest {
}
};
host.setDefaultPath("/ServicesPortal/webdav/My Files");
PreferencesFactory.get().setDefault("ctera.download.directio.enable", String.valueOf(true));
session = new CteraSession(host, new DisabledX509TrustManager(), new DefaultX509KeyManager(), new TestPasswordStore());
keychain = new TestPasswordStore();
session = new CteraSession(host, new DisabledX509TrustManager(), new DefaultX509KeyManager(), keychain);
final LoginConnectionService connect = new LoginConnectionService(LoginCallback.noop, HostKeyCallback.noop,
new TestPasswordStore(), ProgressListener.noop, new DisabledProxyFinder());
keychain, ProgressListener.noop, new DisabledProxyFinder());
connect.check(session, CancelCallback.noop);
}
}
@@ -36,6 +36,7 @@ import ch.cyberduck.core.transfer.TransferStatus;
import ch.cyberduck.test.IntegrationTest;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.RandomUtils;
import org.junit.Test;
import org.junit.experimental.categories.Category;
@@ -52,7 +53,7 @@ import static org.junit.Assert.*;
public class CteraDirectIOReadFeatureTest extends AbstractCteraDirectIOTest {
@Test
public void testReadChunk() throws Exception {
public void testReadSingleChunk() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(), new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
final Local local = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(65536);
@@ -67,10 +68,8 @@ public class CteraDirectIOReadFeatureTest extends AbstractCteraDirectIOTest {
final TransferStatus status = new TransferStatus();
final TransferStatus segment = new TransferStatus().setSegment(true).setLength(content.length);
status.setSegments(Collections.singletonList(segment));
final CteraBulkFeature bulk = new CteraBulkFeature(session, new DefaultVersionIdProvider(session));
bulk.pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test), status), ConnectionCallback.noop);
assertNotNull(segment.getUrl());
assertNotNull(segment.getParameters());
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
final InputStream in = new CteraDirectIOReadFeature(session).read(test, segment, ConnectionCallback.noop);
assertNotNull(in);
final ByteArrayOutputStream buffer = new ByteArrayOutputStream(content.length);
@@ -80,6 +79,174 @@ public class CteraDirectIOReadFeatureTest extends AbstractCteraDirectIOTest {
new CteraDeleteFeature(session).delete(Collections.singletonList(test), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testReadSingleChunkWithOffset() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(),
new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
final Local local = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(65536);
final OutputStream out = local.getOutputStream(false);
assertNotNull(out);
IOUtils.write(content, out);
out.close();
new DAVUploadFeature(session).upload(
new CteraWriteFeature(session), test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), ProgressListener.noop, StreamListener.noop,
new TransferStatus().setLength(content.length),
ConnectionCallback.noop);
final TransferStatus status = new TransferStatus();
final TransferStatus segment = new TransferStatus().setSegment(true).setLength(content.length - 1).setOffset(1L);
status.setSegments(Collections.singletonList(segment));
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
final InputStream in = new CteraDirectIOReadFeature(session).read(test, segment, ConnectionCallback.noop);
assertNotNull(in);
final ByteArrayOutputStream buffer = new ByteArrayOutputStream(content.length);
new StreamCopier(segment, segment).transfer(in, buffer);
in.close();
assertArrayEquals(ArrayUtils.subarray(content, 1, content.length), buffer.toByteArray());
new CteraDeleteFeature(session).delete(Collections.singletonList(test), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testReadSingleChunkEqualDefaultChunksize() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(),
new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
final Local local = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(4194304);
final OutputStream out = local.getOutputStream(false);
assertNotNull(out);
IOUtils.write(content, out);
out.close();
new DAVUploadFeature(session).upload(
new CteraWriteFeature(session), test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), ProgressListener.noop, StreamListener.noop,
new TransferStatus().setLength(content.length),
ConnectionCallback.noop);
final TransferStatus status = new TransferStatus();
final TransferStatus segment = new TransferStatus().setSegment(true).setLength(content.length);
status.setSegments(Collections.singletonList(segment));
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
final InputStream in = new CteraDirectIOReadFeature(session).read(test, segment, ConnectionCallback.noop);
assertNotNull(in);
final ByteArrayOutputStream buffer = new ByteArrayOutputStream(content.length);
new StreamCopier(segment, segment).transfer(in, buffer);
in.close();
assertArrayEquals(content, buffer.toByteArray());
new CteraDeleteFeature(session).delete(Collections.singletonList(test), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testReadSingleChunkEqualDefaultChunksizeWithOffset() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(),
new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
final Local local = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(4194304);
final OutputStream out = local.getOutputStream(false);
assertNotNull(out);
IOUtils.write(content, out);
out.close();
new DAVUploadFeature(session).upload(
new CteraWriteFeature(session), test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), ProgressListener.noop, StreamListener.noop,
new TransferStatus().setLength(content.length),
ConnectionCallback.noop);
final TransferStatus status = new TransferStatus();
final TransferStatus segment = new TransferStatus().setSegment(true).setLength(1L).setOffset(content.length - 1L);
status.setSegments(Collections.singletonList(segment));
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
final InputStream in = new CteraDirectIOReadFeature(session).read(test, segment, ConnectionCallback.noop);
assertNotNull(in);
final ByteArrayOutputStream buffer = new ByteArrayOutputStream(content.length);
new StreamCopier(segment, segment).transfer(in, buffer);
in.close();
assertArrayEquals(ArrayUtils.subarray(content, content.length - 1, content.length), buffer.toByteArray());
new CteraDeleteFeature(session).delete(Collections.singletonList(test), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testReadMultipleChunkSizeAligned() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(),
new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
final Local local = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(4194304 * 2);
final OutputStream out = local.getOutputStream(false);
assertNotNull(out);
IOUtils.write(content, out);
out.close();
new DAVUploadFeature(session).upload(
new CteraWriteFeature(session), test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), ProgressListener.noop, StreamListener.noop,
new TransferStatus().setLength(content.length),
ConnectionCallback.noop);
final TransferStatus status = new TransferStatus();
final TransferStatus segment = new TransferStatus().setSegment(true).setLength(content.length);
status.setSegments(Collections.singletonList(segment));
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
final InputStream in = new CteraDirectIOReadFeature(session).read(test, segment, ConnectionCallback.noop);
assertNotNull(in);
final ByteArrayOutputStream buffer = new ByteArrayOutputStream(content.length);
new StreamCopier(segment, segment).transfer(in, buffer);
in.close();
assertArrayEquals(content, buffer.toByteArray());
new CteraDeleteFeature(session).delete(Collections.singletonList(test), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testReadMultipleChunkSize() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(),
new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
final Local local = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(8388609);
final OutputStream out = local.getOutputStream(false);
assertNotNull(out);
IOUtils.write(content, out);
out.close();
new DAVUploadFeature(session).upload(
new CteraWriteFeature(session), test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), ProgressListener.noop, StreamListener.noop,
new TransferStatus().setLength(content.length),
ConnectionCallback.noop);
final TransferStatus status = new TransferStatus();
final TransferStatus segment = new TransferStatus().setSegment(true).setLength(content.length);
status.setSegments(Collections.singletonList(segment));
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
final InputStream in = new CteraDirectIOReadFeature(session).read(test, segment, ConnectionCallback.noop);
assertNotNull(in);
final ByteArrayOutputStream buffer = new ByteArrayOutputStream(content.length);
new StreamCopier(segment, segment).transfer(in, buffer);
in.close();
assertArrayEquals(content, buffer.toByteArray());
new CteraDeleteFeature(session).delete(Collections.singletonList(test), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testReadMultipleChunkSizeWithOffset() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(),
new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
final Local local = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(8388609);
final OutputStream out = local.getOutputStream(false);
assertNotNull(out);
IOUtils.write(content, out);
out.close();
new DAVUploadFeature(session).upload(
new CteraWriteFeature(session), test, local, new BandwidthThrottle(BandwidthThrottle.UNLIMITED), ProgressListener.noop, StreamListener.noop,
new TransferStatus().setLength(content.length),
ConnectionCallback.noop);
final TransferStatus status = new TransferStatus();
final TransferStatus segment = new TransferStatus().setSegment(true).setOffset(4194304L).setLength(content.length - 4194304);
status.setSegments(Collections.singletonList(segment));
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
final InputStream in = new CteraDirectIOReadFeature(session).read(test, segment, ConnectionCallback.noop);
assertNotNull(in);
final ByteArrayOutputStream buffer = new ByteArrayOutputStream(content.length);
new StreamCopier(segment, segment).transfer(in, buffer);
in.close();
assertArrayEquals(ArrayUtils.subarray(content, 4194304, content.length), buffer.toByteArray());
new CteraDeleteFeature(session).delete(Collections.singletonList(test), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testReadZeroByteFile() throws Exception {
final Path test = new CteraTouchFeature(session).touch(new CteraWriteFeature(session), new Path(new DefaultHomeFinderService(session).find(), new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
@@ -95,10 +262,8 @@ public class CteraDirectIOReadFeatureTest extends AbstractCteraDirectIOTest {
ConnectionCallback.noop);
final TransferStatus status = new TransferStatus().setLength(content.length);
status.setSegments(Collections.emptyList());
final CteraBulkFeature bulk = new CteraBulkFeature(session, new DefaultVersionIdProvider(session));
bulk.pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test), status), ConnectionCallback.noop);
assertNull(status.getUrl());
assertNotNull(status.getParameters().get(CteraDirectIOReadFeature.CTERA_WRAPPEDKEY));
final DefaultVersionIdProvider versionid = new DefaultVersionIdProvider(session);
new CteraBulkFeature(session, versionid).pre(Transfer.Type.download, Collections.singletonMap(new TransferItem(test, local), status), ConnectionCallback.noop);
assertTrue(new DAVFindFeature(session).find(test));
final PathAttributes attributes = new CteraAttributesFinderFeature(session).find(test);
assertEquals(content.length, attributes.getSize());
@@ -272,9 +272,6 @@ ftp.command.lista=true
ftp.command.stat=true
ftp.command.mlsd=true
# Fallback to active or passive mode respectively
ftp.connectmode.fallback=false
# Protect the data channel by default. For TLS, the data connection can have one of two security levels.
# 1) Clear (requested by 'PROT C')
# 2) Private (requested by 'PROT P')
@@ -83,7 +83,7 @@ public class EueWriteFeature extends AbstractHttpWriteFeature<EueWriteFeature.Ch
}
else {
uploadUri = status.getUrl();
resourceId = status.getParameters().get(RESOURCE_ID);
resourceId = status.getParameters().get(RESOURCE_ID).toString();
}
final HttpResponseOutputStream<Chunk> stream = this.write(file, status,
new DelayedHttpEntityCallable<Chunk>(file) {
@@ -19,10 +19,10 @@ package ch.cyberduck.core.ftp;
import ch.cyberduck.core.exception.AccessDeniedException;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionRefusedException;
import ch.cyberduck.core.exception.ConnectionTimeoutException;
import ch.cyberduck.core.exception.InteroperabilityException;
import ch.cyberduck.core.exception.NotfoundException;
import ch.cyberduck.core.preferences.PreferencesFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -33,22 +33,16 @@ public class DataConnectionActionExecutor {
private static final Logger log = LogManager.getLogger(DataConnectionActionExecutor.class);
private final FTPSession session;
private final boolean enabled;
public DataConnectionActionExecutor(final FTPSession session) {
this(session, PreferencesFactory.get().getBoolean("ftp.connectmode.fallback"));
}
public DataConnectionActionExecutor(final FTPSession session, final boolean enabled) {
this.session = session;
this.enabled = enabled;
}
/**
* @param action Action that needs to open a data connection
* @return True if action was successful
*/
public <T> T data(final DataConnectionAction<T> action) throws IOException, BackgroundException {
public <T> T open(final DataConnectionAction<T> action) throws IOException, BackgroundException {
try {
// Make sure to always configure data mode because connect event sets defaults.
final FTPConnectMode mode = session.getConnectMode();
@@ -62,52 +56,15 @@ public class DataConnectionActionExecutor {
}
return action.execute();
}
catch(ConnectionTimeoutException failure) {
log.warn("Timeout opening data socket {}", failure.getMessage());
// Expect 421 response
catch(ConnectionRefusedException | ConnectionTimeoutException failure) {
log.warn("I/O error opening data socket {}", failure.getMessage());
// Drain any pending reply on the control channel to resynchronize after failed data connection
session.getClient().completePendingCommand();
// Fallback handling
if(enabled) {
try {
return this.fallback(action);
}
catch(BackgroundException e) {
log.warn("Connect mode fallback failed with {}", e.getMessage());
// Throw original error message
}
}
throw failure;
}
catch(InteroperabilityException | NotfoundException | AccessDeniedException failure) {
log.warn("Server denied data socket operation with {}", failure.getMessage());
// Fallback handling
if(enabled) {
try {
return this.fallback(action);
}
catch(BackgroundException e) {
log.warn("Connect mode fallback failed with {}", e.getMessage());
// Throw original error message
}
}
throw failure;
}
}
/**
* @param action Action that needs to open a data connection
* @return True if action was successful
*/
protected <T> T fallback(final DataConnectionAction<T> action) throws BackgroundException {
// Fallback to other connect mode
if(session.getClient().getDataConnectionMode() == FTPClient.PASSIVE_LOCAL_DATA_CONNECTION_MODE) {
log.warn("Fallback to active data connection");
session.getClient().enterLocalActiveMode();
}
else if(session.getClient().getDataConnectionMode() == FTPClient.ACTIVE_LOCAL_DATA_CONNECTION_MODE) {
log.warn("Fallback to passive data connection");
session.getClient().enterLocalPassiveMode();
}
return action.execute();
}
}
@@ -61,7 +61,7 @@ public class FTPReadFeature implements Read {
if(status.isAppend()) {
session.getClient().setRestartOffset(status.getOffset());
}
final InputStream in = new DataConnectionActionExecutor(session).data(new DataConnectionAction<InputStream>() {
final InputStream in = new DataConnectionActionExecutor(session).open(new DataConnectionAction<InputStream>() {
@Override
public InputStream execute() throws BackgroundException {
try {
@@ -48,7 +48,7 @@ public class FTPWriteFeature implements Write<Void> {
if(!session.getClient().setFileType(FTPClient.BINARY_FILE_TYPE)) {
throw new FTPException(session.getClient().getReplyCode(), session.getClient().getReplyString());
}
final OutputStream out = new DataConnectionActionExecutor(session).data(new DataConnectionAction<OutputStream>() {
final OutputStream out = new DataConnectionActionExecutor(session).open(new DataConnectionAction<OutputStream>() {
@Override
public OutputStream execute() throws BackgroundException {
try {
@@ -55,7 +55,7 @@ public class FTPDefaultListService implements ListService {
// data connection in type ASCII or type EBCDIC.
throw new FTPException(session.getClient().getReplyCode(), session.getClient().getReplyString());
}
final List<String> list = new DataConnectionActionExecutor(session).data(new DataConnectionAction<List<String>>() {
final List<String> list = new DataConnectionActionExecutor(session).open(new DataConnectionAction<List<String>>() {
@Override
public List<String> execute() throws BackgroundException {
try {
@@ -53,7 +53,7 @@ public class FTPMlsdListService implements ListService {
// data connection in type ASCII or type EBCDIC.
throw new FTPException(session.getClient().getReplyCode(), session.getClient().getReplyString());
}
final List<String> list = new DataConnectionActionExecutor(session).data(new DataConnectionAction<List<String>>() {
final List<String> list = new DataConnectionActionExecutor(session).open(new DataConnectionAction<List<String>>() {
@Override
public List<String> execute() throws BackgroundException {
try {
@@ -17,97 +17,27 @@ package ch.cyberduck.core.ftp;
* Bug fixes, suggestions and comments should be sent to feedback@cyberduck.ch
*/
import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.HostKeyCallback;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.proxy.DisabledProxyFinder;
import ch.cyberduck.core.threading.CancelCallback;
import ch.cyberduck.core.transfer.TransferStatus;
import ch.cyberduck.core.exception.InteroperabilityException;
import ch.cyberduck.test.IntegrationTest;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.util.EnumSet;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
@Category(IntegrationTest.class)
public class DataConnectionActionExecutorTest extends AbstractFTPTest {
@Test
@Ignore
public void testFallbackDataConnectionSocketTimeout() throws Exception {
final Host host = new Host(new FTPProtocol(), "mirror.switch.ch", new Credentials(
PreferencesFactory.get().getProperty("connection.login.anon.name"), null
));
host.setFTPConnectMode(FTPConnectMode.active);
final AtomicInteger count = new AtomicInteger();
final FTPSession session = new FTPSession(host);
session.open(new DisabledProxyFinder(), HostKeyCallback.noop, LoginCallback.noop, CancelCallback.noop);
session.getClient().setDefaultTimeout(2000);
session.getClient().setConnectTimeout(2000);
session.login(LoginCallback.noop, CancelCallback.noop);
final Path file = new Path("/pub/debian/README.html", EnumSet.of(Path.Type.file));
final TransferStatus status = new TransferStatus();
final DataConnectionAction<InputStream> action = new DataConnectionAction<InputStream>() {
@Override
public InputStream execute() throws BackgroundException {
try {
final InputStream in = session.getClient().retrieveFileStream(file.getAbsolute());
if(count.get() == 0) {
throw new FTPExceptionMappingService().map(new SocketTimeoutException());
}
return in;
}
catch(IOException e) {
throw new FTPExceptionMappingService().map(e);
}
}
};
final DataConnectionActionExecutor f = new DataConnectionActionExecutor(session, true) {
@Override
protected <T> T fallback(final DataConnectionAction<T> action) throws BackgroundException {
count.incrementAndGet();
return super.fallback(action);
}
};
f.data(action);
assertEquals(1, count.get());
}
@Test
public void testFallbackDataConnection500Error() throws Exception {
session.getHost().setFTPConnectMode(FTPConnectMode.active);
final AtomicInteger count = new AtomicInteger();
public void testServerError() throws Exception {
final DataConnectionAction<Void> action = new DataConnectionAction<Void>() {
@Override
public Void execute() throws BackgroundException {
if(count.get() == 0) {
throw new FTPExceptionMappingService().map(new FTPException(500, "m"));
}
return null;
throw new FTPExceptionMappingService().map(new FTPException(500, "m"));
}
};
final DataConnectionActionExecutor f = new DataConnectionActionExecutor(session, true) {
@Override
protected <T> T fallback(final DataConnectionAction<T> action) throws BackgroundException {
count.incrementAndGet();
return super.fallback(action);
}
};
f.data(action);
assertEquals(1, count.get());
final DataConnectionActionExecutor f = new DataConnectionActionExecutor(session);
assertThrows(InteroperabilityException.class, () -> f.open(action));
}
}
@@ -19,6 +19,7 @@ import ch.cyberduck.core.AttributedList;
import ch.cyberduck.core.DefaultPathAttributes;
import ch.cyberduck.core.DefaultPathContainerService;
import ch.cyberduck.core.DescriptiveUrl;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.ListProgressListener;
import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.Path;
@@ -66,19 +67,19 @@ public class DriveAttributesFinderFeature implements AttributesFinder, Attribute
if(new DefaultPathContainerService().isContainer(file)) {
return PathAttributes.EMPTY;
}
final Path query;
if(file.isPlaceholder()) {
query = new Path(file.getParent(), FilenameUtils.removeExtension(file.getName()), file.getType(), file.attributes());
}
else {
query = file;
}
final AttributedList<Path> list;
if(new SimplePathPredicate(DriveHomeFinderService.SHARED_DRIVES_NAME).test(file.getParent())) {
list = new DriveTeamDrivesListService(session, fileid).list(file.getParent(), listener);
}
else {
list = new FileidDriveListService(session, fileid, query).list(file.getParent(), listener);
final Path query;
if(file.isPlaceholder()) {
query = new Path(file.getParent(), FilenameUtils.removeExtension(file.getName()), file.getType(), file.attributes());
}
else {
query = file;
}
list = new FileidDriveListService(session, fileid, query).list(file.getParent(), new DisabledListProgressListener());
}
final Path found = list.find(new ListFilteringFeature.ListFilteringPredicate(session.getCaseSensitivity(), file));
if(null == found) {
@@ -23,6 +23,7 @@ import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.SimplePathPredicate;
import ch.cyberduck.core.exception.ListCanceledException;
import ch.cyberduck.core.exception.NotfoundException;
import ch.cyberduck.core.features.Delete;
import ch.cyberduck.core.preferences.PreferencesFactory;
@@ -89,7 +90,12 @@ public class DriveAttributesFinderFeatureTest extends AbstractDriveTest {
final DriveFileIdProvider fileid = new DriveFileIdProvider(session);
new DriveTouchFeature(session, fileid).touch(new DriveWriteFeature(session, fileid), test, new TransferStatus());
final DriveAttributesFinderFeature f = new DriveAttributesFinderFeature(session, fileid);
final PathAttributes attributes = f.find(test);
final PathAttributes attributes = f.find(test, new DisabledListProgressListener() {
@Override
public void chunk(final Path directory, final AttributedList<Path> list) throws ListCanceledException {
fail();
}
});
assertEquals(0L, attributes.getSize());
assertNotNull(attributes.getFileId());
assertNull(attributes.getVersionId());
@@ -34,6 +34,7 @@ import ch.cyberduck.core.shared.DefaultFindFeature;
import ch.cyberduck.core.transfer.TransferStatus;
import ch.cyberduck.test.IntegrationTest;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.experimental.categories.Category;
@@ -46,6 +47,7 @@ import static org.junit.Assert.*;
public class DeleteWorkerTest extends AbstractDriveTest {
@Test
@Ignore
public void testDelete() throws Exception {
final Path home = DriveHomeFinderService.MYDRIVE_FOLDER;
final DriveFileIdProvider fileid = new DriveFileIdProvider(session);
@@ -25,6 +25,7 @@ import ch.cyberduck.core.Local;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.NullFilter;
import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.ProgressListener;
import ch.cyberduck.core.TestProtocol;
@@ -43,7 +44,6 @@ import ch.cyberduck.core.nio.LocalProtocol;
import ch.cyberduck.core.nio.LocalReadFeature;
import ch.cyberduck.core.nio.LocalSession;
import ch.cyberduck.core.notification.DisabledNotificationService;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.proxy.DisabledProxyFinder;
import ch.cyberduck.core.threading.CancelCallback;
import ch.cyberduck.core.transfer.DisabledTransferErrorCallback;
@@ -115,13 +115,14 @@ public class CryptoLocalSingleTransferWorkerTest {
out2.close();
final CryptoVault cryptomator = new CryptoVault(vault);
cryptomator.create(session, new VaultCredentials("test"), vaultVersion);
session.withRegistry(new DefaultVaultRegistry(new DisabledPasswordCallback() {
final DefaultVaultRegistry vaults = new DefaultVaultRegistry(new DisabledPasswordCallback() {
@Override
public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
return new VaultCredentials("test");
}
}));
PreferencesFactory.get().setProperty("factory.vault.class", CryptoVault.class.getName());
});
vaults.add(cryptomator.load(session, PasswordCallback.noop));
session.withRegistry(vaults);
final Transfer t = new UploadTransfer(new Host(new TestProtocol()), Collections.singletonList(new TransferItem(dir1, localDirectory1)), new NullFilter<>());
assertTrue(new SingleTransferWorker(session, session, t, new TransferOptions(), new TransferSpeedometer(t), new DisabledTransferPrompt() {
@Override
@@ -41,6 +41,7 @@ public class OAuthExceptionMappingService extends AbstractExceptionMappingServic
switch(details.getError()) {
// Error code "invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", "unsupported_grant_type", "invalid_scope"
case "invalid_client":
case "invalid_request":
case "unauthorized_client":
case "unsupported_grant_type":
case "invalid_scope":
@@ -24,9 +24,11 @@ import ch.cyberduck.core.onedrive.features.GraphFileIdProvider;
import ch.cyberduck.core.onedrive.features.sharepoint.SiteDrivesListService;
import ch.cyberduck.core.onedrive.features.sharepoint.SitesListService;
import java.util.EnumSet;
import java.util.Optional;
import static ch.cyberduck.core.onedrive.SharepointListService.*;
import static ch.cyberduck.core.onedrive.SharepointListService.DRIVES_CONTAINER;
import static ch.cyberduck.core.onedrive.SharepointListService.SITES_CONTAINER;
public abstract class AbstractSharepointListService implements ListService {
@@ -72,8 +74,8 @@ public abstract class AbstractSharepointListService implements ListService {
protected AttributedList<Path> addSiteItems(final Path directory, final ListProgressListener listener) throws BackgroundException {
final AttributedList<Path> list = new AttributedList<>();
list.add(new Path(directory, DRIVES_NAME.getName(), DRIVES_NAME.getType(), DRIVES_NAME.attributes()));
list.add(new Path(directory, SITES_NAME.getName(), SITES_NAME.getType(), SITES_NAME.attributes()));
list.add(new Path(directory, DRIVES_CONTAINER, EnumSet.of(Path.Type.placeholder, Path.Type.directory, Path.Type.volume)));
list.add(new Path(directory, SITES_CONTAINER, EnumSet.of(Path.Type.placeholder, Path.Type.directory, Path.Type.volume)));
listener.chunk(directory, list);
return list;
}
@@ -16,36 +16,27 @@ package ch.cyberduck.core.onedrive;
*/
import ch.cyberduck.core.AttributedList;
import ch.cyberduck.core.DefaultPathAttributes;
import ch.cyberduck.core.DefaultIOExceptionMappingService;
import ch.cyberduck.core.ListProgressListener;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.onedrive.features.GraphFileIdProvider;
import ch.cyberduck.core.onedrive.features.sharepoint.GroupDrivesListService;
import ch.cyberduck.core.onedrive.features.sharepoint.GroupListService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.nuxeo.onedrive.client.OneDriveAPIException;
import org.nuxeo.onedrive.client.OneDriveRuntimeException;
import org.nuxeo.onedrive.client.types.Site;
import java.io.IOException;
import java.util.EnumSet;
import java.util.Optional;
public class SharepointListService extends AbstractSharepointListService {
static final Logger log = LogManager.getLogger(SharepointListService.class);
public static final String DEFAULT_SITE = "Default";
public static final String DRIVES_CONTAINER = "Drives";
public static final String GROUPS_CONTAINER = "Groups";
public static final String SITES_CONTAINER = "Sites";
public static final Path DEFAULT_NAME = new Path(DEFAULT_SITE, EnumSet.of(Path.Type.volume, Path.Type.placeholder, Path.Type.directory, Path.Type.symboliclink));
public static final Path DRIVES_NAME = new Path(DRIVES_CONTAINER, EnumSet.of(Path.Type.placeholder, Path.Type.directory));
public static final Path GROUPS_NAME = new Path(GROUPS_CONTAINER, EnumSet.of(Path.Type.placeholder, Path.Type.directory));
public static final Path SITES_NAME = new Path(SITES_CONTAINER, EnumSet.of(Path.Type.placeholder, Path.Type.directory));
private final SharepointSession session;
private final GraphFileIdProvider fileid;
@@ -55,48 +46,21 @@ public class SharepointListService extends AbstractSharepointListService {
this.fileid = fileid;
}
private Optional<Path> getDefault(final Path directory) {
try {
final Site site = Site.byId(session.getClient(), "root");
final Site.Metadata metadata = site.getMetadata(null); // query: null: Default return set.
final EnumSet<Path.Type> type = EnumSet.copyOf(DEFAULT_NAME.getType());
final Path path = new Path(directory, DEFAULT_NAME.getName(), type, new DefaultPathAttributes().setFileId(metadata.getId()));
path.setSymlinkTarget(
new Path(SITES_NAME, metadata.getSiteCollection().getHostname(), SITES_NAME.getType(),
new DefaultPathAttributes().setFileId(metadata.getId())));
return Optional.of(path);
}
catch(IOException ex) {
log.error("Cannot get default site. Skipping.", ex);
}
return Optional.empty();
}
@Override
protected AttributedList<Path> getRoot(final Path directory, final ListProgressListener listener) throws BackgroundException {
final AttributedList<Path> list = new AttributedList<>();
getDefault(directory).ifPresent(list::add);
addDefaultItems(list);
list.add(new Path(directory, GROUPS_CONTAINER, EnumSet.of(Path.Type.placeholder, Path.Type.directory, Path.Type.volume)));
list.add(new Path(directory, SITES_CONTAINER, EnumSet.of(Path.Type.placeholder, Path.Type.directory, Path.Type.volume)));
listener.chunk(directory, list);
return list;
}
static void addDefaultItems(final AttributedList<Path> list) {
list.add(GROUPS_NAME);
list.add(SITES_NAME);
}
@Override
protected AttributedList<Path> processList(Path directory, final ListProgressListener listener) throws BackgroundException {
final GraphSession.ContainerItem container = session.getContainer(directory);
if(container.isDrive()) {
return AttributedList.emptyList();
}
// Default?
if(!container.isDefined() && container.getContainerPath().map(p -> DEFAULT_SITE.equals(p.getName())).orElse(false)) {
return addSiteItems(directory, listener);
}
if(container.getCollectionPath().map(p -> GROUPS_CONTAINER.equals(p.getName())).orElse(false)) {
if(!container.isDefined()) {
return new GroupListService(session, fileid).list(directory, listener);
@@ -107,4 +71,21 @@ public class SharepointListService extends AbstractSharepointListService {
}
return AttributedList.emptyList();
}
public Path getDefaultSite() throws BackgroundException {
try {
final Site.Metadata metadata = Site.byId(session.getClient(), "root").getMetadata(null);
return this.list(new Path(SharepointListService.SITES_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory, Path.Type.volume))).find(path -> metadata.getId().equals(path.attributes().getFileId()));
}
catch(OneDriveRuntimeException e) {
throw new GraphExceptionMappingService(fileid).map(e.getCause());
}
catch(OneDriveAPIException e) {
throw new GraphExceptionMappingService(fileid).map(e);
}
catch(IOException e) {
throw new DefaultIOExceptionMappingService().map(e);
}
}
}
@@ -46,21 +46,13 @@ public class SharepointSession extends AbstractSharepointSession {
if(parentContainer.getCollectionPath().map(p -> SharepointListService.GROUPS_CONTAINER.equals(p.getName())).orElse(false)) {
return new Drive(getGroup(parentContainer.getContainerPath().get()), driveId);
}
else if(parentContainer.getContainerPath().map(p -> SharepointListService.DEFAULT_SITE.equals(p.getName())).orElse(false)) {
// Handles /Default-case, which is a site.
return new Drive(getSite(parentContainer.getContainerPath().get()), driveId);
}
else {
// finds:
// Sites/<site name>
final GraphSession.ContainerItem containerItem = getContainer(parentContainer.getContainerPath().get());
if(containerItem.getCollectionPath().map(p -> SharepointListService.SITES_CONTAINER.equals(p.getName())).orElse(false)) {
return new Drive(getSite(containerItem.getContainerPath().get()), driveId);
}
else {
return null;
}
// finds:
// Sites/<site name>
final GraphSession.ContainerItem containerItem = getContainer(parentContainer.getContainerPath().get());
if(containerItem.getCollectionPath().map(p -> SharepointListService.SITES_CONTAINER.equals(p.getName())).orElse(false)) {
return new Drive(getSite(containerItem.getContainerPath().get()), driveId);
}
return null;
}
@Override
@@ -21,7 +21,6 @@ import org.nuxeo.onedrive.client.types.SharePointIds;
import org.nuxeo.onedrive.client.types.Site;
import org.nuxeo.onedrive.client.types.Site.Metadata;
import java.net.URI;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
@@ -137,23 +136,27 @@ public class SitesListService extends AbstractListService<Site.Metadata> {
}
});
if(!result.isEmpty()) {
final Set<Integer> set = duplicates.getOrDefault(file.getName(), new HashSet<>());
set.add(i);
duplicates.put(file.getName(), set);
duplicates.computeIfAbsent(file.getName(), key -> new HashSet<>())
.add(i);
}
}
for(Set<Integer> set : duplicates.values()) {
for(Integer i : set) {
final Path file = list.get(i);
final URI webLink = URI.create(file.attributes().getLink().getUrl());
final String[] path = webLink.getPath().split(String.valueOf(Path.DELIMITER));
final String suffix = path[path.length - 2];
final Path rename = new Path(file.getParent(), String.format("%s (%s)", file.getName(), suffix), file.getType(), file.attributes());
final String siteIdUnique = getSiteId(file.attributes().getFileId());
final Path rename = new Path(file.getParent(), String.format("%s (%s)", file.getName(), siteIdUnique), file.getType(), file.attributes());
list.set(i, rename);
}
}
}
private static String getSiteId(final String fileId) {
// caller ensures that fileId is valid ("tenant,siteId,webId")
final int siteIdStart = fileId.indexOf(',');
final int siteIdEnd = fileId.indexOf(',', siteIdStart + 1);
return fileId.substring(siteIdStart + 1, siteIdEnd);
}
enum SharepointID {
Invalid(0, 0);
@@ -18,7 +18,6 @@ package ch.cyberduck.core.onedrive;
import ch.cyberduck.core.AlphanumericRandomStringService;
import ch.cyberduck.core.AttributedList;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
@@ -46,8 +45,8 @@ public class GraphLockFeatureTest extends AbstractSharepointTest {
@Test
public void testLock() throws Exception {
final ListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(SharepointListService.DEFAULT_NAME, DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final SharepointListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(list.getDefaultSite(), DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final Path drive = drives.get(0);
final Path file = new GraphTouchFeature(session, fileid).touch(new GraphWriteFeature(session, fileid), new Path(drive,
new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
@@ -1,31 +1,13 @@
package ch.cyberduck.core.onedrive;
/*
* Copyright (c) 2002-2018 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.AlphanumericRandomStringService;
import ch.cyberduck.core.AttributedList;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.exception.NotfoundException;
import ch.cyberduck.core.features.Home;
import ch.cyberduck.core.onedrive.features.GraphAttributesFinderFeature;
import ch.cyberduck.core.shared.DefaultHomeFinderService;
import ch.cyberduck.core.shared.PathAttributesHomeFeature;
import ch.cyberduck.core.shared.RootPathContainerService;
import ch.cyberduck.test.IntegrationTest;
import org.junit.Test;
@@ -33,9 +15,7 @@ import org.junit.experimental.categories.Category;
import java.util.EnumSet;
import static ch.cyberduck.core.onedrive.SharepointListService.DRIVES_CONTAINER;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.*;
@Category(IntegrationTest.class)
public class SharepointListServiceTest extends AbstractSharepointTest {
@@ -46,29 +26,54 @@ public class SharepointListServiceTest extends AbstractSharepointTest {
new SharepointListService(session, fileid).list(directory, new DisabledListProgressListener());
}
@Test
public void testListRoot() throws Exception {
final AttributedList<Path> list = new SharepointListService(session, fileid).list(Home.root(), new DisabledListProgressListener());
assertFalse(list.isEmpty());
assertEquals(3, list.size());
}
@Test
public void testListDefault() throws Exception {
new SharepointListService(session, fileid).list(SharepointListService.DEFAULT_NAME, new DisabledListProgressListener());
final SharepointListService list = new SharepointListService(session, fileid);
final Path defaultSite = new Path(list.getDefaultSite()).withAttributes(PathAttributes.EMPTY);
final AttributedList<Path> containers = list.list(defaultSite);
assertEquals(2, containers.size());
}
@Test
public void testListDefaultDriveOverwrite() throws Exception {
final ListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(SharepointListService.DEFAULT_NAME, DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final Path drive = drives.get(0);
new PathAttributesHomeFeature(session, () -> drive, new GraphAttributesFinderFeature(session, fileid), new RootPathContainerService()).find();
list.list(drive, new DisabledListProgressListener());
public void testListRoot() throws Exception {
final Path root = Home.root();
final AttributedList<Path> list = new SharepointListService(session, fileid).list(root, new DisabledListProgressListener());
assertFalse(list.isEmpty());
assertEquals(2, list.size());
for(Path d : list) {
assertSame(root, d.getParent());
}
}
@Test
public void testListGroups() throws Exception {
new SharepointListService(session, fileid).list(SharepointListService.GROUPS_NAME, new DisabledListProgressListener());
final Path container = new Path(SharepointListService.GROUPS_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory));
final AttributedList<Path> list = new SharepointListService(session, fileid).list(container, new DisabledListProgressListener());
for(Path group : list) {
assertSame(container, group.getParent());
}
}
@Test
public void testListSites() throws Exception {
final Path container = new Path(SharepointListService.SITES_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory));
final AttributedList<Path> list = new SharepointListService(session, fileid).list(container, new DisabledListProgressListener());
for(Path site : list) {
assertSame(container, site.getParent());
}
}
@Test
public void testListDrives() throws Exception {
final SharepointListService list = new SharepointListService(session, fileid);
final Path siteWithoutId = new Path(list.getDefaultSite()).withAttributes(PathAttributes.EMPTY);
final Path directory = new Path(siteWithoutId, SharepointListService.DRIVES_CONTAINER, EnumSet.of(Path.Type.directory));
final AttributedList<Path> drives = list.list(directory);
assertFalse(drives.isEmpty());
for(Path d : drives) {
assertSame(directory, d.getParent());
}
}
}
@@ -19,7 +19,6 @@ import ch.cyberduck.core.AbstractPath;
import ch.cyberduck.core.DefaultPathAttributes;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.features.Home;
import ch.cyberduck.core.ssl.DefaultX509KeyManager;
import ch.cyberduck.core.ssl.DisabledX509TrustManager;
@@ -46,29 +45,21 @@ public class SharepointSessionTest {
@Test
public void isAccessible() {
assertFalse(session.isAccessible(Home.root()));
assertFalse(session.isAccessible(SharepointListService.DEFAULT_NAME));
assertFalse(session.isAccessible(SharepointListService.DEFAULT_NAME, false));
final Path defaultSiteDrive =
new Path(
new Path(
SharepointListService.DEFAULT_NAME, DRIVES_CONTAINER, EnumSet.of(AbstractPath.Type.directory)),
"Drive-Id", EnumSet.of(Path.Type.directory));
assertTrue(session.isAccessible(defaultSiteDrive));
assertFalse(session.isAccessible(defaultSiteDrive, false));
assertFalse(session.isAccessible(SharepointListService.SITES_NAME));
assertFalse(session.isAccessible(SharepointListService.SITES_NAME, false));
assertFalse(session.isAccessible(SharepointListService.GROUPS_NAME));
assertFalse(session.isAccessible(SharepointListService.GROUPS_NAME, false));
final Path siteDrive =
new Path(
new Path(
new Path(SharepointListService.SITES_NAME, "Site", EnumSet.of(AbstractPath.Type.directory)),
DRIVES_CONTAINER, EnumSet.of(AbstractPath.Type.directory)),
"Drive-Id", EnumSet.of(Path.Type.directory));
assertFalse(session.isAccessible(new Path(SharepointListService.SITES_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory))));
assertFalse(session.isAccessible(new Path(SharepointListService.SITES_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory)), false));
assertFalse(session.isAccessible(new Path(SharepointListService.GROUPS_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory))));
assertFalse(session.isAccessible(new Path(SharepointListService.GROUPS_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory)), false));
final Path siteDrive = new Path(new Path(new Path(new Path(SharepointListService.SITES_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory)), "Site", EnumSet.of(AbstractPath.Type.directory)),
DRIVES_CONTAINER, EnumSet.of(AbstractPath.Type.directory)), "Drive-Id", EnumSet.of(Path.Type.directory));
assertTrue(session.isAccessible(siteDrive));
assertFalse(session.isAccessible(siteDrive, false));
final Path group = new Path(SharepointListService.GROUPS_NAME, "Group Name", EnumSet.of(Path.Type.directory));
final Path group = new Path(new Path(SharepointListService.GROUPS_CONTAINER,
EnumSet.of(Path.Type.placeholder, Path.Type.directory)), "Group Name", EnumSet.of(Path.Type.directory));
assertFalse(session.isAccessible(group));
assertFalse(session.isAccessible(group, false));
assertTrue(session.isAccessible(new Path(group, "Drive-Id", EnumSet.of(Path.Type.directory))));
@@ -34,7 +34,6 @@ public class SharepointSiteSessionTest {
@Test
public void testHome() {
Assert.assertFalse(session.isHome(SharepointListService.DEFAULT_NAME));
Assert.assertTrue(session.isHome(Home.root()));
}
}
@@ -18,7 +18,6 @@ package ch.cyberduck.core.onedrive;
import ch.cyberduck.core.AlphanumericRandomStringService;
import ch.cyberduck.core.AttributedList;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
@@ -50,8 +49,8 @@ public class SharepointTimestampFeatureTest extends AbstractSharepointTest {
@Test
public void testSetTimestamp() throws Exception {
final ListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(SharepointListService.DEFAULT_NAME, DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final SharepointListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(list.getDefaultSite(), DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final Path drive = drives.get(0);
final Path file = new Path(drive, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file));
new GraphTouchFeature(session, fileid).touch(new GraphWriteFeature(session, fileid), file, new TransferStatus().setMime("x-application/cyberduck"));
@@ -69,8 +68,8 @@ public class SharepointTimestampFeatureTest extends AbstractSharepointTest {
@Test
public void testSetTimestampDirectory() throws Exception {
final ListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(SharepointListService.DEFAULT_NAME, DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final SharepointListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(list.getDefaultSite(), DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final Path drive = drives.get(0);
final Path test = new Path(drive, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.directory));
new GraphDirectoryFeature(session, fileid).mkdir(new GraphWriteFeature(session, fileid), test, null);
@@ -21,7 +21,6 @@ import ch.cyberduck.core.ConnectionCallback;
import ch.cyberduck.core.DefaultPathAttributes;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.DisabledPasswordCallback;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.exception.NotfoundException;
@@ -56,8 +55,8 @@ public class SharepointVersioningFeatureTest extends AbstractSharepointTest {
@Test
public void testList() throws Exception {
final GraphFileIdProvider fileid = new GraphFileIdProvider(session);
final ListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(SharepointListService.DEFAULT_NAME, DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final SharepointListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(list.getDefaultSite(), DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final Path drive = drives.get(0);
final Path test = new GraphTouchFeature(session, fileid).touch(new GraphWriteFeature(session, fileid), new Path(drive, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus());
assertNotNull(test.attributes().getVersionId());
@@ -19,7 +19,6 @@ import ch.cyberduck.core.AlphanumericRandomStringService;
import ch.cyberduck.core.AttributedList;
import ch.cyberduck.core.ConnectionCallback;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
@@ -57,8 +56,8 @@ public class SharepointWriteFeatureTest extends AbstractSharepointTest {
@Test
public void testWrite() throws Exception {
final GraphWriteFeature feature = new GraphWriteFeature(session, fileid);
final ListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(SharepointListService.DEFAULT_NAME, DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final SharepointListService list = new SharepointListService(session, fileid);
final AttributedList<Path> drives = list.list(new Path(list.getDefaultSite(), DRIVES_CONTAINER, EnumSet.of(Path.Type.directory)), new DisabledListProgressListener());
final Path container = drives.get(0);
final Path folder = new GraphDirectoryFeature(session, fileid).mkdir(new GraphWriteFeature(session, fileid), new Path(container, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.directory)), new TransferStatus());
final PathAttributes folderAttributes = new GraphAttributesFinderFeature(session, fileid).find(folder);
@@ -30,7 +30,6 @@ import ch.cyberduck.core.Host;
import ch.cyberduck.core.Local;
import ch.cyberduck.core.LocalFactory;
import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.LocationCallback;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.exception.ConnectionCanceledException;
@@ -157,12 +156,4 @@ public class PromptLoginCallback extends PromptPasswordCallback implements Login
}
throw new LoginCanceledException();
}
@Override
public <T> T getFeature(final Class<T> type) {
if(type == LocationCallback.class) {
return type.cast(new PromptLocationCallback(controller));
}
return null;
}
}
@@ -24,7 +24,7 @@ import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.exception.LoginCanceledException;
import ch.cyberduck.ui.cocoa.controller.PasswordController;
public class PromptPasswordCallback implements PasswordCallback {
public class PromptPasswordCallback extends PromptLocationCallback implements PasswordCallback {
private final ProxyController controller;
@@ -32,6 +32,7 @@ public class PromptPasswordCallback implements PasswordCallback {
private boolean suppressed;
public PromptPasswordCallback(final ProxyController controller) {
super(controller);
this.controller = controller;
}
@@ -502,8 +502,8 @@ public class BookmarkController extends SheetController implements CollectionLis
}
@Override
public void display(final boolean key) {
super.display(key);
public void display(final boolean key, final String frameName) {
super.display(key, frameName);
cascade = this.cascade(cascade);
}
@@ -636,9 +636,7 @@ public class BrowserController extends WindowController implements NSToolbar.Del
}
@Override
public void setWindow(NSWindow window) {
// Save frame rectangle
window.setFrameAutosaveName("Browser");
public void setWindow(final NSWindow window) {
if(window.respondsToSelector(Foundation.selector("setSubtitle:"))) {
window.setSubtitle(StringUtils.EMPTY);
}
@@ -662,16 +660,24 @@ public class BrowserController extends WindowController implements NSToolbar.Del
}
@Override
public void display(final boolean key) {
super.display(key);
public String windowFrameName() {
if(pool != SessionPool.DISCONNECTED) {
return pool.getHost().getUuid();
}
return "Browser";
}
@Override
public void display(final boolean key, final String frameName) {
super.display(key, frameName);
cascade = this.cascade(cascade);
}
@Override
public void windowWillClose(final NSNotification notification) {
// Convert from lower left to top left coordinates
cascade = new NSPoint(this.window().frame().origin.x.doubleValue(),
this.window().frame().origin.y.doubleValue() + this.window().frame().size.height.doubleValue());
cascade = new NSPoint(window.frame().origin.x.doubleValue(),
window.frame().origin.y.doubleValue() + window.frame().size.height.doubleValue());
super.windowWillClose(notification);
}
@@ -1788,7 +1794,7 @@ public class BrowserController extends WindowController implements NSToolbar.Del
@Action
public void searchButtonClicked(final ID sender) {
this.window().makeFirstResponder(searchField);
window.makeFirstResponder(searchField);
}
@Action
@@ -3346,6 +3352,12 @@ public class BrowserController extends WindowController implements NSToolbar.Del
scheduler.shutdown(false);
}
pool.shutdown();
window.setTitle(StringUtils.EMPTY);
if(window.respondsToSelector(Foundation.selector("setSubtitle:"))) {
window.setSubtitle(StringUtils.EMPTY);
}
window.setRepresentedFilename(StringUtils.EMPTY);
window.saveFrameUsingName(pool.getHost().getUuid());
pool = SessionPool.DISCONNECTED;
setWorkdir(null);
cache.clear();
@@ -3353,11 +3365,6 @@ public class BrowserController extends WindowController implements NSToolbar.Del
editor.close();
}
editors.clear();
window.setTitle(StringUtils.EMPTY);
if(window.respondsToSelector(Foundation.selector("setSubtitle:"))) {
window.setSubtitle(StringUtils.EMPTY);
}
window.setRepresentedFilename(StringUtils.EMPTY);
navigation.clear();
disconnected.run();
}
@@ -328,7 +328,6 @@ public class InfoController extends ToolbarWindowController {
@Override
public void setWindow(final NSWindow window) {
window.setFrameAutosaveName("Info");
window.setHidesOnDeactivate(false);
window.setShowsResizeIndicator(true);
window.setContentMinSize(window.frame().size);
@@ -340,14 +339,19 @@ public class InfoController extends ToolbarWindowController {
}
@Override
public void display(final boolean key) {
super.display(key);
public String windowFrameName() {
return "Info";
}
@Override
public void display(final boolean key, final String frameName) {
super.display(key, frameName);
cascade = this.cascade(cascade);
}
@Override
public void windowWillClose(final NSNotification notification) {
cascade = new NSPoint(this.window().frame().origin.x.doubleValue(), this.window().frame().origin.y.doubleValue() + this.window().frame().size.height.doubleValue());
cascade = new NSPoint(window.frame().origin.x.doubleValue(), window.frame().origin.y.doubleValue() + window.frame().size.height.doubleValue());
super.windowWillClose(notification);
}
@@ -1852,7 +1856,7 @@ public class InfoController extends ToolbarWindowController {
* @return True if progress animation has started and settings are toggled
*/
protected boolean toggleS3Settings(final boolean stop) {
this.window().endEditingFor(null);
window.endEditingFor(null);
final Credentials credentials = session.getHost().getCredentials();
boolean enable = session.getHost().getProtocol().getType() == Protocol.Type.s3
|| session.getHost().getProtocol().getType() == Protocol.Type.b2
@@ -2083,7 +2087,7 @@ public class InfoController extends ToolbarWindowController {
* @return True if progress animation has started and settings are toggled
*/
protected boolean toggleAclSettings(final boolean stop) {
this.window().endEditingFor(null);
window.endEditingFor(null);
final boolean enabled = this.validateAclActions(stop);
if(stop) {
aclProgress.stopAnimation(null);
@@ -2114,7 +2118,7 @@ public class InfoController extends ToolbarWindowController {
* @return True if progress animation has started and settings are toggled
*/
protected boolean toggleMetadataSettings(final boolean stop) {
this.window().endEditingFor(null);
window.endEditingFor(null);
final boolean feature = this.validateMetadataActions(stop);
if(stop) {
metadataProgress.stopAnimation(null);
@@ -2166,7 +2170,7 @@ public class InfoController extends ToolbarWindowController {
* @return True if progress animation has started and settings are toggled
*/
protected boolean toggleVersionsSettings(final boolean stop) {
this.window().endEditingFor(null);
window.endEditingFor(null);
final boolean enabled = this.validateVersionsActions(stop);
if(stop) {
versionsProgress.stopAnimation(null);
@@ -2354,7 +2358,7 @@ public class InfoController extends ToolbarWindowController {
* @return True if controls are enabled for the given protocol in idle state
*/
protected boolean togglePermissionSettings(final boolean stop) {
this.window().endEditingFor(null);
window.endEditingFor(null);
final Credentials credentials = session.getHost().getCredentials();
boolean enable = !credentials.isAnonymousLogin() && session.getFeature(UnixPermission.class) != null;
recursiveButton.setEnabled(stop && enable);
@@ -2390,7 +2394,7 @@ public class InfoController extends ToolbarWindowController {
* @return True if controls are enabled for the given protocol in idle state
*/
protected boolean toggleDistributionSettings(final boolean stop) {
this.window().endEditingFor(null);
window.endEditingFor(null);
final Credentials credentials = session.getHost().getCredentials();
final DistributionConfiguration cdn = session.getFeature(DistributionConfiguration.class);
boolean enable = !credentials.isAnonymousLogin() && cdn != null;
@@ -2597,7 +2601,7 @@ public class InfoController extends ToolbarWindowController {
* @return True if progress animation has started and settings are toggled
*/
protected boolean toggleSizeSettings(final boolean stop) {
this.window().endEditingFor(null);
window.endEditingFor(null);
sizeButton.setEnabled(false);
for(Path next : files) {
if(next.isDirectory()) {
@@ -194,9 +194,9 @@ public class MainController extends BundleController implements NSApplication.De
private BookmarkMenuDelegate bookmarkMenuDelegate;
/**
* @param frame Frame autosave name
* @param frameName Frame autosave name
*/
public static BrowserController newDocument(final boolean force, final String frame) {
public static BrowserController newDocument(final boolean force, final String frameName) {
final List<BrowserController> browsers = getBrowsers();
if(!force) {
for(BrowserController controller : browsers) {
@@ -213,10 +213,7 @@ public class MainController extends BundleController implements NSApplication.De
browsers.remove(controller);
}
});
controller.display();
if(StringUtils.isNotBlank(frame)) {
controller.window().setFrameUsingName(frame);
}
controller.display(true, null == frameName ? controller.windowFrameName() : frameName);
browsers.add(controller);
return controller;
}
@@ -1087,7 +1084,6 @@ public class MainController extends BundleController implements NSApplication.De
= new HostDictionary<>().deserialize(browser.getSession().getHost().serialize(SerializerFactory.get()));
serialized.setWorkdir(browser.workdir());
sessions.add(serialized);
browser.window().saveFrameUsingName(serialized.getUuid());
}
}
if(browser.isConnected()) {
@@ -1278,7 +1274,7 @@ public class MainController extends BundleController implements NSApplication.De
if(browser.isMounted()) {
if(new HostUrlProvider().get(browser.getSession().getHost()).equals(new HostUrlProvider().get(h))) {
// Handle browser window already connected to the same host. #4215
browser.display();
browser.display(true, null);
if(Path.Type.directory == detector.detect(h.getDefaultPath())) {
browser.setWorkdir(new Path(PathNormalizer.normalize(h.getDefaultPath()), EnumSet.of(Path.Type.directory)));
}
@@ -332,9 +332,8 @@ public class PreferencesController extends ToolbarWindowController {
}
@Override
public void setWindow(NSWindow window) {
public void setWindow(final NSWindow window) {
window.setExcludedFromWindowsMenu(true);
window.setFrameAutosaveName("Preferences");
if(window.respondsToSelector(Foundation.selector("setToolbarStyle:"))) {
window.setToolbarStyle(NSWindow.NSWindowToolbarStyle.NSWindowToolbarStylePreference);
}
@@ -342,6 +341,11 @@ public class PreferencesController extends ToolbarWindowController {
super.setWindow(window);
}
@Override
public String windowFrameName() {
return "Preferences";
}
@Override
public void awakeFromNib() {
this.window.center();
@@ -2466,7 +2470,7 @@ public class PreferencesController extends ToolbarWindowController {
preferences.setLogging(Level.DEBUG.toString());
break;
default:
preferences.setLogging(Level.ERROR.toString());
preferences.resetLogging();
break;
}
}
@@ -195,8 +195,7 @@ public class TransferController extends WindowController implements TransferList
}
@Override
public void setWindow(NSWindow window) {
window.setFrameAutosaveName("Transfers");
public void setWindow(final NSWindow window) {
window.setContentMinSize(new NSSize(400d, 150d));
window.setMovableByWindowBackground(true);
window.setTitle(LocaleFactory.localizedString("Transfers"));
@@ -206,6 +205,11 @@ public class TransferController extends WindowController implements TransferList
super.setWindow(window);
}
@Override
public String windowFrameName() {
return "Transfers";
}
@Override
public void windowDidBecomeKey(NSNotification notification) {
this.updateHighlight();
@@ -40,7 +40,7 @@ public class ProtocolFactoryTest {
factory.register(ftp);
final Profile ftps = new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/FTPS.cyberduckprofile"));
factory.register(ftps);
final Profile s3 = new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
final Profile s3 = new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
factory.register(s3);
assertSame(ftp, factory.forName(ftp.getIdentifier()));
assertSame(ftp, factory.forName(ftp.getIdentifier(), ftp.getProvider()));
@@ -24,9 +24,9 @@
<key>Bundled</key>
<true/>
<key>Description</key>
<string>AWS S3 (IAM Identity Center)</string>
<string>Amazon S3 (IAM Identity Center)</string>
<key>Default Nickname</key>
<string>AWS S3 (IAM Identity Center)</string>
<string>Amazon S3 (IAM Identity Center)</string>
<key>Hostname Configurable</key>
<false/>
<key>Scopes</key>
@@ -62,7 +62,9 @@ public class S3BucketListService implements RootListService {
// Null if the owner is not available
attr.setOwner(b.getOwner().getId());
}
attr.setCreationDate(b.getCreationDate().getTime());
if(b.getCreationDate() != null) {
attr.setCreationDate(b.getCreationDate().getTime());
}
if(b.isLocationKnown()) {
attr.setRegion(b.getLocation());
}
@@ -46,14 +46,9 @@ import java.util.function.Predicate;
import com.amazonaws.auth.profile.internal.AbstractProfilesConfigFileScanner;
import com.amazonaws.auth.profile.internal.AllProfiles;
import com.amazonaws.auth.profile.internal.BasicProfile;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
/**
* Configure credentials from AWS CLI configuration and SSO cache
@@ -98,11 +93,7 @@ public class S3CredentialsConfigurator implements CredentialsConfigurator {
if(profile.isProcessBasedProfile()) {
// Uses external process to retrieve temporary credentials
final String command = profile.getCredentialProcess();
final ObjectMapper mapper = JsonMapper.builder()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY).build();
final ObjectMapper mapper = new ObjectMapper();
List<String> cmd = new ArrayList<>();
switch(Factory.Platform.getDefault()) {
case windows:
@@ -192,7 +183,7 @@ public class S3CredentialsConfigurator implements CredentialsConfigurator {
}
@Override
public CredentialsConfigurator reload() throws LoginCanceledException {
public S3CredentialsConfigurator reload() throws LoginCanceledException {
// See https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html for configuration behavior
final Local configFile = LocalFactory.get(directory, "config");
final Local credentialsFile = LocalFactory.get(directory, "credentials");
@@ -246,6 +237,7 @@ public class S3CredentialsConfigurator implements CredentialsConfigurator {
|| e.getProperties().containsKey("sso_session");
}
@JsonIgnoreProperties(ignoreUnknown = true)
private static class CachedCredential {
@JsonProperty("AccessKeyId")
private String accessKey;
@@ -24,6 +24,7 @@ import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.HostKeyCallback;
import ch.cyberduck.core.ListService;
import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathContainerService;
@@ -37,6 +38,7 @@ import ch.cyberduck.core.cdn.DistributionConfiguration;
import ch.cyberduck.core.cloudfront.CloudFrontDistributionConfigurationPreloader;
import ch.cyberduck.core.cloudfront.WebsiteCloudFrontDistributionConfiguration;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.features.*;
import ch.cyberduck.core.http.CustomServiceUnavailableRetryStrategy;
import ch.cyberduck.core.http.HttpSession;
@@ -52,6 +54,7 @@ import ch.cyberduck.core.ssl.DefaultX509KeyManager;
import ch.cyberduck.core.ssl.DisabledX509TrustManager;
import ch.cyberduck.core.ssl.X509KeyManager;
import ch.cyberduck.core.ssl.X509TrustManager;
import ch.cyberduck.core.sso.IdentityCenterAuthorizationService;
import ch.cyberduck.core.sso.IdentityCenterCredentialsStrategy;
import ch.cyberduck.core.sso.RegisterClientOAuth2RequestInterceptor;
import ch.cyberduck.core.sts.STSAssumeRoleCredentialsStrategy;
@@ -83,7 +86,11 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import com.amazonaws.auth.profile.internal.BasicProfile;
import static ch.cyberduck.core.s3.S3CredentialsConfigurator.toSsoPredicate;
import static com.amazonaws.services.s3.Headers.REQUESTER_PAYS_HEADER;
import static com.amazonaws.services.s3.Headers.S3_ALTERNATE_DATE;
@@ -260,8 +267,28 @@ public class S3Session extends HttpSession<RequestEntityRestStorageService> {
protected S3CredentialsStrategy configureCredentialsStrategy(final HttpClientBuilder configuration,
final LoginCallback prompt) throws BackgroundException {
if(host.getProtocol().isOAuthConfigurable()) {
if(host.getProtocol().getOAuthScopes().contains("sso:account:access")) {
if(host.getProtocol().getOAuthScopes().contains(IdentityCenterCredentialsStrategy.SSO_ACCOUNT_ACCESS_SCOPE)) {
log.debug("Configure SSO");
final S3CredentialsConfigurator configurator = new S3CredentialsConfigurator().reload();
final Set<BasicProfile> profiles = configurator.getProfiles().values().stream().filter(toSsoPredicate()).collect(Collectors.toSet());
if(!profiles.isEmpty()) {
if(StringUtils.isBlank(host.getCredentials().getUsername())) {
try {
final String profile = IdentityCenterAuthorizationService.prompt(host, prompt,
profiles.stream().map(p -> new Location.Name(p.getProfileName())).collect(Collectors.toSet()), null,
LocaleFactory.localizedString("Select AWS CLI Profile Name", "Credentials"), null).getIdentifier();
log.debug("Configuring credentials from profile {}", profile);
host.setCredentials(configurator.configure(host.setCredentials(new Credentials(profile))));
}
catch(ConnectionCanceledException e) {
// Continue with manual configuration
}
}
else {
// Copy properties from AWS CLI profile
host.setCredentials(configurator.configure(host));
}
}
final OAuth2RequestInterceptor oauth = new RegisterClientOAuth2RequestInterceptor(configuration.build(), host, trust, key, prompt)
.setFlowType(OAuth2AuthorizationService.FlowType.AuthorizationCode);
log.debug("Add interceptor {}", oauth);
@@ -52,6 +52,7 @@ import java.util.EnumSet;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class S3WriteFeature extends AbstractHttpWriteFeature<StorageObject> implements Write<StorageObject> {
private static final Logger log = LogManager.getLogger(S3WriteFeature.class);
@@ -77,7 +78,9 @@ public class S3WriteFeature extends AbstractHttpWriteFeature<StorageObject> impl
final RequestEntityRestStorageService client = session.getClient();
final Path bucket = containerService.getContainer(file);
client.putObjectWithRequestEntityImpl(
bucket.isRoot() ? StringUtils.EMPTY : bucket.getName(), object, entity, status.getParameters());
bucket.isRoot() ? StringUtils.EMPTY : bucket.getName(), object, entity,
status.getParameters().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())));
log.debug("Saved object {} with checksum {}", file, object.getETag());
}
catch(ServiceException e) {
@@ -76,8 +76,8 @@ public class IdentityCenterAuthorizationService {
* @param roleName The friendly name of the role that is assigned to the user.
* @return Short-lived access tokens
*/
public RoleCredentials getRoleCredentials(final OAuthTokens tokens, final String region,
@Nullable String accountId, @Nullable String roleName) throws BackgroundException {
public IdentityCenterRoleCredentials getRoleCredentials(final OAuthTokens tokens, final String region,
@Nullable String accountId, @Nullable String roleName) throws BackgroundException {
final AWSSSO client = AWSSSOClient.builder()
.withRegion(region)
.withClientConfiguration(new CustomClientConfiguration(host,
@@ -91,11 +91,13 @@ public class IdentityCenterAuthorizationService {
.withNextToken(nextToken)
.withAccessToken(tokens.getAccessToken()));
list.addAll(result.getAccountList());
log.debug("Retrieved account list {}", list);
nextToken = result.getNextToken();
}
while(null != nextToken);
if(list.size() == 1) {
accountId = list.get(0).getAccountId();
log.debug("Using default account ID {}", accountId);
}
else {
accountId = prompt(host, prompt, list.stream().map(info -> new Location.Name(info.getAccountId()) {
@@ -116,11 +118,13 @@ public class IdentityCenterAuthorizationService {
.withAccountId(accountId)
.withAccessToken(tokens.getAccessToken()));
list.addAll(result.getRoleList());
log.debug("Retrieved role list {}", list);
nextToken = result.getNextToken();
}
while(null != nextToken);
if(list.size() == 1) {
roleName = list.get(0).getRoleName();
log.debug("Using default role name {}", roleName);
}
else {
roleName = prompt(host, prompt, list.stream().map(info -> new Location.Name(info.getRoleName())).collect(Collectors.toSet()),
@@ -131,10 +135,12 @@ public class IdentityCenterAuthorizationService {
log.debug("Getting role credentials for account {} and role {} with access token {}",
accountId, roleName, tokens);
// Gets STS role credentials using the SSO access token for a given role name that is assigned to the user.
return client.getRoleCredentials(new GetRoleCredentialsRequest()
final RoleCredentials result = client.getRoleCredentials(new GetRoleCredentialsRequest()
.withAccountId(accountId)
.withRoleName(roleName)
.withAccessToken(tokens.getAccessToken())).getRoleCredentials();
return new IdentityCenterRoleCredentials(accountId, roleName,
result.getAccessKeyId(), result.getSecretAccessKey(), result.getSessionToken(), result.getExpiration());
}
catch(AmazonClientException e) {
throw new AmazonServiceExceptionMappingService().map(e);
@@ -156,4 +162,46 @@ public class IdentityCenterAuthorizationService {
}
return new Location.Name(value);
}
public static final class IdentityCenterRoleCredentials {
private final String accountId;
private final String roleName;
private final String accessKeyId;
private final String secretAccessKey;
private final String sessionToken;
private final Long expiration;
public IdentityCenterRoleCredentials(final String accountId, final String roleName, final String accessKeyId, final String secretAccessKey, final String sessionToken, final Long expiration) {
this.accountId = accountId;
this.roleName = roleName;
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
this.sessionToken = sessionToken;
this.expiration = expiration;
}
public String getAccountId() {
return accountId;
}
public String getRoleName() {
return roleName;
}
public String getAccessKeyId() {
return accessKeyId;
}
public String getSecretAccessKey() {
return secretAccessKey;
}
public String getSessionToken() {
return sessionToken;
}
public Long getExpiration() {
return expiration;
}
}
}
@@ -28,13 +28,12 @@ import ch.cyberduck.core.s3.S3CredentialsStrategy;
import ch.cyberduck.core.ssl.X509KeyManager;
import ch.cyberduck.core.ssl.X509TrustManager;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.locks.ReentrantLock;
import com.amazonaws.services.sso.model.RoleCredentials;
public class IdentityCenterCredentialsStrategy extends IdentityCenterAuthorizationService implements S3CredentialsStrategy {
private static final Logger log = LogManager.getLogger(IdentityCenterCredentialsStrategy.class);
@@ -49,6 +48,11 @@ public class IdentityCenterCredentialsStrategy extends IdentityCenterAuthorizati
private final String accountId;
private final String roleName;
/**
* A minimum scope of sso:account:access must be granted to get a refresh token back from the IAM Identity Center service.
*/
public static final String SSO_ACCOUNT_ACCESS_SCOPE = "sso:account:access";
public IdentityCenterCredentialsStrategy(final OAuth2RequestInterceptor oauth, final Host host,
final X509TrustManager trust, final X509KeyManager key, final LoginCallback prompt) throws ConnectionCanceledException {
super(host, trust, key, prompt);
@@ -57,18 +61,12 @@ public class IdentityCenterCredentialsStrategy extends IdentityCenterAuthorizati
this.region = prompt(host, prompt, host.getProtocol().getRegions(), Profile.SSO_REGION_KEY,
LocaleFactory.localizedString(String.format("SSO Region (%s)", Profile.SSO_REGION_KEY), "Credentials"),
host.getProperty(Profile.SSO_REGION_KEY)).getIdentifier();
// Lookup using SSO API if explicit configuration option is missing
this.accountId = host.getProperty(Profile.SSO_ACCOUNT_ID_KEY);
// Lookup using SSO API if explicit configuration option is missing
this.roleName = host.getProperty(Profile.SSO_ROLE_NAME_KEY);
}
public TemporaryAccessTokens refresh(final Credentials credentials) throws BackgroundException {
final RoleCredentials roleCredentials = this.getRoleCredentials(
oauth.save(oauth.validate(credentials.getOauth())), region, accountId, roleName);
log.debug("Received temporary access tokens {}", roleCredentials);
return new TemporaryAccessTokens(roleCredentials.getAccessKeyId(),
roleCredentials.getSecretAccessKey(), roleCredentials.getSessionToken(), roleCredentials.getExpiration());
}
@Override
public Credentials get() throws BackgroundException {
lock.lock();
@@ -78,7 +76,14 @@ public class IdentityCenterCredentialsStrategy extends IdentityCenterAuthorizati
// Get temporary credentials from Identity Center
if(tokens.isExpired()) {
log.debug("Refresh expired tokens {} for {}", tokens, host);
credentials.setTokens(this.refresh(credentials));
final IdentityCenterRoleCredentials role = this.getRoleCredentials(oauth.save(oauth.validate(credentials.getOauth())),
region, accountId, roleName);
log.debug("Received temporary access tokens {}", role);
credentials.setTokens(new TemporaryAccessTokens(role.getAccessKeyId(),
role.getSecretAccessKey(), role.getSessionToken(), role.getExpiration()));
if(StringUtils.isBlank(credentials.getUsername())) {
credentials.setUsername(String.format("%s-%s", role.getRoleName(), role.getAccountId()));
}
}
return credentials;
}
@@ -15,27 +15,23 @@ package ch.cyberduck.core.sso;
* GNU General Public License for more details.
*/
import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.DefaultIOExceptionMappingService;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.LocaleFactory;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.OAuthTokens;
import ch.cyberduck.core.PreferencesUseragentProvider;
import ch.cyberduck.core.Profile;
import ch.cyberduck.core.aws.AmazonSSOOIDCExceptionMappingService;
import ch.cyberduck.core.aws.AmazonServiceExceptionMappingService;
import ch.cyberduck.core.aws.CustomClientConfiguration;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.ConnectionCanceledException;
import ch.cyberduck.core.features.Location;
import ch.cyberduck.core.oauth.OAuth2RequestInterceptor;
import ch.cyberduck.core.s3.S3CredentialsConfigurator;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.ssl.ThreadLocalHostnameDelegatingTrustManager;
import ch.cyberduck.core.ssl.X509KeyManager;
import ch.cyberduck.core.ssl.X509TrustManager;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpRequest;
import org.apache.http.client.HttpClient;
import org.apache.logging.log4j.LogManager;
@@ -45,11 +41,8 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.net.Inet4Address;
import java.net.ServerSocket;
import java.util.Set;
import java.util.stream.Collectors;
import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.profile.internal.BasicProfile;
import com.amazonaws.services.ssooidc.AWSSSOOIDC;
import com.amazonaws.services.ssooidc.AWSSSOOIDCClientBuilder;
import com.amazonaws.services.ssooidc.model.AWSSSOOIDCException;
@@ -60,8 +53,6 @@ import com.amazonaws.services.ssooidc.model.RegisterClientResult;
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.auth.openidconnect.IdTokenResponse;
import static ch.cyberduck.core.s3.S3CredentialsConfigurator.toSsoPredicate;
public class RegisterClientOAuth2RequestInterceptor extends OAuth2RequestInterceptor {
private static final Logger log = LogManager.getLogger(RegisterClientOAuth2RequestInterceptor.class);
@@ -84,23 +75,6 @@ public class RegisterClientOAuth2RequestInterceptor extends OAuth2RequestInterce
final X509TrustManager trust, final X509KeyManager key, final LoginCallback prompt) throws ConnectionCanceledException {
super(client, host, null, null, null, null, host.getProtocol().getOAuthScopes(), true, prompt);
this.host = host;
if(StringUtils.isBlank(host.getCredentials().getUsername())) {
final S3CredentialsConfigurator configurator = new S3CredentialsConfigurator();
configurator.reload();
final Set<BasicProfile> profiles = configurator.getProfiles().values().stream().filter(toSsoPredicate()).collect(Collectors.toSet());
if(!profiles.isEmpty()) {
try {
final String profile = IdentityCenterAuthorizationService.prompt(host, prompt,
profiles.stream().map(p -> new Location.Name(p.getProfileName())).collect(Collectors.toSet()), null,
LocaleFactory.localizedString("Select AWS CLI Profile Name", "Credentials"), null).getIdentifier();
log.debug("Configuring credentials from profile {}", profile);
host.setCredentials(configurator.configure(host.setCredentials(new Credentials(profile))));
}
catch(ConnectionCanceledException e) {
// Continue with manual configuration
}
}
}
this.trust = trust;
this.key = key;
this.region = IdentityCenterAuthorizationService.prompt(host, prompt, host.getProtocol().getRegions(), Profile.SSO_REGION_KEY,
@@ -131,7 +105,7 @@ public class RegisterClientOAuth2RequestInterceptor extends OAuth2RequestInterce
Inet4Address.getLoopbackAddress().getHostAddress(), temp.getLocalPort());
final RegisterClientResult registration = client.registerClient(new RegisterClientRequest()
// The friendly name of the client.
.withClientName(new PreferencesUseragentProvider().get())
.withClientName(PreferencesFactory.get().getProperty("application.name"))
// The service supports only public as a client type.
.withClientType("public")
.withIssuerUrl(issuerUrl)
@@ -74,7 +74,7 @@ public abstract class AbstractS3Test extends VaultTest {
public void setupDefault() throws Exception {
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(
this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
final Host host = new Host(profile, profile.getDefaultHostname(), new Credentials(
PROPERTIES.get("s3.key")
));
@@ -94,7 +94,7 @@ public abstract class AbstractS3Test extends VaultTest {
public void setupVirtualHost() throws Exception {
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(
this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
final Host host = new Host(profile, "test-eu-central-1-cyberduck.s3.amazonaws.com", new Credentials(
PROPERTIES.get("s3.key")
));
@@ -114,7 +114,7 @@ public abstract class AbstractS3Test extends VaultTest {
public void setupCloudFront() throws Exception {
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(
this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
final Host host = new Host(profile, "d4fobtprygi46.cloudfront.net", new Credentials("anonymous")).setRegion("eu-central-1");
cloudfront = new S3Session(host, new DefaultX509TrustManager(), new DefaultX509KeyManager());
final LoginConnectionService login = new LoginConnectionService(new DisabledLoginCallback() {
@@ -126,7 +126,7 @@ public class S3ExceptionMappingServiceTest {
@Test
public void testAlgorithmFailure() {
assertEquals("EC AlgorithmParameters not available. Please contact your web hosting service provider for assistance.",
assertEquals("EC AlgorithmParameters not available. The connection attempt was rejected. The server may be down, or your network may not be properly configured.",
new S3ExceptionMappingService().map(new S3ServiceException(
new SSLException(
new RuntimeException(
@@ -77,7 +77,7 @@ public class S3ProtocolTest {
@Test
public void testDefaultProfile() throws Exception {
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
final Profile profile = new ProfilePlistReader(factory).read(this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
assertTrue(profile.isHostnameConfigurable());
assertFalse(profile.isPortConfigurable());
assertTrue(profile.isUsernameConfigurable());
@@ -130,7 +130,7 @@ public class S3SessionTest extends AbstractS3Test {
public void testConnectDefaultPath() throws Exception {
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(
this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
final Host host = new Host(profile, profile.getDefaultHostname(), new Credentials(
PROPERTIES.get("s3.key"), PROPERTIES.get("s3.secret")
));
@@ -145,7 +145,7 @@ public class S3SessionTest extends AbstractS3Test {
public void testCustomHostnameUnknown() throws Exception {
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(
this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
final Host host = new Host(profile, "testu.cyberduck.ch", new Credentials(
PROPERTIES.get("s3.key"), "s"
));
@@ -122,7 +122,7 @@ public class S3SingleTransferWorkerTest extends AbstractS3Test {
out.close();
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(
this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
final Host host = new Host(profile, profile.getDefaultHostname(), new Credentials(
PROPERTIES.get("s3.key"), PROPERTIES.get("s3.secret")
)) {
@@ -206,7 +206,7 @@ public class S3SingleTransferWorkerTest extends AbstractS3Test {
out.close();
final ProtocolFactory factory = new ProtocolFactory(new HashSet<>(Collections.singleton(new S3Protocol())));
final Profile profile = new ProfilePlistReader(factory).read(
this.getClass().getResourceAsStream("/S3 (HTTPS).cyberduckprofile"));
this.getClass().getResourceAsStream("/Amazon S3.cyberduckprofile"));
final Host host = new Host(profile, profile.getDefaultHostname(), new Credentials(
PROPERTIES.get("s3.key"), PROPERTIES.get("s3.secret")
)) {
@@ -156,7 +156,7 @@ public class SpectraBulkService implements Bulk<Set<UUID>> {
for(Map.Entry<TransferItem, TransferStatus> item : files.entrySet()) {
if(container.getKey().equals(containerService.getContainer(item.getKey().remote))) {
final TransferStatus status = item.getValue();
final Map<String, String> parameters = new HashMap<>(status.getParameters());
final Map<String, Object> parameters = new HashMap<>(status.getParameters());
parameters.put(REQUEST_PARAMETER_JOBID_IDENTIFIER, master.getJobId().toString());
status.setParameters(parameters);
status.setPart(counters.get(containerService.getKey(item.getKey().remote)));
@@ -191,7 +191,7 @@ public class SpectraBulkService implements Bulk<Set<UUID>> {
if(!status.getParameters().containsKey(REQUEST_PARAMETER_JOBID_IDENTIFIER)) {
throw new NotfoundException(String.format("Missing job id parameter in status for %s", file.getName()));
}
final String job = status.getParameters().get(REQUEST_PARAMETER_JOBID_IDENTIFIER);
final String job = status.getParameters().get(REQUEST_PARAMETER_JOBID_IDENTIFIER).toString();
log.debug("Cancel job {}", job);
final Ds3Client client = new SpectraClientBuilder().wrap(session, session.getHost());
client.cancelJobSpectraS3(new CancelJobSpectraS3Request(job));
@@ -224,7 +224,7 @@ public class SpectraBulkService implements Bulk<Set<UUID>> {
if(!status.getParameters().containsKey(REQUEST_PARAMETER_JOBID_IDENTIFIER)) {
throw new NotfoundException(String.format("Missing job id parameter in status for %s", file.getName()));
}
final String job = status.getParameters().get(REQUEST_PARAMETER_JOBID_IDENTIFIER);
final String job = status.getParameters().get(REQUEST_PARAMETER_JOBID_IDENTIFIER).toString();
log.debug("Query status for job {}", job);
// Fetch current list from server
final Ds3Client client = new SpectraClientBuilder().wrap(session, session.getHost());
@@ -305,7 +305,7 @@ public class SpectraBulkService implements Bulk<Set<UUID>> {
chunk.setLength(object.getLength());
chunk.setOffset(object.getOffset());
// Job parameter already present from #pre
final Map<String, String> parameters = new HashMap<>(chunk.getParameters());
final Map<String, Object> parameters = new HashMap<>(chunk.getParameters());
// Set offset for chunk.
parameters.put(REQUEST_PARAMETER_OFFSET, Long.toString(chunk.getOffset()));
chunk.setParameters(parameters);
@@ -34,6 +34,8 @@ import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SpectraReadFeature implements Read {
@@ -64,19 +66,20 @@ public class SpectraReadFeature implements Read {
public InputStream open() throws IOException {
try {
return session.getClient().getObjectImpl(
false,
containerService.getContainer(file).getName(),
containerService.getKey(file),
null,
null,
null,
null,
null,
null,
file.attributes().getVersionId(),
new HashMap<String, Object>(),
chunk.getParameters())
.getDataInputStream();
false,
containerService.getContainer(file).getName(),
containerService.getKey(file),
null,
null,
null,
null,
null,
null,
file.attributes().getVersionId(),
new HashMap<>(),
chunk.getParameters().entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())))
.getDataInputStream();
}
catch(ServiceException e) {
throw new IOException(e.getMessage(), e);
@@ -15,20 +15,7 @@ package ch.cyberduck.core.cryptomator;
* GNU General Public License for more details.
*/
import ch.cyberduck.core.AlphanumericRandomStringService;
import ch.cyberduck.core.ConnectionCallback;
import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.DisabledListProgressListener;
import ch.cyberduck.core.DisabledPasswordCallback;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.Local;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.NullFilter;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.PathAttributes;
import ch.cyberduck.core.ProgressListener;
import ch.cyberduck.core.TestProtocol;
import ch.cyberduck.core.*;
import ch.cyberduck.core.cryptomator.features.CryptoListService;
import ch.cyberduck.core.cryptomator.features.CryptoReadFeature;
import ch.cyberduck.core.features.AttributesFinder;
@@ -38,7 +25,6 @@ import ch.cyberduck.core.io.StreamCopier;
import ch.cyberduck.core.io.StreamListener;
import ch.cyberduck.core.local.DefaultLocalDirectoryFeature;
import ch.cyberduck.core.notification.DisabledNotificationService;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.sftp.AbstractSFTPTest;
import ch.cyberduck.core.sftp.SFTPAttributesFinderFeature;
import ch.cyberduck.core.sftp.SFTPDeleteFeature;
@@ -106,13 +92,14 @@ public class CryptoSFTPSingleTransferWorkerTest extends AbstractSFTPTest {
out2.close();
final CryptoVault cryptomator = new CryptoVault(vault);
cryptomator.create(session, new VaultCredentials("test"), vaultVersion);
session.withRegistry(new DefaultVaultRegistry(new DisabledPasswordCallback() {
final DefaultVaultRegistry vaults = new DefaultVaultRegistry(new DisabledPasswordCallback() {
@Override
public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
return new VaultCredentials("test");
}
}));
PreferencesFactory.get().setProperty("factory.vault.class", CryptoVault.class.getName());
});
vaults.add(cryptomator.load(session, PasswordCallback.noop));
session.withRegistry(vaults);
final Host host = new Host(new TestProtocol());
final Transfer t = new UploadTransfer(host, Collections.singletonList(new TransferItem(dir1, localDirectory1)), new NullFilter<>())
.withOptions(new UploadFilterOptions(host).withTimestamp(true));
@@ -64,7 +64,7 @@ public class TusWriteFeature extends AbstractHttpWriteFeature<Void> {
final DelayedHttpEntityCallable<Void> command = new DelayedHttpEntityCallable<Void>(file) {
@Override
public Void call(final HttpEntity entity) throws BackgroundException {
final HttpPatch request = new HttpPatch(status.getParameters().get(TusUploadFeature.UPLOAD_URL));
final HttpPatch request = new HttpPatch(status.getParameters().get(TusUploadFeature.UPLOAD_URL).toString());
request.setEntity(entity);
request.setHeader(TUS_HEADER_RESUMABLE, TUS_VERSION);
final Checksum checksum = status.getChecksum();
@@ -117,13 +117,13 @@ public class DAVReadFeature implements Read {
if(!status.getParameters().isEmpty()) {
resource.append("?");
}
for(Map.Entry<String, String> parameter : status.getParameters().entrySet()) {
for(Map.Entry<String, ?> parameter : status.getParameters().entrySet()) {
if(!resource.toString().endsWith("?")) {
resource.append("&");
}
resource.append(URIEncoder.encode(parameter.getKey()))
.append("=")
.append(URIEncoder.encode(parameter.getValue()));
.append(URIEncoder.encode(parameter.getValue().toString()));
}
return new HttpGet(resource.toString());
@@ -24,6 +24,7 @@ import ch.cyberduck.core.Local;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginOptions;
import ch.cyberduck.core.NullFilter;
import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.ProgressListener;
import ch.cyberduck.core.TestProtocol;
@@ -47,7 +48,6 @@ import ch.cyberduck.core.io.StreamCopier;
import ch.cyberduck.core.io.StreamListener;
import ch.cyberduck.core.local.DefaultLocalDirectoryFeature;
import ch.cyberduck.core.notification.DisabledNotificationService;
import ch.cyberduck.core.preferences.PreferencesFactory;
import ch.cyberduck.core.shared.DefaultHomeFinderService;
import ch.cyberduck.core.transfer.DisabledTransferErrorCallback;
import ch.cyberduck.core.transfer.DisabledTransferPrompt;
@@ -105,13 +105,14 @@ public class CryptoDAVSingleTransferWorkerTest extends AbstractDAVTest {
out2.close();
final CryptoVault cryptomator = new CryptoVault(vault);
cryptomator.create(session, new VaultCredentials("test"), vaultVersion);
session.withRegistry(new DefaultVaultRegistry(new DisabledPasswordCallback() {
final DefaultVaultRegistry vaults = new DefaultVaultRegistry(new DisabledPasswordCallback() {
@Override
public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
return new VaultCredentials("test");
}
}));
PreferencesFactory.get().setProperty("factory.vault.class", CryptoVault.class.getName());
});
vaults.add(cryptomator.load(session, PasswordCallback.noop));
session.withRegistry(vaults);
final Transfer t = new UploadTransfer(new Host(new TestProtocol()), Collections.singletonList(new TransferItem(dir1, localDirectory1)), new NullFilter<>());
assertTrue(new SingleTransferWorker(session, session, t, new TransferOptions(), new TransferSpeedometer(t), new DisabledTransferPrompt() {
@Override
@@ -145,18 +146,18 @@ public class CryptoDAVSingleTransferWorkerTest extends AbstractDAVTest {
@Test
public void testDownload() throws Exception {
PreferencesFactory.get().setProperty("factory.vault.class", CryptoVault.class.getName());
final Path home = new DefaultHomeFinderService(session).find();
final Path vault = new Path(home, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.directory));
final CryptoVault cryptomator = new CryptoVault(vault);
cryptomator.create(session, new VaultCredentials("test"), vaultVersion);
session.withRegistry(new DefaultVaultRegistry(new DisabledPasswordCallback() {
final DefaultVaultRegistry vaults = new DefaultVaultRegistry(new DisabledPasswordCallback() {
@Override
public Credentials prompt(final Host bookmark, final String title, final String reason, final LoginOptions options) {
return new VaultCredentials("test");
}
}));
});
vaults.add(cryptomator.load(session, PasswordCallback.noop));
session.withRegistry(vaults);
final Path dir1 = cryptomator.getFeature(session, Directory.class, new DAVDirectoryFeature(session)).mkdir(
cryptomator.getFeature(session, Write.class, new DAVWriteFeature(session)), new Path(vault, new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.directory)), new TransferStatus());
final Local localDirectory1 = new Local(System.getProperty("java.io.tmpdir"), new AlphanumericRandomStringService().random());
@@ -193,7 +193,14 @@ namespace Ch.Cyberduck.Ui.Controller
private void View_DebugLogChangedEvent()
{
PreferencesFactory.get().setLogging(View.DebugLog ? Level.DEBUG.toString() : Level.ERROR.toString());
if (View.DebugLog)
{
PreferencesFactory.get().setLogging(Level.DEBUG.name());
}
else
{
PreferencesFactory.get().resetLogging();
}
}
private void View_SegmentedDownloadsChangedEvent()