Merge branch 'master' into feature/GH-17459

This commit is contained in:
David Kocher
2026-05-15 11:45:28 +02:00
18 changed files with 199 additions and 384 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ linux-self-hosted, macos-self-hosted, windows-self-hosted ]
os: [ linux-self-hosted ]
steps:
- uses: actions/checkout@v6
- name: Set up JDK 21
+1
View File
@@ -15,6 +15,7 @@
* [Bugfix] Browser window does not show up in Exposé & Mission Control (macOS) ([#17703](https://trac.cyberduck.io/ticket/17703))
* [Bugfix] Compare public key blob instead of comment when retrieving key from agent (SFTP)
* [Bugfix] Support "Include" directive when reading from OpenSSH config (SFTP) ([#10451](https://trac.cyberduck.io/ticket/10451))
* [Bugfix] Exclude trashed folders in list by default (Backblaze B2) ([#18101](https://trac.cyberduck.io/ticket/18101))
[9.4.1](https://github.com/iterate-ch/cyberduck/compare/release-9-4-0...release-9-4-1)
* [Bugfix] Cleartext uploads to unlocked vault with auto detect disabled in Preferences (
@@ -17,6 +17,7 @@ package ch.cyberduck.core.b2;
import ch.cyberduck.core.AttributedList;
import ch.cyberduck.core.DefaultIOExceptionMappingService;
import ch.cyberduck.core.DefaultPathAttributes;
import ch.cyberduck.core.DefaultPathContainerService;
import ch.cyberduck.core.ListProgressListener;
import ch.cyberduck.core.ListService;
@@ -28,15 +29,27 @@ import ch.cyberduck.core.VersioningConfiguration;
import ch.cyberduck.core.exception.BackgroundException;
import ch.cyberduck.core.exception.NotfoundException;
import ch.cyberduck.core.preferences.HostPreferencesFactory;
import ch.cyberduck.core.threading.BackgroundExceptionCallable;
import ch.cyberduck.core.threading.ThreadPool;
import ch.cyberduck.core.threading.ThreadPoolFactory;
import ch.cyberduck.core.worker.DefaultExceptionMappingService;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.Uninterruptibles;
import synapticloop.b2.Action;
import synapticloop.b2.exception.B2ApiException;
@@ -69,6 +82,7 @@ public class B2ObjectListService implements ListService {
@Override
public AttributedList<Path> list(final Path directory, final ListProgressListener listener) throws BackgroundException {
final ThreadPool pool = ThreadPoolFactory.get("list", HostPreferencesFactory.get(session.getHost()).getInteger("b2.listing.concurrency"));
try {
final AttributedList<Path> objects = new AttributedList<>();
Marker marker = new Marker(null, null);
@@ -93,7 +107,20 @@ public class B2ObjectListService implements ListService {
this.createPrefix(directory),
String.valueOf(Path.DELIMITER));
}
marker = this.parse(directory, objects, response, revisions);
final List<Future<Path>> folders = new ArrayList<>();
marker = this.parse(directory, objects, response, revisions, containerId, pool, folders);
for(Future<Path> f : folders) {
try {
objects.add(Uninterruptibles.getUninterruptibly(f));
}
catch(ExecutionException e) {
log.warn("Listing versioned objects failed with execution failure {}", e.getMessage());
for(Throwable cause : ExceptionUtils.getThrowableList(e)) {
Throwables.throwIfInstanceOf(cause, BackgroundException.class);
}
throw new DefaultExceptionMappingService().map(Throwables.getRootCause(e));
}
}
if(null == marker.nextFileId) {
if(!response.getFiles().isEmpty()) {
hasDirectoryPlaceholder = true;
@@ -114,6 +141,9 @@ public class B2ObjectListService implements ListService {
catch(IOException e) {
throw new DefaultIOExceptionMappingService().map(e);
}
finally {
pool.shutdown(false);
}
}
private String createPrefix(final Path directory) {
@@ -122,7 +152,8 @@ public class B2ObjectListService implements ListService {
}
private Marker parse(final Path directory, final AttributedList<Path> objects,
final B2ListFilesResponse response, final Map<String, Long> revisions) {
final B2ListFilesResponse response, final Map<String, Long> revisions,
final String containerId, final ThreadPool pool, final List<Future<Path>> folders) {
final B2AttributesFinderFeature attr = new B2AttributesFinderFeature(session, fileid);
for(B2FileInfoResponse info : response.getFiles()) {
if(StringUtils.equals(PathNormalizer.name(info.getFileName()), B2PathContainerService.PLACEHOLDER)) {
@@ -136,10 +167,15 @@ public class B2ObjectListService implements ListService {
}
if(StringUtils.isBlank(info.getFileId())) {
// Common prefix
final Path placeholder = new Path(directory.isDirectory() ? directory : directory.getParent(),
PathNormalizer.name(StringUtils.removeEnd(info.getFileName(), String.valueOf(Path.DELIMITER))),
EnumSet.of(Path.Type.directory, Path.Type.placeholder));
objects.add(placeholder);
if(versioning.isEnabled()) {
// Determine trashed state asynchronously by checking for live content beneath the prefix
folders.add(this.submit(pool, directory, containerId, info.getFileName()));
}
else {
objects.add(new Path(directory.isDirectory() ? directory : directory.getParent(),
PathNormalizer.name(StringUtils.removeEnd(info.getFileName(), String.valueOf(Path.DELIMITER))),
EnumSet.of(Path.Type.directory, Path.Type.placeholder)));
}
continue;
}
final PathAttributes attributes = attr.toAttributes(info);
@@ -164,6 +200,44 @@ public class B2ObjectListService implements ListService {
return new Marker(response.getNextFileName(), response.getNextFileId());
}
/**
* Determine path from prefix. Path will have trashed attribute set when no live (non-hidden) content
* exists beneath the prefix.
*
* @param pool Thread pool to run task with
* @param directory The directory for which contents are listed
* @param containerId B2 bucket ID
* @param prefix Common prefix found in directory listing (ends with delimiter)
* @return Path to add to directory list
*/
private Future<Path> submit(final ThreadPool pool, final Path directory, final String containerId, final String prefix) {
return pool.execute(new BackgroundExceptionCallable<Path>() {
@Override
public Path call() throws BackgroundException {
try {
final PathAttributes folderAttributes = new DefaultPathAttributes();
// Query without delimiter to check recursively for any live files beneath this prefix.
// Hide markers are excluded by listFileNames, so an empty result means all content is deleted.
final B2ListFilesResponse liveContent = session.getClient().listFileNames(
containerId, null, 1, prefix, null);
if(liveContent.getFiles().isEmpty()) {
log.debug("Set trashed attribute for prefix {}", prefix);
folderAttributes.setTrashed(true);
}
return new Path(directory.isDirectory() ? directory : directory.getParent(),
PathNormalizer.name(StringUtils.removeEnd(prefix, String.valueOf(Path.DELIMITER))),
EnumSet.of(Path.Type.directory, Path.Type.placeholder), folderAttributes);
}
catch(B2ApiException e) {
throw new B2ExceptionMappingService(fileid).map("Listing directory {0} failed", e, directory);
}
catch(IOException e) {
throw new DefaultIOExceptionMappingService().map(e);
}
}
});
}
private static final class Marker {
public final String nextFilename;
public final String nextFileId;
@@ -412,9 +412,13 @@ public class B2ObjectListServiceTest extends AbstractB2Test {
}
// Nullify version to add delete marker
new B2DeleteFeature(session, fileid).delete(Collections.singletonList(file.withAttributes(new DefaultPathAttributes(file.attributes()).setVersionId(null))), LoginCallback.noop, new Delete.DisabledCallback());
assertTrue(new DefaultFindFeature(session).find(folder1, new DisabledListProgressListener()));
assertTrue(new B2ObjectListService(session, fileid).list(folder1, new DisabledListProgressListener()).contains(folder2));
assertTrue(new DefaultFindFeature(session).find(folder2, new DisabledListProgressListener()));
assertFalse(new DefaultFindFeature(session).find(folder1, new DisabledListProgressListener()));
final AttributedList<Path> list = new B2ObjectListService(session, fileid).list(folder1, new DisabledListProgressListener());
assertTrue(list.contains(folder2));
for(Path f : list) {
assertTrue(f.attributes().isTrashed());
}
assertFalse(new DefaultFindFeature(session).find(folder2, new DisabledListProgressListener()));
assertEquals(2, new B2ObjectListService(session, fileid).list(folder2, new DisabledListProgressListener()).size());
assertThrows(NotfoundException.class, () -> new B2ObjectListService(session, fileid, 1, VersioningConfiguration.empty()).list(folder2, new DisabledListProgressListener()));
for(Path f : new B2ObjectListService(session, fileid).list(folder2, new DisabledListProgressListener())) {
@@ -438,6 +442,79 @@ public class B2ObjectListServiceTest extends AbstractB2Test {
new B2DeleteFeature(session, fileid).delete(Arrays.asList(file1, folder1, bucket), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testListFolderCreateModifyDelete() throws Exception {
final B2VersionIdProvider fileid = new B2VersionIdProvider(session);
final Path bucket = new B2DirectoryFeature(session, fileid).mkdir(
new B2WriteFeature(session, fileid),
new Path(String.format("test-%s", new AsciiRandomStringService().random()), EnumSet.of(Path.Type.directory, Path.Type.volume)),
new TransferStatus());
// Create folder
final Path folder = new B2DirectoryFeature(session, fileid).mkdir(
new B2WriteFeature(session, fileid),
new Path(bucket, new AsciiRandomStringService().random(), EnumSet.of(Path.Type.directory)),
new TransferStatus());
// Create file in folder
final Path file = new Path(folder, new AsciiRandomStringService().random(), EnumSet.of(Path.Type.file));
{
final byte[] content = RandomUtils.nextBytes(32);
final TransferStatus status = new TransferStatus().setLength(content.length);
status.setChecksum(new SHA1ChecksumCompute().compute(new ByteArrayInputStream(content), status));
final HttpResponseOutputStream<BaseB2Response> out = new B2WriteFeature(session, fileid).write(file, status, ConnectionCallback.noop);
IOUtils.write(content, out);
out.close();
file.attributes().setVersionId(((B2FileResponse) out.getStatus()).getFileId());
}
// Versioned listing: file and folder appear (1 version each)
assertTrue(new B2ObjectListService(session, fileid, 10, new VersioningConfiguration(true)).list(folder, new DisabledListProgressListener()).contains(file));
assertTrue(new B2ObjectListService(session, fileid, 10, new VersioningConfiguration(true)).list(bucket, new DisabledListProgressListener()).contains(folder));
// Modify file (overwrite with new content)
{
final byte[] content = RandomUtils.nextBytes(64);
final TransferStatus status = new TransferStatus().setLength(content.length);
status.setChecksum(new SHA1ChecksumCompute().compute(new ByteArrayInputStream(content), status));
final HttpResponseOutputStream<BaseB2Response> out = new B2WriteFeature(session, fileid).write(file, status, ConnectionCallback.noop);
IOUtils.write(content, out);
out.close();
file.attributes().setVersionId(((B2FileResponse) out.getStatus()).getFileId());
}
// Versioned listing: current version + previous duplicate visible
{
final AttributedList<Path> list = new B2ObjectListService(session, fileid, 10, new VersioningConfiguration(true)).list(folder, new DisabledListProgressListener());
assertEquals(2, list.size());
assertTrue(list.contains(file));
assertNull(list.find(new SimplePathPredicate(file)).attributes().getRevision());
assertEquals(Long.valueOf(1L), list.find(path -> path.attributes().isDuplicate()).attributes().getRevision());
}
// Delete file: add hide marker by nullifying version
new B2DeleteFeature(session, fileid).delete(
Collections.singletonList(new Path(file).withAttributes(new DefaultPathAttributes(file.attributes()).setVersionId(null))),
LoginCallback.noop, new Delete.DisabledCallback());
// Delete folder placeholder
new B2DeleteFeature(session, fileid).delete(
Collections.singletonList(folder), LoginCallback.noop, new Delete.DisabledCallback());
// Versioned listing: all file versions are trashed or duplicate (hide marker present, no live entries)
final AttributedList<Path> versions = new B2ObjectListService(session, fileid, 10, new VersioningConfiguration(true)).list(folder, new DisabledListProgressListener());
assertFalse(versions.isEmpty());
for(Path f : versions) {
assertTrue(f.attributes().isTrashed() || f.attributes().isDuplicate());
}
// Versioned bucket listing: folder appears as trashed (no live content beneath prefix)
{
final AttributedList<Path> bucketVersioned = new B2ObjectListService(session, fileid, 10, new VersioningConfiguration(true)).list(bucket, new DisabledListProgressListener());
assertNotNull(bucketVersioned.find(new SimplePathPredicate(folder)));
assertTrue(bucketVersioned.find(new SimplePathPredicate(folder)).attributes().isTrashed());
}
// Non-versioned listing: folder not accessible, file not accessible
assertTrue(new B2ObjectListService(session, fileid, 10, VersioningConfiguration.empty()).list(bucket, new DisabledListProgressListener()).isEmpty());
assertThrows(NotfoundException.class, () -> new B2ObjectListService(session, fileid, 10, VersioningConfiguration.empty()).list(folder, new DisabledListProgressListener()));
// Cleanup
for(Path f : versions) {
new B2DeleteFeature(session, fileid).delete(Collections.singletonList(f), LoginCallback.noop, new Delete.DisabledCallback());
}
new B2DeleteFeature(session, fileid).delete(Collections.singletonList(bucket), LoginCallback.noop, new Delete.DisabledCallback());
}
@Test
public void testListLexicographicSortOrderAssumption() throws Exception {
final B2VersionIdProvider fileid = new B2VersionIdProvider(session);
@@ -19,7 +19,6 @@ 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;
@@ -33,12 +32,10 @@ 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, final VersionIdProvider versionid) {
public CteraDelegatingReadFeature(final CteraSession session) {
this.session = session;
this.versionid = versionid;
this.directio = HostPreferencesFactory.get(session.getHost()).getBoolean("ctera.download.directio.enable");
}
@@ -277,7 +277,7 @@ public class CteraSession extends DAVSession {
return (T) new CteraListService(this);
}
if(type == Read.class) {
return (T) new CteraDelegatingReadFeature(this, versionid);
return (T) new CteraDelegatingReadFeature(this);
}
if(type == Write.class) {
return (T) new CteraWriteFeature(this);
@@ -84,12 +84,12 @@ N.B. no need to check `readpermission` upon mv/cp.
| ACL (CTERA) | POSIX (Folder) | POSIX (File) | Windows `FileSystemRights` (Folder) | Windows `FileSystemRights` (File) | Example (Folder) | Example (File) |
|----------------------------------------------------------------------------------------|-----------------------------------------------------|--------------------------------------|-----------------------------------------------------------|-----------------------------------|-----------------------------------------------------------------|--------------------------------------------------------------|
| - | `---` | - | empty | - | `/ACL test (new user)/NoAccess/` | - |
| `readpermission` | `r-x` | `r--` | `ReadAndExecute` | `Read` | `/ACL test (new user)/ReadOnly/` | `/ACL test (new user)/ReadOnly/ReadOnly.txt` |
| `readpermission`, `createdirectoriespermission` | `rwx` (delete prevented in preflight) | - | `ReadAndExecute`, `CreateDirectories`, `CreateFiles` (!), | - | `/WORM test (new user)/Retention Folder (no write, no delete)/` | - |
| `readpermission`, `deletepermission` | `rwx` (folder/file creation prevented in preflight) | `rw-` (write prevented in preflight) | `ReadAndExecute`, `Delete` | `Read`, `Delete` | `/ACL test (new user)/NoCreateFolderPermission` | `/ACL test (new user)/NoCreateFolderPermission/trayIcon.png` |
| `readpermission`, `deletepermission`, `writepermission` | - | `rwx` | - | `Read`, `Delete`, `Write` | - | `/ACL test (new user)/ReadWrite/Free Access.txt` |
| `readpermission`, `deletepermission`, `writepermission`, `createdirectoriespermission` | `rwx` | - | `ReadAndExecute`, `Delete`, `Write` | - | `/ACL test (new user)/ReadWrite/` | - |
| - | `---` | - | empty | - | `/ACL test/NoAccess/` | - |
| `readpermission` | `r-x` | `r--` | `ReadAndExecute` | `Read` | `/ACL test/ReadOnly/` | `/ACL test/ReadOnly/ReadOnly.txt` |
| `readpermission`, `createdirectoriespermission` | `rwx` (delete prevented in preflight) | - | `ReadAndExecute`, `CreateDirectories`, `CreateFiles` (!), | - | `/WORM test/Retention Folder (no write, no delete)/` | - |
| `readpermission`, `deletepermission` | `rwx` (folder/file creation prevented in preflight) | `rw-` (write prevented in preflight) | `ReadAndExecute`, `Delete` | `Read`, `Delete` | `/ACL test/NoCreateFolderPermission` | `/ACL test/NoCreateFolderPermission/trayIcon.png` |
| `readpermission`, `deletepermission`, `writepermission` | - | `rwx` | - | `Read`, `Delete`, `Write` | - | `/ACL test/ReadWrite/Free Access.txt` |
| `readpermission`, `deletepermission`, `writepermission`, `createdirectoriespermission` | `rwx` | - | `ReadAndExecute`, `Delete`, `Write` | - | `/ACL test/ReadWrite/` | - |
#### References
@@ -1,62 +0,0 @@
package ch.cyberduck.core.ctera;
/*
* Copyright (c) 2002-2022 iterate GmbH. All rights reserved.
* https://cyberduck.io/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.HostKeyCallback;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.LoginConnectionService;
import ch.cyberduck.core.ProgressListener;
import ch.cyberduck.core.proxy.DisabledProxyFinder;
import ch.cyberduck.core.ssl.DefaultX509KeyManager;
import ch.cyberduck.core.ssl.DisabledX509TrustManager;
import ch.cyberduck.core.threading.CancelCallback;
import ch.cyberduck.test.VaultTest;
import org.junit.After;
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
public void setup() throws Exception {
final Host host = new Host(new CteraProtocol(), "dcdirect.ctera.me", new Credentials(PROPERTIES.get("ctera.directio.user"))) {
@Override
public String getProperty(final String key) {
if("ctera.download.directio.enable".equals(key)) {
return String.valueOf(true);
}
return super.getProperty(key);
}
};
host.setDefaultPath("/ServicesPortal/webdav/My Files");
keychain = new TestPasswordStore();
session = new CteraSession(host, new DisabledX509TrustManager(), new DefaultX509KeyManager(), keychain);
final LoginConnectionService connect = new LoginConnectionService(LoginCallback.noop, HostKeyCallback.noop,
keychain, ProgressListener.noop, new DisabledProxyFinder());
connect.check(session, CancelCallback.noop);
}
}
@@ -43,7 +43,15 @@ public class AbstractCteraTest extends VaultTest {
@Before
public void setup() throws Exception {
final Host host = new Host(new CteraProtocol(), "driveconnect.ctera.me", new Credentials(PROPERTIES.get("ctera.user")));
final Host host = new Host(new CteraProtocol(), PROPERTIES.get("ctera.hostname"), new Credentials(PROPERTIES.get("ctera.user"))) {
@Override
public String getProperty(final String key) {
if("ctera.download.directio.enable".equals(key)) {
return String.valueOf(true);
}
return super.getProperty(key);
}
};
host.setDefaultPath("/ServicesPortal/webdav/My Files");
session = new CteraSession(host, new DisabledX509TrustManager(), new DefaultX509KeyManager());
final LoginConnectionService login = new LoginConnectionService(new DisabledLoginCallback() {
@@ -108,7 +108,7 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
@Test
public void testNoAccessAcl() throws Exception {
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test (new user)", EnumSet.of(AbstractPath.Type.directory));
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test", EnumSet.of(AbstractPath.Type.directory));
// list parent folder to inspect attributes
final List<DavResource> noAccess = new CteraListService(session).propfind(home).stream().filter(r -> r.getName().equals("NoAccess")).collect(Collectors.toList());
@@ -119,8 +119,8 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
assertEquals("false", resource.getCustomProps().get(READPERMISSION.getName()));
assertEquals("false", resource.getCustomProps().get(DELETEPERMISSION.getName()));
assertEquals("false", resource.getCustomProps().get(CREATEDIRECTORIESPERMISSION.getName()));
assertEquals("bb64b3a4-399e-45d0-95af-43f1ace6e250:105620641", resource.getCustomProps().get(CTERA_GUID));
assertEquals("105620644", resource.getCustomProps().get(CTERA_FILEID));
assertEquals("05c75d64-bb1e-4be8-8ec3-19370247dfec:913", resource.getCustomProps().get(CTERA_GUID));
assertEquals("47836", resource.getCustomProps().get(CTERA_FILEID));
assertEquals(new Acl(new Acl.CanonicalUser()), new CteraAttributesFinderFeature(session).toAttributes(resource).getAcl());
// find fails with 403 in backend
final AccessDeniedException findException = assertThrows(AccessDeniedException.class, () -> new CteraAttributesFinderFeature(session).find(new Path(home, "NoAccess", EnumSet.of(AbstractPath.Type.directory))));
@@ -133,10 +133,12 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
@Test
public void testNoDeleteAcl() throws Exception {
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test (new user)", EnumSet.of(AbstractPath.Type.directory));
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test", EnumSet.of(AbstractPath.Type.directory));
final Path folder = new Path(home, "NoDelete", EnumSet.of(AbstractPath.Type.directory));
final Acl folderAcl = new CteraAttributesFinderFeature(session).find(folder).getAcl();
assertEquals(new Acl(new Acl.UserAndRole(new Acl.CanonicalUser(), READPERMISSION)), folderAcl);
assertEquals(new Acl(new Acl.UserAndRole(new Acl.CanonicalUser(), READPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), WRITEPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), CREATEDIRECTORIESPERMISSION)), folderAcl);
final Path file = new Path(folder, "RW no delete.txt", EnumSet.of(AbstractPath.Type.file));
final Acl fileAcl = new CteraAttributesFinderFeature(session).find(file).getAcl();
@@ -145,7 +147,7 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
@Test
public void testReadOnlyAcl() throws Exception {
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test (new user)", EnumSet.of(AbstractPath.Type.directory));
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test", EnumSet.of(AbstractPath.Type.directory));
final Path folder = new Path(home, "ReadOnly", EnumSet.of(AbstractPath.Type.directory));
final Acl folderAcl = new CteraAttributesFinderFeature(session).find(folder).getAcl();
assertEquals(new Acl(new Acl.UserAndRole(new Acl.CanonicalUser(), READPERMISSION)), folderAcl);
@@ -157,7 +159,7 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
@Test
public void testNoCreateFolderAcl() throws Exception {
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test (new user)", EnumSet.of(AbstractPath.Type.directory));
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test", EnumSet.of(AbstractPath.Type.directory));
final Path folder = new Path(home, "NoCreateFolderPermission", EnumSet.of(AbstractPath.Type.directory));
final Acl folderAcl = new CteraAttributesFinderFeature(session).find(folder).getAcl();
assertEquals(new Acl(
@@ -176,7 +178,7 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
@Test
public void testReadWriteAcl() throws Exception {
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test (new user)", EnumSet.of(AbstractPath.Type.directory));
final Path home = new Path("/ServicesPortal/webdav/Shared With Me/ACL test", EnumSet.of(AbstractPath.Type.directory));
final Path folder = new Path(home, "ReadWrite", EnumSet.of(AbstractPath.Type.directory));
final Acl folderAcl = new CteraAttributesFinderFeature(session).find(folder).getAcl();
assertEquals(new Acl(
@@ -206,15 +208,7 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
@Test
public void testWORMAcl() throws Exception {
final Path home = new Path("/ServicesPortal/webdav/Shared With Me", EnumSet.of(AbstractPath.Type.directory));
final Path folder = new Path(home, "WORM test (new user)", EnumSet.of(AbstractPath.Type.directory));
final Acl folderAcl = new CteraAttributesFinderFeature(session).find(folder).getAcl();
assertEquals(new Acl(
new Acl.UserAndRole(new Acl.CanonicalUser(), READPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), WRITEPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), DELETEPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), CREATEDIRECTORIESPERMISSION)
), folderAcl);
final Path folder = new Path(home, "WORM test", EnumSet.of(AbstractPath.Type.directory));
final Path subfolder = new Path(folder, "Retention Folder (no write, no delete)", EnumSet.of(AbstractPath.Type.directory));
final Acl subfolderAcl = new CteraAttributesFinderFeature(session).find(subfolder).getAcl();
assertEquals(new Acl(
@@ -227,26 +221,14 @@ public class CteraAttributesFinderFeatureTest extends AbstractCteraTest {
assertEquals(new Acl(
new Acl.UserAndRole(new Acl.CanonicalUser(), READPERMISSION)
), fileAcl);
final Path emptySubfolder = new Path(folder, "Empty WORM folder", EnumSet.of(AbstractPath.Type.directory));
final Acl emptySubfolderAcl = new CteraAttributesFinderFeature(session).find(emptySubfolder).getAcl();
assertEquals(new Acl(
new Acl.UserAndRole(new Acl.CanonicalUser(), READPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), CREATEDIRECTORIESPERMISSION)
), emptySubfolderAcl);
}
@Test
public void testWORMNoRetentionAcl() throws Exception {
final Path home = new Path("/ServicesPortal/webdav/Shared With Me", EnumSet.of(AbstractPath.Type.directory));
final Path folder = new Path(home, "WORM-NoRetention(Delete allowed) (new user)", EnumSet.of(AbstractPath.Type.directory));
final Path folder = new Path(home, "WORM-NoRetention(Delete allowed)", EnumSet.of(AbstractPath.Type.directory));
final Acl folderAcl = new CteraAttributesFinderFeature(session).find(folder).getAcl();
assertEquals(new Acl(
new Acl.UserAndRole(new Acl.CanonicalUser(), READPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), WRITEPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), DELETEPERMISSION),
new Acl.UserAndRole(new Acl.CanonicalUser(), CREATEDIRECTORIESPERMISSION)
), folderAcl);
assertEquals(Acl.EMPTY, folderAcl);
final Path file = new Path(folder, "WORM-DeleteAllowed.txt", EnumSet.of(AbstractPath.Type.file));
final Acl fileAcle = new CteraAttributesFinderFeature(session).find(file).getAcl();
@@ -58,7 +58,7 @@ import java.util.EnumSet;
import static org.junit.Assert.*;
@Category(IntegrationTest.class)
public class CteraConcurrentTransferWorkerTest extends AbstractCteraDirectIOTest {
public class CteraConcurrentTransferWorkerTest extends AbstractCteraTest {
@Test
public void testBelowSegmentSizeUpAndDownload() throws Exception {
@@ -5,11 +5,8 @@ import ch.cyberduck.core.AlphanumericRandomStringService;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.dav.DAVFindFeature;
import ch.cyberduck.core.dav.DAVLockFeature;
import ch.cyberduck.core.exception.AccessDeniedException;
import ch.cyberduck.core.exception.InteroperabilityException;
import ch.cyberduck.core.exception.NotfoundException;
import ch.cyberduck.core.exception.RetriableAccessDeniedException;
import ch.cyberduck.core.features.Delete;
import ch.cyberduck.core.shared.DefaultHomeFinderService;
import ch.cyberduck.core.transfer.TransferStatus;
@@ -37,22 +34,6 @@ public class CteraDeleteFeatureTest extends AbstractCteraTest {
assertFalse(new DAVFindFeature(session).find(test));
}
@Test(expected = RetriableAccessDeniedException.class)
public void testDeleteFileWithLock() throws Exception {
final Path test = new Path(new DefaultHomeFinderService(session).find(), new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file));
new CteraTouchFeature(session).touch(new CteraWriteFeature(session), test, new TransferStatus());
String lock = null;
try {
lock = new DAVLockFeature(session).lock(test);
}
catch(InteroperabilityException e) {
// Not supported
}
assertTrue(new DAVFindFeature(session).find(test));
new CteraDeleteFeature(session).delete(Collections.singletonMap(test, new TransferStatus().setLockId(lock)), LoginCallback.noop, new Delete.DisabledCallback());
assertFalse(new DAVFindFeature(session).find(test));
}
@Test
public void testDeleteDirectory() throws Exception {
final Path test = new Path(new DefaultHomeFinderService(session).find(), new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.directory));
@@ -50,7 +50,7 @@ import java.util.EnumSet;
import static org.junit.Assert.*;
@Category(IntegrationTest.class)
public class CteraDirectIOReadFeatureTest extends AbstractCteraDirectIOTest {
public class CteraDirectIOReadFeatureTest extends AbstractCteraTest {
@Test
public void testReadSingleChunk() throws Exception {
@@ -449,6 +449,7 @@ googledrive.delete.multiple.partition=50
b2.bucket.acl.default=allPrivate
b2.listing.chunksize=1000
b2.listing.concurrency=25
b2.listing.versioning.enable=true
b2.upload.checksum.verify=true
b2.upload.largeobject.auto=true
+2 -2
View File
@@ -325,7 +325,7 @@
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.49</version>
<version>2.2.50</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
@@ -741,7 +741,7 @@
<plugin>
<groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>3.0.79</version>
<version>3.0.80</version>
<configuration>
<configOptions>
<sourceFolder>src/main/java</sourceFolder>
-6
View File
@@ -85,12 +85,6 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
<version>2.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
@@ -1,240 +0,0 @@
package ch.cyberduck.core.cryptomator;
/*
* Copyright (c) 2002-2017 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.ConnectionCallback;
import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.HostKeyCallback;
import ch.cyberduck.core.LoginCallback;
import ch.cyberduck.core.PasswordCallback;
import ch.cyberduck.core.Path;
import ch.cyberduck.core.cryptomator.features.CryptoReadFeature;
import ch.cyberduck.core.cryptomator.impl.v8.CryptomatorVault;
import ch.cyberduck.core.cryptomator.impl.v8.MasterkeyVaultMetadataProvider;
import ch.cyberduck.core.proxy.DisabledProxyFinder;
import ch.cyberduck.core.sftp.SFTPHomeDirectoryService;
import ch.cyberduck.core.sftp.SFTPProtocol;
import ch.cyberduck.core.sftp.SFTPReadFeature;
import ch.cyberduck.core.sftp.SFTPSession;
import ch.cyberduck.core.ssl.DefaultX509KeyManager;
import ch.cyberduck.core.ssl.DisabledX509TrustManager;
import ch.cyberduck.core.threading.CancelCallback;
import ch.cyberduck.core.transfer.TransferStatus;
import ch.cyberduck.core.vault.DefaultVaultRegistry;
import ch.cyberduck.core.vault.VaultCredentials;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.text.RandomStringGenerator;
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.sftp.server.SftpSubsystemFactory;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.EnumSet;
import java.util.concurrent.ThreadLocalRandom;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static org.junit.Assert.assertArrayEquals;
public class SFTPCryptomatorInteroperabilityTest {
private final int PORT_NUMBER = ThreadLocalRandom.current().nextInt(2000, 3000);
private static SshServer server;
private CryptoFileSystem cryptoFileSystem;
private java.nio.file.Path tempDir;
private String passphrase;
@Before
public void startSerer() throws Exception {
server = SshServer.setUpDefaultServer();
server.setPort(PORT_NUMBER);
server.setPasswordAuthenticator((username, password, session) -> true);
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider());
server.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
tempDir = Files.createTempDirectory(String.format("%s-", this.getClass().getName()));
unzipTestVault("/testvault.zip", tempDir.toString());
//TODO switch back to cryptofs based testing as soon as cryptofs is based on new cryptolib
/*
final java.nio.file.Path vault = tempDir.resolve("vault");
Files.createDirectory(vault);
passphrase = new AlphanumericRandomStringService().random();
final SecureRandom csprng;
switch(Factory.Platform.getDefault()) {
case windows:
csprng = ReseedingSecureRandom.create(SecureRandom.getInstanceStrong());
break;
default:
csprng = FastSecureRandomProvider.get().provide();
}
final PerpetualMasterkey mk = Masterkey.generate(csprng);
final MasterkeyFileAccess mkAccess = new MasterkeyFileAccess(PreferencesFactory.get().getProperty("cryptomator.vault.pepper").getBytes(StandardCharsets.UTF_8), csprng);
final java.nio.file.Path mkPath = Paths.get(vault.toString(), DefaultVaultRegistry.DEFAULT_MASTERKEY_FILE_NAME);
mkAccess.persist(mk, mkPath, passphrase);
CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoader(new MasterkeyLoader() {
@Override
public Masterkey loadKey(final URI keyId) throws MasterkeyLoadingFailedException {
return mkAccess.load(mkPath, passphrase);
}
})
.withCipherCombo(CryptorProvider.Scheme.SIV_CTRMAC)
.build();
CryptoFileSystemProvider.initialize(vault, properties, URI.create("test:key"));
cryptoFileSystem = CryptoFileSystemProvider.newFileSystem(vault, properties);
*/
server.setFileSystemFactory(new VirtualFileSystemFactory(tempDir.toAbsolutePath()));
server.start();
}
@After
public void stop() throws Exception {
server.stop();
FileUtils.deleteDirectory(tempDir.toFile());
/*
cryptoFileSystem.close();
FileUtils.deleteDirectory(cryptoFileSystem.getPathToVault().getParent().toFile());
*/
}
private void unzipTestVault(final String zip, final String target) throws Exception {
try(InputStream is = this.getClass().getResourceAsStream(zip);
ZipInputStream zipIn = new ZipInputStream(is)) {
java.nio.file.Path targetDir = Paths.get(target);
Files.createDirectories(targetDir);
ZipEntry entry;
while((entry = zipIn.getNextEntry()) != null) {
java.nio.file.Path filePath = targetDir.resolve(entry.getName());
System.out.println(filePath.toString());
if(entry.isDirectory()) {
Files.createDirectories(filePath);
}
else {
Files.createDirectories(filePath.getParent());
Files.copy(zipIn, filePath, StandardCopyOption.REPLACE_EXISTING);
}
zipIn.closeEntry();
}
}
}
/**
* Create file/folder with Cryptomator, read with Cyberduck
*/
@Ignore(value = "Need a Cryptofs version that is based on the new Cryptolib")
@Test(expected = CryptoInvalidFilenameException.class)
public void testCryptomatorInteroperabilityLongFilename() throws Exception {
// create folder
final java.nio.file.Path targetFolder = cryptoFileSystem.getPath("/", new AlphanumericRandomStringService().random());
Files.createDirectory(targetFolder);
// create file and write some random content
java.nio.file.Path targetFile = targetFolder.resolve(new RandomStringGenerator.Builder().build().generate(220));
final byte[] content = RandomUtils.nextBytes(48768);
Files.write(targetFile, content);
// read with Cyberduck and compare
final Host host = new Host(new SFTPProtocol(), "localhost", PORT_NUMBER, new Credentials("empty", "empty"));
final SFTPSession session = new SFTPSession(host, new DisabledX509TrustManager(), new DefaultX509KeyManager());
session.open(new DisabledProxyFinder(), HostKeyCallback.noop, LoginCallback.noop, CancelCallback.noop);
session.login(LoginCallback.noop, CancelCallback.noop);
final Path home = new SFTPHomeDirectoryService(session).find();
final Path vaultPath = new Path(home, "vault", EnumSet.of(Path.Type.directory));
final AbstractVault cryptomator = new CryptomatorVault(vaultPath);
cryptomator.load(session, new MasterkeyVaultMetadataProvider(new VaultCredentials("12341234")));
session.withRegistry(new DefaultVaultRegistry(PasswordCallback.noop, cryptomator));
Path p = new Path(new Path(vaultPath, targetFolder.getFileName().toString(), EnumSet.of(Path.Type.directory)), targetFile.getFileName().toString(), EnumSet.of(Path.Type.file));
final InputStream read = new CryptoReadFeature(session, new SFTPReadFeature(session), cryptomator).read(p, new TransferStatus(), ConnectionCallback.noop);
final byte[] readContent = new byte[content.length];
IOUtils.readFully(read, readContent);
assertArrayEquals(content, readContent);
}
/**
* Read Cryptomator generated vault with long file and folder names
*/
@Test
public void testCryptomatorInteroperabilityLongFileAndFoldername() throws Exception {
// read with Cyberduck and compare
final Host host = new Host(new SFTPProtocol(), "localhost", PORT_NUMBER, new Credentials("empty", "empty"));
final SFTPSession session = new SFTPSession(host, new DisabledX509TrustManager(), new DefaultX509KeyManager());
session.open(new DisabledProxyFinder(), HostKeyCallback.noop, LoginCallback.noop, CancelCallback.noop);
session.login(LoginCallback.noop, CancelCallback.noop);
final Path vaultPath = new SFTPHomeDirectoryService(session).find();
final AbstractVault cryptomator = new CryptomatorVault(vaultPath);
cryptomator.load(session, new MasterkeyVaultMetadataProvider(new VaultCredentials("12341234")));
session.withRegistry(new DefaultVaultRegistry(PasswordCallback.noop, cryptomator));
/*
Path p = new Path(new Path(vaultPath, targetFolder.getFileName().toString(), EnumSet.of(Path.Type.directory)), targetFile.getFileName().toString(), EnumSet.of(Path.Type.file));
final InputStream read = new CryptoReadFeature(session, new SFTPReadFeature(session), cryptomator).read(p, new TransferStatus(), ConnectionCallback.noop);
final byte[] readContent = new byte[content.length];
IOUtils.readFully(read, readContent);
assertArrayEquals(content, readContent);*/
}
/**
* Create long file/folder with Cryptomator, read with Cyberduck
*/
@Ignore(value = "Need a Cryptofs version that is based on the new Cryptolib")
@Test
public void testCryptomatorInteroperability() throws Exception {
// create folder
final java.nio.file.Path targetFolder = cryptoFileSystem.getPath("/", new AlphanumericRandomStringService().random());
Files.createDirectory(targetFolder);
// create file and write some random content
java.nio.file.Path targetFile = targetFolder.resolve(new AlphanumericRandomStringService().random());
final byte[] content = RandomUtils.nextBytes(20);
Files.write(targetFile, content);
// read with Cyberduck and compare
final Host host = new Host(new SFTPProtocol(), "localhost", PORT_NUMBER, new Credentials("empty", "empty"));
final SFTPSession session = new SFTPSession(host, new DisabledX509TrustManager(), new DefaultX509KeyManager());
session.open(new DisabledProxyFinder(), HostKeyCallback.noop, LoginCallback.noop, CancelCallback.noop);
session.login(LoginCallback.noop, CancelCallback.noop);
final Path home = new SFTPHomeDirectoryService(session).find();
final Path vaultPath = new Path(home, "vault", EnumSet.of(Path.Type.directory));
final AbstractVault cryptomator = new CryptomatorVault(vaultPath);
cryptomator.load(session, new MasterkeyVaultMetadataProvider(new VaultCredentials("12341234")));
session.withRegistry(new DefaultVaultRegistry(PasswordCallback.noop, cryptomator));
Path p = new Path(new Path(vaultPath, targetFolder.getFileName().toString(), EnumSet.of(Path.Type.directory)), targetFile.getFileName().toString(), EnumSet.of(Path.Type.file));
final InputStream read = new CryptoReadFeature(session, new SFTPReadFeature(session), cryptomator).read(p, new TransferStatus(), ConnectionCallback.noop);
final byte[] readContent = new byte[content.length];
IOUtils.readFully(read, readContent);
assertArrayEquals(content, readContent);
}
}
+2
View File
@@ -134,6 +134,8 @@
<li><span class="label label-warning">Bugfix</span> Support "Include" directive when reading from OpenSSH config
(SFTP) (<a target="_blank" href="https://trac.cyberduck.io/ticket/10451">#10451</a>)
</li>
<li><span class="label label-warning">Bugfix</span> Exclude trashed folders in list by default (Backblaze B2) (<a
target="_blank" href="https://trac.cyberduck.io/ticket/#18101">##18101</a></li>
</ul>
<p>