http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java new file mode 100644 index 0000000..d48a19d --- /dev/null +++ b/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.client.simple; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.EnumSet; + +import org.apache.sshd.client.subsystem.sftp.SftpClient; +import org.apache.sshd.common.file.FileSystemFactory; +import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.subsystem.sftp.SftpConstants; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.server.scp.ScpCommandFactory; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; +import org.apache.sshd.util.test.Utils; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class SimpleSftpClientTest extends BaseSimpleClientTestSupport { + private final Path targetPath; + private final Path parentPath; + private final FileSystemFactory fileSystemFactory; + private SimpleSftpClient sftpClient; + + public SimpleSftpClientTest() throws Exception { + targetPath = detectTargetFolder(); + parentPath = targetPath.getParent(); + fileSystemFactory = new VirtualFileSystemFactory(parentPath); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + sshd.setCommandFactory(new ScpCommandFactory()); + sshd.setFileSystemFactory(fileSystemFactory); + client.start(); + sftpClient = new SimpleSftpClientImpl(simple); + } + + @Test + public void testSessionClosedWhenClientClosed() throws Exception { + try (SftpClient sftp = login()) { + assertTrue("SFTP not open", sftp.isOpen()); + + Session session = sftp.getClientSession(); + assertTrue("Session not open", session.isOpen()); + + sftp.close(); + assertFalse("Session not closed", session.isOpen()); + assertFalse("SFTP not closed", sftp.isOpen()); + } + } + + @Test + public void testSftpProxyCalls() throws Exception { + Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName()); + Utils.deleteRecursive(lclSftp); + Path clientFolder = assertHierarchyTargetFolderExists(lclSftp).resolve("client"); + Path clientFile = clientFolder.resolve("file.txt"); + String remoteFileDir = Utils.resolveRelativeRemotePath(parentPath, clientFolder); + String clientFileName = clientFile.getFileName().toString(); + String remoteFilePath = remoteFileDir + "/" + clientFileName; + + try (SftpClient sftp = login()) { + sftp.mkdir(remoteFileDir); + + byte[] written = (getClass().getSimpleName() + "#" + getCurrentTestName() + IoUtils.EOL).getBytes(StandardCharsets.UTF_8); + try (SftpClient.CloseableHandle h = sftp.open(remoteFilePath, EnumSet.of(SftpClient.OpenMode.Write, SftpClient.OpenMode.Create))) { + sftp.write(h, 0L, written); + + SftpClient.Attributes attrs = sftp.stat(h); + assertNotNull("No handle attributes", attrs); + assertEquals("Mismatched remote file size", written.length, attrs.getSize()); + } + + assertTrue("Remote file not created: " + clientFile, Files.exists(clientFile, IoUtils.EMPTY_LINK_OPTIONS)); + byte[] local = Files.readAllBytes(clientFile); + assertArrayEquals("Mismatched remote written data", written, local); + + try (SftpClient.CloseableHandle h = sftp.openDir(remoteFileDir)) { + boolean matchFound = false; + for (SftpClient.DirEntry entry : sftp.listDir(h)) { + String name = entry.getFilename(); + if (clientFileName.equals(name)) { + matchFound = true; + break; + } + } + + assertTrue("No directory entry found for " + clientFileName, matchFound); + } + + sftp.remove(remoteFilePath); + assertFalse("Remote file not removed: " + clientFile, Files.exists(clientFile, IoUtils.EMPTY_LINK_OPTIONS)); + } + } + + private SftpClient login() throws IOException { + return sftpClient.sftpLogin(TEST_LOCALHOST, port, getCurrentTestName(), getCurrentTestName()); + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java new file mode 100644 index 0000000..629bbc9 --- /dev/null +++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClientTestSupport.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.client.subsystem.sftp; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension; +import org.apache.sshd.common.file.FileSystemFactory; +import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.scp.ScpCommandFactory; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.JSchLogger; +import org.apache.sshd.util.test.Utils; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public abstract class AbstractSftpClientTestSupport extends BaseTestSupport { + protected static SshServer sshd; + protected static int port; + protected static SshClient client; + + protected final FileSystemFactory fileSystemFactory; + + protected AbstractSftpClientTestSupport() throws IOException { + Path targetPath = detectTargetFolder(); + Path parentPath = targetPath.getParent(); + fileSystemFactory = new VirtualFileSystemFactory(parentPath); + } + + @BeforeClass + public static void setupClientAndServer() throws Exception { + JSchLogger.init(); + sshd = Utils.setupTestServer(AbstractSftpClientTestSupport.class); + sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + sshd.setCommandFactory(new ScpCommandFactory()); + sshd.start(); + port = sshd.getPort(); + + client = Utils.setupTestClient(AbstractSftpClientTestSupport.class); + client.start(); + } + + @AfterClass + public static void tearDownClientAndServer() throws Exception { + if (sshd != null) { + try { + sshd.stop(true); + } finally { + sshd = null; + } + } + + if (client != null) { + try { + client.stop(); + } finally { + client = null; + } + } + } + + protected void setupServer() throws Exception { + sshd.setFileSystemFactory(fileSystemFactory); + } + + protected SftpClient createSftpClient(ClientSession session) throws IOException { + return SftpClientFactory.instance().createSftpClient(session); + } + + protected SftpClient createSftpClient(ClientSession session, int selector) throws IOException { + return SftpClientFactory.instance().createSftpClient(session, selector); + } + + protected static <E extends SftpClientExtension> E assertExtensionCreated(SftpClient sftp, Class<E> type) { + E instance = sftp.getExtension(type); + assertNotNull("Extension not created: " + type.getSimpleName(), instance); + assertTrue("Extension not supported: " + instance.getName(), instance.isSupported()); + return instance; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java new file mode 100644 index 0000000..e5265d5 --- /dev/null +++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/DefaultCloseableHandleTest.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.client.subsystem.sftp; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle; +import org.apache.sshd.client.subsystem.sftp.SftpClient.Handle; +import org.apache.sshd.client.subsystem.sftp.impl.DefaultCloseableHandle; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.NoIoTestCase; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.MethodSorters; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Category({ NoIoTestCase.class }) +public class DefaultCloseableHandleTest extends BaseTestSupport { + public DefaultCloseableHandleTest() { + super(); + } + + @Test + public void testChannelBehavior() throws IOException { + final byte[] id = getCurrentTestName().getBytes(StandardCharsets.UTF_8); + SftpClient client = Mockito.mock(SftpClient.class); + Mockito.doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + Handle handle = (Handle) args[0]; + assertArrayEquals("Mismatched closing handle", id, handle.getIdentifier()); + return null; + }).when(client).close(ArgumentMatchers.any(Handle.class)); + + CloseableHandle handle = new DefaultCloseableHandle(client, getCurrentTestName(), id); + try { + assertTrue("Handle not initially open", handle.isOpen()); + } finally { + handle.close(); + } + assertFalse("Handle not marked as closed", handle.isOpen()); + // make sure close was called + Mockito.verify(client).close(handle); + } + + @Test + public void testCloseIdempotent() throws IOException { + SftpClient client = Mockito.mock(SftpClient.class); + final AtomicBoolean closeCalled = new AtomicBoolean(false); + Mockito.doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + assertFalse("Close already called on handle=" + args[0], closeCalled.getAndSet(true)); + return null; + }).when(client).close(ArgumentMatchers.any(Handle.class)); + + CloseableHandle handle = new DefaultCloseableHandle(client, getCurrentTestName(), getCurrentTestName().getBytes(StandardCharsets.UTF_8)); + for (int index = 0; index < Byte.SIZE; index++) { + handle.close(); + } + + assertTrue("Close method not called", closeCalled.get()); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java new file mode 100644 index 0000000..4b34f92 --- /dev/null +++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpCommandMain.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.sshd.client.subsystem.sftp; + +/** + * Just a test class used to invoke {@link SftpCommand#main(String[])} in + * order to have logging - which is in {@code test} scope + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public final class SftpCommandMain { + private SftpCommandMain() { + throw new UnsupportedOperationException("No instance"); + } + + public static void main(String[] args) throws Exception { + SftpCommand.main(args); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/251db9b9/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java ---------------------------------------------------------------------- diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java new file mode 100644 index 0000000..5dc9bcf --- /dev/null +++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystemTest.java @@ -0,0 +1,494 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.sshd.client.subsystem.sftp; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclFileAttributeView; +import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.attribute.UserPrincipalNotFoundException; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.file.FileSystemFactory; +import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; +import org.apache.sshd.common.session.Session; +import org.apache.sshd.common.subsystem.sftp.SftpConstants; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.OsUtils; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.scp.ScpCommandFactory; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemEnvironment; +import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.Utils; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class SftpFileSystemTest extends BaseTestSupport { + private static SshServer sshd; + private static int port; + + private final FileSystemFactory fileSystemFactory; + + public SftpFileSystemTest() throws IOException { + Path targetPath = detectTargetFolder(); + Path parentPath = targetPath.getParent(); + fileSystemFactory = new VirtualFileSystemFactory(parentPath); + } + + @BeforeClass + public static void setupServerInstance() throws Exception { + sshd = Utils.setupTestServer(SftpFileSystemTest.class); + sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory())); + sshd.setCommandFactory(new ScpCommandFactory()); + sshd.start(); + port = sshd.getPort(); + } + + @AfterClass + public static void tearDownServerInstance() throws Exception { + if (sshd != null) { + try { + sshd.stop(true); + } finally { + sshd = null; + } + } + } + + @Before + public void setUp() throws Exception { + sshd.setFileSystemFactory(fileSystemFactory); + } + + @Test + public void testFileSystem() throws Exception { + try (FileSystem fs = FileSystems.newFileSystem(createDefaultFileSystemURI(), + GenericUtils.<String, Object>mapBuilder() + .put(SftpFileSystemProvider.READ_BUFFER_PROP_NAME, IoUtils.DEFAULT_COPY_SIZE) + .put(SftpFileSystemProvider.WRITE_BUFFER_PROP_NAME, IoUtils.DEFAULT_COPY_SIZE) + .build())) { + assertTrue("Not an SftpFileSystem", fs instanceof SftpFileSystem); + testFileSystem(fs, ((SftpFileSystem) fs).getVersion()); + } + } + + @Test // see SSHD-578 + public void testFileSystemURIParameters() throws Exception { + Map<String, Object> params = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + params.put("test-class-name", getClass().getSimpleName()); + params.put("test-pkg-name", getClass().getPackage().getName()); + params.put("test-name", getCurrentTestName()); + + int expectedVersion = (SftpSubsystemEnvironment.LOWER_SFTP_IMPL + SftpSubsystemEnvironment.HIGHER_SFTP_IMPL) / 2; + params.put(SftpFileSystemProvider.VERSION_PARAM, expectedVersion); + try (SftpFileSystem fs = (SftpFileSystem) FileSystems.newFileSystem(createDefaultFileSystemURI(params), Collections.<String, Object>emptyMap())) { + try (SftpClient sftpClient = fs.getClient()) { + assertEquals("Mismatched negotiated version", expectedVersion, sftpClient.getVersion()); + + Session session = sftpClient.getClientSession(); + params.forEach((key, expected) -> { + if (SftpFileSystemProvider.VERSION_PARAM.equalsIgnoreCase(key)) { + return; + } + + Object actual = session.getObject(key); + assertEquals("Mismatched value for param '" + key + "'", expected, actual); + }); + } + } + } + + @Test + public void testAttributes() throws IOException { + Path targetPath = detectTargetFolder(); + Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName()); + Utils.deleteRecursive(lclSftp); + + try (FileSystem fs = FileSystems.newFileSystem(createDefaultFileSystemURI(), + GenericUtils.<String, Object>mapBuilder() + .put(SftpFileSystemProvider.READ_BUFFER_PROP_NAME, SftpClient.MIN_READ_BUFFER_SIZE) + .put(SftpFileSystemProvider.WRITE_BUFFER_PROP_NAME, SftpClient.MIN_WRITE_BUFFER_SIZE) + .build())) { + + Path parentPath = targetPath.getParent(); + Path clientFolder = lclSftp.resolve("client"); + String remFilePath = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve("file.txt")); + Path file = fs.getPath(remFilePath); + assertHierarchyTargetFolderExists(file.getParent()); + Files.write(file, (getCurrentTestName() + "\n").getBytes(StandardCharsets.UTF_8)); + + Map<String, Object> attrs = Files.readAttributes(file, "posix:*"); + assertNotNull("No attributes read for " + file, attrs); + + Files.setAttribute(file, "basic:size", 2L); + Files.setAttribute(file, "posix:permissions", PosixFilePermissions.fromString("rwxr-----")); + Files.setAttribute(file, "basic:lastModifiedTime", FileTime.fromMillis(100000L)); + + FileSystem fileSystem = file.getFileSystem(); + try { + UserPrincipalLookupService userLookupService = fileSystem.getUserPrincipalLookupService(); + GroupPrincipal group = userLookupService.lookupPrincipalByGroupName("everyone"); + Files.setAttribute(file, "posix:group", group); + } catch (UserPrincipalNotFoundException e) { + // Also, according to the Javadoc: + // "Where an implementation does not support any notion of + // group then this method always throws UserPrincipalNotFoundException." + // Therefore we are lenient with this exception for Windows + if (OsUtils.isWin32()) { + System.err.println(e.getClass().getSimpleName() + ": " + e.getMessage()); + } else { + throw e; + } + } + } + } + + @Test + public void testRootFileSystem() throws IOException { + Path targetPath = detectTargetFolder(); + Path rootNative = targetPath.resolve("root").toAbsolutePath(); + Utils.deleteRecursive(rootNative); + assertHierarchyTargetFolderExists(rootNative); + + try (FileSystem fs = FileSystems.newFileSystem(URI.create("root:" + rootNative.toUri().toString() + "!/"), null)) { + Path dir = assertHierarchyTargetFolderExists(fs.getPath("test/foo")); + outputDebugMessage("Created %s", dir); + } + } + + @Test // see SSHD-697 + public void testFileChannel() throws IOException { + Path targetPath = detectTargetFolder(); + Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName()); + Path lclFile = lclSftp.resolve(getCurrentTestName() + ".txt"); + Files.deleteIfExists(lclFile); + byte[] expected = (getClass().getName() + "#" + getCurrentTestName() + "(" + new Date() + ")").getBytes(StandardCharsets.UTF_8); + try (FileSystem fs = FileSystems.newFileSystem(createDefaultFileSystemURI(), Collections.emptyMap())) { + Path parentPath = targetPath.getParent(); + String remFilePath = Utils.resolveRelativeRemotePath(parentPath, lclFile); + Path file = fs.getPath(remFilePath); + + FileSystemProvider provider = fs.provider(); + try (FileChannel fc = provider.newFileChannel(file, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE))) { + int writeLen = fc.write(ByteBuffer.wrap(expected)); + assertEquals("Mismatched written length", expected.length, writeLen); + + FileChannel fcPos = fc.position(0L); + assertSame("Mismatched positioned file channel", fc, fcPos); + + byte[] actual = new byte[expected.length]; + int readLen = fc.read(ByteBuffer.wrap(actual)); + assertEquals("Mismatched read len", writeLen, readLen); + assertArrayEquals("Mismatched read data", expected, actual); + } + } + + byte[] actual = Files.readAllBytes(lclFile); + assertArrayEquals("Mismatched persisted data", expected, actual); + } + + @Test + public void testFileStore() throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(createDefaultFileSystemURI(), Collections.emptyMap())) { + Iterable<FileStore> iter = fs.getFileStores(); + assertTrue("Not a list", iter instanceof List<?>); + + List<FileStore> list = (List<FileStore>) iter; + assertEquals("Mismatched stores count", 1, list.size()); + + FileStore store = list.get(0); + assertEquals("Mismatched type", SftpConstants.SFTP_SUBSYSTEM_NAME, store.type()); + assertFalse("Read-only ?", store.isReadOnly()); + + for (String name : fs.supportedFileAttributeViews()) { + assertTrue("Unsupported view name: " + name, store.supportsFileAttributeView(name)); + } + + for (Class<? extends FileAttributeView> type : SftpFileSystemProvider.UNIVERSAL_SUPPORTED_VIEWS) { + assertTrue("Unsupported view type: " + type.getSimpleName(), store.supportsFileAttributeView(type)); + } + } + } + + @Test + public void testMultipleFileStoresOnSameProvider() throws IOException { + try (SshClient client = setupTestClient()) { + client.start(); + + SftpFileSystemProvider provider = new SftpFileSystemProvider(client); + Collection<SftpFileSystem> fsList = new LinkedList<>(); + try { + Collection<String> idSet = new HashSet<>(); + Map<String, Object> empty = Collections.emptyMap(); + for (int index = 0; index < 4; index++) { + String credentials = getCurrentTestName() + "-user-" + index; + SftpFileSystem expected = provider.newFileSystem(createFileSystemURI(credentials, empty), empty); + fsList.add(expected); + + String id = expected.getId(); + assertTrue("Non unique file system id: " + id, idSet.add(id)); + + SftpFileSystem actual = provider.getFileSystem(id); + assertSame("Mismatched cached instances for " + id, expected, actual); + outputDebugMessage("Created file system id: %s", id); + } + + for (SftpFileSystem fs : fsList) { + String id = fs.getId(); + fs.close(); + assertNull("File system not removed from cache: " + id, provider.getFileSystem(id)); + } + } finally { + IOException err = null; + for (FileSystem fs : fsList) { + try { + fs.close(); + } catch (IOException e) { + err = GenericUtils.accumulateException(err, e); + } + } + + client.stop(); + + if (err != null) { + throw err; + } + } + } + } + + @Test + public void testSftpVersionSelector() throws Exception { + final AtomicInteger selected = new AtomicInteger(-1); + SftpVersionSelector selector = (session, current, available) -> { + int value = GenericUtils.stream(available) + .mapToInt(Integer::intValue) + .filter(v -> v != current) + .max() + .orElseGet(() -> current); + selected.set(value); + return value; + }; + + try (SshClient client = setupTestClient()) { + client.start(); + + try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) { + session.addPasswordIdentity(getCurrentTestName()); + session.auth().verify(5L, TimeUnit.SECONDS); + + try (FileSystem fs = createSftpFileSystem(session, selector)) { + assertTrue("Not an SftpFileSystem", fs instanceof SftpFileSystem); + Collection<String> views = fs.supportedFileAttributeViews(); + assertTrue("Universal views (" + SftpFileSystem.UNIVERSAL_SUPPORTED_VIEWS + ") not supported: " + views, + views.containsAll(SftpFileSystem.UNIVERSAL_SUPPORTED_VIEWS)); + int expectedVersion = selected.get(); + assertEquals("Mismatched negotiated version", expectedVersion, ((SftpFileSystem) fs).getVersion()); + testFileSystem(fs, expectedVersion); + } + } finally { + client.stop(); + } + } + } + + private FileSystem createSftpFileSystem(ClientSession session, SftpVersionSelector selector) throws IOException { + return SftpClientFactory.instance().createSftpFileSystem(session, selector); + } + + private void testFileSystem(FileSystem fs, int version) throws Exception { + Iterable<Path> rootDirs = fs.getRootDirectories(); + for (Path root : rootDirs) { + String rootName = root.toString(); + try (DirectoryStream<Path> ds = Files.newDirectoryStream(root)) { + for (Path child : ds) { + String name = child.getFileName().toString(); + assertNotEquals("Unexpected dot name", ".", name); + assertNotEquals("Unexpected dotdot name", "..", name); + outputDebugMessage("[%s] %s", rootName, child); + } + } catch (IOException | RuntimeException e) { + // TODO on Windows one might get share problems for *.sys files + // e.g. "C:\hiberfil.sys: The process cannot access the file because it is being used by another process" + // for now, Windows is less of a target so we are lenient with it + if (OsUtils.isWin32()) { + System.err.println(e.getClass().getSimpleName() + " while accessing children of root=" + root + ": " + e.getMessage()); + } else { + throw e; + } + } + } + + Path targetPath = detectTargetFolder(); + Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName()); + Utils.deleteRecursive(lclSftp); + + Path current = fs.getPath(".").toRealPath().normalize(); + outputDebugMessage("CWD: %s", current); + + Path parentPath = targetPath.getParent(); + Path clientFolder = lclSftp.resolve("client"); + String remFile1Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve("file-1.txt")); + Path file1 = fs.getPath(remFile1Path); + assertHierarchyTargetFolderExists(file1.getParent()); + + String expected = "Hello world: " + getCurrentTestName(); + outputDebugMessage("Write initial data to %s", file1); + Files.write(file1, expected.getBytes(StandardCharsets.UTF_8)); + String buf = new String(Files.readAllBytes(file1), StandardCharsets.UTF_8); + assertEquals("Mismatched read test data", expected, buf); + + if (version >= SftpConstants.SFTP_V4) { + outputDebugMessage("getFileAttributeView(%s)", file1); + AclFileAttributeView aclView = Files.getFileAttributeView(file1, AclFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + assertNotNull("No ACL view for " + file1, aclView); + + Map<String, ?> attrs = Files.readAttributes(file1, "acl:*", LinkOption.NOFOLLOW_LINKS); + outputDebugMessage("readAttributes(%s) %s", file1, attrs); + assertEquals("Mismatched owner for " + file1, aclView.getOwner(), attrs.get("owner")); + + @SuppressWarnings("unchecked") + List<AclEntry> acl = (List<AclEntry>) attrs.get("acl"); + outputDebugMessage("acls(%s) %s", file1, acl); + assertListEquals("Mismatched ACLs for " + file1, aclView.getAcl(), acl); + } + + String remFile2Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve("file-2.txt")); + Path file2 = fs.getPath(remFile2Path); + String remFile3Path = Utils.resolveRelativeRemotePath(parentPath, clientFolder.resolve("file-3.txt")); + Path file3 = fs.getPath(remFile3Path); + try { + outputDebugMessage("Move with failure expected %s => %s", file2, file3); + Files.move(file2, file3, LinkOption.NOFOLLOW_LINKS); + fail("Unexpected success in moving " + file2 + " => " + file3); + } catch (NoSuchFileException e) { + // expected + } + + Files.write(file2, "h".getBytes(StandardCharsets.UTF_8)); + try { + outputDebugMessage("Move with failure expected %s => %s", file1, file2); + Files.move(file1, file2, LinkOption.NOFOLLOW_LINKS); + fail("Unexpected success in moving " + file1 + " => " + file2); + } catch (FileAlreadyExistsException e) { + // expected + } + + outputDebugMessage("Move with success expected %s => %s", file1, file2); + Files.move(file1, file2, LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING); + outputDebugMessage("Move with success expected %s => %s", file2, file1); + Files.move(file2, file1, LinkOption.NOFOLLOW_LINKS); + + Map<String, Object> attrs = Files.readAttributes(file1, "*"); + outputDebugMessage("%s attributes: %s", file1, attrs); + + // TODO there are many issues with symbolic links on Windows + if (OsUtils.isUNIX()) { + Path link = fs.getPath(remFile2Path); + Path linkParent = link.getParent(); + Path relPath = linkParent.relativize(file1); + outputDebugMessage("Create symlink %s => %s", link, relPath); + Files.createSymbolicLink(link, relPath); + assertTrue("Not a symbolic link: " + link, Files.isSymbolicLink(link)); + + Path symLink = Files.readSymbolicLink(link); + assertEquals("mismatched symbolic link name", relPath.toString(), symLink.toString()); + + outputDebugMessage("Delete symlink %s", link); + Files.delete(link); + } + + attrs = Files.readAttributes(file1, "*", LinkOption.NOFOLLOW_LINKS); + outputDebugMessage("%s no-follow attributes: %s", file1, attrs); + assertEquals("Mismatched symlink data", expected, new String(Files.readAllBytes(file1), StandardCharsets.UTF_8)); + + try (FileChannel channel = FileChannel.open(file1)) { + try (FileLock lock = channel.lock()) { + outputDebugMessage("Lock %s: %s", file1, lock); + + try (FileChannel channel2 = FileChannel.open(file1)) { + try (FileLock lock2 = channel2.lock()) { + fail("Unexpected success in re-locking " + file1 + ": " + lock2); + } catch (OverlappingFileLockException e) { + // expected + } + } + } + } + + Files.delete(file1); + } + + private URI createDefaultFileSystemURI() { + return createDefaultFileSystemURI(Collections.emptyMap()); + } + + private URI createDefaultFileSystemURI(Map<String, ?> params) { + return createFileSystemURI(getCurrentTestName(), params); + } + + private URI createFileSystemURI(String username, Map<String, ?> params) { + return createFileSystemURI(username, port, params); + } + + private static URI createFileSystemURI(String username, int port, Map<String, ?> params) { + return SftpFileSystemProvider.createFileSystemURI(TEST_LOCALHOST, port, username, username, params); + } +}