You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/04/25 05:01:22 UTC
[1/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Repository: mina-sshd
Updated Branches:
refs/heads/master cb8982abb -> af415e5fe
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java b/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java
deleted file mode 100644
index 3b651b3..0000000
--- a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClientImpl.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.lang.reflect.Proxy;
-import java.net.SocketAddress;
-import java.security.KeyPair;
-
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-public class SimpleSftpClientImpl extends AbstractLoggingBean implements SimpleSftpClient {
-
- private SimpleClient client;
- private SftpClientFactory sftpClientFactory;
-
- public SimpleSftpClientImpl(SimpleClient client) {
- this(client, null);
- }
-
- public SimpleSftpClientImpl(SimpleClient client, SftpClientFactory sftpClientFactory) {
- this.client = client;
- setSftpClientFactory(sftpClientFactory);
- }
-
- public SimpleClient getClient() {
- return client;
- }
-
- public void setClient(SimpleClient client) {
- this.client = client;
- }
-
- public SftpClientFactory getSftpClientFactory() {
- return sftpClientFactory;
- }
-
- public void setSftpClientFactory(SftpClientFactory sftpClientFactory) {
- this.sftpClientFactory = (sftpClientFactory != null) ? sftpClientFactory : SftpClientFactory.instance();
- }
-
- @Override
- public SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException {
- return createSftpClient(client.sessionLogin(target, username, password));
- }
-
- @Override
- public SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
- return createSftpClient(client.sessionLogin(target, username, identity));
- }
-
- protected SftpClient createSftpClient(final ClientSession session) throws IOException {
- Exception err = null;
- try {
- SftpClient client = sftpClientFactory.createSftpClient(session);
- try {
- return createSftpClient(session, client);
- } catch (Exception e) {
- err = GenericUtils.accumulateException(err, e);
- try {
- client.close();
- } catch (Exception t) {
- if (log.isDebugEnabled()) {
- log.debug("createSftpClient({}) failed ({}) to close client: {}",
- session, t.getClass().getSimpleName(), t.getMessage());
- }
-
- if (log.isTraceEnabled()) {
- log.trace("createSftpClient(" + session + ") client close failure details", t);
- }
- err = GenericUtils.accumulateException(err, t);
- }
- }
- } catch (Exception e) {
- err = GenericUtils.accumulateException(err, e);
- }
-
- // This point is reached if error occurred
- log.warn("createSftpClient({}) failed ({}) to create session: {}",
- session, err.getClass().getSimpleName(), err.getMessage());
-
- try {
- session.close();
- } catch (Exception e) {
- if (log.isDebugEnabled()) {
- log.debug("createSftpClient({}) failed ({}) to close session: {}",
- session, e.getClass().getSimpleName(), e.getMessage());
- }
-
- if (log.isTraceEnabled()) {
- log.trace("createSftpClient(" + session + ") session close failure details", e);
- }
- err = GenericUtils.accumulateException(err, e);
- }
-
- if (err instanceof IOException) {
- throw (IOException) err;
- } else {
- throw new IOException(err);
- }
- }
-
- protected SftpClient createSftpClient(final ClientSession session, final SftpClient client) throws IOException {
- ClassLoader loader = getClass().getClassLoader();
- Class<?>[] interfaces = {SftpClient.class};
- return (SftpClient) Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
- Throwable err = null;
- Object result = null;
- String name = method.getName();
- try {
- result = method.invoke(client, args);
- } catch (Throwable t) {
- if (log.isTraceEnabled()) {
- log.trace("invoke(SftpClient#{}) failed ({}) to execute: {}",
- name, t.getClass().getSimpleName(), t.getMessage());
- }
- err = GenericUtils.accumulateException(err, t);
- }
-
- // propagate the "close" call to the session as well
- if ("close".equals(name) && GenericUtils.isEmpty(args)) {
- try {
- session.close();
- } catch (Throwable t) {
- if (log.isDebugEnabled()) {
- log.debug("invoke(ClientSession#{}) failed ({}) to execute: {}",
- name, t.getClass().getSimpleName(), t.getMessage());
- }
- err = GenericUtils.accumulateException(err, t);
- }
- }
-
- if (err != null) {
- throw err;
- }
-
- return result;
- });
- }
-
- @Override
- public boolean isOpen() {
- return true;
- }
-
- @Override
- public void close() throws IOException {
- // Do nothing
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java
new file mode 100644
index 0000000..c1dd6d6
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClient.java
@@ -0,0 +1,179 @@
+/*
+ * 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.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.channels.Channel;
+import java.security.KeyPair;
+import java.util.Objects;
+
+import org.apache.sshd.client.simple.SimpleClientConfigurator;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * A simplified <U>synchronous</U> API for obtaining SFTP sessions.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SimpleSftpClient extends Channel {
+ /**
+ * Creates an SFTP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param username Username
+ * @param password Password
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(String host, String username, String password) throws IOException {
+ return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+ }
+
+ /**
+ * Creates an SFTP session using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param port The target port
+ * @param username Username
+ * @param password Password
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(String host, int port, String username, String password) throws IOException {
+ return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
+ }
+
+ /**
+ * Creates an SFTP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(String host, String username, KeyPair identity) throws IOException {
+ return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+ }
+
+ /**
+ * Creates an SFTP session using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param port The target port
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(String host, int port, String username, KeyPair identity) throws IOException {
+ return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
+ }
+
+ /**
+ * Creates an SFTP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param username Username
+ * @param password Password
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(InetAddress host, String username, String password) throws IOException {
+ return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+ }
+
+ /**
+ * Creates an SFTP session using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param port The target port
+ * @param username Username
+ * @param password Password
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(InetAddress host, int port, String username, String password) throws IOException {
+ return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, password);
+ }
+
+ /**
+ * Creates an SFTP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
+ return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+ }
+
+ /**
+ * Creates an SFTP session using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param port The target port
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default SftpClient sftpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException {
+ return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, identity);
+ }
+
+ /**
+ * Creates an SFTP session using the provided credentials
+ *
+ * @param target The target {@link SocketAddress}
+ * @param username Username
+ * @param password Password
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException;
+
+ /**
+ * Creates an SFTP session using the provided credentials
+ *
+ * @param target The target {@link SocketAddress}
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException;
+
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java
new file mode 100644
index 0000000..30b4a8b
--- /dev/null
+++ b/sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/impl/SimpleSftpClientImpl.java
@@ -0,0 +1,192 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.lang.reflect.Proxy;
+import java.net.SocketAddress;
+import java.security.KeyPair;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.simple.SimpleClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
+import org.apache.sshd.client.subsystem.sftp.SimpleSftpClient;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.functors.IOFunction;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+public class SimpleSftpClientImpl extends AbstractLoggingBean implements SimpleSftpClient {
+
+ private SimpleClient clientInstance;
+ private SftpClientFactory sftpClientFactory;
+
+ public SimpleSftpClientImpl() {
+ this(null);
+ }
+
+ public SimpleSftpClientImpl(SimpleClient client) {
+ this(client, null);
+ }
+
+ public SimpleSftpClientImpl(SimpleClient client, SftpClientFactory sftpClientFactory) {
+ this.clientInstance = client;
+ setSftpClientFactory(sftpClientFactory);
+ }
+
+ public SimpleClient getClient() {
+ return clientInstance;
+ }
+
+ public void setClient(SimpleClient client) {
+ this.clientInstance = client;
+ }
+
+ public SftpClientFactory getSftpClientFactory() {
+ return sftpClientFactory;
+ }
+
+ public void setSftpClientFactory(SftpClientFactory sftpClientFactory) {
+ this.sftpClientFactory = (sftpClientFactory != null) ? sftpClientFactory : SftpClientFactory.instance();
+ }
+
+ @Override
+ public SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException {
+ return createSftpClient(client -> client.sessionLogin(target, username, password));
+ }
+
+ @Override
+ public SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
+ return createSftpClient(client -> client.sessionLogin(target, username, identity));
+ }
+
+ protected SftpClient createSftpClient(IOFunction<? super SimpleClient, ? extends ClientSession> sessionProvider) throws IOException {
+ SimpleClient client = getClient();
+ ClientSession session = sessionProvider.apply(client);
+ try {
+ SftpClient sftp = createSftpClient(session);
+ session = null; // disable auto-close at finally block
+ return sftp;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
+ protected SftpClient createSftpClient(ClientSession session) throws IOException {
+ Exception err = null;
+ try {
+ SftpClient client = sftpClientFactory.createSftpClient(session);
+ try {
+ return createSftpClient(session, client);
+ } catch (Exception e) {
+ err = GenericUtils.accumulateException(err, e);
+ try {
+ client.close();
+ } catch (Exception t) {
+ if (log.isDebugEnabled()) {
+ log.debug("createSftpClient({}) failed ({}) to close client: {}",
+ session, t.getClass().getSimpleName(), t.getMessage());
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("createSftpClient(" + session + ") client close failure details", t);
+ }
+ err = GenericUtils.accumulateException(err, t);
+ }
+ }
+ } catch (Exception e) {
+ err = GenericUtils.accumulateException(err, e);
+ }
+
+ // This point is reached if error occurred
+ log.warn("createSftpClient({}) failed ({}) to create session: {}",
+ session, err.getClass().getSimpleName(), err.getMessage());
+
+ try {
+ session.close();
+ } catch (Exception e) {
+ if (log.isDebugEnabled()) {
+ log.debug("createSftpClient({}) failed ({}) to close session: {}",
+ session, e.getClass().getSimpleName(), e.getMessage());
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("createSftpClient(" + session + ") session close failure details", e);
+ }
+ err = GenericUtils.accumulateException(err, e);
+ }
+
+ if (err instanceof IOException) {
+ throw (IOException) err;
+ } else {
+ throw new IOException(err);
+ }
+ }
+
+ protected SftpClient createSftpClient(ClientSession session, SftpClient client) throws IOException {
+ ClassLoader loader = getClass().getClassLoader();
+ Class<?>[] interfaces = {SftpClient.class};
+ return (SftpClient) Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
+ Throwable err = null;
+ Object result = null;
+ String name = method.getName();
+ try {
+ result = method.invoke(client, args);
+ } catch (Throwable t) {
+ if (log.isTraceEnabled()) {
+ log.trace("invoke(SftpClient#{}) failed ({}) to execute: {}",
+ name, t.getClass().getSimpleName(), t.getMessage());
+ }
+ err = GenericUtils.accumulateException(err, t);
+ }
+
+ // propagate the "close" call to the session as well
+ if ("close".equals(name) && GenericUtils.isEmpty(args)) {
+ try {
+ session.close();
+ } catch (Throwable t) {
+ if (log.isDebugEnabled()) {
+ log.debug("invoke(ClientSession#{}) failed ({}) to execute: {}",
+ name, t.getClass().getSimpleName(), t.getMessage());
+ }
+ err = GenericUtils.accumulateException(err, t);
+ }
+ }
+
+ if (err != null) {
+ throw err;
+ }
+
+ return result;
+ });
+ }
+
+ @Override
+ public boolean isOpen() {
+ return true;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Do nothing
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/ClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/ClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/ClientTest.java
deleted file mode 100644
index 594c756..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/ClientTest.java
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * 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;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.net.InetSocketAddress;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.apache.sshd.client.channel.ChannelExec;
-import org.apache.sshd.client.channel.ChannelShell;
-import org.apache.sshd.client.channel.ChannelSubsystem;
-import org.apache.sshd.client.channel.ClientChannel;
-import org.apache.sshd.client.future.OpenFuture;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.subsystem.SubsystemClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
-import org.apache.sshd.common.Factory;
-import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.RuntimeSshException;
-import org.apache.sshd.common.Service;
-import org.apache.sshd.common.channel.Channel;
-import org.apache.sshd.common.channel.ChannelListener;
-import org.apache.sshd.common.channel.ChannelListenerManager;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionListener;
-import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.channel.ChannelSession;
-import org.apache.sshd.server.channel.ChannelSessionFactory;
-import org.apache.sshd.server.forward.DirectTcpipFactory;
-import org.apache.sshd.server.session.ServerConnectionServiceFactory;
-import org.apache.sshd.server.session.ServerUserAuthService;
-import org.apache.sshd.server.session.ServerUserAuthServiceFactory;
-import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.apache.sshd.util.test.EchoShell;
-import org.apache.sshd.util.test.EchoShellFactory;
-import org.apache.sshd.util.test.TestChannelListener;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * TODO Add javadoc
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class ClientTest extends BaseTestSupport {
- private SshServer sshd;
- private SshClient client;
- private int port;
- private CountDownLatch authLatch;
- private CountDownLatch channelLatch;
-
- private final AtomicReference<ClientSession> clientSessionHolder = new AtomicReference<>(null);
- @SuppressWarnings("synthetic-access")
- private final SessionListener clientSessionListener = new SessionListener() {
- @Override
- public void sessionCreated(Session session) {
- assertObjectInstanceOf("Non client session creation notification", ClientSession.class, session);
- assertNull("Multiple creation notifications", clientSessionHolder.getAndSet((ClientSession) session));
- }
-
- @Override
- public void sessionEvent(Session session, Event event) {
- assertObjectInstanceOf("Non client session event notification: " + event, ClientSession.class, session);
- assertSame("Mismatched client session event instance: " + event, clientSessionHolder.get(), session);
- }
-
- @Override
- public void sessionException(Session session, Throwable t) {
- assertObjectInstanceOf("Non client session exception notification", ClientSession.class, session);
- assertNotNull("No session exception data", t);
- }
-
- @Override
- public void sessionClosed(Session session) {
- assertObjectInstanceOf("Non client session closure notification", ClientSession.class, session);
- assertSame("Mismatched client session closure instance", clientSessionHolder.getAndSet(null), session);
- }
- };
-
- public ClientTest() {
- super();
- }
-
- @Before
- public void setUp() throws Exception {
- authLatch = new CountDownLatch(0);
- channelLatch = new CountDownLatch(0);
-
- sshd = setupTestServer();
- sshd.setShellFactory(new TestEchoShellFactory());
- sshd.setServiceFactories(Arrays.asList(
- new ServerUserAuthServiceFactory() {
- @Override
- public Service create(Session session) throws IOException {
- return new ServerUserAuthService(session) {
- @SuppressWarnings("synthetic-access")
- @Override
- public void process(int cmd, Buffer buffer) throws Exception {
- authLatch.await();
- super.process(cmd, buffer);
- }
- };
- }
- },
- ServerConnectionServiceFactory.INSTANCE
- ));
- sshd.setChannelFactories(Arrays.asList(
- new ChannelSessionFactory() {
- @Override
- public Channel create() {
- return new ChannelSession() {
- @SuppressWarnings("synthetic-access")
- @Override
- public OpenFuture open(int recipient, long rwsize, long rmpsize, Buffer buffer) {
- try {
- channelLatch.await();
- } catch (InterruptedException e) {
- throw new RuntimeSshException(e);
- }
- return super.open(recipient, rwsize, rmpsize, buffer);
- }
-
- @Override
- public String toString() {
- return "ChannelSession" + "[id=" + getId() + ", recipient=" + getRecipient() + "]";
- }
- };
- }
- },
- DirectTcpipFactory.INSTANCE));
- sshd.start();
- port = sshd.getPort();
-
- client = setupTestClient();
- clientSessionHolder.set(null); // just making sure
- client.addSessionListener(clientSessionListener);
- }
-
- @After
- public void tearDown() throws Exception {
- if (sshd != null) {
- sshd.stop(true);
- }
- if (client != null) {
- client.stop();
- }
- clientSessionHolder.set(null); // just making sure
- }
-
- @Test
- public void testSimpleClientListener() throws Exception {
- AtomicReference<Channel> channelHolder = new AtomicReference<>(null);
- client.addChannelListener(new ChannelListener() {
- @Override
- public void channelOpenSuccess(Channel channel) {
- assertSame("Mismatched opened channel instances", channel, channelHolder.get());
- }
-
- @Override
- public void channelOpenFailure(Channel channel, Throwable reason) {
- assertSame("Mismatched failed open channel instances", channel, channelHolder.get());
- }
-
- @Override
- public void channelInitialized(Channel channel) {
- assertNull("Multiple channel initialization notifications", channelHolder.getAndSet(channel));
- }
-
- @Override
- public void channelStateChanged(Channel channel, String hint) {
- outputDebugMessage("channelStateChanged(%s): %s", channel, hint);
- }
-
- @Override
- public void channelClosed(Channel channel, Throwable reason) {
- assertSame("Mismatched closed channel instances", channel, channelHolder.getAndSet(null));
- }
- });
- sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
-
- client.start();
-
- try (ClientSession session = createTestClientSession()) {
- testClientListener(channelHolder, ChannelShell.class, () -> {
- try {
- return session.createShellChannel();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
- testClientListener(channelHolder, ChannelExec.class, () -> {
- try {
- return session.createExecChannel(getCurrentTestName());
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
- testClientListener(channelHolder, SftpClient.class, () -> {
- try {
- return SftpClientFactory.instance().createSftpClient(session);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
- } finally {
- client.stop();
- }
- }
-
- private <C extends Closeable> void testClientListener(AtomicReference<Channel> channelHolder, Class<C> channelType, Factory<? extends C> factory) throws Exception {
- assertNull(channelType.getSimpleName() + ": Unexpected currently active channel", channelHolder.get());
-
- try (C instance = factory.create()) {
- Channel expectedChannel;
- if (instance instanceof Channel) {
- expectedChannel = (Channel) instance;
- } else if (instance instanceof SubsystemClient) {
- expectedChannel = ((SubsystemClient) instance).getClientChannel();
- } else {
- throw new UnsupportedOperationException("Unknown test instance type" + instance.getClass().getSimpleName());
- }
-
- Channel actualChannel = channelHolder.get();
- assertSame("Mismatched listener " + channelType.getSimpleName() + " instances", expectedChannel, actualChannel);
- }
-
- assertNull(channelType.getSimpleName() + ": Active channel closure not signalled", channelHolder.get());
- }
-
- @Test
- public void testCreateChannelByType() throws Exception {
- client.start();
-
- Collection<ClientChannel> channels = new LinkedList<>();
- try (ClientSession session = createTestClientSession()) {
- // required since we do not use an SFTP subsystem
- PropertyResolverUtils.updateProperty(session, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
- channels.add(session.createChannel(Channel.CHANNEL_SUBSYSTEM, SftpConstants.SFTP_SUBSYSTEM_NAME));
- channels.add(session.createChannel(Channel.CHANNEL_EXEC, getCurrentTestName()));
- channels.add(session.createChannel(Channel.CHANNEL_SHELL, getClass().getSimpleName()));
-
- Set<Integer> ids = new HashSet<>(channels.size());
- for (ClientChannel c : channels) {
- int id = c.getId();
- assertTrue("Channel ID repeated: " + id, ids.add(id));
- }
- } finally {
- for (Closeable c : channels) {
- try {
- c.close();
- } catch (IOException e) {
- // ignored
- }
- }
- client.stop();
- }
-
- assertNull("Session closure not signalled", clientSessionHolder.get());
- }
-
- /**
- * Makes sure that the {@link ChannelListener}s added to the client, session
- * and channel are <U>cumulative</U> - i.e., all of them invoked
- * @throws Exception If failed
- */
- @Test
- public void testChannelListenersPropagation() throws Exception {
- Map<String, TestChannelListener> clientListeners = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- addChannelListener(clientListeners, client, new TestChannelListener(client.getClass().getSimpleName()));
-
- // required since we do not use an SFTP subsystem
- PropertyResolverUtils.updateProperty(client, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
- client.start();
- try (ClientSession session = createTestClientSession()) {
- addChannelListener(clientListeners, session, new TestChannelListener(session.getClass().getSimpleName()));
- assertListenerSizes("ClientSessionOpen", clientListeners, 0, 0);
-
- try (ClientChannel channel = session.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME)) {
- channel.open().verify(5L, TimeUnit.SECONDS);
-
- TestChannelListener channelListener = new TestChannelListener(channel.getClass().getSimpleName());
- // need to emulate them since we are adding the listener AFTER the channel is open
- channelListener.channelInitialized(channel);
- channelListener.channelOpenSuccess(channel);
- channel.addChannelListener(channelListener);
- assertListenerSizes("ClientChannelOpen", clientListeners, 1, 1);
- }
-
- assertListenerSizes("ClientChannelClose", clientListeners, 0, 1);
- } finally {
- client.stop();
- }
-
- assertListenerSizes("ClientStop", clientListeners, 0, 1);
- }
-
- private static void assertListenerSizes(String phase, Map<String, ? extends TestChannelListener> listeners, int activeSize, int openSize) {
- assertListenerSizes(phase, listeners.values(), activeSize, openSize);
- }
-
- private static void assertListenerSizes(String phase, Collection<? extends TestChannelListener> listeners, int activeSize, int openSize) {
- if (GenericUtils.isEmpty(listeners)) {
- return;
- }
-
- for (TestChannelListener l : listeners) {
- if (activeSize >= 0) {
- assertEquals(phase + ": mismatched active channels size for " + l.getName() + " listener", activeSize, GenericUtils.size(l.getActiveChannels()));
- }
-
- if (openSize >= 0) {
- assertEquals(phase + ": mismatched open channels size for " + l.getName() + " listener", openSize, GenericUtils.size(l.getOpenChannels()));
- }
-
- assertEquals(phase + ": unexpected failed channels size for " + l.getName() + " listener", 0, GenericUtils.size(l.getFailedChannels()));
- }
- }
-
- private static <L extends ChannelListener & NamedResource> void addChannelListener(Map<String, L> listeners, ChannelListenerManager manager, L listener) {
- String name = listener.getName();
- assertNull("Duplicate listener named " + name, listeners.put(name, listener));
- manager.addChannelListener(listener);
- }
-
- private ClientSession createTestClientSession() throws IOException {
- ClientSession session = createTestClientSession(TEST_LOCALHOST);
- try {
- InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
- assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
-
- ClientSession returnValue = session;
- session = null; // avoid 'finally' close
- return returnValue;
- } finally {
- if (session != null) {
- session.close();
- }
- }
- }
-
- private ClientSession createTestClientSession(String host) throws IOException {
- ClientSession session = client.connect(getCurrentTestName(), host, port).verify(7L, TimeUnit.SECONDS).getSession();
- try {
- assertNotNull("Client session creation not signalled", clientSessionHolder.get());
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(5L, TimeUnit.SECONDS);
-
- InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
- assertNotNull("No reported connect address", addr);
- assertEquals("Mismatched connect port", port, addr.getPort());
-
- ClientSession returnValue = session;
- session = null; // avoid 'finally' close
- return returnValue;
- } finally {
- if (session != null) {
- session.close();
- }
- }
- }
-
- public static class TestEchoShellFactory extends EchoShellFactory {
- @Override
- public Command create() {
- return new TestEchoShell();
- }
- }
-
- public static class TestEchoShell extends EchoShell {
- // CHECKSTYLE:OFF
- public static CountDownLatch latch;
- // CHECKSTYLE:ON
-
- public TestEchoShell() {
- super();
- }
-
- @Override
- public void destroy() {
- if (latch != null) {
- latch.countDown();
- }
- super.destroy();
- }
- }
-
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java b/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
deleted file mode 100644
index 60b9403..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.junit.After;
-import org.junit.Before;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class BaseSimpleClientTestSupport extends BaseTestSupport {
- public static final long CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
- public static final long AUTH_TIMEOUT = TimeUnit.SECONDS.toMillis(7L);
-
- protected SshServer sshd;
- protected SshClient client;
- protected int port;
- protected SimpleClient simple;
-
- protected BaseSimpleClientTestSupport() {
- super();
- }
-
- @Before
- public void setUp() throws Exception {
- sshd = setupTestServer();
- sshd.start();
- port = sshd.getPort();
- client = setupTestClient();
-
- simple = SshClient.wrapAsSimpleClient(client);
- simple.setConnectTimeout(CONNECT_TIMEOUT);
- simple.setAuthenticationTimeout(AUTH_TIMEOUT);
- }
-
- @After
- public void tearDown() throws Exception {
- if (sshd != null) {
- sshd.stop(true);
- }
- if (simple != null) {
- simple.close();
- }
- if (client != null) {
- client.stop();
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/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
deleted file mode 100644
index d48a19d..0000000
--- a/sshd-sftp/src/test/java/org/apache/sshd/client/simple/SimpleSftpClientTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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:dev@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/af415e5f/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
index 629bbc9..49e6c28 100644
--- 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
@@ -29,7 +29,6 @@ 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;
@@ -58,7 +57,6 @@ public abstract class AbstractSftpClientTestSupport extends BaseTestSupport {
JSchLogger.init();
sshd = Utils.setupTestServer(AbstractSftpClientTestSupport.class);
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
- sshd.setCommandFactory(new ScpCommandFactory());
sshd.start();
port = sshd.getPort();
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java
new file mode 100644
index 0000000..87cafdb
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/ClientTest.java
@@ -0,0 +1,424 @@
+/*
+ * 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.Closeable;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.channel.ChannelExec;
+import org.apache.sshd.client.channel.ChannelShell;
+import org.apache.sshd.client.channel.ChannelSubsystem;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.future.OpenFuture;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.SubsystemClient;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.PropertyResolverUtils;
+import org.apache.sshd.common.RuntimeSshException;
+import org.apache.sshd.common.Service;
+import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.channel.ChannelListener;
+import org.apache.sshd.common.channel.ChannelListenerManager;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionListener;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.channel.ChannelSession;
+import org.apache.sshd.server.channel.ChannelSessionFactory;
+import org.apache.sshd.server.forward.DirectTcpipFactory;
+import org.apache.sshd.server.session.ServerConnectionServiceFactory;
+import org.apache.sshd.server.session.ServerUserAuthService;
+import org.apache.sshd.server.session.ServerUserAuthServiceFactory;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.EchoShell;
+import org.apache.sshd.util.test.EchoShellFactory;
+import org.apache.sshd.util.test.TestChannelListener;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ClientTest extends BaseTestSupport {
+ private SshServer sshd;
+ private SshClient client;
+ private int port;
+ private CountDownLatch authLatch;
+ private CountDownLatch channelLatch;
+
+ private final AtomicReference<ClientSession> clientSessionHolder = new AtomicReference<>(null);
+ @SuppressWarnings("synthetic-access")
+ private final SessionListener clientSessionListener = new SessionListener() {
+ @Override
+ public void sessionCreated(Session session) {
+ assertObjectInstanceOf("Non client session creation notification", ClientSession.class, session);
+ assertNull("Multiple creation notifications", clientSessionHolder.getAndSet((ClientSession) session));
+ }
+
+ @Override
+ public void sessionEvent(Session session, Event event) {
+ assertObjectInstanceOf("Non client session event notification: " + event, ClientSession.class, session);
+ assertSame("Mismatched client session event instance: " + event, clientSessionHolder.get(), session);
+ }
+
+ @Override
+ public void sessionException(Session session, Throwable t) {
+ assertObjectInstanceOf("Non client session exception notification", ClientSession.class, session);
+ assertNotNull("No session exception data", t);
+ }
+
+ @Override
+ public void sessionClosed(Session session) {
+ assertObjectInstanceOf("Non client session closure notification", ClientSession.class, session);
+ assertSame("Mismatched client session closure instance", clientSessionHolder.getAndSet(null), session);
+ }
+ };
+
+ public ClientTest() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ authLatch = new CountDownLatch(0);
+ channelLatch = new CountDownLatch(0);
+
+ sshd = setupTestServer();
+ sshd.setShellFactory(new TestEchoShellFactory());
+ sshd.setServiceFactories(Arrays.asList(
+ new ServerUserAuthServiceFactory() {
+ @Override
+ public Service create(Session session) throws IOException {
+ return new ServerUserAuthService(session) {
+ @SuppressWarnings("synthetic-access")
+ @Override
+ public void process(int cmd, Buffer buffer) throws Exception {
+ authLatch.await();
+ super.process(cmd, buffer);
+ }
+ };
+ }
+ },
+ ServerConnectionServiceFactory.INSTANCE
+ ));
+ sshd.setChannelFactories(Arrays.asList(
+ new ChannelSessionFactory() {
+ @Override
+ public Channel create() {
+ return new ChannelSession() {
+ @SuppressWarnings("synthetic-access")
+ @Override
+ public OpenFuture open(int recipient, long rwsize, long rmpsize, Buffer buffer) {
+ try {
+ channelLatch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeSshException(e);
+ }
+ return super.open(recipient, rwsize, rmpsize, buffer);
+ }
+
+ @Override
+ public String toString() {
+ return "ChannelSession" + "[id=" + getId() + ", recipient=" + getRecipient() + "]";
+ }
+ };
+ }
+ },
+ DirectTcpipFactory.INSTANCE));
+ sshd.start();
+ port = sshd.getPort();
+
+ client = setupTestClient();
+ clientSessionHolder.set(null); // just making sure
+ client.addSessionListener(clientSessionListener);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (sshd != null) {
+ sshd.stop(true);
+ }
+ if (client != null) {
+ client.stop();
+ }
+ clientSessionHolder.set(null); // just making sure
+ }
+
+ @Test
+ public void testSimpleClientListener() throws Exception {
+ AtomicReference<Channel> channelHolder = new AtomicReference<>(null);
+ client.addChannelListener(new ChannelListener() {
+ @Override
+ public void channelOpenSuccess(Channel channel) {
+ assertSame("Mismatched opened channel instances", channel, channelHolder.get());
+ }
+
+ @Override
+ public void channelOpenFailure(Channel channel, Throwable reason) {
+ assertSame("Mismatched failed open channel instances", channel, channelHolder.get());
+ }
+
+ @Override
+ public void channelInitialized(Channel channel) {
+ assertNull("Multiple channel initialization notifications", channelHolder.getAndSet(channel));
+ }
+
+ @Override
+ public void channelStateChanged(Channel channel, String hint) {
+ outputDebugMessage("channelStateChanged(%s): %s", channel, hint);
+ }
+
+ @Override
+ public void channelClosed(Channel channel, Throwable reason) {
+ assertSame("Mismatched closed channel instances", channel, channelHolder.getAndSet(null));
+ }
+ });
+ sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
+
+ client.start();
+
+ try (ClientSession session = createTestClientSession()) {
+ testClientListener(channelHolder, ChannelShell.class, () -> {
+ try {
+ return session.createShellChannel();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ testClientListener(channelHolder, ChannelExec.class, () -> {
+ try {
+ return session.createExecChannel(getCurrentTestName());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ testClientListener(channelHolder, SftpClient.class, () -> {
+ try {
+ return SftpClientFactory.instance().createSftpClient(session);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } finally {
+ client.stop();
+ }
+ }
+
+ private <C extends Closeable> void testClientListener(AtomicReference<Channel> channelHolder, Class<C> channelType, Factory<? extends C> factory) throws Exception {
+ assertNull(channelType.getSimpleName() + ": Unexpected currently active channel", channelHolder.get());
+
+ try (C instance = factory.create()) {
+ Channel expectedChannel;
+ if (instance instanceof Channel) {
+ expectedChannel = (Channel) instance;
+ } else if (instance instanceof SubsystemClient) {
+ expectedChannel = ((SubsystemClient) instance).getClientChannel();
+ } else {
+ throw new UnsupportedOperationException("Unknown test instance type" + instance.getClass().getSimpleName());
+ }
+
+ Channel actualChannel = channelHolder.get();
+ assertSame("Mismatched listener " + channelType.getSimpleName() + " instances", expectedChannel, actualChannel);
+ }
+
+ assertNull(channelType.getSimpleName() + ": Active channel closure not signalled", channelHolder.get());
+ }
+
+ @Test
+ public void testCreateChannelByType() throws Exception {
+ client.start();
+
+ Collection<ClientChannel> channels = new LinkedList<>();
+ try (ClientSession session = createTestClientSession()) {
+ // required since we do not use an SFTP subsystem
+ PropertyResolverUtils.updateProperty(session, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
+ channels.add(session.createChannel(Channel.CHANNEL_SUBSYSTEM, SftpConstants.SFTP_SUBSYSTEM_NAME));
+ channels.add(session.createChannel(Channel.CHANNEL_EXEC, getCurrentTestName()));
+ channels.add(session.createChannel(Channel.CHANNEL_SHELL, getClass().getSimpleName()));
+
+ Set<Integer> ids = new HashSet<>(channels.size());
+ for (ClientChannel c : channels) {
+ int id = c.getId();
+ assertTrue("Channel ID repeated: " + id, ids.add(id));
+ }
+ } finally {
+ for (Closeable c : channels) {
+ try {
+ c.close();
+ } catch (IOException e) {
+ // ignored
+ }
+ }
+ client.stop();
+ }
+
+ assertNull("Session closure not signalled", clientSessionHolder.get());
+ }
+
+ /**
+ * Makes sure that the {@link ChannelListener}s added to the client, session
+ * and channel are <U>cumulative</U> - i.e., all of them invoked
+ * @throws Exception If failed
+ */
+ @Test
+ public void testChannelListenersPropagation() throws Exception {
+ Map<String, TestChannelListener> clientListeners = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ addChannelListener(clientListeners, client, new TestChannelListener(client.getClass().getSimpleName()));
+
+ // required since we do not use an SFTP subsystem
+ PropertyResolverUtils.updateProperty(client, ChannelSubsystem.REQUEST_SUBSYSTEM_REPLY, false);
+ client.start();
+ try (ClientSession session = createTestClientSession()) {
+ addChannelListener(clientListeners, session, new TestChannelListener(session.getClass().getSimpleName()));
+ assertListenerSizes("ClientSessionOpen", clientListeners, 0, 0);
+
+ try (ClientChannel channel = session.createSubsystemChannel(SftpConstants.SFTP_SUBSYSTEM_NAME)) {
+ channel.open().verify(5L, TimeUnit.SECONDS);
+
+ TestChannelListener channelListener = new TestChannelListener(channel.getClass().getSimpleName());
+ // need to emulate them since we are adding the listener AFTER the channel is open
+ channelListener.channelInitialized(channel);
+ channelListener.channelOpenSuccess(channel);
+ channel.addChannelListener(channelListener);
+ assertListenerSizes("ClientChannelOpen", clientListeners, 1, 1);
+ }
+
+ assertListenerSizes("ClientChannelClose", clientListeners, 0, 1);
+ } finally {
+ client.stop();
+ }
+
+ assertListenerSizes("ClientStop", clientListeners, 0, 1);
+ }
+
+ private static void assertListenerSizes(String phase, Map<String, ? extends TestChannelListener> listeners, int activeSize, int openSize) {
+ assertListenerSizes(phase, listeners.values(), activeSize, openSize);
+ }
+
+ private static void assertListenerSizes(String phase, Collection<? extends TestChannelListener> listeners, int activeSize, int openSize) {
+ if (GenericUtils.isEmpty(listeners)) {
+ return;
+ }
+
+ for (TestChannelListener l : listeners) {
+ if (activeSize >= 0) {
+ assertEquals(phase + ": mismatched active channels size for " + l.getName() + " listener", activeSize, GenericUtils.size(l.getActiveChannels()));
+ }
+
+ if (openSize >= 0) {
+ assertEquals(phase + ": mismatched open channels size for " + l.getName() + " listener", openSize, GenericUtils.size(l.getOpenChannels()));
+ }
+
+ assertEquals(phase + ": unexpected failed channels size for " + l.getName() + " listener", 0, GenericUtils.size(l.getFailedChannels()));
+ }
+ }
+
+ private static <L extends ChannelListener & NamedResource> void addChannelListener(Map<String, L> listeners, ChannelListenerManager manager, L listener) {
+ String name = listener.getName();
+ assertNull("Duplicate listener named " + name, listeners.put(name, listener));
+ manager.addChannelListener(listener);
+ }
+
+ private ClientSession createTestClientSession() throws IOException {
+ ClientSession session = createTestClientSession(TEST_LOCALHOST);
+ try {
+ InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+ assertEquals("Mismatched connect host", TEST_LOCALHOST, addr.getHostString());
+
+ ClientSession returnValue = session;
+ session = null; // avoid 'finally' close
+ return returnValue;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
+ private ClientSession createTestClientSession(String host) throws IOException {
+ ClientSession session = client.connect(getCurrentTestName(), host, port).verify(7L, TimeUnit.SECONDS).getSession();
+ try {
+ assertNotNull("Client session creation not signalled", clientSessionHolder.get());
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(5L, TimeUnit.SECONDS);
+
+ InetSocketAddress addr = SshdSocketAddress.toInetSocketAddress(session.getConnectAddress());
+ assertNotNull("No reported connect address", addr);
+ assertEquals("Mismatched connect port", port, addr.getPort());
+
+ ClientSession returnValue = session;
+ session = null; // avoid 'finally' close
+ return returnValue;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
+ public static class TestEchoShellFactory extends EchoShellFactory {
+ @Override
+ public Command create() {
+ return new TestEchoShell();
+ }
+ }
+
+ public static class TestEchoShell extends EchoShell {
+ // CHECKSTYLE:OFF
+ public static CountDownLatch latch;
+ // CHECKSTYLE:ON
+
+ public TestEchoShell() {
+ super();
+ }
+
+ @Override
+ public void destroy() {
+ if (latch != null) {
+ latch.countDown();
+ }
+ super.destroy();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/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
index 5dc9bcf..95f9b66 100644
--- 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
@@ -67,7 +67,6 @@ 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;
@@ -96,7 +95,6 @@ public class SftpFileSystemTest extends BaseTestSupport {
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();
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java
new file mode 100644
index 0000000..2f41c0f
--- /dev/null
+++ b/sshd-sftp/src/test/java/org/apache/sshd/client/subsystem/sftp/SimpleSftpClientTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.EnumSet;
+
+import org.apache.sshd.client.subsystem.sftp.impl.SimpleSftpClientImpl;
+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.subsystem.sftp.SftpSubsystemFactory;
+import org.apache.sshd.util.test.Utils;
+import org.apache.sshd.util.test.client.simple.BaseSimpleClientTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@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.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/af415e5f/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
----------------------------------------------------------------------
diff --git a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
index d6dafee..6b343b0 100644
--- a/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
+++ b/sshd-spring-sftp/src/test/java/org/apache/sshd/spring/integration/sftp/ApacheSshdSftpSessionFactoryTest.java
@@ -48,7 +48,6 @@ import org.apache.sshd.common.subsystem.sftp.SftpConstants;
import org.apache.sshd.common.util.GenericUtils;
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.SftpSubsystemFactory;
import org.apache.sshd.util.test.BaseTestSupport;
import org.apache.sshd.util.test.JSchLogger;
@@ -113,7 +112,6 @@ public class ApacheSshdSftpSessionFactoryTest extends BaseTestSupport {
JSchLogger.init();
sshd = Utils.setupTestServer(ApacheSshdSftpSessionFactoryTest.class);
sshd.setSubsystemFactories(Collections.singletonList(new SftpSubsystemFactory()));
- sshd.setCommandFactory(new ScpCommandFactory());
sshd.start();
port = sshd.getPort();
[2/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
new file mode 100644
index 0000000..99e3e34
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
@@ -0,0 +1,272 @@
+/*
+ * 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.server.scp;
+
+import java.util.Collection;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpFileOpenerHolder;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.util.EventListenerUtils;
+import org.apache.sshd.common.util.ObjectBuilder;
+import org.apache.sshd.common.util.threads.ExecutorServiceConfigurer;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+
+/**
+ * This <code>CommandFactory</code> can be used as a standalone command factory
+ * or can be used to augment another <code>CommandFactory</code> and provides
+ * <code>SCP</code> support.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see ScpCommand
+ */
+public class ScpCommandFactory
+ implements ScpFileOpenerHolder,
+ CommandFactory,
+ Cloneable,
+ ExecutorServiceConfigurer {
+ /**
+ * A useful {@link ObjectBuilder} for {@link ScpCommandFactory}
+ */
+ public static class Builder implements ObjectBuilder<ScpCommandFactory> {
+ private final ScpCommandFactory factory = new ScpCommandFactory();
+
+ public Builder() {
+ super();
+ }
+
+ public Builder withFileOpener(ScpFileOpener opener) {
+ factory.setScpFileOpener(opener);
+ return this;
+ }
+
+ public Builder withDelegate(CommandFactory delegate) {
+ factory.setDelegateCommandFactory(delegate);
+ return this;
+ }
+
+ public Builder withExecutorService(ExecutorService service) {
+ factory.setExecutorService(service);
+ return this;
+ }
+
+ public Builder withShutdownOnExit(boolean shutdown) {
+ factory.setShutdownOnExit(shutdown);
+ return this;
+ }
+
+ public Builder withSendBufferSize(int sendSize) {
+ factory.setSendBufferSize(sendSize);
+ return this;
+ }
+
+ public Builder withReceiveBufferSize(int receiveSize) {
+ factory.setReceiveBufferSize(receiveSize);
+ return this;
+ }
+
+ public Builder addEventListener(ScpTransferEventListener listener) {
+ factory.addEventListener(listener);
+ return this;
+ }
+
+ public Builder removeEventListener(ScpTransferEventListener listener) {
+ factory.removeEventListener(listener);
+ return this;
+ }
+
+ @Override
+ public ScpCommandFactory build() {
+ return factory.clone();
+ }
+ }
+
+ /*
+ * NOTE: we expose setters since there is no problem to change these settings between
+ * successive invocations of the 'createCommand' method
+ */
+ private CommandFactory delegate;
+ private ExecutorService executors;
+ private boolean shutdownExecutor;
+ private ScpFileOpener fileOpener;
+ private int sendBufferSize = ScpHelper.MIN_SEND_BUFFER_SIZE;
+ private int receiveBufferSize = ScpHelper.MIN_RECEIVE_BUFFER_SIZE;
+ private Collection<ScpTransferEventListener> listeners = new CopyOnWriteArraySet<>();
+ private ScpTransferEventListener listenerProxy;
+
+ public ScpCommandFactory() {
+ listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), listeners);
+ }
+
+ @Override
+ public ScpFileOpener getScpFileOpener() {
+ return fileOpener;
+ }
+
+ @Override
+ public void setScpFileOpener(ScpFileOpener fileOpener) {
+ this.fileOpener = fileOpener;
+ }
+
+ public CommandFactory getDelegateCommandFactory() {
+ return delegate;
+ }
+
+ /**
+ * @param factory A {@link CommandFactory} to be used if the
+ * command is not an SCP one. If {@code null} then an {@link IllegalArgumentException}
+ * will be thrown when attempting to invoke {@link #createCommand(String)}
+ * with a non-SCP command
+ */
+ public void setDelegateCommandFactory(CommandFactory factory) {
+ delegate = factory;
+ }
+
+ @Override
+ public ExecutorService getExecutorService() {
+ return executors;
+ }
+
+ /**
+ * @param service An {@link ExecutorService} to be used when
+ * starting {@link ScpCommand} execution. If {@code null} then a single-threaded
+ * ad-hoc service is used. <B>Note:</B> the service will <U>not</U> be shutdown
+ * when the command is terminated - unless it is the ad-hoc service, which will be
+ * shutdown regardless
+ */
+ @Override
+ public void setExecutorService(ExecutorService service) {
+ executors = service;
+ }
+
+ @Override
+ public boolean isShutdownOnExit() {
+ return shutdownExecutor;
+ }
+
+ @Override
+ public void setShutdownOnExit(boolean shutdown) {
+ shutdownExecutor = shutdown;
+ }
+
+ public int getSendBufferSize() {
+ return sendBufferSize;
+ }
+
+ /**
+ * @param sendSize Size (in bytes) of buffer to use when sending files
+ * @see ScpHelper#MIN_SEND_BUFFER_SIZE
+ */
+ public void setSendBufferSize(int sendSize) {
+ if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
+ throw new IllegalArgumentException("<ScpCommandFactory>() send buffer size "
+ + "(" + sendSize + ") below minimum required (" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
+ }
+ sendBufferSize = sendSize;
+ }
+
+ public int getReceiveBufferSize() {
+ return receiveBufferSize;
+ }
+
+ /**
+ * @param receiveSize Size (in bytes) of buffer to use when receiving files
+ * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
+ */
+ public void setReceiveBufferSize(int receiveSize) {
+ if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
+ throw new IllegalArgumentException("<ScpCommandFactory>() receive buffer size "
+ + "(" + receiveSize + ") below minimum required (" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
+ }
+ receiveBufferSize = receiveSize;
+ }
+
+ /**
+ * @param listener The {@link ScpTransferEventListener} to add
+ * @return {@code true} if this is a <U>new</U> listener instance,
+ * {@code false} if the listener is already registered
+ * @throws IllegalArgumentException if {@code null} listener
+ */
+ public boolean addEventListener(ScpTransferEventListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("No listener instance");
+ }
+
+ return listeners.add(listener);
+ }
+
+ /**
+ * @param listener The {@link ScpTransferEventListener} to remove
+ * @return {@code true} if the listener was registered and removed,
+ * {@code false} if the listener was not registered to begin with
+ * @throws IllegalArgumentException if {@code null} listener
+ */
+ public boolean removeEventListener(ScpTransferEventListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("No listener instance");
+ }
+
+ return listeners.remove(listener);
+ }
+
+ /**
+ * Parses a command string and verifies that the basic syntax is
+ * correct. If parsing fails the responsibility is delegated to
+ * the configured {@link CommandFactory} instance; if one exist.
+ *
+ * @param command command to parse
+ * @return configured {@link Command} instance
+ * @throws IllegalArgumentException if not an SCP command and no
+ * delegate command factory is available
+ * @see ScpHelper#SCP_COMMAND_PREFIX
+ */
+ @Override
+ public Command createCommand(String command) {
+ if (command.startsWith(ScpHelper.SCP_COMMAND_PREFIX)) {
+ return new ScpCommand(command,
+ getExecutorService(), isShutdownOnExit(),
+ getSendBufferSize(), getReceiveBufferSize(),
+ getScpFileOpener(), listenerProxy);
+ }
+
+ CommandFactory factory = getDelegateCommandFactory();
+ if (factory != null) {
+ return factory.createCommand(command);
+ }
+
+ throw new IllegalArgumentException("Unknown command, does not begin with '" + ScpHelper.SCP_COMMAND_PREFIX + "': " + command);
+ }
+
+ @Override
+ public ScpCommandFactory clone() {
+ try {
+ ScpCommandFactory other = getClass().cast(super.clone());
+ // clone the listeners set as well
+ other.listeners = new CopyOnWriteArraySet<>(this.listeners);
+ other.listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), other.listeners);
+ return other;
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e); // un-expected...
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java b/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
new file mode 100644
index 0000000..7b7d2e8
--- /dev/null
+++ b/sshd-scp/src/test/java/org/apache/sshd/client/scp/ScpTest.java
@@ -0,0 +1,1203 @@
+/*
+ * 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.scp;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.FileChannel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.channel.Channel;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.random.Random;
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.scp.ScpCommand;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.JSchLogger;
+import org.apache.sshd.util.test.SimpleUserInfo;
+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.Ignore;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+import ch.ethz.ssh2.Connection;
+import ch.ethz.ssh2.ConnectionInfo;
+import ch.ethz.ssh2.SCPClient;
+
+/**
+ * Test for SCP support.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class ScpTest extends BaseTestSupport {
+ private static final ScpTransferEventListener DEBUG_LISTENER = new ScpTransferEventListener() {
+ @Override
+ public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
+ logEvent("starFolderEvent", op, file, false, -1L, perms, null);
+ }
+
+ @Override
+ public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
+ logEvent("startFileEvent", op, file, true, length, perms, null);
+
+ }
+
+ @Override
+ public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
+ logEvent("endFolderEvent", op, file, false, -1L, perms, thrown);
+ }
+
+ @Override
+ public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
+ logEvent("endFileEvent", op, file, true, length, perms, thrown);
+ }
+
+ private void logEvent(String type, FileOperation op, Path path, boolean isFile, long length, Collection<PosixFilePermission> perms, Throwable t) {
+ if (!OUTPUT_DEBUG_MESSAGES) {
+ return; // just in case
+ }
+ StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
+ sb.append('\t').append(type)
+ .append('[').append(op).append(']')
+ .append(' ').append(isFile ? "File" : "Directory").append('=').append(path)
+ .append(' ').append("length=").append(length)
+ .append(' ').append("perms=").append(perms);
+ if (t != null) {
+ sb.append(' ').append("ERROR=").append(t.getClass().getSimpleName()).append(": ").append(t.getMessage());
+ }
+ outputDebugMessage(sb.toString());
+ }
+ };
+
+ private static SshServer sshd;
+ private static int port;
+ private static SshClient client;
+ private final FileSystemFactory fileSystemFactory;
+
+ public ScpTest() 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(ScpTest.class);
+ sshd.setCommandFactory(new ScpCommandFactory());
+ sshd.start();
+ port = sshd.getPort();
+
+ client = Utils.setupTestClient(ScpTest.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;
+ }
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ sshd.setFileSystemFactory(fileSystemFactory);
+ }
+
+ @Test
+ public void testNormalizedScpRemotePaths() throws Exception {
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path localFile = localDir.resolve("file.txt");
+ byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
+ String localPath = localFile.toString();
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ String[] remoteComps = GenericUtils.split(remotePath, '/');
+ Factory<? extends Random> factory = client.getRandomFactory();
+ Random rnd = factory.create();
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(5L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ StringBuilder sb = new StringBuilder(remotePath.length() + Long.SIZE);
+ for (int i = 0; i < Math.max(Long.SIZE, remoteComps.length); i++) {
+ if (sb.length() > 0) {
+ sb.setLength(0); // start again
+ }
+
+ sb.append(remoteComps[0]);
+ for (int j = 1; j < remoteComps.length; j++) {
+ String name = remoteComps[j];
+ slashify(sb, rnd);
+ sb.append(name);
+ }
+ slashify(sb, rnd);
+
+ String path = sb.toString();
+ scp.upload(localPath, path);
+ assertTrue("Remote file not ready for " + path, waitForFile(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L)));
+
+ byte[] actual = Files.readAllBytes(remoteFile);
+ assertArrayEquals("Mismatched uploaded data for " + path, data, actual);
+ Files.delete(remoteFile);
+ assertFalse("Remote file (" + remoteFile + ") not deleted for " + path, Files.exists(remoteFile));
+ }
+ }
+ }
+
+ private static int slashify(StringBuilder sb, Random rnd) {
+ int slashes = 1 /* at least one slash */ + rnd.random(Byte.SIZE);
+ for (int k = 0; k < slashes; k++) {
+ sb.append('/');
+ }
+
+ return slashes;
+ }
+
+ @Test
+ public void testUploadAbsoluteDriveLetter() throws Exception {
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path localFile = localDir.resolve("file-1.txt");
+ byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
+ String localPath = localFile.toString();
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(5L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ scp.upload(localPath, remotePath);
+ assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L));
+
+ Path secondRemote = remoteDir.resolve("file-2.txt");
+ String secondPath = Utils.resolveRelativeRemotePath(parentPath, secondRemote);
+ scp.upload(localPath, secondPath);
+ assertFileLength(secondRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
+
+ Path pathRemote = remoteDir.resolve("file-path.txt");
+ String pathPath = Utils.resolveRelativeRemotePath(parentPath, pathRemote);
+ scp.upload(localFile, pathPath);
+ assertFileLength(pathRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
+ }
+ }
+
+ @Test
+ public void testScpUploadOverwrite() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(5L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path localFile = localDir.resolve("file.txt");
+ Utils.writeFile(localFile, data);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remoteFile = remoteDir.resolve(localFile.getFileName());
+ Utils.writeFile(remoteFile, data + data);
+
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ scp.upload(localFile.toString(), remotePath);
+ assertFileLength(remoteFile, data.length(), TimeUnit.SECONDS.toMillis(5L));
+ }
+ }
+
+ @Test
+ public void testScpUploadZeroLengthFile() throws Exception {
+ Path targetPath = detectTargetFolder();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path zeroLocal = localDir.resolve("zero.txt");
+
+ try (FileChannel fch = FileChannel.open(zeroLocal, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+ if (fch.size() > 0L) {
+ fch.truncate(0L);
+ }
+ }
+ assertEquals("Non-zero size for local file=" + zeroLocal, 0L, Files.size(zeroLocal));
+
+ Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
+ if (Files.exists(zeroRemote)) {
+ Files.delete(zeroRemote);
+ }
+
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(5L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
+ scp.upload(zeroLocal.toString(), remotePath);
+ assertFileLength(zeroRemote, 0L, TimeUnit.SECONDS.toMillis(5L));
+ }
+ }
+
+ @Test
+ public void testScpDownloadZeroLengthFile() throws Exception {
+ Path targetPath = detectTargetFolder();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path zeroLocal = localDir.resolve(getCurrentTestName());
+ if (Files.exists(zeroLocal)) {
+ Files.delete(zeroLocal);
+ }
+
+ Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
+ try (FileChannel fch = FileChannel.open(zeroRemote, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+ if (fch.size() > 0L) {
+ fch.truncate(0L);
+ }
+ }
+ assertEquals("Non-zero size for remote file=" + zeroRemote, 0L, Files.size(zeroRemote));
+
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(5L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
+ scp.download(remotePath, zeroLocal.toString());
+ assertFileLength(zeroLocal, 0L, TimeUnit.SECONDS.toMillis(5L));
+ }
+ }
+
+ @Test
+ @Ignore("TODO investigate why this fails often")
+ public void testScpNativeOnSingleFile() throws Exception {
+ String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path localOutFile = localDir.resolve("file-1.txt");
+ Path remoteDir = scpRoot.resolve("remote");
+ Path remoteOutFile = remoteDir.resolve(localOutFile.getFileName());
+
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ Utils.writeFile(localOutFile, data);
+
+ assertFalse("Remote folder already exists: " + remoteDir, Files.exists(remoteDir));
+
+ String localOutPath = localOutFile.toString();
+ String remoteOutPath = Utils.resolveRelativeRemotePath(parentPath, remoteOutFile);
+ outputDebugMessage("Expect upload failure %s => %s", localOutPath, remoteOutPath);
+ try {
+ scp.upload(localOutPath, remoteOutPath);
+ fail("Expected IOException for 1st time " + remoteOutPath);
+ } catch (IOException e) {
+ // ok
+ }
+
+ assertHierarchyTargetFolderExists(remoteDir);
+ outputDebugMessage("Expect upload success %s => %s", localOutPath, remoteOutPath);
+ scp.upload(localOutPath, remoteOutPath);
+ assertFileLength(remoteOutFile, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+ Path secondLocal = localDir.resolve(localOutFile.getFileName());
+ String downloadTarget = Utils.resolveRelativeRemotePath(parentPath, secondLocal);
+ outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
+ scp.download(remoteOutPath, downloadTarget);
+ assertFileLength(secondLocal, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+ Path localPath = localDir.resolve("file-path.txt");
+ downloadTarget = Utils.resolveRelativeRemotePath(parentPath, localPath);
+ outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
+ scp.download(remoteOutPath, downloadTarget);
+ assertFileLength(localPath, data.length(), TimeUnit.SECONDS.toMillis(11L));
+ }
+ }
+
+ @Test
+ public void testScpNativeOnMultipleFiles() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path local1 = localDir.resolve("file-1.txt");
+ byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+ Path local2 = localDir.resolve("file-2.txt");
+ Files.write(local2, data);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remote1 = remoteDir.resolve(local1.getFileName());
+ String remote1Path = Utils.resolveRelativeRemotePath(parentPath, remote1);
+ String[] locals = {local1.toString(), local2.toString()};
+ try {
+ scp.upload(locals, remote1Path);
+ fail("Unexpected upload success to missing remote file: " + remote1Path);
+ } catch (IOException e) {
+ // Ok
+ }
+
+ Files.write(remote1, data);
+ try {
+ scp.upload(locals, remote1Path);
+ fail("Unexpected upload success to existing remote file: " + remote1Path);
+ } catch (IOException e) {
+ // Ok
+ }
+
+ Path remoteSubDir = assertHierarchyTargetFolderExists(remoteDir.resolve("dir"));
+ scp.upload(locals, Utils.resolveRelativeRemotePath(parentPath, remoteSubDir));
+
+ Path remoteSub1 = remoteSubDir.resolve(local1.getFileName());
+ assertFileLength(remoteSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ Path remoteSub2 = remoteSubDir.resolve(local2.getFileName());
+ assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ String[] remotes = {
+ Utils.resolveRelativeRemotePath(parentPath, remoteSub1),
+ Utils.resolveRelativeRemotePath(parentPath, remoteSub2),
+ };
+
+ try {
+ scp.download(remotes, Utils.resolveRelativeRemotePath(parentPath, local1));
+ fail("Unexpected download success to existing local file: " + local1);
+ } catch (IOException e) {
+ // Ok
+ }
+
+ Path localSubDir = localDir.resolve("dir");
+ try {
+ scp.download(remotes, localSubDir);
+ fail("Unexpected download success to non-existing folder: " + localSubDir);
+ } catch (IOException e) {
+ // Ok
+ }
+
+ assertHierarchyTargetFolderExists(localSubDir);
+ scp.download(remotes, localSubDir);
+
+ assertFileLength(localSubDir.resolve(remoteSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertFileLength(localSubDir.resolve(remoteSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+ }
+ }
+
+ @Test
+ public void testScpNativeOnRecursiveDirs() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = scpRoot.resolve("local");
+ Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
+ Path localSub1 = localSubDir.resolve("file-1.txt");
+ byte[] data = Utils.writeFile(localSub1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+ Path localSub2 = localSubDir.resolve("file-2.txt");
+ Files.write(localSub2, data);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ scp.upload(localSubDir, Utils.resolveRelativeRemotePath(parentPath, remoteDir), ScpClient.Option.Recursive);
+
+ Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
+ assertFileLength(remoteSubDir.resolve(localSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ Utils.deleteRecursive(localSubDir);
+
+ scp.download(Utils.resolveRelativeRemotePath(parentPath, remoteSubDir), localDir, ScpClient.Option.Recursive);
+ assertFileLength(localSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+ }
+ }
+
+ @Test
+ public void testScpNativeOnDirWithPattern() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path local1 = localDir.resolve("file-1.txt");
+ byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+ Path local2 = localDir.resolve("file-2.txt");
+ Files.write(local2, data);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+ scp.upload(localDir.toString() + File.separator + "*", remotePath);
+ assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertFileLength(remoteDir.resolve(local2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ Files.delete(local1);
+ Files.delete(local2);
+ scp.download(remotePath + "/*", localDir);
+ assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertFileLength(local2, data.length, TimeUnit.SECONDS.toMillis(11L));
+ }
+ }
+
+ @Test
+ public void testScpNativeOnMixedDirAndFiles() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = scpRoot.resolve("local");
+ Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
+ Path local1 = localDir.resolve("file-1.txt");
+ byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+ Path localSub2 = localSubDir.resolve("file-2.txt");
+ Files.write(localSub2, data);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+ scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive);
+ assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
+ assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ Files.delete(local1);
+ Utils.deleteRecursive(localSubDir);
+
+ scp.download(remotePath + "/*", localDir);
+ assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertFalse("Unexpected recursive local file: " + localSub2, Files.exists(localSub2));
+
+ Files.delete(local1);
+ scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive);
+ assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L));
+ assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(5L));
+ }
+ }
+
+ @Test
+ public void testScpNativePreserveAttributes() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = scpRoot.resolve("local");
+ Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
+ // convert everything to seconds since this is the SCP timestamps granularity
+ final long lastModMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
+ final long lastModSecs = TimeUnit.MILLISECONDS.toSeconds(lastModMillis);
+ Path local1 = localDir.resolve("file-1.txt");
+ byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
+
+ File lclFile1 = local1.toFile();
+ boolean lcl1ModSet = lclFile1.setLastModified(lastModMillis);
+ lclFile1.setExecutable(true, true);
+ lclFile1.setWritable(false, false);
+
+ Path localSub2 = localSubDir.resolve("file-2.txt");
+ Files.write(localSub2, data);
+ File lclSubFile2 = localSub2.toFile();
+ boolean lclSub2ModSet = lclSubFile2.setLastModified(lastModMillis);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+ scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+
+ Path remote1 = remoteDir.resolve(local1.getFileName());
+ assertFileLength(remote1, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ File remFile1 = remote1.toFile();
+ assertLastModifiedTimeEquals(remFile1, lcl1ModSet, lastModSecs);
+
+ Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
+ Path remoteSub2 = remoteSubDir.resolve(localSub2.getFileName());
+ assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ File remSubFile2 = remoteSub2.toFile();
+ assertLastModifiedTimeEquals(remSubFile2, lclSub2ModSet, lastModSecs);
+
+ Utils.deleteRecursive(localDir);
+ assertHierarchyTargetFolderExists(localDir);
+
+ scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
+ assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertLastModifiedTimeEquals(lclFile1, lcl1ModSet, lastModSecs);
+ assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
+ assertLastModifiedTimeEquals(lclSubFile2, lclSub2ModSet, lastModSecs);
+ }
+ }
+
+ @Test
+ public void testStreamsUploadAndDownload() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClient scp = createScpClient(session);
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remoteFile = remoteDir.resolve("file.txt");
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+ outputDebugMessage("Upload data to %s", remotePath);
+ scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
+ assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ byte[] uploaded = Files.readAllBytes(remoteFile);
+ assertArrayEquals("Mismatched uploaded data", data, uploaded);
+
+ outputDebugMessage("Download data from %s", remotePath);
+ byte[] downloaded = scp.downloadBytes(remotePath);
+ assertArrayEquals("Mismatched downloaded data", uploaded, downloaded);
+ }
+ }
+
+ @Test // see SSHD-649
+ public void testScpFileOpener() throws Exception {
+ class TrackingFileOpener extends DefaultScpFileOpener {
+ private final AtomicInteger readCount = new AtomicInteger(0);
+ private final AtomicInteger writeCount = new AtomicInteger(0);
+
+ TrackingFileOpener() {
+ super();
+ }
+
+ public AtomicInteger getReadCount() {
+ return readCount;
+ }
+
+ public AtomicInteger getWriteCount() {
+ return writeCount;
+ }
+
+ @Override
+ public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
+ int count = readCount.incrementAndGet();
+ outputDebugMessage("openRead(%s)[%s] count=%d", session, file, count);
+ return super.openRead(session, file, options);
+ }
+
+ @Override
+ public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
+ int count = writeCount.incrementAndGet();
+ outputDebugMessage("openWrite(%s)[%s] count=%d", session, file, count);
+ return super.openWrite(session, file, options);
+ }
+ }
+
+ ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
+ ScpFileOpener opener = factory.getScpFileOpener();
+ TrackingFileOpener serverOpener = new TrackingFileOpener();
+ factory.setScpFileOpener(serverOpener);
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ TrackingFileOpener clientOpener = new TrackingFileOpener();
+ ScpClientCreator creator = ScpClientCreator.instance();
+ ScpClient scp = creator.createScpClient(session, clientOpener);
+
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot);
+ Path localFile = remoteDir.resolve("data.txt");
+ byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+ Files.write(localFile, data);
+
+ Path remoteFile = remoteDir.resolve("upload.txt");
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ outputDebugMessage("Upload data to %s", remotePath);
+ scp.upload(localFile, remotePath);
+ assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ AtomicInteger serverRead = serverOpener.getReadCount();
+ assertEquals("Mismatched server upload open read count", 0, serverRead.get());
+
+ AtomicInteger serverWrite = serverOpener.getWriteCount();
+ assertEquals("Mismatched server upload write count", 1, serverWrite.getAndSet(0));
+
+ AtomicInteger clientRead = clientOpener.getReadCount();
+ assertEquals("Mismatched client upload read count", 1, clientRead.getAndSet(0));
+
+ AtomicInteger clientWrite = clientOpener.getWriteCount();
+ assertEquals("Mismatched client upload write count", 0, clientWrite.get());
+
+ Files.delete(localFile);
+ scp.download(remotePath, localFile);
+ assertFileLength(localFile, data.length, TimeUnit.SECONDS.toMillis(11L));
+
+ assertEquals("Mismatched server download open read count", 1, serverRead.getAndSet(0));
+ assertEquals("Mismatched server download write count", 0, serverWrite.get());
+ assertEquals("Mismatched client download read count", 0, clientRead.get());
+ assertEquals("Mismatched client download write count", 1, clientWrite.getAndSet(0));
+ } finally {
+ factory.setScpFileOpener(opener);
+ }
+ }
+
+ @Test // see SSHD-628
+ public void testScpExitStatusPropagation() throws Exception {
+ final int testExitValue = 7365;
+ class InternalScpCommand extends ScpCommand implements ExitCallback {
+ private ExitCallback delegate;
+
+ InternalScpCommand(String command, ExecutorService executorService, boolean shutdownOnExit,
+ int sendSize, int receiveSize, ScpFileOpener opener, ScpTransferEventListener eventListener) {
+ super(command, executorService, shutdownOnExit, sendSize, receiveSize, opener, eventListener);
+ }
+
+ @Override
+ protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
+ outputDebugMessage("writeCommandResponseMessage(%s) status=%d", command, exitValue);
+ super.writeCommandResponseMessage(command, testExitValue, exitMessage);
+ }
+
+ @Override
+ public void setExitCallback(ExitCallback callback) {
+ delegate = callback;
+ super.setExitCallback(this);
+ }
+
+ @Override
+ public void onExit(int exitValue) {
+ onExit(exitValue, Integer.toString(exitValue));
+ }
+
+ @Override
+ public void onExit(int exitValue, String exitMessage) {
+ outputDebugMessage("onExit(%s) status=%d", this, exitValue);
+ if (exitValue == ScpHelper.OK) {
+ delegate.onExit(testExitValue, exitMessage);
+ } else {
+ delegate.onExit(exitValue, exitMessage);
+ }
+ }
+ }
+
+ ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
+ sshd.setCommandFactory(new ScpCommandFactory() {
+ @Override
+ public Command createCommand(String command) {
+ ValidateUtils.checkTrue(command.startsWith(ScpHelper.SCP_COMMAND_PREFIX), "Bad SCP command: %s", command);
+ return new InternalScpCommand(command,
+ getExecutorService(), isShutdownOnExit(),
+ getSendBufferSize(), getReceiveBufferSize(),
+ DefaultScpFileOpener.INSTANCE,
+ ScpTransferEventListener.EMPTY);
+ }
+ });
+
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ ScpClientCreator creator = ScpClientCreator.instance();
+ ScpClient scp = creator.createScpClient(session);
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remoteFile = remoteDir.resolve("file.txt");
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+ outputDebugMessage("Upload data to %s", remotePath);
+ try {
+ scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
+ outputDebugMessage("Upload success to %s", remotePath);
+ } catch (ScpException e) {
+ Integer exitCode = e.getExitStatus();
+ assertNotNull("No upload exit status", exitCode);
+ assertEquals("Mismatched upload exit status", testExitValue, exitCode.intValue());
+ }
+
+ if (Files.deleteIfExists(remoteFile)) {
+ outputDebugMessage("Deleted remote file %s", remoteFile);
+ }
+
+ try (OutputStream out = Files.newOutputStream(remoteFile)) {
+ out.write(data);
+ }
+
+ try {
+ byte[] downloaded = scp.downloadBytes(remotePath);
+ outputDebugMessage("Download success to %s: %s", remotePath, new String(downloaded, StandardCharsets.UTF_8));
+ } catch (ScpException e) {
+ Integer exitCode = e.getExitStatus();
+ assertNotNull("No download exit status", exitCode);
+ assertEquals("Mismatched download exit status", testExitValue, exitCode.intValue());
+ }
+ } finally {
+ sshd.setCommandFactory(factory);
+ }
+ }
+
+ // see http://stackoverflow.com/questions/2717936/file-createnewfile-creates-files-with-last-modified-time-before-actual-creatio
+ // See https://msdn.microsoft.com/en-us/library/ms724290(VS.85).aspx
+ private static void assertLastModifiedTimeEquals(File file, boolean modSuccess, long expectedSeconds) {
+ long expectedMillis = TimeUnit.SECONDS.toMillis(expectedSeconds);
+ long actualMillis = file.lastModified();
+ long actualSeconds = TimeUnit.MILLISECONDS.toSeconds(actualMillis);
+ // if failed to set the local file time, don't expect it to be the same
+ if (!modSuccess) {
+ System.err.append("Failed to set last modified time of ").append(file.getAbsolutePath())
+ .append(" to ").append(String.valueOf(expectedMillis))
+ .append(" - ").println(new Date(expectedMillis));
+ System.err.append("\t\t").append("Current value: ").append(String.valueOf(actualMillis))
+ .append(" - ").println(new Date(actualMillis));
+ return;
+ }
+
+ if (OsUtils.isWin32()) {
+ // The NTFS file system delays updates to the last access time for a file by up to 1 hour after the last access
+ if (expectedSeconds != actualSeconds) {
+ System.err.append("Mismatched last modified time for ").append(file.getAbsolutePath())
+ .append(" - expected=").append(String.valueOf(expectedSeconds))
+ .append('[').append(new Date(expectedMillis).toString()).append(']')
+ .append(", actual=").append(String.valueOf(actualSeconds))
+ .append('[').append(new Date(actualMillis).toString()).append(']')
+ .println();
+ }
+ } else {
+ assertEquals("Mismatched last modified time for " + file.getAbsolutePath(), expectedSeconds, actualSeconds);
+ }
+ }
+
+ @Test
+ public void testJschScp() throws Exception {
+ com.jcraft.jsch.Session session = getJschSession();
+ try {
+ String data = getCurrentTestName() + "\n";
+
+ String unixDir = "target/scp";
+ String fileName = getCurrentTestName() + ".txt";
+ String unixPath = unixDir + "/" + fileName;
+ File root = new File(unixDir);
+ File target = new File(unixPath);
+ Utils.deleteRecursive(root);
+ root.mkdirs();
+ assertTrue("Failed to ensure existence of " + root, root.exists());
+
+ target.delete();
+ assertFalse("Failed to delete 1st time: " + target, target.exists());
+ sendFile(session, unixPath, target, data);
+ assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+ target.delete();
+ assertFalse("Failed to delete 2nd time: " + target, target.exists());
+ sendFile(session, unixDir, target, data);
+ assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
+
+ sendFileError(session, "target", ScpHelper.SCP_COMMAND_PREFIX, data);
+
+ readFileError(session, unixDir);
+
+ assertEquals("Mismatched file data", data, readFile(session, unixPath, target));
+ assertEquals("Mismatched dir data", data, readDir(session, unixDir, target));
+
+ target.delete();
+ root.delete();
+
+ sendDir(session, "target", ScpHelper.SCP_COMMAND_PREFIX, fileName, data);
+ assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
+ } finally {
+ session.disconnect();
+ }
+ }
+
+ protected com.jcraft.jsch.Session getJschSession() throws JSchException {
+ JSch sch = new JSch();
+ com.jcraft.jsch.Session session = sch.getSession(getCurrentTestName(), TEST_LOCALHOST, port);
+ session.setUserInfo(new SimpleUserInfo(getCurrentTestName()));
+ session.connect();
+ return session;
+ }
+
+ @Test
+ public void testWithGanymede() throws Exception {
+ Path targetPath = detectTargetFolder();
+ Path parentPath = targetPath.getParent();
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ byte[] expected = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
+ String fileName = "file.txt";
+ Path remoteFile = remoteDir.resolve(fileName);
+ String mode = ScpHelper.getOctalPermissions(EnumSet.of(
+ PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE,
+ PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
+ PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE
+ ));
+
+ ch.ethz.ssh2.log.Logger.enabled = true;
+ Connection conn = new Connection(TEST_LOCALHOST, port);
+ try {
+ ConnectionInfo info = conn.connect(null, (int) TimeUnit.SECONDS.toMillis(5L), (int) TimeUnit.SECONDS.toMillis(13L));
+ outputDebugMessage("Connected: kex=%s, key-type=%s, c2senc=%s, s2cenc=%s, c2mac=%s, s2cmac=%s",
+ info.keyExchangeAlgorithm, info.serverHostKeyAlgorithm,
+ info.clientToServerCryptoAlgorithm, info.serverToClientCryptoAlgorithm,
+ info.clientToServerMACAlgorithm, info.serverToClientMACAlgorithm);
+ assertTrue("Failed to authenticate", conn.authenticateWithPassword(getCurrentTestName(), getCurrentTestName()));
+
+ SCPClient scpClient = new SCPClient(conn);
+ try (OutputStream output = scpClient.put(fileName, expected.length, remotePath, mode)) {
+ output.write(expected);
+ }
+
+ assertTrue("Remote file not created: " + remoteFile, Files.exists(remoteFile));
+ byte[] remoteData = Files.readAllBytes(remoteFile);
+ assertArrayEquals("Mismatched remote put data", expected, remoteData);
+
+ Arrays.fill(remoteData, (byte) 0); // make sure we start with a clean slate
+ try (InputStream input = scpClient.get(remotePath + "/" + fileName)) {
+ int readLen = input.read(remoteData);
+ assertEquals("Mismatched remote get data size", expected.length, readLen);
+ // make sure we reached EOF
+ assertEquals("Unexpected extra data after read expected size", -1, input.read());
+ }
+
+ assertArrayEquals("Mismatched remote get data", expected, remoteData);
+ } finally {
+ conn.close();
+ }
+ }
+
+ protected String readFile(com.jcraft.jsch.Session session, String path, File target) throws Exception {
+ ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+ c.setCommand("scp -f " + path);
+ c.connect();
+
+ String fileName = target.getName();
+ try (OutputStream os = c.getOutputStream();
+ InputStream is = c.getInputStream()) {
+
+ os.write(0);
+ os.flush();
+
+ String header = readLine(is);
+ String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + fileName;
+ assertEquals("Mismatched header for " + path, expHeader, header);
+
+ String lenValue = header.substring(6, header.indexOf(' ', 6));
+ int length = Integer.parseInt(lenValue);
+ os.write(0);
+ os.flush();
+
+ byte[] buffer = new byte[length];
+ length = is.read(buffer, 0, buffer.length);
+ assertEquals("Mismatched read data length for " + path, length, buffer.length);
+ assertAckReceived(is, "Read data of " + path);
+
+ os.write(0);
+ os.flush();
+
+ return new String(buffer, StandardCharsets.UTF_8);
+ } finally {
+ c.disconnect();
+ }
+ }
+
+ protected String readDir(com.jcraft.jsch.Session session, String path, File target) throws Exception {
+ ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+ c.setCommand("scp -r -f " + path);
+ c.connect();
+
+ try (OutputStream os = c.getOutputStream();
+ InputStream is = c.getInputStream()) {
+ os.write(0);
+ os.flush();
+
+ String header = readLine(is);
+ String expPrefix = "D" + ScpHelper.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 ";
+ assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith(expPrefix));
+ os.write(0);
+ os.flush();
+
+ header = readLine(is);
+ String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + target.getName();
+ assertEquals("Mismatched dir header for " + path, expHeader, header);
+ int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
+ os.write(0);
+ os.flush();
+
+ byte[] buffer = new byte[length];
+ length = is.read(buffer, 0, buffer.length);
+ assertEquals("Mismatched read buffer size for " + path, length, buffer.length);
+ assertAckReceived(is, "Read date of " + path);
+
+ os.write(0);
+ os.flush();
+
+ header = readLine(is);
+ assertEquals("Mismatched end value for " + path, "E", header);
+ os.write(0);
+ os.flush();
+
+ return new String(buffer, StandardCharsets.UTF_8);
+ } finally {
+ c.disconnect();
+ }
+ }
+
+ protected void readFileError(com.jcraft.jsch.Session session, String path) throws Exception {
+ ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+ String command = "scp -f " + path;
+ c.setCommand(command);
+ c.connect();
+
+ try (OutputStream os = c.getOutputStream();
+ InputStream is = c.getInputStream()) {
+
+ os.write(0);
+ os.flush();
+ assertEquals("Mismatched response for command: " + command, 2, is.read());
+ } finally {
+ c.disconnect();
+ }
+ }
+
+ protected void sendFile(com.jcraft.jsch.Session session, String path, File target, String data) throws Exception {
+ ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+ String command = "scp -t " + path;
+ c.setCommand(command);
+ c.connect();
+
+ try (OutputStream os = c.getOutputStream();
+ InputStream is = c.getInputStream()) {
+
+ assertAckReceived(is, command);
+
+ File parent = target.getParentFile();
+ Collection<PosixFilePermission> perms = IoUtils.getPermissions(parent.toPath());
+ String octalPerms = ScpHelper.getOctalPermissions(perms);
+ String name = target.getName();
+ assertAckReceived(os, is, "C" + octalPerms + " " + data.length() + " " + name);
+
+ os.write(data.getBytes(StandardCharsets.UTF_8));
+ os.flush();
+ assertAckReceived(is, "Sent data (length=" + data.length() + ") for " + path + "[" + name + "]");
+
+ os.write(0);
+ os.flush();
+
+ Thread.sleep(100);
+ } finally {
+ c.disconnect();
+ }
+ }
+
+ protected void assertAckReceived(OutputStream os, InputStream is, String command) throws IOException {
+ os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
+ os.flush();
+ assertAckReceived(is, command);
+ }
+
+ protected void assertAckReceived(InputStream is, String command) throws IOException {
+ assertEquals("No ACK for command=" + command, 0, is.read());
+ }
+
+ protected void sendFileError(com.jcraft.jsch.Session session, String path, String name, String data) throws Exception {
+ ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+ String command = "scp -t " + path;
+ c.setCommand(command);
+ c.connect();
+
+ try (OutputStream os = c.getOutputStream();
+ InputStream is = c.getInputStream()) {
+
+ assertAckReceived(is, command);
+
+ command = "C7777 " + data.length() + " " + name;
+ os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
+ os.flush();
+ assertEquals("Mismatched response for command=" + command, 2, is.read());
+ } finally {
+ c.disconnect();
+ }
+ }
+
+ protected void sendDir(com.jcraft.jsch.Session session, String path, String dirName, String fileName, String data) throws Exception {
+ ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
+ String command = "scp -t -r " + path;
+ c.setCommand(command);
+ c.connect();
+
+ try (OutputStream os = c.getOutputStream();
+ InputStream is = c.getInputStream()) {
+
+ assertAckReceived(is, command);
+ assertAckReceived(os, is, "D0755 0 " + dirName);
+ assertAckReceived(os, is, "C7777 " + data.length() + " " + fileName);
+
+ os.write(data.getBytes(StandardCharsets.UTF_8));
+ os.flush();
+ assertAckReceived(is, "Send data of " + path);
+
+ os.write(0);
+ os.flush();
+
+ os.write("E\n".getBytes(StandardCharsets.UTF_8));
+ os.flush();
+ assertAckReceived(is, "Signal end of " + path);
+ } finally {
+ c.disconnect();
+ }
+ }
+
+ private static String readLine(InputStream in) throws IOException {
+ try (OutputStream baos = new ByteArrayOutputStream()) {
+ for (;;) {
+ int c = in.read();
+ if (c == '\n') {
+ return baos.toString();
+ } else if (c == -1) {
+ throw new IOException("End of stream");
+ } else {
+ baos.write(c);
+ }
+ }
+ }
+ }
+
+ private static ScpClient createScpClient(ClientSession session) {
+ ScpClientCreator creator = ScpClientCreator.instance();
+ ScpTransferEventListener listener = getScpTransferEventListener(session);
+ return creator.createScpClient(session, listener);
+ }
+
+ private static ScpTransferEventListener getScpTransferEventListener(ClientSession session) {
+ return OUTPUT_DEBUG_MESSAGES ? DEBUG_LISTENER : ScpTransferEventListener.EMPTY;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java b/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java
new file mode 100644
index 0000000..d9b9b08
--- /dev/null
+++ b/sshd-scp/src/test/java/org/apache/sshd/client/scp/SimpleScpClientTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.scp;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.util.test.Utils;
+import org.apache.sshd.util.test.client.simple.BaseSimpleClientTestSupport;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SimpleScpClientTest extends BaseSimpleClientTestSupport {
+ private final Path targetPath;
+ private final Path parentPath;
+ private final FileSystemFactory fileSystemFactory;
+ private SimpleScpClient scpClient;
+
+ public SimpleScpClientTest() throws Exception {
+ targetPath = detectTargetFolder();
+ parentPath = targetPath.getParent();
+ fileSystemFactory = new VirtualFileSystemFactory(parentPath);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ sshd.setCommandFactory(new ScpCommandFactory());
+ sshd.setFileSystemFactory(fileSystemFactory);
+ client.start();
+ scpClient = new SimpleScpClientImpl(simple);
+ }
+
+ @Test
+ public void testSessionClosedWhenClientClosed() throws Exception {
+ try (CloseableScpClient scp = login()) {
+ assertTrue("SCP not open", scp.isOpen());
+
+ Session session = scp.getClientSession();
+ assertTrue("Session not open", session.isOpen());
+
+ scp.close();
+ assertFalse("Session not closed", session.isOpen());
+ assertFalse("SCP not closed", scp.isOpen());
+ }
+ }
+
+ @Test
+ public void testScpUploadProxy() throws Exception {
+ try (CloseableScpClient scp = login()) {
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path localFile = localDir.resolve("file.txt");
+ String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+ byte[] written = Utils.writeFile(localFile, data);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remoteFile = remoteDir.resolve(localFile.getFileName());
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ scp.upload(localFile, remotePath);
+
+ byte[] uploaded = Files.readAllBytes(remoteFile);
+ assertArrayEquals("Mismatched uploaded data", written, uploaded);
+ }
+ }
+
+ @Test
+ public void testScpDownloadProxy() throws Exception {
+ try (CloseableScpClient scp = login()) {
+ Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Utils.deleteRecursive(scpRoot);
+
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path remoteFile = remoteDir.resolve("file.txt");
+ String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
+ byte[] written = Utils.writeFile(remoteFile, data);
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path localFile = localDir.resolve(remoteFile.getFileName());
+ String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+ scp.download(remotePath, localFile);
+
+ byte[] downloaded = Files.readAllBytes(localFile);
+ assertArrayEquals("Mismatched downloaded data", written, downloaded);
+ }
+ }
+
+ private CloseableScpClient login() throws IOException {
+ return scpClient.scpLogin(TEST_LOCALHOST, port, getCurrentTestName(), getCurrentTestName());
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java b/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java
new file mode 100644
index 0000000..0cd1e91
--- /dev/null
+++ b/sshd-scp/src/test/java/org/apache/sshd/server/scp/ScpCommandFactoryTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.server.scp;
+
+import java.util.concurrent.ExecutorService;
+
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.server.CommandFactory;
+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.Mockito;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+public class ScpCommandFactoryTest extends BaseTestSupport {
+ public ScpCommandFactoryTest() {
+ super();
+ }
+
+ /**
+ * Make sure that the builder returns a factory with the default values
+ * if no {@code withXXX} method is invoked
+ */
+ @Test
+ public void testBuilderDefaultFactoryValues() {
+ ScpCommandFactory factory = new ScpCommandFactory.Builder().build();
+ assertNull("Mismatched delegate", factory.getDelegateCommandFactory());
+ assertNull("Mismatched executor", factory.getExecutorService());
+ assertEquals("Mismatched send size", ScpHelper.MIN_SEND_BUFFER_SIZE, factory.getSendBufferSize());
+ assertEquals("Mismatched receive size", ScpHelper.MIN_RECEIVE_BUFFER_SIZE, factory.getReceiveBufferSize());
+ assertFalse("Mismatched shutdown state", factory.isShutdownOnExit());
+ }
+
+ /**
+ * Make sure that the builder initializes correctly the built factory
+ */
+ @Test
+ public void testBuilderCorrectlyInitializesFactory() {
+ CommandFactory delegate = dummyFactory();
+ ExecutorService service = dummyExecutor();
+ int receiveSize = Short.MAX_VALUE;
+ int sendSize = receiveSize + Long.SIZE;
+ ScpCommandFactory factory = new ScpCommandFactory.Builder()
+ .withDelegate(delegate)
+ .withExecutorService(service)
+ .withSendBufferSize(sendSize)
+ .withReceiveBufferSize(receiveSize)
+ .withShutdownOnExit(true)
+ .build();
+ assertSame("Mismatched delegate", delegate, factory.getDelegateCommandFactory());
+ assertSame("Mismatched executor", service, factory.getExecutorService());
+ assertEquals("Mismatched send size", sendSize, factory.getSendBufferSize());
+ assertEquals("Mismatched receive size", receiveSize, factory.getReceiveBufferSize());
+ assertTrue("Mismatched shutdown state", factory.isShutdownOnExit());
+ }
+
+ /**
+ * <UL>
+ * <LI>
+ * Make sure the builder returns new instances on every call to
+ * {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
+ * </LI>
+ *
+ * <LI>
+ * Make sure values are preserved between successive invocations
+ * of the {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
+ * </LI>
+ * </UL
+ */
+ @Test
+ public void testBuilderUniqueInstance() {
+ ScpCommandFactory.Builder builder = new ScpCommandFactory.Builder();
+ ScpCommandFactory f1 = builder.withDelegate(dummyFactory()).build();
+ ScpCommandFactory f2 = builder.build();
+ assertNotSame("No new instance built", f1, f2);
+ assertSame("Mismatched delegate", f1.getDelegateCommandFactory(), f2.getDelegateCommandFactory());
+
+ ScpCommandFactory f3 = builder.withDelegate(dummyFactory()).build();
+ assertNotSame("Delegate not changed", f1.getDelegateCommandFactory(), f3.getDelegateCommandFactory());
+ }
+
+ private static ExecutorService dummyExecutor() {
+ return Mockito.mock(ExecutorService.class);
+ }
+
+ private static CommandFactory dummyFactory() {
+ return Mockito.mock(CommandFactory.class);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java
----------------------------------------------------------------------
diff --git a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java b/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java
deleted file mode 100644
index 1034046..0000000
--- a/sshd-sftp/src/main/java/org/apache/sshd/client/simple/SimpleSftpClient.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-import java.nio.channels.Channel;
-import java.security.KeyPair;
-import java.util.Objects;
-
-import org.apache.sshd.client.subsystem.sftp.SftpClient;
-import org.apache.sshd.common.util.ValidateUtils;
-
-/**
- * A simplified <U>synchronous</U> API for obtaining SFTP sessions.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface SimpleSftpClient extends Channel {
- /**
- * Creates an SFTP session on the default port and logs in using the provided credentials
- *
- * @param host The target host name or address
- * @param username Username
- * @param password Password
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(String host, String username, String password) throws IOException {
- return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
- }
-
- /**
- * Creates an SFTP session using the provided credentials
- *
- * @param host The target host name or address
- * @param port The target port
- * @param username Username
- * @param password Password
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(String host, int port, String username, String password) throws IOException {
- return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
- }
-
- /**
- * Creates an SFTP session on the default port and logs in using the provided credentials
- *
- * @param host The target host name or address
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(String host, String username, KeyPair identity) throws IOException {
- return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
- }
-
- /**
- * Creates an SFTP session using the provided credentials
- *
- * @param host The target host name or address
- * @param port The target port
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(String host, int port, String username, KeyPair identity) throws IOException {
- return sftpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
- }
-
- /**
- * Creates an SFTP session on the default port and logs in using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param username Username
- * @param password Password
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(InetAddress host, String username, String password) throws IOException {
- return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
- }
-
- /**
- * Creates an SFTP session using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param port The target port
- * @param username Username
- * @param password Password
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(InetAddress host, int port, String username, String password) throws IOException {
- return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, password);
- }
-
- /**
- * Creates an SFTP session on the default port and logs in using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
- return sftpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
- }
-
- /**
- * Creates an SFTP session using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param port The target port
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- default SftpClient sftpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException {
- return sftpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, identity);
- }
-
- /**
- * Creates an SFTP session using the provided credentials
- *
- * @param target The target {@link SocketAddress}
- * @param username Username
- * @param password Password
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- SftpClient sftpLogin(SocketAddress target, String username, String password) throws IOException;
-
- /**
- * Creates an SFTP session using the provided credentials
- *
- * @param target The target {@link SocketAddress}
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link SftpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- SftpClient sftpLogin(SocketAddress target, String username, KeyPair identity) throws IOException;
-
-}
[6/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
deleted file mode 100644
index 6b57443..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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.common.scp.helpers;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.StreamCorruptedException;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributeView;
-import java.nio.file.attribute.FileTime;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpTargetStreamResolver;
-import org.apache.sshd.common.scp.ScpTimestamp;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class LocalFileScpTargetStreamResolver extends AbstractLoggingBean implements ScpTargetStreamResolver {
- protected final Path path;
- protected final ScpFileOpener opener;
- protected final Boolean status;
- private Path file;
-
- public LocalFileScpTargetStreamResolver(Path path, ScpFileOpener opener) throws IOException {
- LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
- this.status = IoUtils.checkFileExists(path, linkOptions);
- if (status == null) {
- throw new AccessDeniedException("Receive target file path existence status cannot be determined: " + path);
- }
-
- this.path = path;
- this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
- }
-
- @Override
- public OutputStream resolveTargetStream(Session session, String name, long length,
- Set<PosixFilePermission> perms, OpenOption... options) throws IOException {
- if (file != null) {
- throw new StreamCorruptedException("resolveTargetStream(" + name + ")[" + perms + "] already resolved: " + file);
- }
-
- LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
- if (status && Files.isDirectory(path, linkOptions)) {
- String localName = name.replace('/', File.separatorChar); // in case we are running on Windows
- file = path.resolve(localName);
- } else if (status && Files.isRegularFile(path, linkOptions)) {
- file = path;
- } else if (!status) {
- Path parent = path.getParent();
-
- Boolean parentStatus = IoUtils.checkFileExists(parent, linkOptions);
- if (parentStatus == null) {
- throw new AccessDeniedException("Receive file parent (" + parent + ") existence status cannot be determined for " + path);
- }
-
- if (parentStatus && Files.isDirectory(parent, linkOptions)) {
- file = path;
- }
- }
-
- if (file == null) {
- throw new IOException("Can not write to " + path);
- }
-
- Boolean fileStatus = IoUtils.checkFileExists(file, linkOptions);
- if (fileStatus == null) {
- throw new AccessDeniedException("Receive file existence status cannot be determined: " + file);
- }
-
- if (fileStatus) {
- if (Files.isDirectory(file, linkOptions)) {
- throw new IOException("File is a directory: " + file);
- }
-
- if (!Files.isWritable(file)) {
- throw new IOException("Can not write to file: " + file);
- }
- }
-
- if (log.isTraceEnabled()) {
- log.trace("resolveTargetStream(" + name + "): " + file);
- }
-
- return opener.openWrite(session, file, options);
- }
-
- @Override
- public Path getEventListenerFilePath() {
- if (file == null) {
- return path;
- } else {
- return file;
- }
- }
-
- @Override
- public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
- if (file == null) {
- throw new StreamCorruptedException("postProcessReceivedData(" + name + ")[" + perms + "] No currently resolved data");
- }
-
- if (preserve) {
- updateFileProperties(name, file, perms, time);
- }
- }
-
- protected void updateFileProperties(String name, Path path, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
- boolean traceEnabled = log.isTraceEnabled();
- if (traceEnabled) {
- log.trace("updateFileProperties(" + name + ")[" + path + "] permissions: " + perms);
- }
- IoUtils.setPermissions(path, perms);
-
- if (time != null) {
- BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class);
- FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS);
- FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS);
- if (traceEnabled) {
- log.trace("updateFileProperties(" + name + ")[" + path + "] last-modified=" + lastModified + ", last-access=" + lastAccess);
- }
-
- view.setTimes(lastModified, lastAccess, null);
- }
- }
-
- @Override
- public String toString() {
- return String.valueOf(getEventListenerFilePath());
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java
new file mode 100644
index 0000000..a55ac2e
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/functors/IOFunction.java
@@ -0,0 +1,86 @@
+/*
+ * 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.common.util.io.functors;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Invokes some I/O function on the input returning some output
+ * and potentially throwing an {@link IOException} in the process
+ *
+ * @param <T> Type of input
+ * @param <R> Type of output
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface IOFunction<T, R> {
+ R apply(T t) throws IOException;
+
+ /**
+ * Returns a composed function that first applies the {@code before}
+ * function to its input, and then applies this function to the result.
+ * If evaluation of either function throws an exception, it is relayed to
+ * the caller of the composed function.
+ *
+ * @param <V> the type of input to the {@code before} function, and to the
+ * composed function
+ * @param before the function to apply before this function is applied
+ * @return a composed function that first applies the {@code before}
+ * function and then applies this function
+ * @throws NullPointerException if before is null
+ *
+ * @see #andThen(IOFunction)
+ */
+ default <V> IOFunction<V, R> compose(IOFunction<? super V, ? extends T> before) {
+ Objects.requireNonNull(before, "No composing function provided");
+ return (V v) -> apply(before.apply(v));
+ }
+
+ /**
+ * Returns a composed function that first applies this function to
+ * its input, and then applies the {@code after} function to the result.
+ * If evaluation of either function throws an exception, it is relayed to
+ * the caller of the composed function.
+ *
+ * @param <V> the type of output of the {@code after} function, and of the
+ * composed function
+ * @param after the function to apply after this function is applied
+ * @return a composed function that first applies this function and then
+ * applies the {@code after} function
+ * @throws NullPointerException if after is null
+ *
+ * @see #compose(IOFunction)
+ */
+ default <V> IOFunction<T, V> andThen(IOFunction<? super R, ? extends V> after) {
+ Objects.requireNonNull(after, "No composing function provided");
+ return (T t) -> after.apply(apply(t));
+ }
+
+ /**
+ * Returns a function that always returns its input argument.
+ *
+ * @param <T> the type of the input and output objects to the function
+ * @return a function that always returns its input argument
+ */
+ static <T> IOFunction<T, T> identity() {
+ return t -> t;
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
deleted file mode 100644
index e80f791..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
+++ /dev/null
@@ -1,350 +0,0 @@
-/*
- * 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.server.scp;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.FileSystem;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-import org.apache.sshd.common.file.FileSystemAware;
-import org.apache.sshd.common.scp.ScpException;
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionHolder;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
-import org.apache.sshd.common.util.threads.ThreadUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SessionAware;
-import org.apache.sshd.server.session.ServerSession;
-import org.apache.sshd.server.session.ServerSessionHolder;
-
-/**
- * This commands provide SCP support on both server and client side.
- * Permissions and preservation of access / modification times on files
- * are not supported.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ScpCommand
- extends AbstractLoggingBean
- implements Command, Runnable, FileSystemAware, SessionAware,
- SessionHolder<Session>, ServerSessionHolder, ExecutorServiceCarrier {
-
- protected final String name;
- protected final int sendBufferSize;
- protected final int receiveBufferSize;
- protected final ScpFileOpener opener;
- protected boolean optR;
- protected boolean optT;
- protected boolean optF;
- protected boolean optD;
- protected boolean optP; // TODO: handle modification times
- protected FileSystem fileSystem;
- protected String path;
- protected InputStream in;
- protected OutputStream out;
- protected OutputStream err;
- protected ExitCallback callback;
- protected IOException error;
- protected Future<?> pendingFuture;
- protected ScpTransferEventListener listener;
- protected ServerSession serverSession;
-
- private ExecutorService executorService;
- private boolean shutdownOnExit;
-
- /**
- * @param command The command to be executed
- * @param executorService An {@link ExecutorService} to be used when
- * {@link #start(Environment)}-ing execution. If {@code null} an ad-hoc
- * single-threaded service is created and used.
- * @param shutdownOnExit If {@code true} the {@link ExecutorService#shutdownNow()}
- * will be called when command terminates - unless it is the ad-hoc
- * service, which will be shutdown regardless
- * @param sendSize Size (in bytes) of buffer to use when sending files
- * @param receiveSize Size (in bytes) of buffer to use when receiving files
- * @param fileOpener The {@link ScpFileOpener} - if {@code null} then {@link DefaultScpFileOpener} is used
- * @param eventListener An {@link ScpTransferEventListener} - may be {@code null}
- * @see ThreadUtils#newSingleThreadExecutor(String)
- * @see ScpHelper#MIN_SEND_BUFFER_SIZE
- * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
- */
- public ScpCommand(String command,
- ExecutorService executorService, boolean shutdownOnExit,
- int sendSize, int receiveSize,
- ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
- name = command;
-
- if (executorService == null) {
- String poolName = command.replace(' ', '_').replace('/', ':');
- this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
- this.shutdownOnExit = true; // we always close the ad-hoc executor service
- } else {
- this.executorService = executorService;
- this.shutdownOnExit = shutdownOnExit;
- }
-
- if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
- throw new IllegalArgumentException("<ScpCommmand>(" + command + ") send buffer size "
- + "(" + sendSize + ") below minimum required "
- + "(" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
- }
- sendBufferSize = sendSize;
-
- if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
- throw new IllegalArgumentException("<ScpCommmand>(" + command + ") receive buffer size "
- + "(" + sendSize + ") below minimum required "
- + "(" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
- }
- receiveBufferSize = receiveSize;
-
- opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
- listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
-
- boolean debugEnabled = log.isDebugEnabled();
- if (debugEnabled) {
- log.debug("Executing command {}", command);
- }
-
- String[] args = GenericUtils.split(command, ' ');
- int numArgs = GenericUtils.length(args);
- for (int i = 1; i < numArgs; i++) {
- String argVal = args[i];
- if (argVal.charAt(0) == '-') {
- for (int j = 1; j < argVal.length(); j++) {
- char option = argVal.charAt(j);
- switch (option) {
- case 'f':
- optF = true;
- break;
- case 'p':
- optP = true;
- break;
- case 'r':
- optR = true;
- break;
- case 't':
- optT = true;
- break;
- case 'd':
- optD = true;
- break;
- default: // ignored
- if (debugEnabled) {
- log.debug("Unknown flag ('{}') in command={}", option, command);
- }
- }
- }
- } else {
- String prevArg = args[i - 1];
- path = command.substring(command.indexOf(prevArg) + prevArg.length() + 1);
-
- int pathLen = path.length();
- char startDelim = path.charAt(0);
- char endDelim = (pathLen > 2) ? path.charAt(pathLen - 1) : '\0';
- // remove quotes
- if ((pathLen > 2) && (startDelim == endDelim) && ((startDelim == '\'') || (startDelim == '"'))) {
- path = path.substring(1, pathLen - 1);
- }
- break;
- }
- }
-
- if ((!optF) && (!optT)) {
- error = new IOException("Either -f or -t option should be set for " + command);
- }
- }
-
- @Override
- public ExecutorService getExecutorService() {
- return executorService;
- }
-
- @Override
- public boolean isShutdownOnExit() {
- return shutdownOnExit;
- }
-
- @Override
- public Session getSession() {
- return getServerSession();
- }
-
- @Override
- public ServerSession getServerSession() {
- return serverSession;
- }
-
- @Override
- public void setSession(ServerSession session) {
- serverSession = session;
- }
-
- @Override
- public void setInputStream(InputStream in) {
- this.in = in;
- }
-
- @Override
- public void setOutputStream(OutputStream out) {
- this.out = out;
- }
-
- @Override
- public void setErrorStream(OutputStream err) {
- this.err = err;
- }
-
- @Override
- public void setExitCallback(ExitCallback callback) {
- this.callback = callback;
- }
-
- @Override
- public void setFileSystem(FileSystem fs) {
- this.fileSystem = fs;
- }
-
- @Override
- public void start(Environment env) throws IOException {
- if (error != null) {
- throw error;
- }
-
- try {
- ExecutorService executors = getExecutorService();
- pendingFuture = executors.submit(this);
- } catch (RuntimeException e) { // e.g., RejectedExecutionException
- log.error("Failed (" + e.getClass().getSimpleName() + ") to start command=" + name + ": " + e.getMessage(), e);
- throw new IOException(e);
- }
- }
-
- @Override
- public void destroy() {
- // if thread has not completed, cancel it
- boolean debugEnabled = log.isDebugEnabled();
- if ((pendingFuture != null) && (!pendingFuture.isDone())) {
- boolean result = pendingFuture.cancel(true);
- // TODO consider waiting some reasonable (?) amount of time for cancellation
- if (debugEnabled) {
- log.debug("destroy() - cancel pending future=" + result);
- }
- }
-
- pendingFuture = null;
-
- ExecutorService executors = getExecutorService();
- if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
- Collection<Runnable> runners = executors.shutdownNow();
- if (debugEnabled) {
- log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
- }
- }
- this.executorService = null;
-
- try {
- fileSystem.close();
- } catch (UnsupportedOperationException e) {
- // Ignore
- } catch (IOException e) {
- log.debug("Error closing FileSystem", e);
- }
- }
-
- @Override
- public void run() {
- int exitValue = ScpHelper.OK;
- String exitMessage = null;
- ScpHelper helper = new ScpHelper(getServerSession(), in, out, fileSystem, opener, listener);
- try {
- if (optT) {
- helper.receive(helper.resolveLocalPath(path), optR, optD, optP, receiveBufferSize);
- } else if (optF) {
- helper.send(Collections.singletonList(path), optR, optP, sendBufferSize);
- } else {
- throw new IOException("Unsupported mode");
- }
- } catch (IOException e) {
- ServerSession session = getServerSession();
- boolean debugEnabled = log.isDebugEnabled();
- try {
- Integer statusCode = null;
- if (e instanceof ScpException) {
- statusCode = ((ScpException) e).getExitStatus();
- }
- exitValue = (statusCode == null) ? ScpHelper.ERROR : statusCode;
- // this is an exception so status cannot be OK/WARNING
- if ((exitValue == ScpHelper.OK) || (exitValue == ScpHelper.WARNING)) {
- if (debugEnabled) {
- log.debug("run({})[{}] normalize status code={}", session, name, exitValue);
- }
- exitValue = ScpHelper.ERROR;
- }
- exitMessage = GenericUtils.trimToEmpty(e.getMessage());
- writeCommandResponseMessage(name, exitValue, exitMessage);
- } catch (IOException e2) {
- if (debugEnabled) {
- log.debug("run({})[{}] Failed ({}) to send error response: {}",
- session, name, e.getClass().getSimpleName(), e.getMessage());
- }
- if (log.isTraceEnabled()) {
- log.trace("run(" + session + ")[" + name + "] error response failure details", e2);
- }
- }
-
- if (debugEnabled) {
- log.debug("run({})[{}] Failed ({}) to run command: {}",
- session, name, e.getClass().getSimpleName(), e.getMessage());
- }
- if (log.isTraceEnabled()) {
- log.trace("run(" + session + ")[" + name + "] command execution failure details", e);
- }
- } finally {
- if (callback != null) {
- callback.onExit(exitValue, GenericUtils.trimToEmpty(exitMessage));
- }
- }
- }
-
- protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
- if (log.isDebugEnabled()) {
- log.debug("writeCommandResponseMessage({}) command='{}', exit-status={}: {}",
- getServerSession(), command, exitValue, exitMessage);
- }
- ScpHelper.sendResponseMessage(out, exitValue, exitMessage);
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + "(" + getSession() + ") " + name;
- }
-}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
deleted file mode 100644
index 99e3e34..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/ScpCommandFactory.java
+++ /dev/null
@@ -1,272 +0,0 @@
-/*
- * 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.server.scp;
-
-import java.util.Collection;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.ExecutorService;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpFileOpenerHolder;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-import org.apache.sshd.common.util.EventListenerUtils;
-import org.apache.sshd.common.util.ObjectBuilder;
-import org.apache.sshd.common.util.threads.ExecutorServiceConfigurer;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.CommandFactory;
-
-/**
- * This <code>CommandFactory</code> can be used as a standalone command factory
- * or can be used to augment another <code>CommandFactory</code> and provides
- * <code>SCP</code> support.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- * @see ScpCommand
- */
-public class ScpCommandFactory
- implements ScpFileOpenerHolder,
- CommandFactory,
- Cloneable,
- ExecutorServiceConfigurer {
- /**
- * A useful {@link ObjectBuilder} for {@link ScpCommandFactory}
- */
- public static class Builder implements ObjectBuilder<ScpCommandFactory> {
- private final ScpCommandFactory factory = new ScpCommandFactory();
-
- public Builder() {
- super();
- }
-
- public Builder withFileOpener(ScpFileOpener opener) {
- factory.setScpFileOpener(opener);
- return this;
- }
-
- public Builder withDelegate(CommandFactory delegate) {
- factory.setDelegateCommandFactory(delegate);
- return this;
- }
-
- public Builder withExecutorService(ExecutorService service) {
- factory.setExecutorService(service);
- return this;
- }
-
- public Builder withShutdownOnExit(boolean shutdown) {
- factory.setShutdownOnExit(shutdown);
- return this;
- }
-
- public Builder withSendBufferSize(int sendSize) {
- factory.setSendBufferSize(sendSize);
- return this;
- }
-
- public Builder withReceiveBufferSize(int receiveSize) {
- factory.setReceiveBufferSize(receiveSize);
- return this;
- }
-
- public Builder addEventListener(ScpTransferEventListener listener) {
- factory.addEventListener(listener);
- return this;
- }
-
- public Builder removeEventListener(ScpTransferEventListener listener) {
- factory.removeEventListener(listener);
- return this;
- }
-
- @Override
- public ScpCommandFactory build() {
- return factory.clone();
- }
- }
-
- /*
- * NOTE: we expose setters since there is no problem to change these settings between
- * successive invocations of the 'createCommand' method
- */
- private CommandFactory delegate;
- private ExecutorService executors;
- private boolean shutdownExecutor;
- private ScpFileOpener fileOpener;
- private int sendBufferSize = ScpHelper.MIN_SEND_BUFFER_SIZE;
- private int receiveBufferSize = ScpHelper.MIN_RECEIVE_BUFFER_SIZE;
- private Collection<ScpTransferEventListener> listeners = new CopyOnWriteArraySet<>();
- private ScpTransferEventListener listenerProxy;
-
- public ScpCommandFactory() {
- listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), listeners);
- }
-
- @Override
- public ScpFileOpener getScpFileOpener() {
- return fileOpener;
- }
-
- @Override
- public void setScpFileOpener(ScpFileOpener fileOpener) {
- this.fileOpener = fileOpener;
- }
-
- public CommandFactory getDelegateCommandFactory() {
- return delegate;
- }
-
- /**
- * @param factory A {@link CommandFactory} to be used if the
- * command is not an SCP one. If {@code null} then an {@link IllegalArgumentException}
- * will be thrown when attempting to invoke {@link #createCommand(String)}
- * with a non-SCP command
- */
- public void setDelegateCommandFactory(CommandFactory factory) {
- delegate = factory;
- }
-
- @Override
- public ExecutorService getExecutorService() {
- return executors;
- }
-
- /**
- * @param service An {@link ExecutorService} to be used when
- * starting {@link ScpCommand} execution. If {@code null} then a single-threaded
- * ad-hoc service is used. <B>Note:</B> the service will <U>not</U> be shutdown
- * when the command is terminated - unless it is the ad-hoc service, which will be
- * shutdown regardless
- */
- @Override
- public void setExecutorService(ExecutorService service) {
- executors = service;
- }
-
- @Override
- public boolean isShutdownOnExit() {
- return shutdownExecutor;
- }
-
- @Override
- public void setShutdownOnExit(boolean shutdown) {
- shutdownExecutor = shutdown;
- }
-
- public int getSendBufferSize() {
- return sendBufferSize;
- }
-
- /**
- * @param sendSize Size (in bytes) of buffer to use when sending files
- * @see ScpHelper#MIN_SEND_BUFFER_SIZE
- */
- public void setSendBufferSize(int sendSize) {
- if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
- throw new IllegalArgumentException("<ScpCommandFactory>() send buffer size "
- + "(" + sendSize + ") below minimum required (" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
- }
- sendBufferSize = sendSize;
- }
-
- public int getReceiveBufferSize() {
- return receiveBufferSize;
- }
-
- /**
- * @param receiveSize Size (in bytes) of buffer to use when receiving files
- * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
- */
- public void setReceiveBufferSize(int receiveSize) {
- if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
- throw new IllegalArgumentException("<ScpCommandFactory>() receive buffer size "
- + "(" + receiveSize + ") below minimum required (" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
- }
- receiveBufferSize = receiveSize;
- }
-
- /**
- * @param listener The {@link ScpTransferEventListener} to add
- * @return {@code true} if this is a <U>new</U> listener instance,
- * {@code false} if the listener is already registered
- * @throws IllegalArgumentException if {@code null} listener
- */
- public boolean addEventListener(ScpTransferEventListener listener) {
- if (listener == null) {
- throw new IllegalArgumentException("No listener instance");
- }
-
- return listeners.add(listener);
- }
-
- /**
- * @param listener The {@link ScpTransferEventListener} to remove
- * @return {@code true} if the listener was registered and removed,
- * {@code false} if the listener was not registered to begin with
- * @throws IllegalArgumentException if {@code null} listener
- */
- public boolean removeEventListener(ScpTransferEventListener listener) {
- if (listener == null) {
- throw new IllegalArgumentException("No listener instance");
- }
-
- return listeners.remove(listener);
- }
-
- /**
- * Parses a command string and verifies that the basic syntax is
- * correct. If parsing fails the responsibility is delegated to
- * the configured {@link CommandFactory} instance; if one exist.
- *
- * @param command command to parse
- * @return configured {@link Command} instance
- * @throws IllegalArgumentException if not an SCP command and no
- * delegate command factory is available
- * @see ScpHelper#SCP_COMMAND_PREFIX
- */
- @Override
- public Command createCommand(String command) {
- if (command.startsWith(ScpHelper.SCP_COMMAND_PREFIX)) {
- return new ScpCommand(command,
- getExecutorService(), isShutdownOnExit(),
- getSendBufferSize(), getReceiveBufferSize(),
- getScpFileOpener(), listenerProxy);
- }
-
- CommandFactory factory = getDelegateCommandFactory();
- if (factory != null) {
- return factory.createCommand(command);
- }
-
- throw new IllegalArgumentException("Unknown command, does not begin with '" + ScpHelper.SCP_COMMAND_PREFIX + "': " + command);
- }
-
- @Override
- public ScpCommandFactory clone() {
- try {
- ScpCommandFactory other = getClass().cast(super.clone());
- // clone the listeners set as well
- other.listeners = new CopyOnWriteArraySet<>(this.listeners);
- other.listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), other.listeners);
- return other;
- } catch (CloneNotSupportedException e) {
- throw new RuntimeException(e); // un-expected...
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java
deleted file mode 100644
index d847291..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/server/scp/UnknownCommand.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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.server.scp;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.Objects;
-
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.Environment;
-import org.apache.sshd.server.ExitCallback;
-
-/**
- * Implementation of an unknown command that can be returned by <code>CommandFactory</code>
- * when the command is not known, as it is supposed to always
- * return a valid <code>Command</code> object.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class UnknownCommand implements Command, Runnable {
-
- private final String command;
- private final String message;
- @SuppressWarnings("unused")
- private InputStream in;
- @SuppressWarnings("unused")
- private OutputStream out;
- private OutputStream err;
- private ExitCallback callback;
-
- public UnknownCommand(String command) {
- this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
- this.message = "Unknown command: " + command;
- }
-
- public String getCommand() {
- return command;
- }
-
- public String getMessage() {
- return message;
- }
-
- @Override
- public void setInputStream(InputStream in) {
- this.in = in;
- }
-
- @Override
- public void setOutputStream(OutputStream out) {
- this.out = out;
- }
-
- @Override
- public void setErrorStream(OutputStream err) {
- this.err = err;
- }
-
- @Override
- public void setExitCallback(ExitCallback callback) {
- this.callback = callback;
- }
-
- @Override
- public void run() {
- String errorMessage = getMessage();
- try {
- try {
- err.write(errorMessage.getBytes(StandardCharsets.UTF_8));
- err.write('\n');
- } finally {
- err.flush();
- }
- } catch (IOException e) {
- // ignored
- }
-
- if (callback != null) {
- callback.onExit(1, errorMessage);
- }
- }
-
- @Override
- public void start(Environment env) throws IOException {
- Thread thread = new Thread(this);
- thread.setDaemon(true);
- thread.start();
- }
-
- @Override
- public void destroy() {
- // ignored
- }
-
- @Override
- public int hashCode() {
- return Objects.hashCode(getCommand());
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == null) {
- return false;
- }
- if (obj == this) {
- return true;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
-
- return Objects.equals(this.getCommand(), ((UnknownCommand) obj).getCommand());
- }
-
- @Override
- public String toString() {
- return getMessage();
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java
new file mode 100644
index 0000000..59d970c
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommand.java
@@ -0,0 +1,138 @@
+/*
+ * 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.server.shell;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+
+/**
+ * Implementation of an unknown command that can be returned by <code>CommandFactory</code>
+ * when the command is not known, as it is supposed to always
+ * return a valid <code>Command</code> object.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class UnknownCommand implements Command, Runnable {
+
+ private final String command;
+ private final String message;
+ @SuppressWarnings("unused")
+ private InputStream in;
+ @SuppressWarnings("unused")
+ private OutputStream out;
+ private OutputStream err;
+ private ExitCallback callback;
+
+ public UnknownCommand(String command) {
+ this.command = ValidateUtils.checkNotNullAndNotEmpty(command, "No command");
+ this.message = "Unknown command: " + command;
+ }
+
+ public String getCommand() {
+ return command;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public void setInputStream(InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
+
+ @Override
+ public void setExitCallback(ExitCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ String errorMessage = getMessage();
+ try {
+ try {
+ err.write(errorMessage.getBytes(StandardCharsets.UTF_8));
+ err.write('\n');
+ } finally {
+ err.flush();
+ }
+ } catch (IOException e) {
+ // ignored
+ }
+
+ if (callback != null) {
+ callback.onExit(1, errorMessage);
+ }
+ }
+
+ @Override
+ public void start(Environment env) throws IOException {
+ Thread thread = new Thread(this);
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ @Override
+ public void destroy() {
+ // ignored
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(getCommand());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj == this) {
+ return true;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ return Objects.equals(this.getCommand(), ((UnknownCommand) obj).getCommand());
+ }
+
+ @Override
+ public String toString() {
+ return getMessage();
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java
new file mode 100644
index 0000000..871ca60
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/server/shell/UnknownCommandFactory.java
@@ -0,0 +1,39 @@
+/*
+ * 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.server.shell;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.CommandFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class UnknownCommandFactory implements CommandFactory {
+ public static final UnknownCommandFactory INSTANCE = new UnknownCommandFactory();
+
+ public UnknownCommandFactory() {
+ super();
+ }
+
+ @Override
+ public Command createCommand(String command) {
+ return new UnknownCommand(command);
+ }
+}
[8/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Posted by lg...@apache.org.
[SSHD-818] Split SCP code (client + server) to its own module
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/af415e5f
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/af415e5f
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/af415e5f
Branch: refs/heads/master
Commit: af415e5fec5fadf1c397371313cec20d3b4428f4
Parents: cb8982a
Author: Goldstein Lyor <ly...@c-b4.com>
Authored: Mon Apr 23 16:32:34 2018 +0300
Committer: Goldstein Lyor <ly...@c-b4.com>
Committed: Wed Apr 25 08:01:09 2018 +0300
----------------------------------------------------------------------
README.md | 146 ++-
assembly/pom.xml | 5 +
pom.xml | 1 +
sshd-cli/pom.xml | 5 +
.../apache/sshd/cli/client/ScpCommandMain.java | 57 +-
.../apache/sshd/cli/server/SshServerMain.java | 1 -
sshd-contrib/pom.xml | 5 +
...SimpleAccessControlScpEventListenerTest.java | 4 +-
.../sshd/client/ClientFactoryManager.java | 2 -
.../java/org/apache/sshd/client/SshClient.java | 12 -
.../sshd/client/scp/AbstractScpClient.java | 278 ----
.../sshd/client/scp/CloseableScpClient.java | 32 -
.../sshd/client/scp/DefaultScpClient.java | 159 ---
.../client/scp/DefaultScpStreamResolver.java | 88 --
.../org/apache/sshd/client/scp/ScpClient.java | 174 ---
.../sshd/client/scp/ScpClientCreator.java | 97 --
.../client/session/AbstractClientSession.java | 31 -
.../sshd/client/session/ClientSession.java | 6 +-
.../client/simple/AbstractSimpleClient.java | 106 --
.../apache/sshd/client/simple/SimpleClient.java | 1 -
.../sshd/client/simple/SimpleScpClient.java | 159 ---
...AbstractScpTransferEventListenerAdapter.java | 74 --
.../apache/sshd/common/scp/ScpException.java | 56 -
.../apache/sshd/common/scp/ScpFileOpener.java | 284 -----
.../sshd/common/scp/ScpFileOpenerHolder.java | 37 -
.../org/apache/sshd/common/scp/ScpHelper.java | 837 ------------
.../org/apache/sshd/common/scp/ScpLocation.java | 227 ----
.../sshd/common/scp/ScpReceiveLineHandler.java | 36 -
.../common/scp/ScpSourceStreamResolver.java | 73 --
.../common/scp/ScpTargetStreamResolver.java | 67 -
.../apache/sshd/common/scp/ScpTimestamp.java | 69 -
.../common/scp/ScpTransferEventListener.java | 105 --
.../scp/helpers/DefaultScpFileOpener.java | 75 --
.../LocalFileScpSourceStreamResolver.java | 97 --
.../LocalFileScpTargetStreamResolver.java | 159 ---
.../common/util/io/functors/IOFunction.java | 86 ++
.../org/apache/sshd/server/scp/ScpCommand.java | 350 -----
.../sshd/server/scp/ScpCommandFactory.java | 272 ----
.../apache/sshd/server/scp/UnknownCommand.java | 138 --
.../sshd/server/shell/UnknownCommand.java | 138 ++
.../server/shell/UnknownCommandFactory.java | 39 +
.../org/apache/sshd/client/scp/ScpTest.java | 1197 -----------------
.../simple/BaseSimpleClientTestSupport.java | 70 -
.../sshd/client/simple/SimpleScpClientTest.java | 119 --
.../client/simple/SimpleSessionClientTest.java | 1 +
.../server/command/ScpCommandFactoryTest.java | 114 --
.../sshd/util/test/UnknownCommandFactory.java | 40 -
.../java/org/apache/sshd/util/test/Utils.java | 1 +
.../simple/BaseSimpleClientTestSupport.java | 71 ++
.../sshd/git/AbstractGitCommandFactory.java | 2 +-
sshd-scp/pom.xml | 201 +++
.../sshd/client/scp/AbstractScpClient.java | 278 ++++
.../client/scp/AbstractScpClientCreator.java | 63 +
.../sshd/client/scp/CloseableScpClient.java | 32 +
.../sshd/client/scp/DefaultScpClient.java | 159 +++
.../client/scp/DefaultScpClientCreator.java | 42 +
.../client/scp/DefaultScpStreamResolver.java | 88 ++
.../org/apache/sshd/client/scp/ScpClient.java | 174 +++
.../sshd/client/scp/ScpClientCreator.java | 106 ++
.../apache/sshd/client/scp/SimpleScpClient.java | 178 +++
.../sshd/client/scp/SimpleScpClientImpl.java | 153 +++
...AbstractScpTransferEventListenerAdapter.java | 74 ++
.../apache/sshd/common/scp/ScpException.java | 56 +
.../apache/sshd/common/scp/ScpFileOpener.java | 284 +++++
.../sshd/common/scp/ScpFileOpenerHolder.java | 37 +
.../org/apache/sshd/common/scp/ScpHelper.java | 837 ++++++++++++
.../org/apache/sshd/common/scp/ScpLocation.java | 227 ++++
.../sshd/common/scp/ScpReceiveLineHandler.java | 36 +
.../common/scp/ScpSourceStreamResolver.java | 73 ++
.../common/scp/ScpTargetStreamResolver.java | 67 +
.../apache/sshd/common/scp/ScpTimestamp.java | 69 +
.../common/scp/ScpTransferEventListener.java | 105 ++
.../scp/helpers/DefaultScpFileOpener.java | 75 ++
.../LocalFileScpSourceStreamResolver.java | 97 ++
.../LocalFileScpTargetStreamResolver.java | 159 +++
.../org/apache/sshd/server/scp/ScpCommand.java | 350 +++++
.../sshd/server/scp/ScpCommandFactory.java | 272 ++++
.../org/apache/sshd/client/scp/ScpTest.java | 1203 ++++++++++++++++++
.../sshd/client/scp/SimpleScpClientTest.java | 121 ++
.../sshd/server/scp/ScpCommandFactoryTest.java | 113 ++
.../sshd/client/simple/SimpleSftpClient.java | 179 ---
.../client/simple/SimpleSftpClientImpl.java | 171 ---
.../client/subsystem/sftp/SimpleSftpClient.java | 179 +++
.../sftp/impl/SimpleSftpClientImpl.java | 192 +++
.../java/org/apache/sshd/client/ClientTest.java | 426 -------
.../simple/BaseSimpleClientTestSupport.java | 70 -
.../client/simple/SimpleSftpClientTest.java | 129 --
.../sftp/AbstractSftpClientTestSupport.java | 2 -
.../sshd/client/subsystem/sftp/ClientTest.java | 424 ++++++
.../subsystem/sftp/SftpFileSystemTest.java | 2 -
.../subsystem/sftp/SimpleSftpClientTest.java | 128 ++
.../sftp/ApacheSshdSftpSessionFactoryTest.java | 2 -
92 files changed, 7168 insertions(+), 6668 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index f512a3f..5a1e249 100644
--- a/README.md
+++ b/README.md
@@ -533,17 +533,80 @@ it will be **closed** automatically when the stream using it is closed.
## SCP
-Besides the `ScpTransferEventListener`, the SCP module also uses a `ScpFileOpener` instance in order to access
-the local files - client or server-side. The default implementation simply opens an [InputStream](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html)
+Both client-side and server-side SCP are supported. Starting from version 2.0, the SCP related code is located in the `sshd-scp` module, so you need
+to add this additional dependency to your maven project:
+
+```xml
+
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-sscp</artifactId>
+ <version>...same as sshd-core...</version>
+ </dependency>
+
+```
+
+### Client-side SCP
+
+In order to obtain an `ScpClient` one needs to use an `ScpClientCreator`:
+
+```java
+
+ClientSession session = ... obtain an instance ...
+ScpClientCreator creator = ... obtain an instance ...
+ScpClient client = creator.createScpClient(session);
+
+```
+
+A default `ScpClientCreator` instance is provided as part of the module - see `ScpClientCreator.instance()`
+
+#### ScpFileOpener(s)
+
+As part of the `ScpClientCreator`, the SCP module also uses a `ScpFileOpener` instance in order to access
+the local files. The default implementation simply opens an [InputStream](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html)
or [OutputStream](https://docs.oracle.com/javase/8/docs/api/java/io/OutputStream.html) on the requested local path. However,
-the user may replace it and intercept the calls - e.g., for logging, for wrapping/filtering the streams, etc... **Note:**
-due to SCP protocol limitations one cannot change the **size** of the input/output since it is passed as part of the command
+the user may replace it and intercept the calls - e.g., for logging, for wrapping/filtering the streams, etc... The user may
+attach a default opener that will be automatically attached to **all** clients created unless specifically overridden:
+
+```java
+
+ClientSession session = ... obtain an instance ...
+ScpClientCreator creator = ... obtain an instance ...
+creator.setScpFileOpener(new MySuperDuperOpener());
+
+ScpClient client1 = creator.createScpClient(session); // <<== automatically uses MySuperDuperOpener
+ScpClient client2 = creator.createScpClient(session, new SomeOtherOpener()); // <<== uses SomeOtherOpener instead of MySuperDuperOpener
+
+```
+
+**Note:** due to SCP protocol limitations one cannot change the **size** of the input/output since it is passed as part of the command
**before** the file opener is invoked - so there are a few limitations on what one can do within this interface implementation.
+#### ScpTransferEventListener(s)
+
+The `ScpClientCreator` can also be used to attach a default `ScpTransferEventListener` that will be attached to
+**all** created SCP client instances through that creator - unless specifically overridden:
+
+```java
+
+ClientSession session = ... obtain an instance ...
+ScpClientCreator creator = ... obtain an instance ...
+creator.setScpTransferEventListener(new MySuperDuperListener());
+
+ScpClient client1 = creator.createScpClient(session); // <<== automatically uses MySuperDuperListener
+ScpClient client2 = creator.createScpClient(session, new SomeOtherListener()); // <<== uses SomeOtherListener instead of MySuperDuperListener
+
+```
+
+### Server-side SCP
+
+The `ScpCommandFactory` allows users to attach an `ScpFileOpener` and/or `ScpTransferEventListener` having the same behavior as the client - i.e.,
+monitoring and intervention on the accessed local files.
## SFTP
-Both client-side and server-side SFTP are supported. Starting from SSHD 1.8.0, the SFTP related code is located in the `sshd-sftp`, so you need to add this additional dependency to your maven project:
+Both client-side and server-side SFTP are supported. Starting from version 2.0, the SFTP related code is located in the `sshd-sftp`, so you need to add
+this additional dependency to your maven project:
```xml
@@ -567,19 +630,48 @@ On the server side, the following code needs to be added:
```
+### `SftpEventListener`
+
+(See above more details...) - users may register an `SftpEventListener` (or more...) in the `SftpSubsystemFactory` in
+order to monitor and even intervene in the susbsytem's functionality.
+
### Client-side SFTP
+In order to obtain an `SftpClient` instance one needs to use an `SftpClientFactory`:
+
+
```java
- SftpClient client = SftpClientFactory.instance().createSftpClient(session);
+ ClientSession session = ...obtain session...
+ SftpClientFactory factory = ...obtain factory...
+ SftpClient client = factory.createSftpClient(session);
```
-### `SftpEventListener`
+A default client factory implementations is provided in the module - see `SftpClientFactory.instance()`
+
+
+### Using a custom `SftpClientFactory`
+
+The code creates `SftpClient`-s and `SftpFileSystem`-s using a default built-in `SftpClientFactory` instance (see
+`DefaultSftpClientFactory`). Users may choose to use a custom factory in order to provide their own
+implementations - e.g., in order to override some default behavior - e.g.:
+
+```java
+
+ SshClient client = ... setup client...
+
+ try (ClientSession session = client.connect(user, host, port).verify(timeout).getSession()) {
+ session.addPasswordIdentity(password);
+ session.auth.verify(timeout);
-See above...
-In addition to the `SftpEventListener` there are a few more SFTP-related special interfaces and modules.
+ // User-specific factory
+ try (SftpClient sftp = MySpecialSessionSftpClientFactory.INSTANCE.createSftpClient(session)) {
+ ... instance created through SpecialSessionSftpClientFactory ...
+ }
+ }
+```
### Version selection via `SftpVersionSelector`
@@ -603,7 +695,8 @@ range.
session.addPasswordIdentity(password);
session.auth.verify(timeout);
- try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session, myVersionSelector)) {
+ SftpClientFactory factory = SftpClientFactory.instance();
+ try (SftpClient sftp = factory.createSftpClient(session, myVersionSelector)) {
... do SFTP related stuff...
}
}
@@ -615,30 +708,6 @@ the version, and all we can do at the server is require a **specific** version v
configuration key. For more advanced restrictions one needs to sub-class `SftpSubSystem` and provide a non-default
`SftpSubsystemFactory` that uses the sub-classed code.
-
-### Using a custom `SftpClientFactory`
-
-The code creates `SftpClient`-s and `SftpFileSystem`-s using a default built-in `SftpClientFactory` instance (see
-`DefaultSftpClientFactory`). Users may choose to use a custom factory in order to provide their own
-implementations - e.g., in order to override some default behavior - e.g.:
-
-```java
-
- SshClient client = ... setup client...
-
- try (ClientSession session = client.connect(user, host, port).verify(timeout).getSession()) {
- session.addPasswordIdentity(password);
- session.auth.verify(timeout);
-
- // User-specific factory
- try (SftpClient sftp = MySpecialSessionSftpClientFactory.INSTANCE.createSftpClient(session)) {
- ... instance created through SpecialSessionSftpClientFactory ...
- }
- }
-
-```
-
-
### Using `SftpFileSystemProvider` to create an `SftpFileSystem`
@@ -796,7 +865,8 @@ UTF-8 is used. **Note:** the value can be a charset name or a `java.nio.charset.
PropertyResolverUtils.updateProperty(session, SftpClient.NAME_DECODING_CHARSET, "ISO-8859-4");
session.authenticate(...);
- try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session)) {
+ SftpClientFactory factory = SftpClientFactory.instance();
+ try (SftpClient sftp = factory.createSftpClient(session)) {
for (DirEntry entry : sftp.readDir(...some path...)) {
...handle entry assuming ISO-8859-4 (inherited from the session) encoded names...
}
@@ -848,7 +918,8 @@ On the client side, all the supported extensions are classes that implement `Sft
session.addPasswordIdentity(password);
session.auth().verify(timeout);
- try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session)) {
+ SftpClientFactory factory = SftpClientFactory.instance();
+ try (SftpClient sftp = factory.createSftpClient(session)) {
Map<String, byte[]> extensions = sftp.getServerExtensions();
// Key=extension name, value=registered parser instance
Map<String, ?> data = ParserUtils.parse(extensions);
@@ -880,7 +951,8 @@ One can skip all the conditional code if a specific known extension is required:
session.addPasswordIdentity(password);
session.auth().verify(timeout);
- try (SftpClient sftp = SftpClientFactory.instance().createSftpClient(session)) {
+ SftpClientFactory factory = SftpClientFactory.instance();
+ try (SftpClient sftp = factory.createSftpClient(session)) {
// Returns null if extension is not supported by remote server
SpaceAvailableExtension space = sftp.getExtension(SpaceAvailableExtension.class);
if (space != null) {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/assembly/pom.xml
----------------------------------------------------------------------
diff --git a/assembly/pom.xml b/assembly/pom.xml
index 53db1ba..7b103e1 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -43,6 +43,11 @@
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-scp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>${project.version}</version>
</dependency>
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index b88a195..f161f1f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1006,6 +1006,7 @@
<modules>
<module>sshd-core</module>
+ <module>sshd-scp</module>
<module>sshd-sftp</module>
<module>sshd-mina</module>
<module>sshd-netty</module>
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-cli/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-cli/pom.xml b/sshd-cli/pom.xml
index 0f9bb98..0925da8 100644
--- a/sshd-cli/pom.xml
+++ b/sshd-cli/pom.xml
@@ -44,6 +44,11 @@
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-scp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>${project.version}</version>
</dependency>
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
index 8a9e4a9..3f233ef 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
@@ -35,11 +35,13 @@ import java.util.logging.Level;
import org.apache.sshd.client.scp.ScpClient;
import org.apache.sshd.client.scp.ScpClient.Option;
+import org.apache.sshd.client.scp.ScpClientCreator;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.scp.ScpLocation;
import org.apache.sshd.common.scp.ScpTransferEventListener;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.io.NoCloseInputStream;
+import org.apache.sshd.common.util.threads.ThreadUtils;
/**
* TODO Add javadoc
@@ -70,13 +72,14 @@ public class ScpCommandMain extends SshClientCliSupport {
String argName = args[index];
// handled by 'setupClientSession'
if (isArgumentedOption(SCP_PORT_OPTION, argName)) {
- if ((index + 1) >= numArgs) {
+ index++;
+ if (index >= numArgs) {
error = showError(stderr, "option requires an argument: " + argName);
break;
}
effective.add(argName);
- effective.add(args[++index]);
+ effective.add(args[index]);
} else if ("-r".equals(argName) || "-p".equals(argName)
|| "-q".equals(argName) || "-C".equals(argName)
|| "-v".equals(argName) || "-vv".equals(argName) || "-vvv".equals(argName)) {
@@ -85,13 +88,14 @@ public class ScpCommandMain extends SshClientCliSupport {
error = showError(stderr, "Unknown option: " + argName);
break;
} else {
- if ((index + 1) >= numArgs) {
+ index++;
+ if (index >= numArgs) {
error = showError(stderr, "Not enough arguments");
break;
}
ScpLocation source = new ScpLocation(argName);
- ScpLocation target = new ScpLocation(args[++index]);
+ ScpLocation target = new ScpLocation(args[index]);
if (index < (numArgs - 1)) {
error = showError(stderr, "Unexpected extra arguments");
break;
@@ -117,6 +121,42 @@ public class ScpCommandMain extends SshClientCliSupport {
return effective.toArray(new String[effective.size()]);
}
+ public static ScpClientCreator resolveScpClientCreator(PrintStream stderr, String... args) {
+ String className = null;
+ for (int index = 0, numArgs = GenericUtils.length(args); index < numArgs; index++) {
+ String argName = args[index];
+ if ("-creator".equals(argName)) {
+ index++;
+ if (index >= numArgs) {
+ showError(stderr, "option requires an argument: " + argName);
+ return null;
+ }
+
+ className = args[index];
+ }
+ }
+
+ if (GenericUtils.isEmpty(className)) {
+ className = System.getProperty(ScpClientCreator.class.getName());
+ }
+
+ if (GenericUtils.isEmpty(className)) {
+ return ScpClientCreator.instance();
+ }
+
+ try {
+ ClassLoader cl = ThreadUtils.resolveDefaultClassLoader(ScpClientCreator.class);
+ Class<?> clazz = cl.loadClass(className);
+ return ScpClientCreator.class.cast(clazz.newInstance());
+ } catch (Exception e) {
+ stderr.append("Failed (").append(e.getClass().getSimpleName()).append(')')
+ .append(" to instantiate ").append(className)
+ .append(": ").println(e.getMessage());
+ stderr.flush();
+ return null;
+ }
+ }
+
public static void main(String[] args) throws Exception {
PrintStream stdout = System.out;
PrintStream stderr = System.err;
@@ -134,11 +174,12 @@ public class ScpCommandMain extends SshClientCliSupport {
}
}
- ClientSession session = (logStream == null) || GenericUtils.isEmpty(args)
+ ScpClientCreator creator = resolveScpClientCreator(stderr, args);
+ ClientSession session = ((logStream == null) || (creator == null) || GenericUtils.isEmpty(args))
? null : setupClientSession(SCP_PORT_OPTION, stdin, stdout, stderr, args);
if (session == null) {
stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity] [-io nio2|mina|netty]"
- + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value]"
+ + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value] [-o creator=class name]"
+ " [-c cipherlist] [-m maclist] [-w password] [-C] <source> <target>");
stderr.println();
stderr.println("Where <source> or <target> are either 'user@host:file' or a local file path");
@@ -163,7 +204,7 @@ public class ScpCommandMain extends SshClientCliSupport {
}
if (!quiet) {
- session.setScpTransferEventListener(new ScpTransferEventListener() {
+ creator.setScpTransferEventListener(new ScpTransferEventListener() {
@Override
public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
logEvent("startFolderEvent", op, file, -1L, perms, null);
@@ -200,7 +241,7 @@ public class ScpCommandMain extends SshClientCliSupport {
});
}
- ScpClient client = session.createScpClient();
+ ScpClient client = creator.createScpClient(session);
ScpLocation source = new ScpLocation(args[numArgs - 2]);
ScpLocation target = new ScpLocation(args[numArgs - 1]);
if (source.isLocal()) {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
----------------------------------------------------------------------
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
index cba9d24..05c1069 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/server/SshServerMain.java
@@ -56,7 +56,6 @@ public class SshServerMain extends SshServerCliSupport {
public static void main(String[] args) throws Exception {
int port = 8000;
- String provider;
boolean error = false;
String hostKeyType = AbstractGeneratorHostKeyProvider.DEFAULT_ALGORITHM;
int hostKeySize = 0;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-contrib/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-contrib/pom.xml b/sshd-contrib/pom.xml
index e904686..f0ff459 100644
--- a/sshd-contrib/pom.xml
+++ b/sshd-contrib/pom.xml
@@ -53,6 +53,11 @@
</dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-scp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>${project.version}</version>
</dependency>
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
----------------------------------------------------------------------
diff --git a/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java b/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
index 70bcd44..4312dda 100644
--- a/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
+++ b/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
@@ -28,6 +28,7 @@ import java.util.concurrent.TimeUnit;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.scp.ScpClient;
+import org.apache.sshd.client.scp.ScpClientCreator;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.file.FileSystemFactory;
import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
@@ -89,7 +90,8 @@ public class SimpleAccessControlScpEventListenerTest extends BaseTestSupport {
session.addPasswordIdentity(getCurrentTestName());
session.auth().verify(5L, TimeUnit.SECONDS);
- ScpClient scp = session.createScpClient();
+ ScpClientCreator creator = ScpClientCreator.instance();
+ ScpClient scp = creator.createScpClient(session);
Path targetPath = detectTargetFolder();
Path parentPath = targetPath.getParent();
Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
index 31b2a22..26ca4bd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/ClientFactoryManager.java
@@ -23,7 +23,6 @@ import org.apache.sshd.client.config.keys.ClientIdentityLoader;
import org.apache.sshd.client.session.ClientProxyConnectorHolder;
import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
-import org.apache.sshd.common.scp.ScpFileOpenerHolder;
/**
* The <code>ClientFactoryManager</code> enable the retrieval of additional
@@ -33,7 +32,6 @@ import org.apache.sshd.common.scp.ScpFileOpenerHolder;
*/
public interface ClientFactoryManager
extends FactoryManager,
- ScpFileOpenerHolder,
ClientProxyConnectorHolder,
ClientAuthenticationManager {
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 1d034c1..e387bb6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -74,7 +74,6 @@ import org.apache.sshd.common.io.IoConnectFuture;
import org.apache.sshd.common.io.IoConnector;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
-import org.apache.sshd.common.scp.ScpFileOpener;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
@@ -162,7 +161,6 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
private ClientIdentityLoader clientIdentityLoader;
private FilePasswordProvider filePasswordProvider;
private PasswordIdentityProvider passwordIdentityProvider;
- private ScpFileOpener scpOpener;
private final List<Object> identities = new CopyOnWriteArrayList<>();
private final AuthenticationIdentitiesProvider identitiesProvider;
@@ -191,16 +189,6 @@ public class SshClient extends AbstractFactoryManager implements ClientFactoryMa
}
@Override
- public ScpFileOpener getScpFileOpener() {
- return scpOpener;
- }
-
- @Override
- public void setScpFileOpener(ScpFileOpener opener) {
- scpOpener = opener;
- }
-
- @Override
public ServerKeyVerifier getServerKeyVerifier() {
return serverKeyVerifier;
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
deleted file mode 100644
index 81c20db..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * 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.scp;
-
-import java.io.IOException;
-import java.nio.file.FileSystem;
-import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.channel.ChannelExec;
-import org.apache.sshd.client.channel.ClientChannel;
-import org.apache.sshd.client.channel.ClientChannelEvent;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.FactoryManager;
-import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.scp.ScpException;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class AbstractScpClient extends AbstractLoggingBean implements ScpClient {
- public static final Set<ClientChannelEvent> COMMAND_WAIT_EVENTS =
- Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.CLOSED));
-
- protected AbstractScpClient() {
- super();
- }
-
- @Override
- public final ClientSession getSession() {
- return getClientSession();
- }
-
- @Override
- public void download(String[] remote, String local, Collection<Option> options) throws IOException {
- local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
- remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", (Object) remote);
-
- if (remote.length > 1) {
- options = addTargetIsDirectory(options);
- }
-
- for (String r : remote) {
- download(r, local, options);
- }
- }
-
- @Override
- public void download(String[] remote, Path local, Collection<Option> options) throws IOException {
- remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", (Object) remote);
-
- if (remote.length > 1) {
- options = addTargetIsDirectory(options);
- }
-
- for (String r : remote) {
- download(r, local, options);
- }
- }
-
- @Override
- public void download(String remote, Path local, Collection<Option> options) throws IOException {
- local = ValidateUtils.checkNotNull(local, "Invalid argument local: %s", local);
- remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", remote);
-
- LinkOption[] opts = IoUtils.getLinkOptions(true);
- if (Files.isDirectory(local, opts)) {
- options = addTargetIsDirectory(options);
- }
-
- if (options.contains(Option.TargetIsDirectory)) {
- Boolean status = IoUtils.checkFileExists(local, opts);
- if (status == null) {
- throw new SshException("Target directory " + local.toString() + " is probably inaccesible");
- }
-
- if (!status) {
- throw new SshException("Target directory " + local.toString() + " does not exist");
- }
-
- if (!Files.isDirectory(local, opts)) {
- throw new SshException("Target directory " + local.toString() + " is not a directory");
- }
- }
-
- download(remote, local.getFileSystem(), local, options);
- }
-
- @Override
- public void download(String remote, String local, Collection<Option> options) throws IOException {
- local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
-
- ClientSession session = getClientSession();
- FactoryManager manager = session.getFactoryManager();
- FileSystemFactory factory = manager.getFileSystemFactory();
- FileSystem fs = factory.createFileSystem(session);
- try {
- download(remote, fs, fs.getPath(local), options);
- } finally {
- try {
- fs.close();
- } catch (UnsupportedOperationException e) {
- if (log.isDebugEnabled()) {
- log.debug("download({}) {} => {} - failed ({}) to close file system={}: {}",
- session, remote, local, e.getClass().getSimpleName(), fs, e.getMessage());
- }
- }
- }
- }
-
- protected abstract void download(String remote, FileSystem fs, Path local, Collection<Option> options) throws IOException;
-
- @Override
- public void upload(String[] local, String remote, Collection<Option> options) throws IOException {
- final Collection<String> paths = Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", (Object) local));
- runUpload(remote, options, paths, (helper, local1, sendOptions) ->
- helper.send(local1,
- sendOptions.contains(Option.Recursive),
- sendOptions.contains(Option.PreserveAttributes),
- ScpHelper.DEFAULT_SEND_BUFFER_SIZE));
- }
-
- @Override
- public void upload(Path[] local, String remote, Collection<Option> options) throws IOException {
- final Collection<Path> paths = Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", (Object) local));
- runUpload(remote, options, paths, (helper, local1, sendOptions) ->
- helper.sendPaths(local1,
- sendOptions.contains(Option.Recursive),
- sendOptions.contains(Option.PreserveAttributes),
- ScpHelper.DEFAULT_SEND_BUFFER_SIZE));
- }
-
- protected abstract <T> void runUpload(String remote, Collection<Option> options, Collection<T> local, AbstractScpClient.ScpOperationExecutor<T> executor) throws IOException;
-
- /**
- * Invoked by the various <code>upload/download</code> methods after having successfully
- * completed the remote copy command and (optionally) having received an exit status
- * from the remote server. If no exit status received within {@link FactoryManager#CHANNEL_CLOSE_TIMEOUT}
- * the no further action is taken. Otherwise, the exit status is examined to ensure it
- * is either OK or WARNING - if not, an {@link ScpException} is thrown
- *
- * @param cmd The attempted remote copy command
- * @param channel The {@link ClientChannel} through which the command was sent - <B>Note:</B>
- * then channel may be in the process of being closed
- * @throws IOException If failed the command
- * @see #handleCommandExitStatus(String, Integer)
- */
- protected void handleCommandExitStatus(String cmd, ClientChannel channel) throws IOException {
- // give a chance for the exit status to be received
- long timeout = channel.getLongProperty(SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT, DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT);
- if (timeout <= 0L) {
- handleCommandExitStatus(cmd, (Integer) null);
- return;
- }
-
- long waitStart = System.nanoTime();
- Collection<ClientChannelEvent> events = channel.waitFor(COMMAND_WAIT_EVENTS, timeout);
- long waitEnd = System.nanoTime();
- if (log.isDebugEnabled()) {
- log.debug("handleCommandExitStatus({}) cmd='{}', waited={} nanos, events={}",
- getClientSession(), cmd, waitEnd - waitStart, events);
- }
-
- /*
- * There are sometimes race conditions in the order in which channels are closed and exit-status
- * sent by the remote peer (if at all), thus there is no guarantee that we will have an exit
- * status here
- */
- handleCommandExitStatus(cmd, channel.getExitStatus());
- }
-
- /**
- * Invoked by the various <code>upload/download</code> methods after having successfully
- * completed the remote copy command and (optionally) having received an exit status
- * from the remote server
- *
- * @param cmd The attempted remote copy command
- * @param exitStatus The exit status - if {@code null} then no status was reported
- * @throws IOException If failed the command
- */
- protected void handleCommandExitStatus(String cmd, Integer exitStatus) throws IOException {
- if (log.isDebugEnabled()) {
- log.debug("handleCommandExitStatus({}) cmd='{}', exit-status={}", getClientSession(), cmd, ScpHelper.getExitStatusName(exitStatus));
- }
-
- if (exitStatus == null) {
- return;
- }
-
- int statusCode = exitStatus;
- switch (statusCode) {
- case ScpHelper.OK: // do nothing
- break;
- case ScpHelper.WARNING:
- log.warn("handleCommandExitStatus({}) cmd='{}' may have terminated with some problems", getClientSession(), cmd);
- break;
- default:
- throw new ScpException("Failed to run command='" + cmd + "': " + ScpHelper.getExitStatusName(exitStatus), exitStatus);
- }
- }
-
- protected Collection<Option> addTargetIsDirectory(Collection<Option> options) {
- if (GenericUtils.isEmpty(options) || (!options.contains(Option.TargetIsDirectory))) {
- // create a copy in case the original collection is un-modifiable
- options = GenericUtils.isEmpty(options) ? EnumSet.noneOf(Option.class) : GenericUtils.of(options);
- options.add(Option.TargetIsDirectory);
- }
-
- return options;
- }
-
- protected ChannelExec openCommandChannel(ClientSession session, String cmd) throws IOException {
- long waitTimeout = session.getLongProperty(SCP_EXEC_CHANNEL_OPEN_TIMEOUT, DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT);
- ChannelExec channel = session.createExecChannel(cmd);
-
- long startTime = System.nanoTime();
- try {
- channel.open().verify(waitTimeout);
- long endTime = System.nanoTime();
- long nanosWait = endTime - startTime;
- if (log.isTraceEnabled()) {
- log.trace("openCommandChannel(" + session + ")[" + cmd + "]"
- + " completed after " + nanosWait
- + " nanos out of " + TimeUnit.MILLISECONDS.toNanos(waitTimeout));
- }
-
- return channel;
- } catch (IOException | RuntimeException e) {
- long endTime = System.nanoTime();
- long nanosWait = endTime - startTime;
- if (log.isTraceEnabled()) {
- log.trace("openCommandChannel(" + session + ")[" + cmd + "]"
- + " failed (" + e.getClass().getSimpleName() + ")"
- + " to complete after " + nanosWait
- + " nanos out of " + TimeUnit.MILLISECONDS.toNanos(waitTimeout)
- + ": " + e.getMessage());
- }
-
- channel.close(false);
- throw e;
- }
- }
-
- @FunctionalInterface
- public interface ScpOperationExecutor<T> {
- void execute(ScpHelper helper, Collection<T> local, Collection<Option> options) throws IOException;
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
deleted file mode 100644
index 40afaf7..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.scp;
-
-import java.nio.channels.Channel;
-
-/**
- * An {@link ScpClient} wrapper that also closes the underlying session
- * when closed
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface CloseableScpClient extends ScpClient, Channel {
- // Marker interface
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
deleted file mode 100644
index 16d0cb2..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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.scp;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.FileSystem;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.Objects;
-
-import org.apache.sshd.client.channel.ChannelExec;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.FactoryManager;
-import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.file.util.MockFileSystem;
-import org.apache.sshd.common.file.util.MockPath;
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTimestamp;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.util.ValidateUtils;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class DefaultScpClient extends AbstractScpClient {
- protected final ScpFileOpener opener;
- protected final ScpTransferEventListener listener;
- private final ClientSession clientSession;
-
- public DefaultScpClient(
- ClientSession clientSession, ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
- this.clientSession = Objects.requireNonNull(clientSession, "No client session");
- this.opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
- this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
- }
-
- @Override
- public ClientSession getClientSession() {
- return clientSession;
- }
-
- @Override
- public void download(String remote, OutputStream local) throws IOException {
- String cmd = ScpClient.createReceiveCommand(remote, Collections.emptyList());
- ClientSession session = getClientSession();
- ChannelExec channel = openCommandChannel(session, cmd);
- try (InputStream invOut = channel.getInvertedOut();
- OutputStream invIn = channel.getInvertedIn()) {
- // NOTE: we use a mock file system since we expect no invocations for it
- ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
- helper.receiveFileStream(local, ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
- handleCommandExitStatus(cmd, channel);
- } finally {
- channel.close(false);
- }
- }
-
- @Override
- protected void download(String remote, FileSystem fs, Path local, Collection<Option> options) throws IOException {
- String cmd = ScpClient.createReceiveCommand(remote, options);
- ClientSession session = getClientSession();
- ChannelExec channel = openCommandChannel(session, cmd);
- try (InputStream invOut = channel.getInvertedOut();
- OutputStream invIn = channel.getInvertedIn()) {
- ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
- helper.receive(local,
- options.contains(Option.Recursive),
- options.contains(Option.TargetIsDirectory),
- options.contains(Option.PreserveAttributes),
- ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
- handleCommandExitStatus(cmd, channel);
- } finally {
- channel.close(false);
- }
- }
-
- @Override
- public void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
- int namePos = ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified").lastIndexOf('/');
- String name = (namePos < 0)
- ? remote
- : ValidateUtils.checkNotNullAndNotEmpty(remote.substring(namePos + 1), "No name value in remote=%s", remote);
- Collection<Option> options = (time != null) ? EnumSet.of(Option.PreserveAttributes) : Collections.emptySet();
- String cmd = ScpClient.createSendCommand(remote, options);
- ClientSession session = getClientSession();
- ChannelExec channel = openCommandChannel(session, cmd);
- try (InputStream invOut = channel.getInvertedOut();
- OutputStream invIn = channel.getInvertedIn()) {
- // NOTE: we use a mock file system since we expect no invocations for it
- ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
- Path mockPath = new MockPath(remote);
- helper.sendStream(new DefaultScpStreamResolver(name, mockPath, perms, time, size, local, cmd),
- options.contains(Option.PreserveAttributes), ScpHelper.DEFAULT_SEND_BUFFER_SIZE);
- handleCommandExitStatus(cmd, channel);
- } finally {
- channel.close(false);
- }
- }
-
- @Override
- protected <T> void runUpload(String remote, Collection<Option> options, Collection<T> local, AbstractScpClient.ScpOperationExecutor<T> executor) throws IOException {
- local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
- remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", remote);
- if (local.size() > 1) {
- options = addTargetIsDirectory(options);
- }
-
- String cmd = ScpClient.createSendCommand(remote, options);
- ClientSession session = getClientSession();
- ChannelExec channel = openCommandChannel(session, cmd);
- try {
- FactoryManager manager = session.getFactoryManager();
- FileSystemFactory factory = manager.getFileSystemFactory();
- FileSystem fs = factory.createFileSystem(session);
-
- try (InputStream invOut = channel.getInvertedOut();
- OutputStream invIn = channel.getInvertedIn()) {
- ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
- executor.execute(helper, local, options);
- } finally {
- try {
- fs.close();
- } catch (UnsupportedOperationException e) {
- if (log.isDebugEnabled()) {
- log.debug("runUpload({}) {} => {} - failed ({}) to close file system={}: {}",
- session, remote, local, e.getClass().getSimpleName(), fs, e.getMessage());
- }
- }
- }
- handleCommandExitStatus(cmd, channel);
- } finally {
- channel.close(false);
- }
- }
-}
-
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
deleted file mode 100644
index e6362b8..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.scp;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Collection;
-
-import org.apache.sshd.common.scp.ScpSourceStreamResolver;
-import org.apache.sshd.common.scp.ScpTimestamp;
-import org.apache.sshd.common.session.Session;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class DefaultScpStreamResolver implements ScpSourceStreamResolver {
- private final String name;
- private final Path mockPath;
- private final Collection<PosixFilePermission> perms;
- private final ScpTimestamp time;
- private final long size;
- private final java.io.InputStream local;
- private final String cmd;
-
- public DefaultScpStreamResolver(String name, Path mockPath, Collection<PosixFilePermission> perms, ScpTimestamp time, long size, InputStream local, String cmd) {
- this.name = name;
- this.mockPath = mockPath;
- this.perms = perms;
- this.time = time;
- this.size = size;
- this.local = local;
- this.cmd = cmd;
- }
-
- @Override
- public String getFileName() throws java.io.IOException {
- return name;
- }
-
- @Override
- public Path getEventListenerFilePath() {
- return mockPath;
- }
-
- @Override
- public Collection<PosixFilePermission> getPermissions() throws IOException {
- return perms;
- }
-
- @Override
- public ScpTimestamp getTimestamp() throws IOException {
- return time;
- }
-
- @Override
- public long getSize() throws IOException {
- return size;
- }
-
- @Override
- public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
- return local;
- }
-
- @Override
- public String toString() {
- return cmd;
- }
-}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java
deleted file mode 100644
index b2a6091..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClient.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * 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.scp;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Collection;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.session.ClientSessionHolder;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTimestamp;
-import org.apache.sshd.common.session.SessionHolder;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpClient extends SessionHolder<ClientSession>, ClientSessionHolder {
- enum Option {
- Recursive,
- PreserveAttributes,
- TargetIsDirectory
- }
-
- /**
- * Configurable value of the {@link org.apache.sshd.common.FactoryManager}
- * for controlling the wait timeout for opening a channel for an SCP command
- * in milliseconds. If not specified, then {@link #DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT}
- * value is used
- */
- String SCP_EXEC_CHANNEL_OPEN_TIMEOUT = "scp-exec-channel-open-timeout";
- long DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT = TimeUnit.SECONDS.toMillis(30L);
-
- /**
- * Configurable value of the {@link org.apache.sshd.common.FactoryManager}
- * for controlling the wait timeout for waiting on a channel exit status'
- * for an SCP command in milliseconds. If not specified, then
- * {@link #DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT}
- * value is used. If non-positive, then no wait is performed and the command
- * is assumed to have completed successfully.
- */
- String SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT = "scp-exec-channel-exit-status-timeout";
- long DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
-
- default void download(String remote, String local, Option... options) throws IOException {
- download(remote, local, GenericUtils.of(options));
- }
-
- void download(String remote, String local, Collection<Option> options) throws IOException;
-
- default void download(String remote, Path local, Option... options) throws IOException {
- download(remote, local, GenericUtils.of(options));
- }
-
- void download(String remote, Path local, Collection<Option> options) throws IOException;
-
- // NOTE: the remote location MUST be a file or an exception is generated
- void download(String remote, OutputStream local) throws IOException;
-
- default byte[] downloadBytes(String remote) throws IOException {
- try (ByteArrayOutputStream local = new ByteArrayOutputStream()) {
- download(remote, local);
- return local.toByteArray();
- }
- }
-
- default void download(String[] remote, String local, Option... options) throws IOException {
- download(remote, local, GenericUtils.of(options));
- }
-
- default void download(String[] remote, Path local, Option... options) throws IOException {
- download(remote, local, GenericUtils.of(options));
- }
-
- void download(String[] remote, String local, Collection<Option> options) throws IOException;
-
- void download(String[] remote, Path local, Collection<Option> options) throws IOException;
-
- default void upload(String local, String remote, Option... options) throws IOException {
- upload(local, remote, GenericUtils.of(options));
- }
-
- default void upload(String local, String remote, Collection<Option> options) throws IOException {
- upload(new String[]{ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local)}, remote, options);
- }
-
- default void upload(Path local, String remote, Option... options) throws IOException {
- upload(local, remote, GenericUtils.of(options));
- }
-
- default void upload(Path local, String remote, Collection<Option> options) throws IOException {
- upload(new Path[]{ValidateUtils.checkNotNull(local, "Invalid local argument: %s", local)}, remote, GenericUtils.of(options));
- }
-
- default void upload(String[] local, String remote, Option... options) throws IOException {
- upload(local, remote, GenericUtils.of(options));
- }
-
- void upload(String[] local, String remote, Collection<Option> options) throws IOException;
-
- default void upload(Path[] local, String remote, Option... options) throws IOException {
- upload(local, remote, GenericUtils.of(options));
- }
-
- void upload(Path[] local, String remote, Collection<Option> options) throws IOException;
-
- // NOTE: due to SCP command limitations, the amount of data to be uploaded must be known a-priori
- // To upload a dynamic amount of data use SFTP
- default void upload(byte[] data, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
- upload(data, 0, data.length, remote, perms, time);
- }
-
- default void upload(byte[] data, int offset, int len, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
- try (InputStream local = new ByteArrayInputStream(data, offset, len)) {
- upload(local, remote, len, perms, time);
- }
- }
-
- void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException;
-
- static String createSendCommand(String remote, Collection<Option> options) {
- StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX);
- if (options.contains(Option.Recursive)) {
- sb.append(" -r");
- }
- if (options.contains(Option.TargetIsDirectory)) {
- sb.append(" -d");
- }
- if (options.contains(Option.PreserveAttributes)) {
- sb.append(" -p");
- }
-
- sb.append(" -t").append(" --").append(" ").append(remote);
- return sb.toString();
- }
-
- static String createReceiveCommand(String remote, Collection<Option> options) {
- ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified");
- StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX);
- if (options.contains(Option.Recursive)) {
- sb.append(" -r");
- }
- if (options.contains(Option.PreserveAttributes)) {
- sb.append(" -p");
- }
-
- sb.append(" -f").append(" --").append(' ').append(remote);
- return sb.toString();
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java b/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
deleted file mode 100644
index 8ae54a4..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.scp;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpFileOpenerHolder;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpClientCreator extends ScpFileOpenerHolder {
- /**
- * Create an SCP client from this session.
- *
- * @return An {@link ScpClient} instance. <B>Note:</B> uses the currently
- * registered {@link ScpTransferEventListener} and {@link ScpFileOpener} if any
- * @see #setScpFileOpener(ScpFileOpener)
- * @see #setScpTransferEventListener(ScpTransferEventListener)
- */
- default ScpClient createScpClient() {
- return createScpClient(getScpFileOpener(), getScpTransferEventListener());
- }
-
- /**
- * Create an SCP client from this session.
- *
- * @param listener A {@link ScpTransferEventListener} that can be used
- * to receive information about the SCP operations - may be {@code null}
- * to indicate no more events are required. <B>Note:</B> this listener
- * is used <U>instead</U> of any listener set via {@link #setScpTransferEventListener(ScpTransferEventListener)}
- * @return An {@link ScpClient} instance
- */
- default ScpClient createScpClient(ScpTransferEventListener listener) {
- return createScpClient(getScpFileOpener(), listener);
- }
-
- /**
- * Create an SCP client from this session.
- *
- * @param opener The {@link ScpFileOpener} to use to control how local files
- * are read/written. If {@code null} then a default opener is used.
- * <B>Note:</B> this opener is used <U>instead</U> of any instance
- * set via {@link #setScpFileOpener(ScpFileOpener)}
- * @return An {@link ScpClient} instance
- */
- default ScpClient createScpClient(ScpFileOpener opener) {
- return createScpClient(opener, getScpTransferEventListener());
- }
-
- /**
- * Create an SCP client from this session.
- *
- * @param opener The {@link ScpFileOpener} to use to control how local files
- * are read/written. If {@code null} then a default opener is used.
- * <B>Note:</B> this opener is used <U>instead</U> of any instance
- * set via {@link #setScpFileOpener(ScpFileOpener)}
- * @param listener A {@link ScpTransferEventListener} that can be used
- * to receive information about the SCP operations - may be {@code null}
- * to indicate no more events are required. <B>Note:</B> this listener
- * is used <U>instead</U> of any listener set via
- * {@link #setScpTransferEventListener(ScpTransferEventListener)}
- * @return An {@link ScpClient} instance
- */
- ScpClient createScpClient(ScpFileOpener opener, ScpTransferEventListener listener);
-
- /**
- * @return The last {@link ScpTransferEventListener} set via
- * {@link #setScpTransferEventListener(ScpTransferEventListener)}
- */
- ScpTransferEventListener getScpTransferEventListener();
-
- /**
- * @param listener A default {@link ScpTransferEventListener} that can be used
- * to receive information about the SCP operations - may be {@code null}
- * to indicate no more events are required
- * @see #createScpClient(ScpTransferEventListener)
- */
- void setScpTransferEventListener(ScpTransferEventListener listener);
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index d57a799..1cb309a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -40,8 +40,6 @@ import org.apache.sshd.client.channel.ChannelShell;
import org.apache.sshd.client.channel.ChannelSubsystem;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
-import org.apache.sshd.client.scp.DefaultScpClient;
-import org.apache.sshd.client.scp.ScpClient;
import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.NamedResource;
@@ -59,8 +57,6 @@ import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.IoWriteFuture;
import org.apache.sshd.common.kex.KexProposalOption;
import org.apache.sshd.common.kex.KexState;
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
import org.apache.sshd.common.session.ConnectionService;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.session.helpers.AbstractConnectionService;
@@ -82,8 +78,6 @@ public abstract class AbstractClientSession extends AbstractSession implements C
private UserInteraction userInteraction;
private PasswordIdentityProvider passwordIdentityProvider;
private List<NamedFactory<UserAuth>> userAuthFactories;
- private ScpTransferEventListener scpListener;
- private ScpFileOpener scpOpener;
private SocketAddress connectAddress;
private ClientProxyConnector proxyConnector;
@@ -301,31 +295,6 @@ public abstract class AbstractClientSession extends AbstractSession implements C
}
@Override
- public ScpFileOpener getScpFileOpener() {
- return resolveEffectiveProvider(ScpFileOpener.class, scpOpener, getFactoryManager().getScpFileOpener());
- }
-
- @Override
- public void setScpFileOpener(ScpFileOpener opener) {
- scpOpener = opener;
- }
-
- @Override
- public ScpTransferEventListener getScpTransferEventListener() {
- return scpListener;
- }
-
- @Override
- public void setScpTransferEventListener(ScpTransferEventListener listener) {
- scpListener = listener;
- }
-
- @Override
- public ScpClient createScpClient(ScpFileOpener opener, ScpTransferEventListener listener) {
- return new DefaultScpClient(this, opener, listener);
- }
-
- @Override
public SshdSocketAddress startLocalPortForwarding(SshdSocketAddress local, SshdSocketAddress remote) throws IOException {
ForwardingFilter filter = getForwardingFilter();
return filter.startLocalPortForwarding(local, remote);
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index 96caf2e..1d68a20 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -42,7 +42,6 @@ import org.apache.sshd.client.channel.ChannelSubsystem;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.channel.ClientChannelEvent;
import org.apache.sshd.client.future.AuthFuture;
-import org.apache.sshd.client.scp.ScpClientCreator;
import org.apache.sshd.client.session.forward.DynamicPortForwardingTracker;
import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
import org.apache.sshd.common.forward.PortForwardingManager;
@@ -81,9 +80,8 @@ import org.apache.sshd.common.util.net.SshdSocketAddress;
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public interface ClientSession
- extends Session, ScpClientCreator,
- ClientProxyConnectorHolder, ClientAuthenticationManager,
- PortForwardingManager {
+ extends Session, ClientProxyConnectorHolder,
+ ClientAuthenticationManager, PortForwardingManager {
enum ClientSessionEvent {
TIMEOUT,
CLOSED,
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
index 247be60..dc5ef87 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClient.java
@@ -19,19 +19,6 @@
package org.apache.sshd.client.simple;
-import java.io.IOException;
-import java.lang.reflect.Proxy;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-import java.security.KeyPair;
-import java.util.Objects;
-
-import org.apache.sshd.client.scp.CloseableScpClient;
-import org.apache.sshd.client.scp.ScpClient;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
/**
@@ -41,97 +28,4 @@ public abstract class AbstractSimpleClient extends AbstractLoggingBean implement
protected AbstractSimpleClient() {
super();
}
-
- @Override
- public CloseableScpClient scpLogin(String host, String username, String password) throws IOException {
- return scpLogin(host, DEFAULT_PORT, username, password);
- }
-
- @Override
- public CloseableScpClient scpLogin(String host, int port, String username, String password) throws IOException {
- return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
- }
-
- @Override
- public CloseableScpClient scpLogin(String host, String username, KeyPair identity) throws IOException {
- return scpLogin(host, DEFAULT_PORT, username, identity);
- }
-
- @Override
- public CloseableScpClient scpLogin(String host, int port, String username, KeyPair identity) throws IOException {
- return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
- }
-
- @Override
- public CloseableScpClient scpLogin(InetAddress host, String username, String password) throws IOException {
- return scpLogin(host, DEFAULT_PORT, username, password);
- }
-
- @Override
- public CloseableScpClient scpLogin(InetAddress host, int port, String username, String password) throws IOException {
- return scpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, password);
- }
-
- @Override
- public CloseableScpClient scpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
- return scpLogin(host, DEFAULT_PORT, username, identity);
- }
-
- @Override
- public CloseableScpClient scpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException {
- return scpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, identity);
- }
-
- @Override
- public CloseableScpClient scpLogin(SocketAddress target, String username, String password) throws IOException {
- return createScpClient(sessionLogin(target, username, password));
- }
-
- @Override
- public CloseableScpClient scpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
- return createScpClient(sessionLogin(target, username, identity));
- }
-
- protected CloseableScpClient createScpClient(ClientSession session) throws IOException {
- try {
- ScpClient client = Objects.requireNonNull(session, "No client session").createScpClient();
- ClassLoader loader = getClass().getClassLoader();
- Class<?>[] interfaces = {CloseableScpClient.class};
- return (CloseableScpClient) Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
- String name = method.getName();
- try {
- // The Channel implementation is provided by the session
- if (("close".equals(name) || "isOpen".equals(name)) && GenericUtils.isEmpty(args)) {
- return method.invoke(session, args);
- } else {
- return method.invoke(client, args);
- }
- } catch (Throwable t) {
- if (log.isTraceEnabled()) {
- log.trace("invoke(CloseableScpClient#{}) failed ({}) to execute: {}",
- name, t.getClass().getSimpleName(), t.getMessage());
- }
- throw t;
- }
- });
- } catch (Exception e) {
- log.warn("createScpClient({}) failed ({}) to create proxy: {}",
- session, e.getClass().getSimpleName(), e.getMessage());
- try {
- session.close();
- } catch (Exception t) {
- if (log.isDebugEnabled()) {
- log.debug("createScpClient({}) failed ({}) to close session: {}",
- session, t.getClass().getSimpleName(), t.getMessage());
- }
-
- if (log.isTraceEnabled()) {
- log.trace("createScpClient(" + session + ") session close failure details", t);
- }
- e.addSuppressed(t);
- }
-
- throw GenericUtils.toIOException(e);
- }
- }
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
index 6fff133..174f600 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleClient.java
@@ -31,7 +31,6 @@ import java.nio.channels.Channel;
public interface SimpleClient
extends SimpleClientConfigurator,
SimpleSessionClient,
- SimpleScpClient,
Channel {
// marker interface
}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java
deleted file mode 100644
index eeeb7a2..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleScpClient.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * 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.net.InetAddress;
-import java.net.SocketAddress;
-import java.nio.channels.Channel;
-import java.security.KeyPair;
-
-import org.apache.sshd.client.scp.CloseableScpClient;
-
-/**
- * A simplified <U>synchronous</U> API for obtaining SCP sessions.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface SimpleScpClient extends SimpleClientConfigurator, Channel {
- /**
- * Creates an SCP session on the default port and logs in using the provided credentials
- *
- * @param host The target host name or address
- * @param username Username
- * @param password Password
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(String host, String username, String password) throws IOException;
-
- /**
- * Creates an SCP session using the provided credentials
- *
- * @param host The target host name or address
- * @param port The target port
- * @param username Username
- * @param password Password
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(String host, int port, String username, String password) throws IOException;
-
- /**
- * Creates an SCP session on the default port and logs in using the provided credentials
- *
- * @param host The target host name or address
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(String host, String username, KeyPair identity) throws IOException;
-
- /**
- * Creates an SCP session using the provided credentials
- *
- * @param host The target host name or address
- * @param port The target port
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(String host, int port, String username, KeyPair identity) throws IOException;
-
- /**
- * Creates an SCP session on the default port and logs in using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param username Username
- * @param password Password
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(InetAddress host, String username, String password) throws IOException;
-
- /**
- * Creates an SCP session using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param port The target port
- * @param username Username
- * @param password Password
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(InetAddress host, int port, String username, String password) throws IOException;
-
- /**
- * Creates an SCP session on the default port and logs in using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(InetAddress host, String username, KeyPair identity) throws IOException;
-
- /**
- * Creates an SCP session using the provided credentials
- *
- * @param host The target host {@link InetAddress}
- * @param port The target port
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException;
-
- /**
- * Creates an SCP session using the provided credentials
- *
- * @param target The target {@link SocketAddress}
- * @param username Username
- * @param password Password
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(SocketAddress target, String username, String password) throws IOException;
-
- /**
- * Creates an SCP session using the provided credentials
- *
- * @param target The target {@link SocketAddress}
- * @param username Username
- * @param identity The {@link KeyPair} identity
- * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
- * underlying session
- * @throws IOException If failed to login or authenticate
- */
- CloseableScpClient scpLogin(SocketAddress target, String username, KeyPair identity) throws IOException;
-}
[3/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
new file mode 100644
index 0000000..1cbea2d
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
@@ -0,0 +1,837 @@
+/*
+ * 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.common.scp;
+
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.file.util.MockPath;
+import org.apache.sshd.common.scp.ScpTransferEventListener.FileOperation;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.io.LimitInputStream;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@SuppressWarnings("PMD.AvoidUsingOctalValues")
+public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Session> {
+ /**
+ * Command prefix used to identify SCP commands
+ */
+ public static final String SCP_COMMAND_PREFIX = "scp";
+
+ public static final int OK = 0;
+ public static final int WARNING = 1;
+ public static final int ERROR = 2;
+
+ /**
+ * Default size (in bytes) of send / receive buffer size
+ */
+ public static final int DEFAULT_COPY_BUFFER_SIZE = IoUtils.DEFAULT_COPY_SIZE;
+ public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
+ public static final int DEFAULT_SEND_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
+
+ /**
+ * The minimum size for sending / receiving files
+ */
+ public static final int MIN_COPY_BUFFER_SIZE = Byte.MAX_VALUE;
+ public static final int MIN_RECEIVE_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
+ public static final int MIN_SEND_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
+
+ public static final int S_IRUSR = 0000400;
+ public static final int S_IWUSR = 0000200;
+ public static final int S_IXUSR = 0000100;
+ public static final int S_IRGRP = 0000040;
+ public static final int S_IWGRP = 0000020;
+ public static final int S_IXGRP = 0000010;
+ public static final int S_IROTH = 0000004;
+ public static final int S_IWOTH = 0000002;
+ public static final int S_IXOTH = 0000001;
+
+ public static final String DEFAULT_DIR_OCTAL_PERMISSIONS = "0755";
+ public static final String DEFAULT_FILE_OCTAL_PERMISSIONS = "0644";
+
+ protected final InputStream in;
+ protected final OutputStream out;
+ protected final FileSystem fileSystem;
+ protected final ScpFileOpener opener;
+ protected final ScpTransferEventListener listener;
+
+ private final Session sessionInstance;
+
+ public ScpHelper(Session session, InputStream in, OutputStream out,
+ FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) {
+ this.sessionInstance = Objects.requireNonNull(session, "No session");
+ this.in = Objects.requireNonNull(in, "No input stream");
+ this.out = Objects.requireNonNull(out, "No output stream");
+ this.fileSystem = fileSystem;
+ this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
+ this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
+ }
+
+ @Override
+ public Session getSession() {
+ return sessionInstance;
+ }
+
+ public void receiveFileStream(OutputStream local, int bufferSize) throws IOException {
+ receive((line, isDir, timestamp) -> {
+ if (isDir) {
+ throw new StreamCorruptedException("Cannot download a directory into a file stream: " + line);
+ }
+
+ Path path = new MockPath(line);
+ receiveStream(line, new ScpTargetStreamResolver() {
+ @Override
+ @SuppressWarnings("synthetic-access")
+ public OutputStream resolveTargetStream(
+ Session session, String name, long length, Set<PosixFilePermission> perms, OpenOption... options)
+ throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("resolveTargetStream({}) name={}, perms={}, len={} - started local stream download",
+ ScpHelper.this, name, perms, length);
+ }
+ return local;
+ }
+
+ @Override
+ public Path getEventListenerFilePath() {
+ return path;
+ }
+
+ @Override
+ @SuppressWarnings("synthetic-access")
+ public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("postProcessReceivedData({}) name={}, perms={}, preserve={} time={}",
+ ScpHelper.this, name, perms, preserve, time);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return line;
+ }
+ }, timestamp, false, bufferSize);
+ });
+ }
+
+ public void receive(Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize) throws IOException {
+ Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+ Path path = opener.resolveIncomingReceiveLocation(localPath, recursive, shouldBeDir, preserve);
+ receive((line, isDir, time) -> {
+ if (recursive && isDir) {
+ receiveDir(line, path, time, preserve, bufferSize);
+ } else {
+ receiveFile(line, path, time, preserve, bufferSize);
+ }
+ });
+ }
+
+ protected void receive(ScpReceiveLineHandler handler) throws IOException {
+ ack();
+ ScpTimestamp time = null;
+ for (;;) {
+ String line;
+ boolean isDir = false;
+ int c = readAck(true);
+ switch (c) {
+ case -1:
+ return;
+ case 'D':
+ isDir = true;
+ line = String.valueOf((char) c) + readLine();
+ if (log.isDebugEnabled()) {
+ log.debug("receive({}) - Received 'D' header: {}", this, line);
+ }
+ break;
+ case 'C':
+ line = String.valueOf((char) c) + readLine();
+ if (log.isDebugEnabled()) {
+ log.debug("receive({}) - Received 'C' header: {}", this, line);
+ }
+ break;
+ case 'T':
+ line = String.valueOf((char) c) + readLine();
+ if (log.isDebugEnabled()) {
+ log.debug("receive({}) - Received 'T' header: {}", this, line);
+ }
+ time = ScpTimestamp.parseTime(line);
+ ack();
+ continue;
+ case 'E':
+ line = String.valueOf((char) c) + readLine();
+ if (log.isDebugEnabled()) {
+ log.debug("receive({}) - Received 'E' header: {}", this, line);
+ }
+ ack();
+ return;
+ default:
+ //a real ack that has been acted upon already
+ continue;
+ }
+
+ try {
+ handler.process(line, isDir, time);
+ } finally {
+ time = null;
+ }
+ }
+ }
+
+ public void receiveDir(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
+ Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+ if (log.isDebugEnabled()) {
+ log.debug("receiveDir({})[{}] Receiving directory {} - preserve={}, time={}, buffer-size={}",
+ this, header, path, preserve, time, bufferSize);
+ }
+ if (!header.startsWith("D")) {
+ throw new IOException("Expected a 'D; message but got '" + header + "'");
+ }
+
+ Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
+ int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
+ String name = header.substring(header.indexOf(' ', 6) + 1);
+ if (length != 0) {
+ throw new IOException("Expected 0 length for directory=" + name + " but got " + length);
+ }
+
+ Path file = opener.resolveIncomingFilePath(path, name, preserve, perms, time);
+
+ ack();
+
+ time = null;
+ listener.startFolderEvent(FileOperation.RECEIVE, path, perms);
+ try {
+ for (;;) {
+ header = readLine();
+ if (log.isDebugEnabled()) {
+ log.debug("receiveDir({})[{}] Received header: {}", this, file, header);
+ }
+ if (header.startsWith("C")) {
+ receiveFile(header, file, time, preserve, bufferSize);
+ time = null;
+ } else if (header.startsWith("D")) {
+ receiveDir(header, file, time, preserve, bufferSize);
+ time = null;
+ } else if (header.equals("E")) {
+ ack();
+ break;
+ } else if (header.startsWith("T")) {
+ time = ScpTimestamp.parseTime(header);
+ ack();
+ } else {
+ throw new IOException("Unexpected message: '" + header + "'");
+ }
+ }
+ } catch (IOException | RuntimeException e) {
+ listener.endFolderEvent(FileOperation.RECEIVE, path, perms, e);
+ throw e;
+ }
+ listener.endFolderEvent(FileOperation.RECEIVE, path, perms, null);
+ }
+
+ public void receiveFile(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
+ Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+ if (log.isDebugEnabled()) {
+ log.debug("receiveFile({})[{}] Receiving file {} - preserve={}, time={}, buffer-size={}",
+ this, header, path, preserve, time, bufferSize);
+ }
+
+ receiveStream(header, opener.createScpTargetStreamResolver(path), time, preserve, bufferSize);
+ }
+
+ public void receiveStream(String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
+ if (!header.startsWith("C")) {
+ throw new IOException("receiveStream(" + resolver + ") Expected a C message but got '" + header + "'");
+ }
+
+ if (bufferSize < MIN_RECEIVE_BUFFER_SIZE) {
+ throw new IOException("receiveStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_RECEIVE_BUFFER_SIZE + ")");
+ }
+
+ Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
+ long length = Long.parseLong(header.substring(6, header.indexOf(' ', 6)));
+ String name = header.substring(header.indexOf(' ', 6) + 1);
+ if (length < 0L) { // TODO consider throwing an exception...
+ log.warn("receiveStream({})[{}] bad length in header: {}", this, resolver, header);
+ }
+
+ // if file size is less than buffer size allocate only expected file size
+ int bufSize;
+ boolean debugEnabled = log.isDebugEnabled();
+ if (length == 0L) {
+ if (debugEnabled) {
+ log.debug("receiveStream({})[{}] zero file size (perhaps special file) using copy buffer size={}",
+ this, resolver, MIN_RECEIVE_BUFFER_SIZE);
+ }
+ bufSize = MIN_RECEIVE_BUFFER_SIZE;
+ } else {
+ bufSize = (int) Math.min(length, bufferSize);
+ }
+
+ if (bufSize < 0) { // TODO consider throwing an exception
+ log.warn("receiveStream({})[{}] bad buffer size ({}) using default ({})",
+ this, resolver, bufSize, MIN_RECEIVE_BUFFER_SIZE);
+ bufSize = MIN_RECEIVE_BUFFER_SIZE;
+ }
+
+ try (
+ InputStream is = new LimitInputStream(this.in, length);
+ OutputStream os = resolver.resolveTargetStream(getSession(), name, length, perms)
+ ) {
+ ack();
+
+ Path file = resolver.getEventListenerFilePath();
+ listener.startFileEvent(FileOperation.RECEIVE, file, length, perms);
+ try {
+ IoUtils.copy(is, os, bufSize);
+ } catch (IOException | RuntimeException e) {
+ listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, e);
+ throw e;
+ }
+ listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, null);
+ }
+
+ resolver.postProcessReceivedData(name, preserve, perms, time);
+
+ ack();
+
+ int replyCode = readAck(false);
+ if (debugEnabled) {
+ log.debug("receiveStream({})[{}] ack reply code={}", this, resolver, replyCode);
+ }
+ validateAckReplyCode("receiveStream", resolver, replyCode, false);
+ }
+
+ public String readLine() throws IOException {
+ return readLine(false);
+ }
+
+ public String readLine(boolean canEof) throws IOException {
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Byte.MAX_VALUE)) {
+ for (;;) {
+ int c = in.read();
+ if (c == '\n') {
+ return baos.toString(StandardCharsets.UTF_8.name());
+ } else if (c == -1) {
+ if (!canEof) {
+ throw new EOFException("EOF while await end of line");
+ }
+ return null;
+ } else {
+ baos.write(c);
+ }
+ }
+ }
+ }
+
+ public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
+ int readyCode = readAck(false);
+ boolean debugEnabled = log.isDebugEnabled();
+ if (debugEnabled) {
+ log.debug("send({}) ready code={}", paths, readyCode);
+ }
+ validateOperationReadyCode("send", "Paths", readyCode, false);
+
+ LinkOption[] options = IoUtils.getLinkOptions(true);
+ for (String pattern : paths) {
+ pattern = pattern.replace('/', File.separatorChar);
+
+ int idx = pattern.indexOf('*'); // check if wildcard used
+ if (idx >= 0) {
+ String basedir = "";
+ String fixedPart = pattern.substring(0, idx);
+ int lastSep = fixedPart.lastIndexOf(File.separatorChar);
+ if (lastSep >= 0) {
+ basedir = pattern.substring(0, lastSep);
+ pattern = pattern.substring(lastSep + 1);
+ }
+
+ Iterable<String> included = opener.getMatchingFilesToSend(basedir, pattern);
+ for (String path : included) {
+ Path file = resolveLocalPath(basedir, path);
+ if (opener.sendAsRegularFile(file, options)) {
+ sendFile(file, preserve, bufferSize);
+ } else if (opener.sendAsDirectory(file, options)) {
+ if (!recursive) {
+ if (debugEnabled) {
+ log.debug("send({}) {}: not a regular file", this, path);
+ }
+ sendWarning(path.replace(File.separatorChar, '/') + " not a regular file");
+ } else {
+ sendDir(file, preserve, bufferSize);
+ }
+ } else {
+ if (debugEnabled) {
+ log.debug("send({}) {}: unknown file type", this, path);
+ }
+ sendWarning(path.replace(File.separatorChar, '/') + " unknown file type");
+ }
+ }
+ } else {
+ send(resolveLocalPath(pattern), recursive, preserve, bufferSize, options);
+ }
+ }
+ }
+
+ public void sendPaths(Collection<? extends Path> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
+ int readyCode = readAck(false);
+ if (log.isDebugEnabled()) {
+ log.debug("sendPaths({}) ready code={}", paths, readyCode);
+ }
+ validateOperationReadyCode("sendPaths", "Paths", readyCode, false);
+
+ LinkOption[] options = IoUtils.getLinkOptions(true);
+ for (Path file : paths) {
+ send(file, recursive, preserve, bufferSize, options);
+ }
+ }
+
+ protected void send(Path local, boolean recursive, boolean preserve, int bufferSize, LinkOption... options) throws IOException {
+ Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+ Path file = opener.resolveOutgoingFilePath(localPath, options);
+ if (opener.sendAsRegularFile(file, options)) {
+ sendFile(file, preserve, bufferSize);
+ } else if (opener.sendAsDirectory(file, options)) {
+ if (!recursive) {
+ throw new IOException(file + " not a regular file");
+ } else {
+ sendDir(file, preserve, bufferSize);
+ }
+ } else {
+ throw new IOException(file + ": unknown file type");
+ }
+ }
+
+ public Path resolveLocalPath(String basedir, String subpath) throws IOException {
+ if (GenericUtils.isEmpty(basedir)) {
+ return resolveLocalPath(subpath);
+ } else {
+ return resolveLocalPath(basedir + File.separator + subpath);
+ }
+ }
+
+ /**
+ * @param commandPath The command path using the <U>local</U> file separator
+ * @return The resolved absolute and normalized local {@link Path}
+ * @throws IOException If failed to resolve the path
+ * @throws InvalidPathException If invalid local path value
+ */
+ public Path resolveLocalPath(String commandPath) throws IOException, InvalidPathException {
+ Path p = opener.resolveLocalPath(fileSystem, commandPath);
+ if (log.isTraceEnabled()) {
+ log.trace("resolveLocalPath({}) {}: {}", this, commandPath, p);
+ }
+
+ return p;
+ }
+
+ public void sendFile(Path local, boolean preserve, int bufferSize) throws IOException {
+ Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+ if (log.isDebugEnabled()) {
+ log.debug("sendFile({})[preserve={},buffer-size={}] Sending file {}", this, preserve, bufferSize, path);
+ }
+
+ sendStream(opener.createScpSourceStreamResolver(path), preserve, bufferSize);
+ }
+
+ public void sendStream(ScpSourceStreamResolver resolver, boolean preserve, int bufferSize) throws IOException {
+ if (bufferSize < MIN_SEND_BUFFER_SIZE) {
+ throw new IOException("sendStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_SEND_BUFFER_SIZE + ")");
+ }
+
+ long fileSize = resolver.getSize();
+ // if file size is less than buffer size allocate only expected file size
+ int bufSize;
+ boolean debugEnabled = log.isDebugEnabled();
+ if (fileSize <= 0L) {
+ if (debugEnabled) {
+ log.debug("sendStream({})[{}] unknown file size ({}) perhaps special file - using copy buffer size={}",
+ this, resolver, fileSize, MIN_SEND_BUFFER_SIZE);
+ }
+ bufSize = MIN_SEND_BUFFER_SIZE;
+ } else {
+ bufSize = (int) Math.min(fileSize, bufferSize);
+ }
+
+ if (bufSize < 0) { // TODO consider throwing an exception
+ log.warn("sendStream({})[{}] bad buffer size ({}) using default ({})",
+ this, resolver, bufSize, MIN_SEND_BUFFER_SIZE);
+ bufSize = MIN_SEND_BUFFER_SIZE;
+ }
+
+ ScpTimestamp time = resolver.getTimestamp();
+ if (preserve && (time != null)) {
+ String cmd = "T" + TimeUnit.MILLISECONDS.toSeconds(time.getLastModifiedTime())
+ + " " + "0" + " " + TimeUnit.MILLISECONDS.toSeconds(time.getLastAccessTime())
+ + " " + "0";
+ if (debugEnabled) {
+ log.debug("sendStream({})[{}] send timestamp={} command: {}", this, resolver, time, cmd);
+ }
+ out.write(cmd.getBytes(StandardCharsets.UTF_8));
+ out.write('\n');
+ out.flush();
+
+ int readyCode = readAck(false);
+ if (debugEnabled) {
+ log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, cmd, readyCode);
+ }
+ validateAckReplyCode(cmd, resolver, readyCode, false);
+ }
+
+ Set<PosixFilePermission> perms = EnumSet.copyOf(resolver.getPermissions());
+ String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_FILE_OCTAL_PERMISSIONS : getOctalPermissions(perms);
+ String fileName = resolver.getFileName();
+ String cmd = "C" + octalPerms + " " + fileSize + " " + fileName;
+ if (debugEnabled) {
+ log.debug("sendStream({})[{}] send 'C' command: {}", this, resolver, cmd);
+ }
+ out.write(cmd.getBytes(StandardCharsets.UTF_8));
+ out.write('\n');
+ out.flush();
+
+ int readyCode = readAck(false);
+ if (debugEnabled) {
+ log.debug("sendStream({})[{}] command='{}' ready code={}",
+ this, resolver, cmd.substring(0, cmd.length() - 1), readyCode);
+ }
+ validateAckReplyCode(cmd, resolver, readyCode, false);
+
+ try (InputStream in = resolver.resolveSourceStream(getSession())) {
+ Path path = resolver.getEventListenerFilePath();
+ listener.startFileEvent(FileOperation.SEND, path, fileSize, perms);
+ try {
+ IoUtils.copy(in, out, bufSize);
+ } catch (IOException | RuntimeException e) {
+ listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, e);
+ throw e;
+ }
+ listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, null);
+ }
+ ack();
+
+ readyCode = readAck(false);
+ if (debugEnabled) {
+ log.debug("sendStream({})[{}] command='{}' reply code={}", this, resolver, cmd, readyCode);
+ }
+ validateAckReplyCode("sendStream", resolver, readyCode, false);
+ }
+
+ protected void validateOperationReadyCode(String command, Object location, int readyCode, boolean eofAllowed) throws IOException {
+ validateCommandStatusCode(command, location, readyCode, eofAllowed);
+ }
+
+ protected void validateAckReplyCode(String command, Object location, int replyCode, boolean eofAllowed) throws IOException {
+ validateCommandStatusCode(command, location, replyCode, eofAllowed);
+ }
+
+ protected void validateCommandStatusCode(String command, Object location, int statusCode, boolean eofAllowed) throws IOException {
+ switch (statusCode) {
+ case -1:
+ if (!eofAllowed) {
+ throw new EOFException("Unexpected EOF for command='" + command + "' on " + location);
+ }
+ break;
+ case OK:
+ break;
+ case WARNING:
+ break;
+ default:
+ throw new ScpException("Bad reply code (" + statusCode + ") for command='" + command + "' on " + location, statusCode);
+ }
+ }
+
+ public void sendDir(Path local, boolean preserve, int bufferSize) throws IOException {
+ Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
+ boolean debugEnabled = log.isDebugEnabled();
+ if (debugEnabled) {
+ log.debug("sendDir({}) Sending directory {} - preserve={}, buffer-size={}",
+ this, path, preserve, bufferSize);
+ }
+
+ LinkOption[] options = IoUtils.getLinkOptions(true);
+ if (preserve) {
+ BasicFileAttributes basic = opener.getLocalBasicFileAttributes(path, options);
+ FileTime lastModified = basic.lastModifiedTime();
+ FileTime lastAccess = basic.lastAccessTime();
+ String cmd = "T" + lastModified.to(TimeUnit.SECONDS) + " "
+ + "0" + " " + lastAccess.to(TimeUnit.SECONDS) + " "
+ + "0";
+ if (debugEnabled) {
+ log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}",
+ this, path, lastModified, lastAccess, cmd);
+ }
+
+ out.write(cmd.getBytes(StandardCharsets.UTF_8));
+ out.write('\n');
+ out.flush();
+
+ int readyCode = readAck(false);
+ if (debugEnabled) {
+ if (debugEnabled) {
+ log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode);
+ }
+ }
+ validateAckReplyCode(cmd, path, readyCode, false);
+ }
+
+ Set<PosixFilePermission> perms = opener.getLocalFilePermissions(path, options);
+ String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_DIR_OCTAL_PERMISSIONS : getOctalPermissions(perms);
+ String cmd = "D" + octalPerms + " " + "0" + " " + Objects.toString(path.getFileName(), null);
+ if (debugEnabled) {
+ log.debug("sendDir({})[{}] send 'D' command: {}", this, path, cmd);
+ }
+ out.write(cmd.getBytes(StandardCharsets.UTF_8));
+ out.write('\n');
+ out.flush();
+
+ int readyCode = readAck(false);
+ if (debugEnabled) {
+ log.debug("sendDir({})[{}] command='{}' ready code={}",
+ this, path, cmd.substring(0, cmd.length() - 1), readyCode);
+ }
+ validateAckReplyCode(cmd, path, readyCode, false);
+
+ try (DirectoryStream<Path> children = opener.getLocalFolderChildren(path)) {
+ listener.startFolderEvent(FileOperation.SEND, path, perms);
+
+ try {
+ for (Path child : children) {
+ if (opener.sendAsRegularFile(child, options)) {
+ sendFile(child, preserve, bufferSize);
+ } else if (opener.sendAsDirectory(child, options)) {
+ sendDir(child, preserve, bufferSize);
+ }
+ }
+ } catch (IOException | RuntimeException e) {
+ listener.endFolderEvent(FileOperation.SEND, path, perms, e);
+ throw e;
+ }
+
+ listener.endFolderEvent(FileOperation.SEND, path, perms, null);
+ }
+
+ if (debugEnabled) {
+ log.debug("sendDir({})[{}] send 'E' command", this, path);
+ }
+ out.write("E\n".getBytes(StandardCharsets.UTF_8));
+ out.flush();
+
+ readyCode = readAck(false);
+ if (debugEnabled) {
+ log.debug("sendDir({})[{}] 'E' command reply code=", this, path, readyCode);
+ }
+ validateAckReplyCode("E", path, readyCode, false);
+ }
+
+ public static String getOctalPermissions(Collection<PosixFilePermission> perms) {
+ int pf = 0;
+
+ for (PosixFilePermission p : perms) {
+ switch (p) {
+ case OWNER_READ:
+ pf |= S_IRUSR;
+ break;
+ case OWNER_WRITE:
+ pf |= S_IWUSR;
+ break;
+ case OWNER_EXECUTE:
+ pf |= S_IXUSR;
+ break;
+ case GROUP_READ:
+ pf |= S_IRGRP;
+ break;
+ case GROUP_WRITE:
+ pf |= S_IWGRP;
+ break;
+ case GROUP_EXECUTE:
+ pf |= S_IXGRP;
+ break;
+ case OTHERS_READ:
+ pf |= S_IROTH;
+ break;
+ case OTHERS_WRITE:
+ pf |= S_IWOTH;
+ break;
+ case OTHERS_EXECUTE:
+ pf |= S_IXOTH;
+ break;
+ default: // ignored
+ }
+ }
+
+ return String.format("%04o", pf);
+ }
+
+ public static Set<PosixFilePermission> parseOctalPermissions(String str) {
+ int perms = Integer.parseInt(str, 8);
+ Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class);
+ if ((perms & S_IRUSR) != 0) {
+ p.add(PosixFilePermission.OWNER_READ);
+ }
+ if ((perms & S_IWUSR) != 0) {
+ p.add(PosixFilePermission.OWNER_WRITE);
+ }
+ if ((perms & S_IXUSR) != 0) {
+ p.add(PosixFilePermission.OWNER_EXECUTE);
+ }
+ if ((perms & S_IRGRP) != 0) {
+ p.add(PosixFilePermission.GROUP_READ);
+ }
+ if ((perms & S_IWGRP) != 0) {
+ p.add(PosixFilePermission.GROUP_WRITE);
+ }
+ if ((perms & S_IXGRP) != 0) {
+ p.add(PosixFilePermission.GROUP_EXECUTE);
+ }
+ if ((perms & S_IROTH) != 0) {
+ p.add(PosixFilePermission.OTHERS_READ);
+ }
+ if ((perms & S_IWOTH) != 0) {
+ p.add(PosixFilePermission.OTHERS_WRITE);
+ }
+ if ((perms & S_IXOTH) != 0) {
+ p.add(PosixFilePermission.OTHERS_EXECUTE);
+ }
+
+ return p;
+ }
+
+ protected void sendWarning(String message) throws IOException {
+ sendResponseMessage(WARNING, message);
+ }
+
+ protected void sendError(String message) throws IOException {
+ sendResponseMessage(ERROR, message);
+ }
+
+ protected void sendResponseMessage(int level, String message) throws IOException {
+ sendResponseMessage(out, level, message);
+ }
+
+ public static <O extends OutputStream> O sendWarning(O out, String message) throws IOException {
+ return sendResponseMessage(out, WARNING, message);
+ }
+
+ public static <O extends OutputStream> O sendError(O out, String message) throws IOException {
+ return sendResponseMessage(out, ERROR, message);
+ }
+
+ public static <O extends OutputStream> O sendResponseMessage(O out, int level, String message) throws IOException {
+ out.write(level);
+ out.write(message.getBytes(StandardCharsets.UTF_8));
+ out.write('\n');
+ out.flush();
+ return out;
+ }
+
+ public static String getExitStatusName(Integer exitStatus) {
+ if (exitStatus == null) {
+ return "null";
+ }
+
+ switch (exitStatus) {
+ case OK:
+ return "OK";
+ case WARNING:
+ return "WARNING";
+ case ERROR:
+ return "ERROR";
+ default:
+ return exitStatus.toString();
+ }
+ }
+
+ public void ack() throws IOException {
+ out.write(0);
+ out.flush();
+ }
+
+ public int readAck(boolean canEof) throws IOException {
+ int c = in.read();
+ switch (c) {
+ case -1:
+ if (log.isDebugEnabled()) {
+ log.debug("readAck({})[EOF={}] received EOF", this, canEof);
+ }
+ if (!canEof) {
+ throw new EOFException("readAck - EOF before ACK");
+ }
+ break;
+ case OK:
+ if (log.isDebugEnabled()) {
+ log.debug("readAck({})[EOF={}] read OK", this, canEof);
+ }
+ break;
+ case WARNING: {
+ if (log.isDebugEnabled()) {
+ log.debug("readAck({})[EOF={}] read warning message", this, canEof);
+ }
+
+ String line = readLine();
+ log.warn("readAck({})[EOF={}] - Received warning: {}", this, canEof, line);
+ break;
+ }
+ case ERROR: {
+ if (log.isDebugEnabled()) {
+ log.debug("readAck({})[EOF={}] read error message", this, canEof);
+ }
+ String line = readLine();
+ if (log.isDebugEnabled()) {
+ log.debug("readAck({})[EOF={}] received error: {}", this, canEof, line);
+ }
+ throw new ScpException("Received nack: " + line, c);
+ }
+ default:
+ break;
+ }
+ return c;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[" + getSession() + "]";
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
new file mode 100644
index 0000000..d2a9afc
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
@@ -0,0 +1,227 @@
+/*
+ * 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.common.scp;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+import org.apache.sshd.common.auth.MutableUserHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.OsUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * Represents a local or remote SCP location in the format {@code user@host:path}
+ * for a remote path and a simple path for a local one. If user is omitted for a
+ * remote path then current user is used.
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpLocation implements MutableUserHolder, Serializable, Cloneable {
+ public static final char HOST_PART_SEPARATOR = ':';
+ public static final char USERNAME_PART_SEPARATOR = '@';
+
+ private static final long serialVersionUID = 5450230457030600136L;
+
+ private String host;
+ private String username;
+ private String path;
+
+ public ScpLocation() {
+ this(null);
+ }
+
+ /**
+ * @param locSpec The location specification - ignored if {@code null}/empty
+ * @see #update(String, ScpLocation)
+ * @throws IllegalArgumentException if invalid specification
+ */
+ public ScpLocation(String locSpec) {
+ update(locSpec, this);
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public boolean isLocal() {
+ return GenericUtils.isEmpty(getHost());
+ }
+
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ @Override
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ /**
+ * Resolves the effective username to use for a remote location.
+ * If username not set then uses the current username
+ *
+ * @return The resolved username
+ * @see #getUsername()
+ * @see OsUtils#getCurrentUser()
+ */
+ public String resolveUsername() {
+ String user = getUsername();
+ if (GenericUtils.isEmpty(user)) {
+ return OsUtils.getCurrentUser();
+ } else {
+ return user;
+ }
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getHost(), resolveUsername(), OsUtils.getComparablePath(getPath()));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ ScpLocation other = (ScpLocation) obj;
+ if (this.isLocal() != other.isLocal()) {
+ return false;
+ }
+
+ String thisPath = OsUtils.getComparablePath(getPath());
+ String otherPath = OsUtils.getComparablePath(other.getPath());
+ if (!Objects.equals(thisPath, otherPath)) {
+ return false;
+ }
+
+ if (isLocal()) {
+ return true;
+ }
+
+ // we know other is also remote or we would not have reached this point
+ return Objects.equals(resolveUsername(), other.resolveUsername())
+ && Objects.equals(getHost(), other.getHost());
+ }
+
+ @Override
+ public ScpLocation clone() {
+ try {
+ return getClass().cast(super.clone());
+ } catch (CloneNotSupportedException e) { // unexpected
+ throw new RuntimeException("Failed to clone " + toString(), e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ String p = getPath();
+ if (isLocal()) {
+ return p;
+ }
+
+ return resolveUsername() + String.valueOf(USERNAME_PART_SEPARATOR)
+ + getHost() + String.valueOf(HOST_PART_SEPARATOR) + p;
+ }
+
+ /**
+ * Parses a local or remote SCP location in the format {@code user@host:path}
+ *
+ * @param locSpec The location specification - ignored if {@code null}/empty
+ * @return The {@link ScpLocation} or {@code null} if no specification provider
+ * @throws IllegalArgumentException if invalid specification
+ * @see #update(String, ScpLocation)
+ */
+ public static ScpLocation parse(String locSpec) {
+ return GenericUtils.isEmpty(locSpec) ? null : update(locSpec, new ScpLocation());
+ }
+
+ /**
+ * Parses a local or remote SCP location in the format {@code user@host:path}
+ *
+ * @param <L> Type of {@link ScpLocation} being updated
+ * @param locSpec The location specification - ignored if {@code null}/empty
+ * @param location The {@link ScpLocation} to update - never {@code null}
+ * @return The updated location (unless no specification)
+ * @throws IllegalArgumentException if invalid specification
+ */
+ public static <L extends ScpLocation> L update(String locSpec, L location) {
+ Objects.requireNonNull(location, "No location to update");
+ if (GenericUtils.isEmpty(locSpec)) {
+ return location;
+ }
+
+ location.setHost(null);
+ location.setUsername(null);
+
+ int pos = locSpec.indexOf(HOST_PART_SEPARATOR);
+ if (pos < 0) { // assume a local path
+ location.setPath(locSpec);
+ return location;
+ }
+
+ /*
+ * NOTE !!! in such a case there may be confusion with a host named 'a',
+ * but there is a limit to how smart we can be...
+ */
+ if ((pos == 1) && OsUtils.isWin32()) {
+ char drive = locSpec.charAt(0);
+ if (((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z'))) {
+ location.setPath(locSpec);
+ return location;
+ }
+ }
+
+ String login = locSpec.substring(0, pos);
+ ValidateUtils.checkTrue(pos < (locSpec.length() - 1), "Invalid remote specification (missing path): %s", locSpec);
+ location.setPath(locSpec.substring(pos + 1));
+
+ pos = login.indexOf(USERNAME_PART_SEPARATOR);
+ ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (missing username): %s", locSpec);
+ if (pos < 0) {
+ location.setHost(login);
+ } else {
+ location.setUsername(login.substring(0, pos));
+ ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (missing host): %s", locSpec);
+ location.setHost(login.substring(pos + 1));
+ }
+
+ return location;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
new file mode 100644
index 0000000..d0e611c
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.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.common.scp;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FunctionalInterface
+public interface ScpReceiveLineHandler {
+ /**
+ * @param line Received SCP input line
+ * @param isDir Does the input line refer to a directory
+ * @param time The received {@link ScpTimestamp} - may be {@code null}
+ * @throws IOException If failed to process the line
+ */
+ void process(String line, boolean isDir, ScpTimestamp time) throws IOException;
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
new file mode 100644
index 0000000..feeecbc
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
@@ -0,0 +1,73 @@
+/*
+ * 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.common.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+
+import org.apache.sshd.common.session.Session;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpSourceStreamResolver {
+ /**
+ * @return The uploaded file name
+ * @throws IOException If failed to resolve the name
+ */
+ String getFileName() throws IOException;
+
+ /**
+ * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
+ */
+ Path getEventListenerFilePath();
+
+ /**
+ * @return The permissions to be used for uploading a file
+ * @throws IOException If failed to generate the required permissions
+ */
+ Collection<PosixFilePermission> getPermissions() throws IOException;
+
+ /**
+ * @return The {@link ScpTimestamp} to use for uploading the file
+ * if {@code null} then no need to send this information
+ * @throws IOException If failed to generate the required data
+ */
+ ScpTimestamp getTimestamp() throws IOException;
+
+ /**
+ * @return An estimated size of the expected number of bytes to be uploaded.
+ * If non-positive then assumed to be unknown.
+ * @throws IOException If failed to generate an estimate
+ */
+ long getSize() throws IOException;
+
+ /**
+ * @param session The {@link Session} through which file is transmitted
+ * @param options The {@link OpenOption}s may be {@code null}/empty
+ * @return The {@link InputStream} containing the data to be uploaded
+ * @throws IOException If failed to create the stream
+ */
+ InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException;
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
new file mode 100644
index 0000000..9a70302
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
@@ -0,0 +1,67 @@
+/*
+ * 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.common.scp;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.apache.sshd.common.session.Session;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpTargetStreamResolver {
+ /**
+ * Called when receiving a file in order to obtain an output stream
+ * for the incoming data
+ *
+ * @param session The associated {@link Session}
+ * @param name File name as received from remote site
+ * @param length Number of bytes expected to receive
+ * @param perms The {@link Set} of {@link PosixFilePermission} expected
+ * @param options The {@link OpenOption}s to use - may be {@code null}/empty
+ * @return The {@link OutputStream} to write the incoming data
+ * @throws IOException If failed to create the stream
+ */
+ OutputStream resolveTargetStream(Session session, String name, long length,
+ Set<PosixFilePermission> perms, OpenOption... options) throws IOException;
+
+ /**
+ * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
+ */
+ Path getEventListenerFilePath();
+
+ /**
+ * Called after successful reception of the data (and after closing the stream)
+ *
+ * @param name File name as received from remote site
+ * @param preserve If {@code true} then the resolver should attempt to preserve
+ * the specified permissions and timestamp
+ * @param perms The {@link Set} of {@link PosixFilePermission} expected
+ * @param time If not {@code null} then the required timestamp(s) on the
+ * incoming data
+ * @throws IOException If failed to post-process the incoming data
+ */
+ void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException;
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
new file mode 100644
index 0000000..e804de9
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
@@ -0,0 +1,69 @@
+/*
+ * 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.common.scp;
+
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.util.GenericUtils;
+
+/**
+ * Represents an SCP timestamp definition
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpTimestamp {
+ private final long lastModifiedTime;
+ private final long lastAccessTime;
+
+ public ScpTimestamp(long modTime, long accTime) {
+ lastModifiedTime = modTime;
+ lastAccessTime = accTime;
+ }
+
+ public long getLastModifiedTime() {
+ return lastModifiedTime;
+ }
+
+ public long getLastAccessTime() {
+ return lastAccessTime;
+ }
+
+ @Override
+ public String toString() {
+ return "modified=" + new Date(lastModifiedTime)
+ + ";accessed=" + new Date(lastAccessTime);
+ }
+
+ /**
+ * @param line The time specification - format:
+ * {@code T<mtime-sec> <mtime-micros> <atime-sec> <atime-micros>}
+ * where specified times are in seconds since UTC
+ * @return The {@link ScpTimestamp} value with the timestamps converted to
+ * <U>milliseconds</U>
+ * @throws NumberFormatException if bad numerical values - <B>Note:</B>
+ * does not check if 1st character is 'T'.
+ * @see <A HREF="https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works">How the SCP protocol works</A>
+ */
+ public static ScpTimestamp parseTime(String line) throws NumberFormatException {
+ String[] numbers = GenericUtils.split(line.substring(1), ' ');
+ return new ScpTimestamp(TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[0])),
+ TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2])));
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
new file mode 100644
index 0000000..d7954e0
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
@@ -0,0 +1,105 @@
+/*
+ * 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.common.scp;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.apache.sshd.common.util.SshdEventListener;
+
+/**
+ * Can be registered in order to receive events about SCP transfers
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpTransferEventListener extends SshdEventListener {
+ enum FileOperation {
+ SEND,
+ RECEIVE
+ }
+
+ /**
+ * An "empty" implementation to be used instead of {@code null}s
+ */
+ ScpTransferEventListener EMPTY = new ScpTransferEventListener() {
+ @Override
+ public String toString() {
+ return "EMPTY";
+ }
+ };
+
+ /**
+ * @param op The {@link FileOperation}
+ * @param file The <U>local</U> referenced file {@link Path}
+ * @param length Size (in bytes) of transferred data
+ * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
+ * once transfer is complete
+ * @throws IOException If failed to handle the event
+ */
+ default void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) throws IOException {
+ // ignored
+ }
+
+ /**
+ * @param op The {@link FileOperation}
+ * @param file The <U>local</U> referenced file {@link Path}
+ * @param length Size (in bytes) of transferred data
+ * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
+ * once transfer is complete
+ * @param thrown The result of the operation attempt - if {@code null} then
+ * reception was successful
+ * @throws IOException If failed to handle the event
+ */
+ default void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
+ throws IOException {
+ // ignored
+ }
+
+ /**
+ * @param op The {@link FileOperation}
+ * @param file The <U>local</U> referenced folder {@link Path}
+ * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
+ * once transfer is complete
+ * @throws IOException If failed to handle the event
+ */
+ default void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
+ // ignored
+ }
+
+ /**
+ * @param op The {@link FileOperation}
+ * @param file The <U>local</U> referenced file {@link Path}
+ * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
+ * once transfer is complete
+ * @param thrown The result of the operation attempt - if {@code null} then
+ * reception was successful
+ * @throws IOException If failed to handle the event
+ */
+ default void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
+ throws IOException {
+ // ignored
+ }
+
+ static <L extends ScpTransferEventListener> L validateListener(L listener) {
+ return SshdEventListener.validateListener(listener, ScpTransferEventListener.class.getSimpleName());
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
new file mode 100644
index 0000000..bb6ae3b
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
@@ -0,0 +1,75 @@
+/*
+ * 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.common.scp.helpers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpSourceStreamResolver;
+import org.apache.sshd.common.scp.ScpTargetStreamResolver;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpFileOpener extends AbstractLoggingBean implements ScpFileOpener {
+ public static final DefaultScpFileOpener INSTANCE = new DefaultScpFileOpener();
+
+ public DefaultScpFileOpener() {
+ super();
+ }
+
+ @Override
+ public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("openRead({}) file={}, options={}",
+ session, file, Arrays.toString(options));
+ }
+
+ return Files.newInputStream(file, options);
+ }
+
+ @Override
+ public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("openWrite({}) file={}, options={}",
+ session, file, Arrays.toString(options));
+ }
+
+ return Files.newOutputStream(file, options);
+ }
+
+ @Override
+ public ScpSourceStreamResolver createScpSourceStreamResolver(Path path) throws IOException {
+ return new LocalFileScpSourceStreamResolver(path, this);
+ }
+
+ @Override
+ public ScpTargetStreamResolver createScpTargetStreamResolver(Path path) throws IOException {
+ return new LocalFileScpTargetStreamResolver(path, this);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
new file mode 100644
index 0000000..8ce9b61
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
@@ -0,0 +1,97 @@
+/*
+ * 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.common.scp.helpers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpSourceStreamResolver;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implements ScpSourceStreamResolver {
+ protected final Path path;
+ protected final ScpFileOpener opener;
+ protected final Path name;
+ protected final Set<PosixFilePermission> perms;
+ protected final long size;
+ protected final ScpTimestamp time;
+
+ public LocalFileScpSourceStreamResolver(Path path, ScpFileOpener opener) throws IOException {
+ this.path = Objects.requireNonNull(path, "No path specified");
+ this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
+ this.name = path.getFileName();
+ this.perms = IoUtils.getPermissions(path);
+
+ BasicFileAttributes basic = Files.getFileAttributeView(path, BasicFileAttributeView.class).readAttributes();
+ this.size = basic.size();
+ this.time = new ScpTimestamp(basic.lastModifiedTime().toMillis(), basic.lastAccessTime().toMillis());
+ }
+
+ @Override
+ public String getFileName() throws IOException {
+ return name.toString();
+ }
+
+ @Override
+ public Collection<PosixFilePermission> getPermissions() throws IOException {
+ return perms;
+ }
+
+ @Override
+ public ScpTimestamp getTimestamp() throws IOException {
+ return time;
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return size;
+ }
+
+ @Override
+ public Path getEventListenerFilePath() {
+ return path;
+ }
+
+ @Override
+ public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
+ return opener.openRead(session, getEventListenerFilePath(), options);
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(getEventListenerFilePath());
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
new file mode 100644
index 0000000..6b57443
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpTargetStreamResolver.java
@@ -0,0 +1,159 @@
+/*
+ * 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.common.scp.helpers;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StreamCorruptedException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpTargetStreamResolver;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class LocalFileScpTargetStreamResolver extends AbstractLoggingBean implements ScpTargetStreamResolver {
+ protected final Path path;
+ protected final ScpFileOpener opener;
+ protected final Boolean status;
+ private Path file;
+
+ public LocalFileScpTargetStreamResolver(Path path, ScpFileOpener opener) throws IOException {
+ LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
+ this.status = IoUtils.checkFileExists(path, linkOptions);
+ if (status == null) {
+ throw new AccessDeniedException("Receive target file path existence status cannot be determined: " + path);
+ }
+
+ this.path = path;
+ this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
+ }
+
+ @Override
+ public OutputStream resolveTargetStream(Session session, String name, long length,
+ Set<PosixFilePermission> perms, OpenOption... options) throws IOException {
+ if (file != null) {
+ throw new StreamCorruptedException("resolveTargetStream(" + name + ")[" + perms + "] already resolved: " + file);
+ }
+
+ LinkOption[] linkOptions = IoUtils.getLinkOptions(true);
+ if (status && Files.isDirectory(path, linkOptions)) {
+ String localName = name.replace('/', File.separatorChar); // in case we are running on Windows
+ file = path.resolve(localName);
+ } else if (status && Files.isRegularFile(path, linkOptions)) {
+ file = path;
+ } else if (!status) {
+ Path parent = path.getParent();
+
+ Boolean parentStatus = IoUtils.checkFileExists(parent, linkOptions);
+ if (parentStatus == null) {
+ throw new AccessDeniedException("Receive file parent (" + parent + ") existence status cannot be determined for " + path);
+ }
+
+ if (parentStatus && Files.isDirectory(parent, linkOptions)) {
+ file = path;
+ }
+ }
+
+ if (file == null) {
+ throw new IOException("Can not write to " + path);
+ }
+
+ Boolean fileStatus = IoUtils.checkFileExists(file, linkOptions);
+ if (fileStatus == null) {
+ throw new AccessDeniedException("Receive file existence status cannot be determined: " + file);
+ }
+
+ if (fileStatus) {
+ if (Files.isDirectory(file, linkOptions)) {
+ throw new IOException("File is a directory: " + file);
+ }
+
+ if (!Files.isWritable(file)) {
+ throw new IOException("Can not write to file: " + file);
+ }
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("resolveTargetStream(" + name + "): " + file);
+ }
+
+ return opener.openWrite(session, file, options);
+ }
+
+ @Override
+ public Path getEventListenerFilePath() {
+ if (file == null) {
+ return path;
+ } else {
+ return file;
+ }
+ }
+
+ @Override
+ public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ if (file == null) {
+ throw new StreamCorruptedException("postProcessReceivedData(" + name + ")[" + perms + "] No currently resolved data");
+ }
+
+ if (preserve) {
+ updateFileProperties(name, file, perms, time);
+ }
+ }
+
+ protected void updateFileProperties(String name, Path path, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ boolean traceEnabled = log.isTraceEnabled();
+ if (traceEnabled) {
+ log.trace("updateFileProperties(" + name + ")[" + path + "] permissions: " + perms);
+ }
+ IoUtils.setPermissions(path, perms);
+
+ if (time != null) {
+ BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class);
+ FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS);
+ FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS);
+ if (traceEnabled) {
+ log.trace("updateFileProperties(" + name + ")[" + path + "] last-modified=" + lastModified + ", last-access=" + lastAccess);
+ }
+
+ view.setTimes(lastModified, lastAccess, null);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(getEventListenerFilePath());
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
new file mode 100644
index 0000000..e80f791
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/server/scp/ScpCommand.java
@@ -0,0 +1,350 @@
+/*
+ * 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.server.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileSystem;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.sshd.common.file.FileSystemAware;
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.session.SessionHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.SessionAware;
+import org.apache.sshd.server.session.ServerSession;
+import org.apache.sshd.server.session.ServerSessionHolder;
+
+/**
+ * This commands provide SCP support on both server and client side.
+ * Permissions and preservation of access / modification times on files
+ * are not supported.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpCommand
+ extends AbstractLoggingBean
+ implements Command, Runnable, FileSystemAware, SessionAware,
+ SessionHolder<Session>, ServerSessionHolder, ExecutorServiceCarrier {
+
+ protected final String name;
+ protected final int sendBufferSize;
+ protected final int receiveBufferSize;
+ protected final ScpFileOpener opener;
+ protected boolean optR;
+ protected boolean optT;
+ protected boolean optF;
+ protected boolean optD;
+ protected boolean optP; // TODO: handle modification times
+ protected FileSystem fileSystem;
+ protected String path;
+ protected InputStream in;
+ protected OutputStream out;
+ protected OutputStream err;
+ protected ExitCallback callback;
+ protected IOException error;
+ protected Future<?> pendingFuture;
+ protected ScpTransferEventListener listener;
+ protected ServerSession serverSession;
+
+ private ExecutorService executorService;
+ private boolean shutdownOnExit;
+
+ /**
+ * @param command The command to be executed
+ * @param executorService An {@link ExecutorService} to be used when
+ * {@link #start(Environment)}-ing execution. If {@code null} an ad-hoc
+ * single-threaded service is created and used.
+ * @param shutdownOnExit If {@code true} the {@link ExecutorService#shutdownNow()}
+ * will be called when command terminates - unless it is the ad-hoc
+ * service, which will be shutdown regardless
+ * @param sendSize Size (in bytes) of buffer to use when sending files
+ * @param receiveSize Size (in bytes) of buffer to use when receiving files
+ * @param fileOpener The {@link ScpFileOpener} - if {@code null} then {@link DefaultScpFileOpener} is used
+ * @param eventListener An {@link ScpTransferEventListener} - may be {@code null}
+ * @see ThreadUtils#newSingleThreadExecutor(String)
+ * @see ScpHelper#MIN_SEND_BUFFER_SIZE
+ * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
+ */
+ public ScpCommand(String command,
+ ExecutorService executorService, boolean shutdownOnExit,
+ int sendSize, int receiveSize,
+ ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
+ name = command;
+
+ if (executorService == null) {
+ String poolName = command.replace(' ', '_').replace('/', ':');
+ this.executorService = ThreadUtils.newSingleThreadExecutor(poolName);
+ this.shutdownOnExit = true; // we always close the ad-hoc executor service
+ } else {
+ this.executorService = executorService;
+ this.shutdownOnExit = shutdownOnExit;
+ }
+
+ if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
+ throw new IllegalArgumentException("<ScpCommmand>(" + command + ") send buffer size "
+ + "(" + sendSize + ") below minimum required "
+ + "(" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
+ }
+ sendBufferSize = sendSize;
+
+ if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
+ throw new IllegalArgumentException("<ScpCommmand>(" + command + ") receive buffer size "
+ + "(" + sendSize + ") below minimum required "
+ + "(" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
+ }
+ receiveBufferSize = receiveSize;
+
+ opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
+ listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
+
+ boolean debugEnabled = log.isDebugEnabled();
+ if (debugEnabled) {
+ log.debug("Executing command {}", command);
+ }
+
+ String[] args = GenericUtils.split(command, ' ');
+ int numArgs = GenericUtils.length(args);
+ for (int i = 1; i < numArgs; i++) {
+ String argVal = args[i];
+ if (argVal.charAt(0) == '-') {
+ for (int j = 1; j < argVal.length(); j++) {
+ char option = argVal.charAt(j);
+ switch (option) {
+ case 'f':
+ optF = true;
+ break;
+ case 'p':
+ optP = true;
+ break;
+ case 'r':
+ optR = true;
+ break;
+ case 't':
+ optT = true;
+ break;
+ case 'd':
+ optD = true;
+ break;
+ default: // ignored
+ if (debugEnabled) {
+ log.debug("Unknown flag ('{}') in command={}", option, command);
+ }
+ }
+ }
+ } else {
+ String prevArg = args[i - 1];
+ path = command.substring(command.indexOf(prevArg) + prevArg.length() + 1);
+
+ int pathLen = path.length();
+ char startDelim = path.charAt(0);
+ char endDelim = (pathLen > 2) ? path.charAt(pathLen - 1) : '\0';
+ // remove quotes
+ if ((pathLen > 2) && (startDelim == endDelim) && ((startDelim == '\'') || (startDelim == '"'))) {
+ path = path.substring(1, pathLen - 1);
+ }
+ break;
+ }
+ }
+
+ if ((!optF) && (!optT)) {
+ error = new IOException("Either -f or -t option should be set for " + command);
+ }
+ }
+
+ @Override
+ public ExecutorService getExecutorService() {
+ return executorService;
+ }
+
+ @Override
+ public boolean isShutdownOnExit() {
+ return shutdownOnExit;
+ }
+
+ @Override
+ public Session getSession() {
+ return getServerSession();
+ }
+
+ @Override
+ public ServerSession getServerSession() {
+ return serverSession;
+ }
+
+ @Override
+ public void setSession(ServerSession session) {
+ serverSession = session;
+ }
+
+ @Override
+ public void setInputStream(InputStream in) {
+ this.in = in;
+ }
+
+ @Override
+ public void setOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ @Override
+ public void setErrorStream(OutputStream err) {
+ this.err = err;
+ }
+
+ @Override
+ public void setExitCallback(ExitCallback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void setFileSystem(FileSystem fs) {
+ this.fileSystem = fs;
+ }
+
+ @Override
+ public void start(Environment env) throws IOException {
+ if (error != null) {
+ throw error;
+ }
+
+ try {
+ ExecutorService executors = getExecutorService();
+ pendingFuture = executors.submit(this);
+ } catch (RuntimeException e) { // e.g., RejectedExecutionException
+ log.error("Failed (" + e.getClass().getSimpleName() + ") to start command=" + name + ": " + e.getMessage(), e);
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ // if thread has not completed, cancel it
+ boolean debugEnabled = log.isDebugEnabled();
+ if ((pendingFuture != null) && (!pendingFuture.isDone())) {
+ boolean result = pendingFuture.cancel(true);
+ // TODO consider waiting some reasonable (?) amount of time for cancellation
+ if (debugEnabled) {
+ log.debug("destroy() - cancel pending future=" + result);
+ }
+ }
+
+ pendingFuture = null;
+
+ ExecutorService executors = getExecutorService();
+ if ((executors != null) && (!executors.isShutdown()) && isShutdownOnExit()) {
+ Collection<Runnable> runners = executors.shutdownNow();
+ if (debugEnabled) {
+ log.debug("destroy() - shutdown executor service - runners count=" + runners.size());
+ }
+ }
+ this.executorService = null;
+
+ try {
+ fileSystem.close();
+ } catch (UnsupportedOperationException e) {
+ // Ignore
+ } catch (IOException e) {
+ log.debug("Error closing FileSystem", e);
+ }
+ }
+
+ @Override
+ public void run() {
+ int exitValue = ScpHelper.OK;
+ String exitMessage = null;
+ ScpHelper helper = new ScpHelper(getServerSession(), in, out, fileSystem, opener, listener);
+ try {
+ if (optT) {
+ helper.receive(helper.resolveLocalPath(path), optR, optD, optP, receiveBufferSize);
+ } else if (optF) {
+ helper.send(Collections.singletonList(path), optR, optP, sendBufferSize);
+ } else {
+ throw new IOException("Unsupported mode");
+ }
+ } catch (IOException e) {
+ ServerSession session = getServerSession();
+ boolean debugEnabled = log.isDebugEnabled();
+ try {
+ Integer statusCode = null;
+ if (e instanceof ScpException) {
+ statusCode = ((ScpException) e).getExitStatus();
+ }
+ exitValue = (statusCode == null) ? ScpHelper.ERROR : statusCode;
+ // this is an exception so status cannot be OK/WARNING
+ if ((exitValue == ScpHelper.OK) || (exitValue == ScpHelper.WARNING)) {
+ if (debugEnabled) {
+ log.debug("run({})[{}] normalize status code={}", session, name, exitValue);
+ }
+ exitValue = ScpHelper.ERROR;
+ }
+ exitMessage = GenericUtils.trimToEmpty(e.getMessage());
+ writeCommandResponseMessage(name, exitValue, exitMessage);
+ } catch (IOException e2) {
+ if (debugEnabled) {
+ log.debug("run({})[{}] Failed ({}) to send error response: {}",
+ session, name, e.getClass().getSimpleName(), e.getMessage());
+ }
+ if (log.isTraceEnabled()) {
+ log.trace("run(" + session + ")[" + name + "] error response failure details", e2);
+ }
+ }
+
+ if (debugEnabled) {
+ log.debug("run({})[{}] Failed ({}) to run command: {}",
+ session, name, e.getClass().getSimpleName(), e.getMessage());
+ }
+ if (log.isTraceEnabled()) {
+ log.trace("run(" + session + ")[" + name + "] command execution failure details", e);
+ }
+ } finally {
+ if (callback != null) {
+ callback.onExit(exitValue, GenericUtils.trimToEmpty(exitMessage));
+ }
+ }
+ }
+
+ protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("writeCommandResponseMessage({}) command='{}', exit-status={}: {}",
+ getServerSession(), command, exitValue, exitMessage);
+ }
+ ScpHelper.sendResponseMessage(out, exitValue, exitMessage);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "(" + getSession() + ") " + name;
+ }
+}
\ No newline at end of file
[7/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
deleted file mode 100644
index d929a07..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Set;
-
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * A no-op implementation of {@link ScpTransferEventListener} for those who wish to
- * implement only a small number of methods. By default, all non-overridden methods
- * simply log at TRACE level their invocation parameters
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class AbstractScpTransferEventListenerAdapter
- extends AbstractLoggingBean
- implements ScpTransferEventListener {
- protected AbstractScpTransferEventListenerAdapter() {
- super();
- }
-
- @Override
- public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms)
- throws IOException {
- if (log.isTraceEnabled()) {
- log.trace("startFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")");
- }
- }
-
- @Override
- public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
- throws IOException {
- if (log.isTraceEnabled()) {
- log.trace("endFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")"
- + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
- }
- }
-
- @Override
- public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
- if (log.isTraceEnabled()) {
- log.trace("startFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")");
- }
- }
-
- @Override
- public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
- throws IOException {
- if (log.isTraceEnabled()) {
- log.trace("endFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")"
- + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpException.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpException.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpException.java
deleted file mode 100644
index 9ae17c7..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpException.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.IOException;
-import java.util.Objects;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ScpException extends IOException {
- private static final long serialVersionUID = 7734851624372451732L;
- private final Integer exitStatus;
-
- public ScpException(String message) {
- this(message, null);
- }
-
- public ScpException(Integer exitStatus) {
- this("Exit status=" + ScpHelper.getExitStatusName(Objects.requireNonNull(exitStatus, "No exit status")), exitStatus);
- }
-
- public ScpException(String message, Integer exitStatus) {
- this(message, null, exitStatus);
- }
-
- public ScpException(Throwable cause, Integer exitStatus) {
- this(Objects.requireNonNull(cause, "No cause").getMessage(), cause, exitStatus);
- }
-
- public ScpException(String message, Throwable cause, Integer exitStatus) {
- super(message, cause);
- this.exitStatus = exitStatus;
- }
-
- public Integer getExitStatus() {
- return exitStatus;
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
deleted file mode 100644
index 78e033f..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.AccessDeniedException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.FileSystem;
-import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
-import java.nio.file.LinkOption;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributeView;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.SshException;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.SelectorUtils;
-import org.apache.sshd.common.util.io.DirectoryScanner;
-import org.apache.sshd.common.util.io.IoUtils;
-
-/**
- * Plug-in mechanism for users to intervene in the SCP process - e.g.,
- * apply some kind of traffic shaping mechanism, display upload/download
- * progress, etc...
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpFileOpener {
- /**
- * Invoked when receiving a new file to via a directory command
- *
- * @param localPath The target local path
- * @param name The target file name
- * @param preserve Whether requested to preserve the permissions and timestamp
- * @param permissions The requested file permissions
- * @param time The requested {@link ScpTimestamp} - may be {@code null} if nothing to update
- * @return The actual target file path for the incoming file/directory
- * @throws IOException If failed to resolve the file path
- * @see #updateFileProperties(Path, Set, ScpTimestamp) updateFileProperties
- */
- default Path resolveIncomingFilePath(
- Path localPath, String name, boolean preserve, Set<PosixFilePermission> permissions, ScpTimestamp time)
- throws IOException {
- LinkOption[] options = IoUtils.getLinkOptions(true);
- Boolean status = IoUtils.checkFileExists(localPath, options);
- if (status == null) {
- throw new AccessDeniedException("Receive directory existence status cannot be determined: " + localPath);
- }
-
- Path file = null;
- if (status && Files.isDirectory(localPath, options)) {
- String localName = name.replace('/', File.separatorChar);
- file = localPath.resolve(localName);
- } else if (!status) {
- Path parent = localPath.getParent();
-
- status = IoUtils.checkFileExists(parent, options);
- if (status == null) {
- throw new AccessDeniedException("Receive directory parent (" + parent + ") existence status cannot be determined for " + localPath);
- }
-
- if (status && Files.isDirectory(parent, options)) {
- file = localPath;
- }
- }
-
- if (file == null) {
- throw new IOException("Cannot write to " + localPath);
- }
-
- status = IoUtils.checkFileExists(file, options);
- if (status == null) {
- throw new AccessDeniedException("Receive directory file existence status cannot be determined: " + file);
- }
-
- if (!(status && Files.isDirectory(file, options))) {
- Files.createDirectory(file);
- }
-
- if (preserve) {
- updateFileProperties(file, permissions, time);
- }
-
- return file;
- }
-
- /**
- * Invoked when required to send a pattern of files
- *
- * @param basedir The base directory - may be {@code null}/empty to indicate CWD
- * @param pattern The required pattern
- * @return The matching <U>relative paths</U> of the children to send
- */
- default Iterable<String> getMatchingFilesToSend(String basedir, String pattern) {
- String[] matches = new DirectoryScanner(basedir, pattern).scan();
- if (GenericUtils.isEmpty(matches)) {
- return Collections.emptyList();
- }
-
- return Arrays.asList(matches);
- }
-
- /**
- * Invoked on a local path in order to decide whether it should be sent
- * as a file or as a directory
- *
- * @param path The local {@link Path}
- * @param options The {@link LinkOption}-s
- * @return Whether to send the file as a regular one - <B>Note:</B> if {@code false}
- * then the {@link #sendAsDirectory(Path, LinkOption...)} is consulted.
- * @throws IOException If failed to decide
- */
- default boolean sendAsRegularFile(Path path, LinkOption... options) throws IOException {
- return Files.isRegularFile(path, options);
- }
-
- /**
- * Invoked on a local path in order to decide whether it should be sent
- * as a file or as a directory
- *
- * @param path The local {@link Path}
- * @param options The {@link LinkOption}-s
- * @return Whether to send the file as a directory - <B>Note:</B> if {@code true}
- * then {@link #getLocalFolderChildren(Path)} is consulted
- * @throws IOException If failed to decide
- */
- default boolean sendAsDirectory(Path path, LinkOption... options) throws IOException {
- return Files.isDirectory(path, options);
- }
-
- /**
- * Invoked when required to send all children of a local directory
- *
- * @param path The local folder {@link Path}{
- * @return The {@link DirectoryStream} of children to send - <B>Note:</B> for each child
- * the decision whether to send it as a file or a directory will be reached by consulting
- * the respective {@link #sendAsRegularFile(Path, LinkOption...) sendAsRegularFile} and
- * {@link #sendAsDirectory(Path, LinkOption...) sendAsDirectory} methods
- * @throws IOException If failed to provide the children stream
- * @see #sendAsDirectory(Path, LinkOption...) sendAsDirectory
- */
- default DirectoryStream<Path> getLocalFolderChildren(Path path) throws IOException {
- return Files.newDirectoryStream(path);
- }
-
- default BasicFileAttributes getLocalBasicFileAttributes(Path path, LinkOption... options) throws IOException {
- return Files.getFileAttributeView(path, BasicFileAttributeView.class, options).readAttributes();
- }
-
- default Set<PosixFilePermission> getLocalFilePermissions(Path path, LinkOption... options) throws IOException {
- return IoUtils.getPermissions(path, options);
- }
-
- /**
- * @param fileSystem The <U>local</U> {@link FileSystem} on which local file should reside
- * @param commandPath The command path using the <U>local</U> file separator
- * @return The resolved absolute and normalized local {@link Path}
- * @throws IOException If failed to resolve the path
- * @throws InvalidPathException If invalid local path value
- */
- default Path resolveLocalPath(FileSystem fileSystem, String commandPath) throws IOException, InvalidPathException {
- String path = SelectorUtils.translateToLocalFileSystemPath(commandPath, File.separatorChar, fileSystem);
- Path lcl = fileSystem.getPath(path);
- Path abs = lcl.isAbsolute() ? lcl : lcl.toAbsolutePath();
- return abs.normalize();
- }
-
- /**
- * Invoked when a request to receive something is processed
- *
- * @param path The local target {@link Path} of the request
- * @param recursive Whether the request is recursive
- * @param shouldBeDir Whether target path is expected to be a directory
- * @param preserve Whether target path is expected to preserve attributes (permissions, times)
- * @return The effective target path - default=same as input
- * @throws IOException If failed to resolve target location
- */
- default Path resolveIncomingReceiveLocation(
- Path path, boolean recursive, boolean shouldBeDir, boolean preserve)
- throws IOException {
- if (!shouldBeDir) {
- return path;
- }
- LinkOption[] options = IoUtils.getLinkOptions(true);
- Boolean status = IoUtils.checkFileExists(path, options);
- if (status == null) {
- throw new SshException("Target directory " + path + " is most like inaccessible");
- }
- if (!status) {
- throw new SshException("Target directory " + path + " does not exist");
- }
- if (!Files.isDirectory(path, options)) {
- throw new SshException("Target directory " + path + " is not a directory");
- }
-
- return path;
- }
-
- /**
- * Called when there is a candidate file/folder for sending
- *
- * @param localPath The original file/folder {@link Path} for sending
- * @param options The {@link LinkOption}-s to use for validation
- * @return The effective outgoing file path (default=same as input)
- * @throws IOException If failed to resolve
- */
- default Path resolveOutgoingFilePath(Path localPath, LinkOption... options) throws IOException {
- Boolean status = IoUtils.checkFileExists(localPath, options);
- if (status == null) {
- throw new AccessDeniedException("Send file existence status cannot be determined: " + localPath);
- }
- if (!status) {
- throw new IOException(localPath + ": no such file or directory");
- }
-
- return localPath;
- }
-
- /**
- * Create an input stream to read from a file
- *
- * @param session The {@link Session} requesting the access
- * @param file The requested local file {@link Path}
- * @param options The {@link OpenOption}s - may be {@code null}/empty
- * @return The open {@link InputStream} never {@code null}
- * @throws IOException If failed to open the file
- */
- InputStream openRead(Session session, Path file, OpenOption... options) throws IOException;
-
- ScpSourceStreamResolver createScpSourceStreamResolver(Path path) throws IOException;
-
- /**
- * Create an output stream to write to a file
- *
- * @param session The {@link Session} requesting the access
- * @param file The requested local file {@link Path}
- * @param options The {@link OpenOption}s - may be {@code null}/empty
- * @return The open {@link OutputStream} never {@code null}
- * @throws IOException If failed to open the file
- */
- OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException;
-
- ScpTargetStreamResolver createScpTargetStreamResolver(Path path) throws IOException;
-
- static void updateFileProperties(Path file, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
- IoUtils.setPermissions(file, perms);
-
- if (time != null) {
- BasicFileAttributeView view = Files.getFileAttributeView(file, BasicFileAttributeView.class);
- FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS);
- FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS);
- view.setTimes(lastModified, lastAccess, null);
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
deleted file mode 100644
index b492129..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.common.scp;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpFileOpenerHolder {
- /**
- * @return The last {@link ScpFileOpener} set via call
- * to {@link #setScpFileOpener(ScpFileOpener)}
- */
- ScpFileOpener getScpFileOpener();
-
- /**
- * @param opener The default {@link ScpFileOpener} to use - if {@code null}
- * then a default opener is used
- */
- void setScpFileOpener(ScpFileOpener opener);
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
deleted file mode 100644
index 1cbea2d..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
+++ /dev/null
@@ -1,837 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.StreamCorruptedException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.DirectoryStream;
-import java.nio.file.FileSystem;
-import java.nio.file.InvalidPathException;
-import java.nio.file.LinkOption;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Collection;
-import java.util.EnumSet;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.file.util.MockPath;
-import org.apache.sshd.common.scp.ScpTransferEventListener.FileOperation;
-import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.session.SessionHolder;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.io.LimitInputStream;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@SuppressWarnings("PMD.AvoidUsingOctalValues")
-public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Session> {
- /**
- * Command prefix used to identify SCP commands
- */
- public static final String SCP_COMMAND_PREFIX = "scp";
-
- public static final int OK = 0;
- public static final int WARNING = 1;
- public static final int ERROR = 2;
-
- /**
- * Default size (in bytes) of send / receive buffer size
- */
- public static final int DEFAULT_COPY_BUFFER_SIZE = IoUtils.DEFAULT_COPY_SIZE;
- public static final int DEFAULT_RECEIVE_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
- public static final int DEFAULT_SEND_BUFFER_SIZE = DEFAULT_COPY_BUFFER_SIZE;
-
- /**
- * The minimum size for sending / receiving files
- */
- public static final int MIN_COPY_BUFFER_SIZE = Byte.MAX_VALUE;
- public static final int MIN_RECEIVE_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
- public static final int MIN_SEND_BUFFER_SIZE = MIN_COPY_BUFFER_SIZE;
-
- public static final int S_IRUSR = 0000400;
- public static final int S_IWUSR = 0000200;
- public static final int S_IXUSR = 0000100;
- public static final int S_IRGRP = 0000040;
- public static final int S_IWGRP = 0000020;
- public static final int S_IXGRP = 0000010;
- public static final int S_IROTH = 0000004;
- public static final int S_IWOTH = 0000002;
- public static final int S_IXOTH = 0000001;
-
- public static final String DEFAULT_DIR_OCTAL_PERMISSIONS = "0755";
- public static final String DEFAULT_FILE_OCTAL_PERMISSIONS = "0644";
-
- protected final InputStream in;
- protected final OutputStream out;
- protected final FileSystem fileSystem;
- protected final ScpFileOpener opener;
- protected final ScpTransferEventListener listener;
-
- private final Session sessionInstance;
-
- public ScpHelper(Session session, InputStream in, OutputStream out,
- FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) {
- this.sessionInstance = Objects.requireNonNull(session, "No session");
- this.in = Objects.requireNonNull(in, "No input stream");
- this.out = Objects.requireNonNull(out, "No output stream");
- this.fileSystem = fileSystem;
- this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
- this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
- }
-
- @Override
- public Session getSession() {
- return sessionInstance;
- }
-
- public void receiveFileStream(OutputStream local, int bufferSize) throws IOException {
- receive((line, isDir, timestamp) -> {
- if (isDir) {
- throw new StreamCorruptedException("Cannot download a directory into a file stream: " + line);
- }
-
- Path path = new MockPath(line);
- receiveStream(line, new ScpTargetStreamResolver() {
- @Override
- @SuppressWarnings("synthetic-access")
- public OutputStream resolveTargetStream(
- Session session, String name, long length, Set<PosixFilePermission> perms, OpenOption... options)
- throws IOException {
- if (log.isDebugEnabled()) {
- log.debug("resolveTargetStream({}) name={}, perms={}, len={} - started local stream download",
- ScpHelper.this, name, perms, length);
- }
- return local;
- }
-
- @Override
- public Path getEventListenerFilePath() {
- return path;
- }
-
- @Override
- @SuppressWarnings("synthetic-access")
- public void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
- if (log.isDebugEnabled()) {
- log.debug("postProcessReceivedData({}) name={}, perms={}, preserve={} time={}",
- ScpHelper.this, name, perms, preserve, time);
- }
- }
-
- @Override
- public String toString() {
- return line;
- }
- }, timestamp, false, bufferSize);
- });
- }
-
- public void receive(Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize) throws IOException {
- Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
- Path path = opener.resolveIncomingReceiveLocation(localPath, recursive, shouldBeDir, preserve);
- receive((line, isDir, time) -> {
- if (recursive && isDir) {
- receiveDir(line, path, time, preserve, bufferSize);
- } else {
- receiveFile(line, path, time, preserve, bufferSize);
- }
- });
- }
-
- protected void receive(ScpReceiveLineHandler handler) throws IOException {
- ack();
- ScpTimestamp time = null;
- for (;;) {
- String line;
- boolean isDir = false;
- int c = readAck(true);
- switch (c) {
- case -1:
- return;
- case 'D':
- isDir = true;
- line = String.valueOf((char) c) + readLine();
- if (log.isDebugEnabled()) {
- log.debug("receive({}) - Received 'D' header: {}", this, line);
- }
- break;
- case 'C':
- line = String.valueOf((char) c) + readLine();
- if (log.isDebugEnabled()) {
- log.debug("receive({}) - Received 'C' header: {}", this, line);
- }
- break;
- case 'T':
- line = String.valueOf((char) c) + readLine();
- if (log.isDebugEnabled()) {
- log.debug("receive({}) - Received 'T' header: {}", this, line);
- }
- time = ScpTimestamp.parseTime(line);
- ack();
- continue;
- case 'E':
- line = String.valueOf((char) c) + readLine();
- if (log.isDebugEnabled()) {
- log.debug("receive({}) - Received 'E' header: {}", this, line);
- }
- ack();
- return;
- default:
- //a real ack that has been acted upon already
- continue;
- }
-
- try {
- handler.process(line, isDir, time);
- } finally {
- time = null;
- }
- }
- }
-
- public void receiveDir(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
- Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
- if (log.isDebugEnabled()) {
- log.debug("receiveDir({})[{}] Receiving directory {} - preserve={}, time={}, buffer-size={}",
- this, header, path, preserve, time, bufferSize);
- }
- if (!header.startsWith("D")) {
- throw new IOException("Expected a 'D; message but got '" + header + "'");
- }
-
- Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
- int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
- String name = header.substring(header.indexOf(' ', 6) + 1);
- if (length != 0) {
- throw new IOException("Expected 0 length for directory=" + name + " but got " + length);
- }
-
- Path file = opener.resolveIncomingFilePath(path, name, preserve, perms, time);
-
- ack();
-
- time = null;
- listener.startFolderEvent(FileOperation.RECEIVE, path, perms);
- try {
- for (;;) {
- header = readLine();
- if (log.isDebugEnabled()) {
- log.debug("receiveDir({})[{}] Received header: {}", this, file, header);
- }
- if (header.startsWith("C")) {
- receiveFile(header, file, time, preserve, bufferSize);
- time = null;
- } else if (header.startsWith("D")) {
- receiveDir(header, file, time, preserve, bufferSize);
- time = null;
- } else if (header.equals("E")) {
- ack();
- break;
- } else if (header.startsWith("T")) {
- time = ScpTimestamp.parseTime(header);
- ack();
- } else {
- throw new IOException("Unexpected message: '" + header + "'");
- }
- }
- } catch (IOException | RuntimeException e) {
- listener.endFolderEvent(FileOperation.RECEIVE, path, perms, e);
- throw e;
- }
- listener.endFolderEvent(FileOperation.RECEIVE, path, perms, null);
- }
-
- public void receiveFile(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
- Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
- if (log.isDebugEnabled()) {
- log.debug("receiveFile({})[{}] Receiving file {} - preserve={}, time={}, buffer-size={}",
- this, header, path, preserve, time, bufferSize);
- }
-
- receiveStream(header, opener.createScpTargetStreamResolver(path), time, preserve, bufferSize);
- }
-
- public void receiveStream(String header, ScpTargetStreamResolver resolver, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
- if (!header.startsWith("C")) {
- throw new IOException("receiveStream(" + resolver + ") Expected a C message but got '" + header + "'");
- }
-
- if (bufferSize < MIN_RECEIVE_BUFFER_SIZE) {
- throw new IOException("receiveStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_RECEIVE_BUFFER_SIZE + ")");
- }
-
- Set<PosixFilePermission> perms = parseOctalPermissions(header.substring(1, 5));
- long length = Long.parseLong(header.substring(6, header.indexOf(' ', 6)));
- String name = header.substring(header.indexOf(' ', 6) + 1);
- if (length < 0L) { // TODO consider throwing an exception...
- log.warn("receiveStream({})[{}] bad length in header: {}", this, resolver, header);
- }
-
- // if file size is less than buffer size allocate only expected file size
- int bufSize;
- boolean debugEnabled = log.isDebugEnabled();
- if (length == 0L) {
- if (debugEnabled) {
- log.debug("receiveStream({})[{}] zero file size (perhaps special file) using copy buffer size={}",
- this, resolver, MIN_RECEIVE_BUFFER_SIZE);
- }
- bufSize = MIN_RECEIVE_BUFFER_SIZE;
- } else {
- bufSize = (int) Math.min(length, bufferSize);
- }
-
- if (bufSize < 0) { // TODO consider throwing an exception
- log.warn("receiveStream({})[{}] bad buffer size ({}) using default ({})",
- this, resolver, bufSize, MIN_RECEIVE_BUFFER_SIZE);
- bufSize = MIN_RECEIVE_BUFFER_SIZE;
- }
-
- try (
- InputStream is = new LimitInputStream(this.in, length);
- OutputStream os = resolver.resolveTargetStream(getSession(), name, length, perms)
- ) {
- ack();
-
- Path file = resolver.getEventListenerFilePath();
- listener.startFileEvent(FileOperation.RECEIVE, file, length, perms);
- try {
- IoUtils.copy(is, os, bufSize);
- } catch (IOException | RuntimeException e) {
- listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, e);
- throw e;
- }
- listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, null);
- }
-
- resolver.postProcessReceivedData(name, preserve, perms, time);
-
- ack();
-
- int replyCode = readAck(false);
- if (debugEnabled) {
- log.debug("receiveStream({})[{}] ack reply code={}", this, resolver, replyCode);
- }
- validateAckReplyCode("receiveStream", resolver, replyCode, false);
- }
-
- public String readLine() throws IOException {
- return readLine(false);
- }
-
- public String readLine(boolean canEof) throws IOException {
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream(Byte.MAX_VALUE)) {
- for (;;) {
- int c = in.read();
- if (c == '\n') {
- return baos.toString(StandardCharsets.UTF_8.name());
- } else if (c == -1) {
- if (!canEof) {
- throw new EOFException("EOF while await end of line");
- }
- return null;
- } else {
- baos.write(c);
- }
- }
- }
- }
-
- public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
- int readyCode = readAck(false);
- boolean debugEnabled = log.isDebugEnabled();
- if (debugEnabled) {
- log.debug("send({}) ready code={}", paths, readyCode);
- }
- validateOperationReadyCode("send", "Paths", readyCode, false);
-
- LinkOption[] options = IoUtils.getLinkOptions(true);
- for (String pattern : paths) {
- pattern = pattern.replace('/', File.separatorChar);
-
- int idx = pattern.indexOf('*'); // check if wildcard used
- if (idx >= 0) {
- String basedir = "";
- String fixedPart = pattern.substring(0, idx);
- int lastSep = fixedPart.lastIndexOf(File.separatorChar);
- if (lastSep >= 0) {
- basedir = pattern.substring(0, lastSep);
- pattern = pattern.substring(lastSep + 1);
- }
-
- Iterable<String> included = opener.getMatchingFilesToSend(basedir, pattern);
- for (String path : included) {
- Path file = resolveLocalPath(basedir, path);
- if (opener.sendAsRegularFile(file, options)) {
- sendFile(file, preserve, bufferSize);
- } else if (opener.sendAsDirectory(file, options)) {
- if (!recursive) {
- if (debugEnabled) {
- log.debug("send({}) {}: not a regular file", this, path);
- }
- sendWarning(path.replace(File.separatorChar, '/') + " not a regular file");
- } else {
- sendDir(file, preserve, bufferSize);
- }
- } else {
- if (debugEnabled) {
- log.debug("send({}) {}: unknown file type", this, path);
- }
- sendWarning(path.replace(File.separatorChar, '/') + " unknown file type");
- }
- }
- } else {
- send(resolveLocalPath(pattern), recursive, preserve, bufferSize, options);
- }
- }
- }
-
- public void sendPaths(Collection<? extends Path> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
- int readyCode = readAck(false);
- if (log.isDebugEnabled()) {
- log.debug("sendPaths({}) ready code={}", paths, readyCode);
- }
- validateOperationReadyCode("sendPaths", "Paths", readyCode, false);
-
- LinkOption[] options = IoUtils.getLinkOptions(true);
- for (Path file : paths) {
- send(file, recursive, preserve, bufferSize, options);
- }
- }
-
- protected void send(Path local, boolean recursive, boolean preserve, int bufferSize, LinkOption... options) throws IOException {
- Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
- Path file = opener.resolveOutgoingFilePath(localPath, options);
- if (opener.sendAsRegularFile(file, options)) {
- sendFile(file, preserve, bufferSize);
- } else if (opener.sendAsDirectory(file, options)) {
- if (!recursive) {
- throw new IOException(file + " not a regular file");
- } else {
- sendDir(file, preserve, bufferSize);
- }
- } else {
- throw new IOException(file + ": unknown file type");
- }
- }
-
- public Path resolveLocalPath(String basedir, String subpath) throws IOException {
- if (GenericUtils.isEmpty(basedir)) {
- return resolveLocalPath(subpath);
- } else {
- return resolveLocalPath(basedir + File.separator + subpath);
- }
- }
-
- /**
- * @param commandPath The command path using the <U>local</U> file separator
- * @return The resolved absolute and normalized local {@link Path}
- * @throws IOException If failed to resolve the path
- * @throws InvalidPathException If invalid local path value
- */
- public Path resolveLocalPath(String commandPath) throws IOException, InvalidPathException {
- Path p = opener.resolveLocalPath(fileSystem, commandPath);
- if (log.isTraceEnabled()) {
- log.trace("resolveLocalPath({}) {}: {}", this, commandPath, p);
- }
-
- return p;
- }
-
- public void sendFile(Path local, boolean preserve, int bufferSize) throws IOException {
- Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
- if (log.isDebugEnabled()) {
- log.debug("sendFile({})[preserve={},buffer-size={}] Sending file {}", this, preserve, bufferSize, path);
- }
-
- sendStream(opener.createScpSourceStreamResolver(path), preserve, bufferSize);
- }
-
- public void sendStream(ScpSourceStreamResolver resolver, boolean preserve, int bufferSize) throws IOException {
- if (bufferSize < MIN_SEND_BUFFER_SIZE) {
- throw new IOException("sendStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum (" + MIN_SEND_BUFFER_SIZE + ")");
- }
-
- long fileSize = resolver.getSize();
- // if file size is less than buffer size allocate only expected file size
- int bufSize;
- boolean debugEnabled = log.isDebugEnabled();
- if (fileSize <= 0L) {
- if (debugEnabled) {
- log.debug("sendStream({})[{}] unknown file size ({}) perhaps special file - using copy buffer size={}",
- this, resolver, fileSize, MIN_SEND_BUFFER_SIZE);
- }
- bufSize = MIN_SEND_BUFFER_SIZE;
- } else {
- bufSize = (int) Math.min(fileSize, bufferSize);
- }
-
- if (bufSize < 0) { // TODO consider throwing an exception
- log.warn("sendStream({})[{}] bad buffer size ({}) using default ({})",
- this, resolver, bufSize, MIN_SEND_BUFFER_SIZE);
- bufSize = MIN_SEND_BUFFER_SIZE;
- }
-
- ScpTimestamp time = resolver.getTimestamp();
- if (preserve && (time != null)) {
- String cmd = "T" + TimeUnit.MILLISECONDS.toSeconds(time.getLastModifiedTime())
- + " " + "0" + " " + TimeUnit.MILLISECONDS.toSeconds(time.getLastAccessTime())
- + " " + "0";
- if (debugEnabled) {
- log.debug("sendStream({})[{}] send timestamp={} command: {}", this, resolver, time, cmd);
- }
- out.write(cmd.getBytes(StandardCharsets.UTF_8));
- out.write('\n');
- out.flush();
-
- int readyCode = readAck(false);
- if (debugEnabled) {
- log.debug("sendStream({})[{}] command='{}' ready code={}", this, resolver, cmd, readyCode);
- }
- validateAckReplyCode(cmd, resolver, readyCode, false);
- }
-
- Set<PosixFilePermission> perms = EnumSet.copyOf(resolver.getPermissions());
- String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_FILE_OCTAL_PERMISSIONS : getOctalPermissions(perms);
- String fileName = resolver.getFileName();
- String cmd = "C" + octalPerms + " " + fileSize + " " + fileName;
- if (debugEnabled) {
- log.debug("sendStream({})[{}] send 'C' command: {}", this, resolver, cmd);
- }
- out.write(cmd.getBytes(StandardCharsets.UTF_8));
- out.write('\n');
- out.flush();
-
- int readyCode = readAck(false);
- if (debugEnabled) {
- log.debug("sendStream({})[{}] command='{}' ready code={}",
- this, resolver, cmd.substring(0, cmd.length() - 1), readyCode);
- }
- validateAckReplyCode(cmd, resolver, readyCode, false);
-
- try (InputStream in = resolver.resolveSourceStream(getSession())) {
- Path path = resolver.getEventListenerFilePath();
- listener.startFileEvent(FileOperation.SEND, path, fileSize, perms);
- try {
- IoUtils.copy(in, out, bufSize);
- } catch (IOException | RuntimeException e) {
- listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, e);
- throw e;
- }
- listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, null);
- }
- ack();
-
- readyCode = readAck(false);
- if (debugEnabled) {
- log.debug("sendStream({})[{}] command='{}' reply code={}", this, resolver, cmd, readyCode);
- }
- validateAckReplyCode("sendStream", resolver, readyCode, false);
- }
-
- protected void validateOperationReadyCode(String command, Object location, int readyCode, boolean eofAllowed) throws IOException {
- validateCommandStatusCode(command, location, readyCode, eofAllowed);
- }
-
- protected void validateAckReplyCode(String command, Object location, int replyCode, boolean eofAllowed) throws IOException {
- validateCommandStatusCode(command, location, replyCode, eofAllowed);
- }
-
- protected void validateCommandStatusCode(String command, Object location, int statusCode, boolean eofAllowed) throws IOException {
- switch (statusCode) {
- case -1:
- if (!eofAllowed) {
- throw new EOFException("Unexpected EOF for command='" + command + "' on " + location);
- }
- break;
- case OK:
- break;
- case WARNING:
- break;
- default:
- throw new ScpException("Bad reply code (" + statusCode + ") for command='" + command + "' on " + location, statusCode);
- }
- }
-
- public void sendDir(Path local, boolean preserve, int bufferSize) throws IOException {
- Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
- boolean debugEnabled = log.isDebugEnabled();
- if (debugEnabled) {
- log.debug("sendDir({}) Sending directory {} - preserve={}, buffer-size={}",
- this, path, preserve, bufferSize);
- }
-
- LinkOption[] options = IoUtils.getLinkOptions(true);
- if (preserve) {
- BasicFileAttributes basic = opener.getLocalBasicFileAttributes(path, options);
- FileTime lastModified = basic.lastModifiedTime();
- FileTime lastAccess = basic.lastAccessTime();
- String cmd = "T" + lastModified.to(TimeUnit.SECONDS) + " "
- + "0" + " " + lastAccess.to(TimeUnit.SECONDS) + " "
- + "0";
- if (debugEnabled) {
- log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}",
- this, path, lastModified, lastAccess, cmd);
- }
-
- out.write(cmd.getBytes(StandardCharsets.UTF_8));
- out.write('\n');
- out.flush();
-
- int readyCode = readAck(false);
- if (debugEnabled) {
- if (debugEnabled) {
- log.debug("sendDir({})[{}] command='{}' ready code={}", this, path, cmd, readyCode);
- }
- }
- validateAckReplyCode(cmd, path, readyCode, false);
- }
-
- Set<PosixFilePermission> perms = opener.getLocalFilePermissions(path, options);
- String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms)) ? DEFAULT_DIR_OCTAL_PERMISSIONS : getOctalPermissions(perms);
- String cmd = "D" + octalPerms + " " + "0" + " " + Objects.toString(path.getFileName(), null);
- if (debugEnabled) {
- log.debug("sendDir({})[{}] send 'D' command: {}", this, path, cmd);
- }
- out.write(cmd.getBytes(StandardCharsets.UTF_8));
- out.write('\n');
- out.flush();
-
- int readyCode = readAck(false);
- if (debugEnabled) {
- log.debug("sendDir({})[{}] command='{}' ready code={}",
- this, path, cmd.substring(0, cmd.length() - 1), readyCode);
- }
- validateAckReplyCode(cmd, path, readyCode, false);
-
- try (DirectoryStream<Path> children = opener.getLocalFolderChildren(path)) {
- listener.startFolderEvent(FileOperation.SEND, path, perms);
-
- try {
- for (Path child : children) {
- if (opener.sendAsRegularFile(child, options)) {
- sendFile(child, preserve, bufferSize);
- } else if (opener.sendAsDirectory(child, options)) {
- sendDir(child, preserve, bufferSize);
- }
- }
- } catch (IOException | RuntimeException e) {
- listener.endFolderEvent(FileOperation.SEND, path, perms, e);
- throw e;
- }
-
- listener.endFolderEvent(FileOperation.SEND, path, perms, null);
- }
-
- if (debugEnabled) {
- log.debug("sendDir({})[{}] send 'E' command", this, path);
- }
- out.write("E\n".getBytes(StandardCharsets.UTF_8));
- out.flush();
-
- readyCode = readAck(false);
- if (debugEnabled) {
- log.debug("sendDir({})[{}] 'E' command reply code=", this, path, readyCode);
- }
- validateAckReplyCode("E", path, readyCode, false);
- }
-
- public static String getOctalPermissions(Collection<PosixFilePermission> perms) {
- int pf = 0;
-
- for (PosixFilePermission p : perms) {
- switch (p) {
- case OWNER_READ:
- pf |= S_IRUSR;
- break;
- case OWNER_WRITE:
- pf |= S_IWUSR;
- break;
- case OWNER_EXECUTE:
- pf |= S_IXUSR;
- break;
- case GROUP_READ:
- pf |= S_IRGRP;
- break;
- case GROUP_WRITE:
- pf |= S_IWGRP;
- break;
- case GROUP_EXECUTE:
- pf |= S_IXGRP;
- break;
- case OTHERS_READ:
- pf |= S_IROTH;
- break;
- case OTHERS_WRITE:
- pf |= S_IWOTH;
- break;
- case OTHERS_EXECUTE:
- pf |= S_IXOTH;
- break;
- default: // ignored
- }
- }
-
- return String.format("%04o", pf);
- }
-
- public static Set<PosixFilePermission> parseOctalPermissions(String str) {
- int perms = Integer.parseInt(str, 8);
- Set<PosixFilePermission> p = EnumSet.noneOf(PosixFilePermission.class);
- if ((perms & S_IRUSR) != 0) {
- p.add(PosixFilePermission.OWNER_READ);
- }
- if ((perms & S_IWUSR) != 0) {
- p.add(PosixFilePermission.OWNER_WRITE);
- }
- if ((perms & S_IXUSR) != 0) {
- p.add(PosixFilePermission.OWNER_EXECUTE);
- }
- if ((perms & S_IRGRP) != 0) {
- p.add(PosixFilePermission.GROUP_READ);
- }
- if ((perms & S_IWGRP) != 0) {
- p.add(PosixFilePermission.GROUP_WRITE);
- }
- if ((perms & S_IXGRP) != 0) {
- p.add(PosixFilePermission.GROUP_EXECUTE);
- }
- if ((perms & S_IROTH) != 0) {
- p.add(PosixFilePermission.OTHERS_READ);
- }
- if ((perms & S_IWOTH) != 0) {
- p.add(PosixFilePermission.OTHERS_WRITE);
- }
- if ((perms & S_IXOTH) != 0) {
- p.add(PosixFilePermission.OTHERS_EXECUTE);
- }
-
- return p;
- }
-
- protected void sendWarning(String message) throws IOException {
- sendResponseMessage(WARNING, message);
- }
-
- protected void sendError(String message) throws IOException {
- sendResponseMessage(ERROR, message);
- }
-
- protected void sendResponseMessage(int level, String message) throws IOException {
- sendResponseMessage(out, level, message);
- }
-
- public static <O extends OutputStream> O sendWarning(O out, String message) throws IOException {
- return sendResponseMessage(out, WARNING, message);
- }
-
- public static <O extends OutputStream> O sendError(O out, String message) throws IOException {
- return sendResponseMessage(out, ERROR, message);
- }
-
- public static <O extends OutputStream> O sendResponseMessage(O out, int level, String message) throws IOException {
- out.write(level);
- out.write(message.getBytes(StandardCharsets.UTF_8));
- out.write('\n');
- out.flush();
- return out;
- }
-
- public static String getExitStatusName(Integer exitStatus) {
- if (exitStatus == null) {
- return "null";
- }
-
- switch (exitStatus) {
- case OK:
- return "OK";
- case WARNING:
- return "WARNING";
- case ERROR:
- return "ERROR";
- default:
- return exitStatus.toString();
- }
- }
-
- public void ack() throws IOException {
- out.write(0);
- out.flush();
- }
-
- public int readAck(boolean canEof) throws IOException {
- int c = in.read();
- switch (c) {
- case -1:
- if (log.isDebugEnabled()) {
- log.debug("readAck({})[EOF={}] received EOF", this, canEof);
- }
- if (!canEof) {
- throw new EOFException("readAck - EOF before ACK");
- }
- break;
- case OK:
- if (log.isDebugEnabled()) {
- log.debug("readAck({})[EOF={}] read OK", this, canEof);
- }
- break;
- case WARNING: {
- if (log.isDebugEnabled()) {
- log.debug("readAck({})[EOF={}] read warning message", this, canEof);
- }
-
- String line = readLine();
- log.warn("readAck({})[EOF={}] - Received warning: {}", this, canEof, line);
- break;
- }
- case ERROR: {
- if (log.isDebugEnabled()) {
- log.debug("readAck({})[EOF={}] read error message", this, canEof);
- }
- String line = readLine();
- if (log.isDebugEnabled()) {
- log.debug("readAck({})[EOF={}] received error: {}", this, canEof, line);
- }
- throw new ScpException("Received nack: " + line, c);
- }
- default:
- break;
- }
- return c;
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + "[" + getSession() + "]";
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
deleted file mode 100644
index d2a9afc..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpLocation.java
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.Serializable;
-import java.util.Objects;
-
-import org.apache.sshd.common.auth.MutableUserHolder;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-
-/**
- * Represents a local or remote SCP location in the format {@code user@host:path}
- * for a remote path and a simple path for a local one. If user is omitted for a
- * remote path then current user is used.
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ScpLocation implements MutableUserHolder, Serializable, Cloneable {
- public static final char HOST_PART_SEPARATOR = ':';
- public static final char USERNAME_PART_SEPARATOR = '@';
-
- private static final long serialVersionUID = 5450230457030600136L;
-
- private String host;
- private String username;
- private String path;
-
- public ScpLocation() {
- this(null);
- }
-
- /**
- * @param locSpec The location specification - ignored if {@code null}/empty
- * @see #update(String, ScpLocation)
- * @throws IllegalArgumentException if invalid specification
- */
- public ScpLocation(String locSpec) {
- update(locSpec, this);
- }
-
- public String getHost() {
- return host;
- }
-
- public void setHost(String host) {
- this.host = host;
- }
-
- public boolean isLocal() {
- return GenericUtils.isEmpty(getHost());
- }
-
- @Override
- public String getUsername() {
- return username;
- }
-
- @Override
- public void setUsername(String username) {
- this.username = username;
- }
-
- /**
- * Resolves the effective username to use for a remote location.
- * If username not set then uses the current username
- *
- * @return The resolved username
- * @see #getUsername()
- * @see OsUtils#getCurrentUser()
- */
- public String resolveUsername() {
- String user = getUsername();
- if (GenericUtils.isEmpty(user)) {
- return OsUtils.getCurrentUser();
- } else {
- return user;
- }
- }
-
- public String getPath() {
- return path;
- }
-
- public void setPath(String path) {
- this.path = path;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(getHost(), resolveUsername(), OsUtils.getComparablePath(getPath()));
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj == null) {
- return false;
- }
- if (this == obj) {
- return true;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
-
- ScpLocation other = (ScpLocation) obj;
- if (this.isLocal() != other.isLocal()) {
- return false;
- }
-
- String thisPath = OsUtils.getComparablePath(getPath());
- String otherPath = OsUtils.getComparablePath(other.getPath());
- if (!Objects.equals(thisPath, otherPath)) {
- return false;
- }
-
- if (isLocal()) {
- return true;
- }
-
- // we know other is also remote or we would not have reached this point
- return Objects.equals(resolveUsername(), other.resolveUsername())
- && Objects.equals(getHost(), other.getHost());
- }
-
- @Override
- public ScpLocation clone() {
- try {
- return getClass().cast(super.clone());
- } catch (CloneNotSupportedException e) { // unexpected
- throw new RuntimeException("Failed to clone " + toString(), e);
- }
- }
-
- @Override
- public String toString() {
- String p = getPath();
- if (isLocal()) {
- return p;
- }
-
- return resolveUsername() + String.valueOf(USERNAME_PART_SEPARATOR)
- + getHost() + String.valueOf(HOST_PART_SEPARATOR) + p;
- }
-
- /**
- * Parses a local or remote SCP location in the format {@code user@host:path}
- *
- * @param locSpec The location specification - ignored if {@code null}/empty
- * @return The {@link ScpLocation} or {@code null} if no specification provider
- * @throws IllegalArgumentException if invalid specification
- * @see #update(String, ScpLocation)
- */
- public static ScpLocation parse(String locSpec) {
- return GenericUtils.isEmpty(locSpec) ? null : update(locSpec, new ScpLocation());
- }
-
- /**
- * Parses a local or remote SCP location in the format {@code user@host:path}
- *
- * @param <L> Type of {@link ScpLocation} being updated
- * @param locSpec The location specification - ignored if {@code null}/empty
- * @param location The {@link ScpLocation} to update - never {@code null}
- * @return The updated location (unless no specification)
- * @throws IllegalArgumentException if invalid specification
- */
- public static <L extends ScpLocation> L update(String locSpec, L location) {
- Objects.requireNonNull(location, "No location to update");
- if (GenericUtils.isEmpty(locSpec)) {
- return location;
- }
-
- location.setHost(null);
- location.setUsername(null);
-
- int pos = locSpec.indexOf(HOST_PART_SEPARATOR);
- if (pos < 0) { // assume a local path
- location.setPath(locSpec);
- return location;
- }
-
- /*
- * NOTE !!! in such a case there may be confusion with a host named 'a',
- * but there is a limit to how smart we can be...
- */
- if ((pos == 1) && OsUtils.isWin32()) {
- char drive = locSpec.charAt(0);
- if (((drive >= 'a') && (drive <= 'z')) || ((drive >= 'A') && (drive <= 'Z'))) {
- location.setPath(locSpec);
- return location;
- }
- }
-
- String login = locSpec.substring(0, pos);
- ValidateUtils.checkTrue(pos < (locSpec.length() - 1), "Invalid remote specification (missing path): %s", locSpec);
- location.setPath(locSpec.substring(pos + 1));
-
- pos = login.indexOf(USERNAME_PART_SEPARATOR);
- ValidateUtils.checkTrue(pos != 0, "Invalid remote specification (missing username): %s", locSpec);
- if (pos < 0) {
- location.setHost(login);
- } else {
- location.setUsername(login.substring(0, pos));
- ValidateUtils.checkTrue(pos < (login.length() - 1), "Invalid remote specification (missing host): %s", locSpec);
- location.setHost(login.substring(pos + 1));
- }
-
- return location;
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
deleted file mode 100644
index d0e611c..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpReceiveLineHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.IOException;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FunctionalInterface
-public interface ScpReceiveLineHandler {
- /**
- * @param line Received SCP input line
- * @param isDir Does the input line refer to a directory
- * @param time The received {@link ScpTimestamp} - may be {@code null}
- * @throws IOException If failed to process the line
- */
- void process(String line, boolean isDir, ScpTimestamp time) throws IOException;
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
deleted file mode 100644
index feeecbc..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpSourceStreamResolver.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Collection;
-
-import org.apache.sshd.common.session.Session;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpSourceStreamResolver {
- /**
- * @return The uploaded file name
- * @throws IOException If failed to resolve the name
- */
- String getFileName() throws IOException;
-
- /**
- * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
- */
- Path getEventListenerFilePath();
-
- /**
- * @return The permissions to be used for uploading a file
- * @throws IOException If failed to generate the required permissions
- */
- Collection<PosixFilePermission> getPermissions() throws IOException;
-
- /**
- * @return The {@link ScpTimestamp} to use for uploading the file
- * if {@code null} then no need to send this information
- * @throws IOException If failed to generate the required data
- */
- ScpTimestamp getTimestamp() throws IOException;
-
- /**
- * @return An estimated size of the expected number of bytes to be uploaded.
- * If non-positive then assumed to be unknown.
- * @throws IOException If failed to generate an estimate
- */
- long getSize() throws IOException;
-
- /**
- * @param session The {@link Session} through which file is transmitted
- * @param options The {@link OpenOption}s may be {@code null}/empty
- * @return The {@link InputStream} containing the data to be uploaded
- * @throws IOException If failed to create the stream
- */
- InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException;
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
deleted file mode 100644
index 9a70302..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTargetStreamResolver.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Set;
-
-import org.apache.sshd.common.session.Session;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpTargetStreamResolver {
- /**
- * Called when receiving a file in order to obtain an output stream
- * for the incoming data
- *
- * @param session The associated {@link Session}
- * @param name File name as received from remote site
- * @param length Number of bytes expected to receive
- * @param perms The {@link Set} of {@link PosixFilePermission} expected
- * @param options The {@link OpenOption}s to use - may be {@code null}/empty
- * @return The {@link OutputStream} to write the incoming data
- * @throws IOException If failed to create the stream
- */
- OutputStream resolveTargetStream(Session session, String name, long length,
- Set<PosixFilePermission> perms, OpenOption... options) throws IOException;
-
- /**
- * @return The {@link Path} to use when invoking the {@link ScpTransferEventListener}
- */
- Path getEventListenerFilePath();
-
- /**
- * Called after successful reception of the data (and after closing the stream)
- *
- * @param name File name as received from remote site
- * @param preserve If {@code true} then the resolver should attempt to preserve
- * the specified permissions and timestamp
- * @param perms The {@link Set} of {@link PosixFilePermission} expected
- * @param time If not {@code null} then the required timestamp(s) on the
- * incoming data
- * @throws IOException If failed to post-process the incoming data
- */
- void postProcessReceivedData(String name, boolean preserve, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException;
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
deleted file mode 100644
index e804de9..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTimestamp.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-
-import org.apache.sshd.common.util.GenericUtils;
-
-/**
- * Represents an SCP timestamp definition
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class ScpTimestamp {
- private final long lastModifiedTime;
- private final long lastAccessTime;
-
- public ScpTimestamp(long modTime, long accTime) {
- lastModifiedTime = modTime;
- lastAccessTime = accTime;
- }
-
- public long getLastModifiedTime() {
- return lastModifiedTime;
- }
-
- public long getLastAccessTime() {
- return lastAccessTime;
- }
-
- @Override
- public String toString() {
- return "modified=" + new Date(lastModifiedTime)
- + ";accessed=" + new Date(lastAccessTime);
- }
-
- /**
- * @param line The time specification - format:
- * {@code T<mtime-sec> <mtime-micros> <atime-sec> <atime-micros>}
- * where specified times are in seconds since UTC
- * @return The {@link ScpTimestamp} value with the timestamps converted to
- * <U>milliseconds</U>
- * @throws NumberFormatException if bad numerical values - <B>Note:</B>
- * does not check if 1st character is 'T'.
- * @see <A HREF="https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works">How the SCP protocol works</A>
- */
- public static ScpTimestamp parseTime(String line) throws NumberFormatException {
- String[] numbers = GenericUtils.split(line.substring(1), ' ');
- return new ScpTimestamp(TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[0])),
- TimeUnit.SECONDS.toMillis(Long.parseLong(numbers[2])));
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
deleted file mode 100644
index d7954e0..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * 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.common.scp;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Set;
-
-import org.apache.sshd.common.util.SshdEventListener;
-
-/**
- * Can be registered in order to receive events about SCP transfers
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public interface ScpTransferEventListener extends SshdEventListener {
- enum FileOperation {
- SEND,
- RECEIVE
- }
-
- /**
- * An "empty" implementation to be used instead of {@code null}s
- */
- ScpTransferEventListener EMPTY = new ScpTransferEventListener() {
- @Override
- public String toString() {
- return "EMPTY";
- }
- };
-
- /**
- * @param op The {@link FileOperation}
- * @param file The <U>local</U> referenced file {@link Path}
- * @param length Size (in bytes) of transferred data
- * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
- * once transfer is complete
- * @throws IOException If failed to handle the event
- */
- default void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) throws IOException {
- // ignored
- }
-
- /**
- * @param op The {@link FileOperation}
- * @param file The <U>local</U> referenced file {@link Path}
- * @param length Size (in bytes) of transferred data
- * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
- * once transfer is complete
- * @param thrown The result of the operation attempt - if {@code null} then
- * reception was successful
- * @throws IOException If failed to handle the event
- */
- default void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
- throws IOException {
- // ignored
- }
-
- /**
- * @param op The {@link FileOperation}
- * @param file The <U>local</U> referenced folder {@link Path}
- * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
- * once transfer is complete
- * @throws IOException If failed to handle the event
- */
- default void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
- // ignored
- }
-
- /**
- * @param op The {@link FileOperation}
- * @param file The <U>local</U> referenced file {@link Path}
- * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
- * once transfer is complete
- * @param thrown The result of the operation attempt - if {@code null} then
- * reception was successful
- * @throws IOException If failed to handle the event
- */
- default void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
- throws IOException {
- // ignored
- }
-
- static <L extends ScpTransferEventListener> L validateListener(L listener) {
- return SshdEventListener.validateListener(listener, ScpTransferEventListener.class.getSimpleName());
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
deleted file mode 100644
index bb6ae3b..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/DefaultScpFileOpener.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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.common.scp.helpers;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.util.Arrays;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpSourceStreamResolver;
-import org.apache.sshd.common.scp.ScpTargetStreamResolver;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class DefaultScpFileOpener extends AbstractLoggingBean implements ScpFileOpener {
- public static final DefaultScpFileOpener INSTANCE = new DefaultScpFileOpener();
-
- public DefaultScpFileOpener() {
- super();
- }
-
- @Override
- public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
- if (log.isDebugEnabled()) {
- log.debug("openRead({}) file={}, options={}",
- session, file, Arrays.toString(options));
- }
-
- return Files.newInputStream(file, options);
- }
-
- @Override
- public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
- if (log.isDebugEnabled()) {
- log.debug("openWrite({}) file={}, options={}",
- session, file, Arrays.toString(options));
- }
-
- return Files.newOutputStream(file, options);
- }
-
- @Override
- public ScpSourceStreamResolver createScpSourceStreamResolver(Path path) throws IOException {
- return new LocalFileScpSourceStreamResolver(path, this);
- }
-
- @Override
- public ScpTargetStreamResolver createScpTargetStreamResolver(Path path) throws IOException {
- return new LocalFileScpTargetStreamResolver(path, this);
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
deleted file mode 100644
index 8ce9b61..0000000
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/helpers/LocalFileScpSourceStreamResolver.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.common.scp.helpers;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributeView;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Collection;
-import java.util.Objects;
-import java.util.Set;
-
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpSourceStreamResolver;
-import org.apache.sshd.common.scp.ScpTimestamp;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class LocalFileScpSourceStreamResolver extends AbstractLoggingBean implements ScpSourceStreamResolver {
- protected final Path path;
- protected final ScpFileOpener opener;
- protected final Path name;
- protected final Set<PosixFilePermission> perms;
- protected final long size;
- protected final ScpTimestamp time;
-
- public LocalFileScpSourceStreamResolver(Path path, ScpFileOpener opener) throws IOException {
- this.path = Objects.requireNonNull(path, "No path specified");
- this.opener = (opener == null) ? DefaultScpFileOpener.INSTANCE : opener;
- this.name = path.getFileName();
- this.perms = IoUtils.getPermissions(path);
-
- BasicFileAttributes basic = Files.getFileAttributeView(path, BasicFileAttributeView.class).readAttributes();
- this.size = basic.size();
- this.time = new ScpTimestamp(basic.lastModifiedTime().toMillis(), basic.lastAccessTime().toMillis());
- }
-
- @Override
- public String getFileName() throws IOException {
- return name.toString();
- }
-
- @Override
- public Collection<PosixFilePermission> getPermissions() throws IOException {
- return perms;
- }
-
- @Override
- public ScpTimestamp getTimestamp() throws IOException {
- return time;
- }
-
- @Override
- public long getSize() throws IOException {
- return size;
- }
-
- @Override
- public Path getEventListenerFilePath() {
- return path;
- }
-
- @Override
- public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
- return opener.openRead(session, getEventListenerFilePath(), options);
- }
-
- @Override
- public String toString() {
- return String.valueOf(getEventListenerFilePath());
- }
-}
[5/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
deleted file mode 100644
index 2212d7d..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/scp/ScpTest.java
+++ /dev/null
@@ -1,1197 +0,0 @@
-/*
- * 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.scp;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.channels.FileChannel;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.OpenOption;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.nio.file.attribute.PosixFilePermission;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Date;
-import java.util.EnumSet;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import com.jcraft.jsch.ChannelExec;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.common.Factory;
-import org.apache.sshd.common.channel.Channel;
-import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
-import org.apache.sshd.common.random.Random;
-import org.apache.sshd.common.scp.ScpException;
-import org.apache.sshd.common.scp.ScpFileOpener;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.scp.ScpTransferEventListener;
-import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.ValidateUtils;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.ExitCallback;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.scp.ScpCommand;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.apache.sshd.util.test.JSchLogger;
-import org.apache.sshd.util.test.SimpleUserInfo;
-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;
-
-import ch.ethz.ssh2.Connection;
-import ch.ethz.ssh2.ConnectionInfo;
-import ch.ethz.ssh2.SCPClient;
-
-/**
- * Test for SCP support.
- *
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class ScpTest extends BaseTestSupport {
- private static final ScpTransferEventListener DEBUG_LISTENER = new ScpTransferEventListener() {
- @Override
- public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
- logEvent("starFolderEvent", op, file, false, -1L, perms, null);
- }
-
- @Override
- public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
- logEvent("startFileEvent", op, file, true, length, perms, null);
-
- }
-
- @Override
- public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
- logEvent("endFolderEvent", op, file, false, -1L, perms, thrown);
- }
-
- @Override
- public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
- logEvent("endFileEvent", op, file, true, length, perms, thrown);
- }
-
- private void logEvent(String type, FileOperation op, Path path, boolean isFile, long length, Collection<PosixFilePermission> perms, Throwable t) {
- if (!OUTPUT_DEBUG_MESSAGES) {
- return; // just in case
- }
- StringBuilder sb = new StringBuilder(Byte.MAX_VALUE);
- sb.append('\t').append(type)
- .append('[').append(op).append(']')
- .append(' ').append(isFile ? "File" : "Directory").append('=').append(path)
- .append(' ').append("length=").append(length)
- .append(' ').append("perms=").append(perms);
- if (t != null) {
- sb.append(' ').append("ERROR=").append(t.getClass().getSimpleName()).append(": ").append(t.getMessage());
- }
- outputDebugMessage(sb.toString());
- }
- };
-
- private static SshServer sshd;
- private static int port;
- private static SshClient client;
- private final FileSystemFactory fileSystemFactory;
-
- public ScpTest() 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(ScpTest.class);
- sshd.setCommandFactory(new ScpCommandFactory());
- sshd.start();
- port = sshd.getPort();
-
- client = Utils.setupTestClient(ScpTest.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;
- }
- }
- }
-
- @Before
- public void setUp() throws Exception {
- sshd.setFileSystemFactory(fileSystemFactory);
- }
-
- @Test
- public void testNormalizedScpRemotePaths() throws Exception {
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path localFile = localDir.resolve("file.txt");
- byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
- String localPath = localFile.toString();
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- String[] remoteComps = GenericUtils.split(remotePath, '/');
- Factory<? extends Random> factory = client.getRandomFactory();
- Random rnd = factory.create();
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(5L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- StringBuilder sb = new StringBuilder(remotePath.length() + Long.SIZE);
- for (int i = 0; i < Math.max(Long.SIZE, remoteComps.length); i++) {
- if (sb.length() > 0) {
- sb.setLength(0); // start again
- }
-
- sb.append(remoteComps[0]);
- for (int j = 1; j < remoteComps.length; j++) {
- String name = remoteComps[j];
- slashify(sb, rnd);
- sb.append(name);
- }
- slashify(sb, rnd);
-
- String path = sb.toString();
- scp.upload(localPath, path);
- assertTrue("Remote file not ready for " + path, waitForFile(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L)));
-
- byte[] actual = Files.readAllBytes(remoteFile);
- assertArrayEquals("Mismatched uploaded data for " + path, data, actual);
- Files.delete(remoteFile);
- assertFalse("Remote file (" + remoteFile + ") not deleted for " + path, Files.exists(remoteFile));
- }
- }
- }
-
- private static int slashify(StringBuilder sb, Random rnd) {
- int slashes = 1 /* at least one slash */ + rnd.random(Byte.SIZE);
- for (int k = 0; k < slashes; k++) {
- sb.append('/');
- }
-
- return slashes;
- }
-
- @Test
- public void testUploadAbsoluteDriveLetter() throws Exception {
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path localFile = localDir.resolve("file-1.txt");
- byte[] data = Utils.writeFile(localFile, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remoteFile = remoteDir.resolve(localFile.getFileName().toString());
- String localPath = localFile.toString();
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(5L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- scp.upload(localPath, remotePath);
- assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(5L));
-
- Path secondRemote = remoteDir.resolve("file-2.txt");
- String secondPath = Utils.resolveRelativeRemotePath(parentPath, secondRemote);
- scp.upload(localPath, secondPath);
- assertFileLength(secondRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
-
- Path pathRemote = remoteDir.resolve("file-path.txt");
- String pathPath = Utils.resolveRelativeRemotePath(parentPath, pathRemote);
- scp.upload(localFile, pathPath);
- assertFileLength(pathRemote, data.length, TimeUnit.SECONDS.toMillis(5L));
- }
- }
-
- @Test
- public void testScpUploadOverwrite() throws Exception {
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(5L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
-
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path localFile = localDir.resolve("file.txt");
- Utils.writeFile(localFile, data);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remoteFile = remoteDir.resolve(localFile.getFileName());
- Utils.writeFile(remoteFile, data + data);
-
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- scp.upload(localFile.toString(), remotePath);
- assertFileLength(remoteFile, data.length(), TimeUnit.SECONDS.toMillis(5L));
- }
- }
-
- @Test
- public void testScpUploadZeroLengthFile() throws Exception {
- Path targetPath = detectTargetFolder();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path zeroLocal = localDir.resolve("zero.txt");
-
- try (FileChannel fch = FileChannel.open(zeroLocal, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
- if (fch.size() > 0L) {
- fch.truncate(0L);
- }
- }
- assertEquals("Non-zero size for local file=" + zeroLocal, 0L, Files.size(zeroLocal));
-
- Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
- if (Files.exists(zeroRemote)) {
- Files.delete(zeroRemote);
- }
-
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(5L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
- scp.upload(zeroLocal.toString(), remotePath);
- assertFileLength(zeroRemote, 0L, TimeUnit.SECONDS.toMillis(5L));
- }
- }
-
- @Test
- public void testScpDownloadZeroLengthFile() throws Exception {
- Path targetPath = detectTargetFolder();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path zeroLocal = localDir.resolve(getCurrentTestName());
- if (Files.exists(zeroLocal)) {
- Files.delete(zeroLocal);
- }
-
- Path zeroRemote = remoteDir.resolve(zeroLocal.getFileName());
- try (FileChannel fch = FileChannel.open(zeroRemote, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
- if (fch.size() > 0L) {
- fch.truncate(0L);
- }
- }
- assertEquals("Non-zero size for remote file=" + zeroRemote, 0L, Files.size(zeroRemote));
-
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(5L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- String remotePath = Utils.resolveRelativeRemotePath(targetPath.getParent(), zeroRemote);
- scp.download(remotePath, zeroLocal.toString());
- assertFileLength(zeroLocal, 0L, TimeUnit.SECONDS.toMillis(5L));
- }
- }
-
- @Test
- public void testScpNativeOnSingleFile() throws Exception {
- String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
-
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path localOutFile = localDir.resolve("file-1.txt");
- Path remoteDir = scpRoot.resolve("remote");
- Path remoteOutFile = remoteDir.resolve(localOutFile.getFileName());
-
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- Utils.writeFile(localOutFile, data);
-
- assertFalse("Remote folder already exists: " + remoteDir, Files.exists(remoteDir));
-
- String localOutPath = localOutFile.toString();
- String remoteOutPath = Utils.resolveRelativeRemotePath(parentPath, remoteOutFile);
- outputDebugMessage("Expect upload failure %s => %s", localOutPath, remoteOutPath);
- try {
- scp.upload(localOutPath, remoteOutPath);
- fail("Expected IOException for 1st time " + remoteOutPath);
- } catch (IOException e) {
- // ok
- }
-
- assertHierarchyTargetFolderExists(remoteDir);
- outputDebugMessage("Expect upload success %s => %s", localOutPath, remoteOutPath);
- scp.upload(localOutPath, remoteOutPath);
- assertFileLength(remoteOutFile, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
- Path secondLocal = localDir.resolve(localOutFile.getFileName());
- String downloadTarget = Utils.resolveRelativeRemotePath(parentPath, secondLocal);
- outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
- scp.download(remoteOutPath, downloadTarget);
- assertFileLength(secondLocal, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
- Path localPath = localDir.resolve("file-path.txt");
- downloadTarget = Utils.resolveRelativeRemotePath(parentPath, localPath);
- outputDebugMessage("Expect download success %s => %s", remoteOutPath, downloadTarget);
- scp.download(remoteOutPath, downloadTarget);
- assertFileLength(localPath, data.length(), TimeUnit.SECONDS.toMillis(11L));
- }
- }
-
- @Test
- public void testScpNativeOnMultipleFiles() throws Exception {
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path local1 = localDir.resolve("file-1.txt");
- byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
- Path local2 = localDir.resolve("file-2.txt");
- Files.write(local2, data);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remote1 = remoteDir.resolve(local1.getFileName());
- String remote1Path = Utils.resolveRelativeRemotePath(parentPath, remote1);
- String[] locals = {local1.toString(), local2.toString()};
- try {
- scp.upload(locals, remote1Path);
- fail("Unexpected upload success to missing remote file: " + remote1Path);
- } catch (IOException e) {
- // Ok
- }
-
- Files.write(remote1, data);
- try {
- scp.upload(locals, remote1Path);
- fail("Unexpected upload success to existing remote file: " + remote1Path);
- } catch (IOException e) {
- // Ok
- }
-
- Path remoteSubDir = assertHierarchyTargetFolderExists(remoteDir.resolve("dir"));
- scp.upload(locals, Utils.resolveRelativeRemotePath(parentPath, remoteSubDir));
-
- Path remoteSub1 = remoteSubDir.resolve(local1.getFileName());
- assertFileLength(remoteSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
-
- Path remoteSub2 = remoteSubDir.resolve(local2.getFileName());
- assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
-
- String[] remotes = {
- Utils.resolveRelativeRemotePath(parentPath, remoteSub1),
- Utils.resolveRelativeRemotePath(parentPath, remoteSub2),
- };
-
- try {
- scp.download(remotes, Utils.resolveRelativeRemotePath(parentPath, local1));
- fail("Unexpected download success to existing local file: " + local1);
- } catch (IOException e) {
- // Ok
- }
-
- Path localSubDir = localDir.resolve("dir");
- try {
- scp.download(remotes, localSubDir);
- fail("Unexpected download success to non-existing folder: " + localSubDir);
- } catch (IOException e) {
- // Ok
- }
-
- assertHierarchyTargetFolderExists(localSubDir);
- scp.download(remotes, localSubDir);
-
- assertFileLength(localSubDir.resolve(remoteSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
- assertFileLength(localSubDir.resolve(remoteSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
- }
- }
-
- @Test
- public void testScpNativeOnRecursiveDirs() throws Exception {
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = scpRoot.resolve("local");
- Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
- Path localSub1 = localSubDir.resolve("file-1.txt");
- byte[] data = Utils.writeFile(localSub1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
- Path localSub2 = localSubDir.resolve("file-2.txt");
- Files.write(localSub2, data);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- scp.upload(localSubDir, Utils.resolveRelativeRemotePath(parentPath, remoteDir), ScpClient.Option.Recursive);
-
- Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
- assertFileLength(remoteSubDir.resolve(localSub1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
- assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
- Utils.deleteRecursive(localSubDir);
-
- scp.download(Utils.resolveRelativeRemotePath(parentPath, remoteSubDir), localDir, ScpClient.Option.Recursive);
- assertFileLength(localSub1, data.length, TimeUnit.SECONDS.toMillis(11L));
- assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
- }
- }
-
- @Test
- public void testScpNativeOnDirWithPattern() throws Exception {
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path local1 = localDir.resolve("file-1.txt");
- byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
- Path local2 = localDir.resolve("file-2.txt");
- Files.write(local2, data);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
- scp.upload(localDir.toString() + File.separator + "*", remotePath);
- assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
- assertFileLength(remoteDir.resolve(local2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
- Files.delete(local1);
- Files.delete(local2);
- scp.download(remotePath + "/*", localDir);
- assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
- assertFileLength(local2, data.length, TimeUnit.SECONDS.toMillis(11L));
- }
- }
-
- @Test
- public void testScpNativeOnMixedDirAndFiles() throws Exception {
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = scpRoot.resolve("local");
- Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
- Path local1 = localDir.resolve("file-1.txt");
- byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
- Path localSub2 = localSubDir.resolve("file-2.txt");
- Files.write(localSub2, data);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
- scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive);
- assertFileLength(remoteDir.resolve(local1.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
- Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
- assertFileLength(remoteSubDir.resolve(localSub2.getFileName()), data.length, TimeUnit.SECONDS.toMillis(11L));
-
- Files.delete(local1);
- Utils.deleteRecursive(localSubDir);
-
- scp.download(remotePath + "/*", localDir);
- assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
- assertFalse("Unexpected recursive local file: " + localSub2, Files.exists(localSub2));
-
- Files.delete(local1);
- scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive);
- assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(5L));
- assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(5L));
- }
- }
-
- @Test
- public void testScpNativePreserveAttributes() throws Exception {
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = scpRoot.resolve("local");
- Path localSubDir = assertHierarchyTargetFolderExists(localDir.resolve("dir"));
- // convert everything to seconds since this is the SCP timestamps granularity
- final long lastModMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1);
- final long lastModSecs = TimeUnit.MILLISECONDS.toSeconds(lastModMillis);
- Path local1 = localDir.resolve("file-1.txt");
- byte[] data = Utils.writeFile(local1, getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL);
-
- File lclFile1 = local1.toFile();
- boolean lcl1ModSet = lclFile1.setLastModified(lastModMillis);
- lclFile1.setExecutable(true, true);
- lclFile1.setWritable(false, false);
-
- Path localSub2 = localSubDir.resolve("file-2.txt");
- Files.write(localSub2, data);
- File lclSubFile2 = localSub2.toFile();
- boolean lclSub2ModSet = lclSubFile2.setLastModified(lastModMillis);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
- scp.upload(localDir.toString() + File.separator + "*", remotePath, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
-
- Path remote1 = remoteDir.resolve(local1.getFileName());
- assertFileLength(remote1, data.length, TimeUnit.SECONDS.toMillis(11L));
-
- File remFile1 = remote1.toFile();
- assertLastModifiedTimeEquals(remFile1, lcl1ModSet, lastModSecs);
-
- Path remoteSubDir = remoteDir.resolve(localSubDir.getFileName());
- Path remoteSub2 = remoteSubDir.resolve(localSub2.getFileName());
- assertFileLength(remoteSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
-
- File remSubFile2 = remoteSub2.toFile();
- assertLastModifiedTimeEquals(remSubFile2, lclSub2ModSet, lastModSecs);
-
- Utils.deleteRecursive(localDir);
- assertHierarchyTargetFolderExists(localDir);
-
- scp.download(remotePath + "/*", localDir, ScpClient.Option.Recursive, ScpClient.Option.PreserveAttributes);
- assertFileLength(local1, data.length, TimeUnit.SECONDS.toMillis(11L));
- assertLastModifiedTimeEquals(lclFile1, lcl1ModSet, lastModSecs);
- assertFileLength(localSub2, data.length, TimeUnit.SECONDS.toMillis(11L));
- assertLastModifiedTimeEquals(lclSubFile2, lclSub2ModSet, lastModSecs);
- }
- }
-
- @Test
- public void testStreamsUploadAndDownload() throws Exception {
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = createScpClient(session);
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remoteFile = remoteDir.resolve("file.txt");
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
- outputDebugMessage("Upload data to %s", remotePath);
- scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
- assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
-
- byte[] uploaded = Files.readAllBytes(remoteFile);
- assertArrayEquals("Mismatched uploaded data", data, uploaded);
-
- outputDebugMessage("Download data from %s", remotePath);
- byte[] downloaded = scp.downloadBytes(remotePath);
- assertArrayEquals("Mismatched downloaded data", uploaded, downloaded);
- }
- }
-
- @Test // see SSHD-649
- public void testScpFileOpener() throws Exception {
- class TrackingFileOpener extends DefaultScpFileOpener {
- private final AtomicInteger readCount = new AtomicInteger(0);
- private final AtomicInteger writeCount = new AtomicInteger(0);
-
- TrackingFileOpener() {
- super();
- }
-
- public AtomicInteger getReadCount() {
- return readCount;
- }
-
- public AtomicInteger getWriteCount() {
- return writeCount;
- }
-
- @Override
- public InputStream openRead(Session session, Path file, OpenOption... options) throws IOException {
- int count = readCount.incrementAndGet();
- outputDebugMessage("openRead(%s)[%s] count=%d", session, file, count);
- return super.openRead(session, file, options);
- }
-
- @Override
- public OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException {
- int count = writeCount.incrementAndGet();
- outputDebugMessage("openWrite(%s)[%s] count=%d", session, file, count);
- return super.openWrite(session, file, options);
- }
- }
-
- ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
- ScpFileOpener opener = factory.getScpFileOpener();
- TrackingFileOpener serverOpener = new TrackingFileOpener();
- factory.setScpFileOpener(serverOpener);
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- TrackingFileOpener clientOpener = new TrackingFileOpener();
- ScpClient scp = session.createScpClient(clientOpener);
-
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot);
- Path localFile = remoteDir.resolve("data.txt");
- byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
- Files.write(localFile, data);
-
- Path remoteFile = remoteDir.resolve("upload.txt");
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- outputDebugMessage("Upload data to %s", remotePath);
- scp.upload(localFile, remotePath);
- assertFileLength(remoteFile, data.length, TimeUnit.SECONDS.toMillis(11L));
-
- AtomicInteger serverRead = serverOpener.getReadCount();
- assertEquals("Mismatched server upload open read count", 0, serverRead.get());
-
- AtomicInteger serverWrite = serverOpener.getWriteCount();
- assertEquals("Mismatched server upload write count", 1, serverWrite.getAndSet(0));
-
- AtomicInteger clientRead = clientOpener.getReadCount();
- assertEquals("Mismatched client upload read count", 1, clientRead.getAndSet(0));
-
- AtomicInteger clientWrite = clientOpener.getWriteCount();
- assertEquals("Mismatched client upload write count", 0, clientWrite.get());
-
- Files.delete(localFile);
- scp.download(remotePath, localFile);
- assertFileLength(localFile, data.length, TimeUnit.SECONDS.toMillis(11L));
-
- assertEquals("Mismatched server download open read count", 1, serverRead.getAndSet(0));
- assertEquals("Mismatched server download write count", 0, serverWrite.get());
- assertEquals("Mismatched client download read count", 0, clientRead.get());
- assertEquals("Mismatched client download write count", 1, clientWrite.getAndSet(0));
- } finally {
- factory.setScpFileOpener(opener);
- }
- }
-
- @Test // see SSHD-628
- public void testScpExitStatusPropagation() throws Exception {
- final int testExitValue = 7365;
- class InternalScpCommand extends ScpCommand implements ExitCallback {
- private ExitCallback delegate;
-
- InternalScpCommand(String command, ExecutorService executorService, boolean shutdownOnExit,
- int sendSize, int receiveSize, ScpFileOpener opener, ScpTransferEventListener eventListener) {
- super(command, executorService, shutdownOnExit, sendSize, receiveSize, opener, eventListener);
- }
-
- @Override
- protected void writeCommandResponseMessage(String command, int exitValue, String exitMessage) throws IOException {
- outputDebugMessage("writeCommandResponseMessage(%s) status=%d", command, exitValue);
- super.writeCommandResponseMessage(command, testExitValue, exitMessage);
- }
-
- @Override
- public void setExitCallback(ExitCallback callback) {
- delegate = callback;
- super.setExitCallback(this);
- }
-
- @Override
- public void onExit(int exitValue) {
- onExit(exitValue, Integer.toString(exitValue));
- }
-
- @Override
- public void onExit(int exitValue, String exitMessage) {
- outputDebugMessage("onExit(%s) status=%d", this, exitValue);
- if (exitValue == ScpHelper.OK) {
- delegate.onExit(testExitValue, exitMessage);
- } else {
- delegate.onExit(exitValue, exitMessage);
- }
- }
- }
-
- ScpCommandFactory factory = (ScpCommandFactory) sshd.getCommandFactory();
- sshd.setCommandFactory(new ScpCommandFactory() {
- @Override
- public Command createCommand(String command) {
- ValidateUtils.checkTrue(command.startsWith(ScpHelper.SCP_COMMAND_PREFIX), "Bad SCP command: %s", command);
- return new InternalScpCommand(command,
- getExecutorService(), isShutdownOnExit(),
- getSendBufferSize(), getReceiveBufferSize(),
- DefaultScpFileOpener.INSTANCE,
- ScpTransferEventListener.EMPTY);
- }
- });
-
- try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(13L, TimeUnit.SECONDS).getSession()) {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(11L, TimeUnit.SECONDS);
-
- ScpClient scp = session.createScpClient();
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remoteFile = remoteDir.resolve("file.txt");
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
- outputDebugMessage("Upload data to %s", remotePath);
- try {
- scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
- outputDebugMessage("Upload success to %s", remotePath);
- } catch (ScpException e) {
- Integer exitCode = e.getExitStatus();
- assertNotNull("No upload exit status", exitCode);
- assertEquals("Mismatched upload exit status", testExitValue, exitCode.intValue());
- }
-
- if (Files.deleteIfExists(remoteFile)) {
- outputDebugMessage("Deleted remote file %s", remoteFile);
- }
-
- try (OutputStream out = Files.newOutputStream(remoteFile)) {
- out.write(data);
- }
-
- try {
- byte[] downloaded = scp.downloadBytes(remotePath);
- outputDebugMessage("Download success to %s: %s", remotePath, new String(downloaded, StandardCharsets.UTF_8));
- } catch (ScpException e) {
- Integer exitCode = e.getExitStatus();
- assertNotNull("No download exit status", exitCode);
- assertEquals("Mismatched download exit status", testExitValue, exitCode.intValue());
- }
- } finally {
- sshd.setCommandFactory(factory);
- }
- }
-
- // see http://stackoverflow.com/questions/2717936/file-createnewfile-creates-files-with-last-modified-time-before-actual-creatio
- // See https://msdn.microsoft.com/en-us/library/ms724290(VS.85).aspx
- private static void assertLastModifiedTimeEquals(File file, boolean modSuccess, long expectedSeconds) {
- long expectedMillis = TimeUnit.SECONDS.toMillis(expectedSeconds);
- long actualMillis = file.lastModified();
- long actualSeconds = TimeUnit.MILLISECONDS.toSeconds(actualMillis);
- // if failed to set the local file time, don't expect it to be the same
- if (!modSuccess) {
- System.err.append("Failed to set last modified time of ").append(file.getAbsolutePath())
- .append(" to ").append(String.valueOf(expectedMillis))
- .append(" - ").println(new Date(expectedMillis));
- System.err.append("\t\t").append("Current value: ").append(String.valueOf(actualMillis))
- .append(" - ").println(new Date(actualMillis));
- return;
- }
-
- if (OsUtils.isWin32()) {
- // The NTFS file system delays updates to the last access time for a file by up to 1 hour after the last access
- if (expectedSeconds != actualSeconds) {
- System.err.append("Mismatched last modified time for ").append(file.getAbsolutePath())
- .append(" - expected=").append(String.valueOf(expectedSeconds))
- .append('[').append(new Date(expectedMillis).toString()).append(']')
- .append(", actual=").append(String.valueOf(actualSeconds))
- .append('[').append(new Date(actualMillis).toString()).append(']')
- .println();
- }
- } else {
- assertEquals("Mismatched last modified time for " + file.getAbsolutePath(), expectedSeconds, actualSeconds);
- }
- }
-
- @Test
- public void testJschScp() throws Exception {
- com.jcraft.jsch.Session session = getJschSession();
- try {
- String data = getCurrentTestName() + "\n";
-
- String unixDir = "target/scp";
- String fileName = getCurrentTestName() + ".txt";
- String unixPath = unixDir + "/" + fileName;
- File root = new File(unixDir);
- File target = new File(unixPath);
- Utils.deleteRecursive(root);
- root.mkdirs();
- assertTrue("Failed to ensure existence of " + root, root.exists());
-
- target.delete();
- assertFalse("Failed to delete 1st time: " + target, target.exists());
- sendFile(session, unixPath, target, data);
- assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
- target.delete();
- assertFalse("Failed to delete 2nd time: " + target, target.exists());
- sendFile(session, unixDir, target, data);
- assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
-
- sendFileError(session, "target", ScpHelper.SCP_COMMAND_PREFIX, data);
-
- readFileError(session, unixDir);
-
- assertEquals("Mismatched file data", data, readFile(session, unixPath, target));
- assertEquals("Mismatched dir data", data, readDir(session, unixDir, target));
-
- target.delete();
- root.delete();
-
- sendDir(session, "target", ScpHelper.SCP_COMMAND_PREFIX, fileName, data);
- assertFileLength(target, data.length(), TimeUnit.SECONDS.toMillis(11L));
- } finally {
- session.disconnect();
- }
- }
-
- protected com.jcraft.jsch.Session getJschSession() throws JSchException {
- JSch sch = new JSch();
- com.jcraft.jsch.Session session = sch.getSession(getCurrentTestName(), TEST_LOCALHOST, port);
- session.setUserInfo(new SimpleUserInfo(getCurrentTestName()));
- session.connect();
- return session;
- }
-
- @Test
- public void testWithGanymede() throws Exception {
- Path targetPath = detectTargetFolder();
- Path parentPath = targetPath.getParent();
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- byte[] expected = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteDir);
- String fileName = "file.txt";
- Path remoteFile = remoteDir.resolve(fileName);
- String mode = ScpHelper.getOctalPermissions(EnumSet.of(
- PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE,
- PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE,
- PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE
- ));
-
- ch.ethz.ssh2.log.Logger.enabled = true;
- Connection conn = new Connection(TEST_LOCALHOST, port);
- try {
- ConnectionInfo info = conn.connect(null, (int) TimeUnit.SECONDS.toMillis(5L), (int) TimeUnit.SECONDS.toMillis(13L));
- outputDebugMessage("Connected: kex=%s, key-type=%s, c2senc=%s, s2cenc=%s, c2mac=%s, s2cmac=%s",
- info.keyExchangeAlgorithm, info.serverHostKeyAlgorithm,
- info.clientToServerCryptoAlgorithm, info.serverToClientCryptoAlgorithm,
- info.clientToServerMACAlgorithm, info.serverToClientMACAlgorithm);
- assertTrue("Failed to authenticate", conn.authenticateWithPassword(getCurrentTestName(), getCurrentTestName()));
-
- SCPClient scpClient = new SCPClient(conn);
- try (OutputStream output = scpClient.put(fileName, expected.length, remotePath, mode)) {
- output.write(expected);
- }
-
- assertTrue("Remote file not created: " + remoteFile, Files.exists(remoteFile));
- byte[] remoteData = Files.readAllBytes(remoteFile);
- assertArrayEquals("Mismatched remote put data", expected, remoteData);
-
- Arrays.fill(remoteData, (byte) 0); // make sure we start with a clean slate
- try (InputStream input = scpClient.get(remotePath + "/" + fileName)) {
- int readLen = input.read(remoteData);
- assertEquals("Mismatched remote get data size", expected.length, readLen);
- // make sure we reached EOF
- assertEquals("Unexpected extra data after read expected size", -1, input.read());
- }
-
- assertArrayEquals("Mismatched remote get data", expected, remoteData);
- } finally {
- conn.close();
- }
- }
-
- protected String readFile(com.jcraft.jsch.Session session, String path, File target) throws Exception {
- ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
- c.setCommand("scp -f " + path);
- c.connect();
-
- String fileName = target.getName();
- try (OutputStream os = c.getOutputStream();
- InputStream is = c.getInputStream()) {
-
- os.write(0);
- os.flush();
-
- String header = readLine(is);
- String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + fileName;
- assertEquals("Mismatched header for " + path, expHeader, header);
-
- String lenValue = header.substring(6, header.indexOf(' ', 6));
- int length = Integer.parseInt(lenValue);
- os.write(0);
- os.flush();
-
- byte[] buffer = new byte[length];
- length = is.read(buffer, 0, buffer.length);
- assertEquals("Mismatched read data length for " + path, length, buffer.length);
- assertAckReceived(is, "Read data of " + path);
-
- os.write(0);
- os.flush();
-
- return new String(buffer, StandardCharsets.UTF_8);
- } finally {
- c.disconnect();
- }
- }
-
- protected String readDir(com.jcraft.jsch.Session session, String path, File target) throws Exception {
- ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
- c.setCommand("scp -r -f " + path);
- c.connect();
-
- try (OutputStream os = c.getOutputStream();
- InputStream is = c.getInputStream()) {
- os.write(0);
- os.flush();
-
- String header = readLine(is);
- String expPrefix = "D" + ScpHelper.DEFAULT_DIR_OCTAL_PERMISSIONS + " 0 ";
- assertTrue("Bad header prefix for " + path + ": " + header, header.startsWith(expPrefix));
- os.write(0);
- os.flush();
-
- header = readLine(is);
- String expHeader = "C" + ScpHelper.DEFAULT_FILE_OCTAL_PERMISSIONS + " " + target.length() + " " + target.getName();
- assertEquals("Mismatched dir header for " + path, expHeader, header);
- int length = Integer.parseInt(header.substring(6, header.indexOf(' ', 6)));
- os.write(0);
- os.flush();
-
- byte[] buffer = new byte[length];
- length = is.read(buffer, 0, buffer.length);
- assertEquals("Mismatched read buffer size for " + path, length, buffer.length);
- assertAckReceived(is, "Read date of " + path);
-
- os.write(0);
- os.flush();
-
- header = readLine(is);
- assertEquals("Mismatched end value for " + path, "E", header);
- os.write(0);
- os.flush();
-
- return new String(buffer, StandardCharsets.UTF_8);
- } finally {
- c.disconnect();
- }
- }
-
- protected void readFileError(com.jcraft.jsch.Session session, String path) throws Exception {
- ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
- String command = "scp -f " + path;
- c.setCommand(command);
- c.connect();
-
- try (OutputStream os = c.getOutputStream();
- InputStream is = c.getInputStream()) {
-
- os.write(0);
- os.flush();
- assertEquals("Mismatched response for command: " + command, 2, is.read());
- } finally {
- c.disconnect();
- }
- }
-
- protected void sendFile(com.jcraft.jsch.Session session, String path, File target, String data) throws Exception {
- ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
- String command = "scp -t " + path;
- c.setCommand(command);
- c.connect();
-
- try (OutputStream os = c.getOutputStream();
- InputStream is = c.getInputStream()) {
-
- assertAckReceived(is, command);
-
- File parent = target.getParentFile();
- Collection<PosixFilePermission> perms = IoUtils.getPermissions(parent.toPath());
- String octalPerms = ScpHelper.getOctalPermissions(perms);
- String name = target.getName();
- assertAckReceived(os, is, "C" + octalPerms + " " + data.length() + " " + name);
-
- os.write(data.getBytes(StandardCharsets.UTF_8));
- os.flush();
- assertAckReceived(is, "Sent data (length=" + data.length() + ") for " + path + "[" + name + "]");
-
- os.write(0);
- os.flush();
-
- Thread.sleep(100);
- } finally {
- c.disconnect();
- }
- }
-
- protected void assertAckReceived(OutputStream os, InputStream is, String command) throws IOException {
- os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
- os.flush();
- assertAckReceived(is, command);
- }
-
- protected void assertAckReceived(InputStream is, String command) throws IOException {
- assertEquals("No ACK for command=" + command, 0, is.read());
- }
-
- protected void sendFileError(com.jcraft.jsch.Session session, String path, String name, String data) throws Exception {
- ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
- String command = "scp -t " + path;
- c.setCommand(command);
- c.connect();
-
- try (OutputStream os = c.getOutputStream();
- InputStream is = c.getInputStream()) {
-
- assertAckReceived(is, command);
-
- command = "C7777 " + data.length() + " " + name;
- os.write((command + "\n").getBytes(StandardCharsets.UTF_8));
- os.flush();
- assertEquals("Mismatched response for command=" + command, 2, is.read());
- } finally {
- c.disconnect();
- }
- }
-
- protected void sendDir(com.jcraft.jsch.Session session, String path, String dirName, String fileName, String data) throws Exception {
- ChannelExec c = (ChannelExec) session.openChannel(Channel.CHANNEL_EXEC);
- String command = "scp -t -r " + path;
- c.setCommand(command);
- c.connect();
-
- try (OutputStream os = c.getOutputStream();
- InputStream is = c.getInputStream()) {
-
- assertAckReceived(is, command);
- assertAckReceived(os, is, "D0755 0 " + dirName);
- assertAckReceived(os, is, "C7777 " + data.length() + " " + fileName);
-
- os.write(data.getBytes(StandardCharsets.UTF_8));
- os.flush();
- assertAckReceived(is, "Send data of " + path);
-
- os.write(0);
- os.flush();
-
- os.write("E\n".getBytes(StandardCharsets.UTF_8));
- os.flush();
- assertAckReceived(is, "Signal end of " + path);
- } finally {
- c.disconnect();
- }
- }
-
- private static String readLine(InputStream in) throws IOException {
- try (OutputStream baos = new ByteArrayOutputStream()) {
- for (;;) {
- int c = in.read();
- if (c == '\n') {
- return baos.toString();
- } else if (c == -1) {
- throw new IOException("End of stream");
- } else {
- baos.write(c);
- }
- }
- }
- }
-
- private static ScpClient createScpClient(ClientSession session) {
- return session.createScpClient(getScpTransferEventListener(session));
- }
-
- private static ScpTransferEventListener getScpTransferEventListener(ClientSession session) {
- return OUTPUT_DEBUG_MESSAGES ? DEBUG_LISTENER : ScpTransferEventListener.EMPTY;
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
deleted file mode 100644
index 60b9403..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/simple/BaseSimpleClientTestSupport.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.util.concurrent.TimeUnit;
-
-import org.apache.sshd.client.SshClient;
-import org.apache.sshd.server.SshServer;
-import org.apache.sshd.util.test.BaseTestSupport;
-import org.junit.After;
-import org.junit.Before;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public abstract class BaseSimpleClientTestSupport extends BaseTestSupport {
- public static final long CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
- public static final long AUTH_TIMEOUT = TimeUnit.SECONDS.toMillis(7L);
-
- protected SshServer sshd;
- protected SshClient client;
- protected int port;
- protected SimpleClient simple;
-
- protected BaseSimpleClientTestSupport() {
- super();
- }
-
- @Before
- public void setUp() throws Exception {
- sshd = setupTestServer();
- sshd.start();
- port = sshd.getPort();
- client = setupTestClient();
-
- simple = SshClient.wrapAsSimpleClient(client);
- simple.setConnectTimeout(CONNECT_TIMEOUT);
- simple.setAuthenticationTimeout(AUTH_TIMEOUT);
- }
-
- @After
- public void tearDown() throws Exception {
- if (sshd != null) {
- sshd.stop(true);
- }
- if (simple != null) {
- simple.close();
- }
- if (client != null) {
- client.stop();
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java
deleted file mode 100644
index ab96bc1..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleScpClientTest.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.file.Files;
-import java.nio.file.Path;
-
-import org.apache.sshd.client.scp.CloseableScpClient;
-import org.apache.sshd.common.file.FileSystemFactory;
-import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.common.session.Session;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-import org.apache.sshd.util.test.Utils;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class SimpleScpClientTest extends BaseSimpleClientTestSupport {
- private final Path targetPath;
- private final Path parentPath;
- private final FileSystemFactory fileSystemFactory;
-
- public SimpleScpClientTest() throws Exception {
- targetPath = detectTargetFolder();
- parentPath = targetPath.getParent();
- fileSystemFactory = new VirtualFileSystemFactory(parentPath);
- }
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- sshd.setCommandFactory(new ScpCommandFactory());
- sshd.setFileSystemFactory(fileSystemFactory);
- client.start();
- }
-
- @Test
- public void testSessionClosedWhenClientClosed() throws Exception {
- try (CloseableScpClient scp = login()) {
- assertTrue("SCP not open", scp.isOpen());
-
- Session session = scp.getClientSession();
- assertTrue("Session not open", session.isOpen());
-
- scp.close();
- assertFalse("Session not closed", session.isOpen());
- assertFalse("SCP not closed", scp.isOpen());
- }
- }
-
- @Test
- public void testScpUploadProxy() throws Exception {
- try (CloseableScpClient scp = login()) {
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path localFile = localDir.resolve("file.txt");
- String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
- byte[] written = Utils.writeFile(localFile, data);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remoteFile = remoteDir.resolve(localFile.getFileName());
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- scp.upload(localFile, remotePath);
-
- byte[] uploaded = Files.readAllBytes(remoteFile);
- assertArrayEquals("Mismatched uploaded data", written, uploaded);
- }
- }
-
- @Test
- public void testScpDownloadProxy() throws Exception {
- try (CloseableScpClient scp = login()) {
- Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
- Utils.deleteRecursive(scpRoot);
-
- Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
- Path remoteFile = remoteDir.resolve("file.txt");
- String data = getClass().getName() + "#" + getCurrentTestName() + IoUtils.EOL;
- byte[] written = Utils.writeFile(remoteFile, data);
- Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
- Path localFile = localDir.resolve(remoteFile.getFileName());
- String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
- scp.download(remotePath, localFile);
-
- byte[] downloaded = Files.readAllBytes(localFile);
- assertArrayEquals("Mismatched downloaded data", written, downloaded);
- }
- }
-
- protected CloseableScpClient login() throws IOException {
- return simple.scpLogin(TEST_LOCALHOST, port, getCurrentTestName(), getCurrentTestName());
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java b/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
index a77e807..ba3ee68 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/simple/SimpleSessionClientTest.java
@@ -33,6 +33,7 @@ import org.apache.sshd.server.auth.password.PasswordAuthenticator;
import org.apache.sshd.server.auth.password.RejectAllPasswordAuthenticator;
import org.apache.sshd.server.auth.pubkey.RejectAllPublickeyAuthenticator;
import org.apache.sshd.util.test.Utils;
+import org.apache.sshd.util.test.client.simple.BaseSimpleClientTestSupport;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java b/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java
deleted file mode 100644
index 8ffe6ac..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/server/command/ScpCommandFactoryTest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * 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.server.command;
-
-import java.util.concurrent.ExecutorService;
-
-import org.apache.sshd.common.scp.ScpHelper;
-import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.ScpCommandFactory;
-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.Mockito;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-@Category({ NoIoTestCase.class })
-public class ScpCommandFactoryTest extends BaseTestSupport {
- public ScpCommandFactoryTest() {
- super();
- }
-
- /**
- * Make sure that the builder returns a factory with the default values
- * if no {@code withXXX} method is invoked
- */
- @Test
- public void testBuilderDefaultFactoryValues() {
- ScpCommandFactory factory = new ScpCommandFactory.Builder().build();
- assertNull("Mismatched delegate", factory.getDelegateCommandFactory());
- assertNull("Mismatched executor", factory.getExecutorService());
- assertEquals("Mismatched send size", ScpHelper.MIN_SEND_BUFFER_SIZE, factory.getSendBufferSize());
- assertEquals("Mismatched receive size", ScpHelper.MIN_RECEIVE_BUFFER_SIZE, factory.getReceiveBufferSize());
- assertFalse("Mismatched shutdown state", factory.isShutdownOnExit());
- }
-
- /**
- * Make sure that the builder initializes correctly the built factory
- */
- @Test
- public void testBuilderCorrectlyInitializesFactory() {
- CommandFactory delegate = dummyFactory();
- ExecutorService service = dummyExecutor();
- int receiveSize = Short.MAX_VALUE;
- int sendSize = receiveSize + Long.SIZE;
- ScpCommandFactory factory = new ScpCommandFactory.Builder()
- .withDelegate(delegate)
- .withExecutorService(service)
- .withSendBufferSize(sendSize)
- .withReceiveBufferSize(receiveSize)
- .withShutdownOnExit(true)
- .build();
- assertSame("Mismatched delegate", delegate, factory.getDelegateCommandFactory());
- assertSame("Mismatched executor", service, factory.getExecutorService());
- assertEquals("Mismatched send size", sendSize, factory.getSendBufferSize());
- assertEquals("Mismatched receive size", receiveSize, factory.getReceiveBufferSize());
- assertTrue("Mismatched shutdown state", factory.isShutdownOnExit());
- }
-
- /**
- * <UL>
- * <LI>
- * Make sure the builder returns new instances on every call to
- * {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
- * </LI>
- *
- * <LI>
- * Make sure values are preserved between successive invocations
- * of the {@link org.apache.sshd.server.scp.ScpCommandFactory.Builder#build()} method
- * </LI>
- * </UL
- */
- @Test
- public void testBuilderUniqueInstance() {
- ScpCommandFactory.Builder builder = new ScpCommandFactory.Builder();
- ScpCommandFactory f1 = builder.withDelegate(dummyFactory()).build();
- ScpCommandFactory f2 = builder.build();
- assertNotSame("No new instance built", f1, f2);
- assertSame("Mismatched delegate", f1.getDelegateCommandFactory(), f2.getDelegateCommandFactory());
-
- ScpCommandFactory f3 = builder.withDelegate(dummyFactory()).build();
- assertNotSame("Delegate not changed", f1.getDelegateCommandFactory(), f3.getDelegateCommandFactory());
- }
-
- private static ExecutorService dummyExecutor() {
- return Mockito.mock(ExecutorService.class);
- }
-
- private static CommandFactory dummyFactory() {
- return Mockito.mock(CommandFactory.class);
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java b/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java
deleted file mode 100644
index 6beb011..0000000
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/UnknownCommandFactory.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.util.test;
-
-import org.apache.sshd.server.Command;
-import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
-
-/**
- * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
- */
-public class UnknownCommandFactory implements CommandFactory {
- public static final UnknownCommandFactory INSTANCE = new UnknownCommandFactory();
-
- public UnknownCommandFactory() {
- super();
- }
-
- @Override
- public Command createCommand(String command) {
- return new UnknownCommand(command);
- }
-}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java b/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
index 73e8f7c..a007239 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/Utils.java
@@ -67,6 +67,7 @@ import org.apache.sshd.common.util.security.SecurityUtils;
import org.apache.sshd.server.SshServer;
import org.apache.sshd.server.auth.pubkey.AcceptAllPublickeyAuthenticator;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
+import org.apache.sshd.server.shell.UnknownCommandFactory;
public final class Utils {
/**
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java
new file mode 100644
index 0000000..f42a550
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/client/simple/BaseSimpleClientTestSupport.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util.test.client.simple;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.simple.SimpleClient;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.junit.After;
+import org.junit.Before;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class BaseSimpleClientTestSupport extends BaseTestSupport {
+ public static final long CONNECT_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
+ public static final long AUTH_TIMEOUT = TimeUnit.SECONDS.toMillis(7L);
+
+ protected SshServer sshd;
+ protected SshClient client;
+ protected int port;
+ protected SimpleClient simple;
+
+ protected BaseSimpleClientTestSupport() {
+ super();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ sshd = setupTestServer();
+ sshd.start();
+ port = sshd.getPort();
+ client = setupTestClient();
+
+ simple = SshClient.wrapAsSimpleClient(client);
+ simple.setConnectTimeout(CONNECT_TIMEOUT);
+ simple.setAuthenticationTimeout(AUTH_TIMEOUT);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (sshd != null) {
+ sshd.stop(true);
+ }
+ if (simple != null) {
+ simple.close();
+ }
+ if (client != null) {
+ client.stop();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
----------------------------------------------------------------------
diff --git a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
index 0d42626..64eab6a 100644
--- a/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
+++ b/sshd-git/src/main/java/org/apache/sshd/git/AbstractGitCommandFactory.java
@@ -25,7 +25,7 @@ import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.threads.ExecutorServiceCarrier;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.CommandFactory;
-import org.apache.sshd.server.scp.UnknownCommand;
+import org.apache.sshd.server.shell.UnknownCommand;
/**
* TODO Add javadoc
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/pom.xml
----------------------------------------------------------------------
diff --git a/sshd-scp/pom.xml b/sshd-scp/pom.xml
new file mode 100644
index 0000000..2af3446
--- /dev/null
+++ b/sshd-scp/pom.xml
@@ -0,0 +1,201 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+
+ <!--
+
+ 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.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.sshd</groupId>
+ <artifactId>sshd</artifactId>
+ <version>2.0.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>sshd-scp</artifactId>
+ <name>Apache Mina SSHD :: SCP</name>
+ <packaging>jar</packaging>
+ <inceptionYear>2018</inceptionYear>
+
+ <properties>
+ <projectRoot>${project.basedir}/..</projectRoot>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-core</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>jcl-over-slf4j</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.jcraft</groupId>
+ <artifactId>jsch</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>ch.ethz.ganymed</groupId>
+ <artifactId>ganymed-ssh2</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.jcraft</groupId>
+ <artifactId>jzlib</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/filtered-resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <redirectTestOutputToFile>true</redirectTestOutputToFile>
+ <reportsDirectory>${project.build.directory}/surefire-reports-nio2</reportsDirectory>
+ <systemProperties>
+ <org.apache.sshd.common.io.IoServiceFactoryFactory>org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory</org.apache.sshd.common.io.IoServiceFactoryFactory>
+ </systemProperties>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <additionalparam>-Xdoclint:none</additionalparam>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>test-mina</id>
+ <activation>
+ <activeByDefault>true</activeByDefault>
+ </activation>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-mina</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>mina</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <redirectTestOutputToFile>true</redirectTestOutputToFile>
+ <reportsDirectory>${project.build.directory}/surefire-reports-mina</reportsDirectory>
+ <systemProperties>
+ <org.apache.sshd.common.io.IoServiceFactoryFactory>org.apache.sshd.common.io.mina.MinaServiceFactoryFactory</org.apache.sshd.common.io.IoServiceFactoryFactory>
+ </systemProperties>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
+ <profile>
+ <id>test-netty</id>
+ <activation>
+ <activeByDefault>true</activeByDefault>
+ </activation>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.sshd</groupId>
+ <artifactId>sshd-netty</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>netty</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <redirectTestOutputToFile>true</redirectTestOutputToFile>
+ <reportsDirectory>${project.build.directory}/surefire-reports-netty</reportsDirectory>
+ <systemProperties>
+ <org.apache.sshd.common.io.IoServiceFactoryFactory>org.apache.sshd.netty.NettyIoServiceFactoryFactory</org.apache.sshd.common.io.IoServiceFactoryFactory>
+ </systemProperties>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
[4/8] mina-sshd git commit: [SSHD-818] Split SCP code (client +
server) to its own module
Posted by lg...@apache.org.
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
new file mode 100644
index 0000000..81c20db
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClient.java
@@ -0,0 +1,278 @@
+/*
+ * 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.scp;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.channel.ChannelExec;
+import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.channel.ClientChannelEvent;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractScpClient extends AbstractLoggingBean implements ScpClient {
+ public static final Set<ClientChannelEvent> COMMAND_WAIT_EVENTS =
+ Collections.unmodifiableSet(EnumSet.of(ClientChannelEvent.EXIT_STATUS, ClientChannelEvent.CLOSED));
+
+ protected AbstractScpClient() {
+ super();
+ }
+
+ @Override
+ public final ClientSession getSession() {
+ return getClientSession();
+ }
+
+ @Override
+ public void download(String[] remote, String local, Collection<Option> options) throws IOException {
+ local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", (Object) remote);
+
+ if (remote.length > 1) {
+ options = addTargetIsDirectory(options);
+ }
+
+ for (String r : remote) {
+ download(r, local, options);
+ }
+ }
+
+ @Override
+ public void download(String[] remote, Path local, Collection<Option> options) throws IOException {
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", (Object) remote);
+
+ if (remote.length > 1) {
+ options = addTargetIsDirectory(options);
+ }
+
+ for (String r : remote) {
+ download(r, local, options);
+ }
+ }
+
+ @Override
+ public void download(String remote, Path local, Collection<Option> options) throws IOException {
+ local = ValidateUtils.checkNotNull(local, "Invalid argument local: %s", local);
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", remote);
+
+ LinkOption[] opts = IoUtils.getLinkOptions(true);
+ if (Files.isDirectory(local, opts)) {
+ options = addTargetIsDirectory(options);
+ }
+
+ if (options.contains(Option.TargetIsDirectory)) {
+ Boolean status = IoUtils.checkFileExists(local, opts);
+ if (status == null) {
+ throw new SshException("Target directory " + local.toString() + " is probably inaccesible");
+ }
+
+ if (!status) {
+ throw new SshException("Target directory " + local.toString() + " does not exist");
+ }
+
+ if (!Files.isDirectory(local, opts)) {
+ throw new SshException("Target directory " + local.toString() + " is not a directory");
+ }
+ }
+
+ download(remote, local.getFileSystem(), local, options);
+ }
+
+ @Override
+ public void download(String remote, String local, Collection<Option> options) throws IOException {
+ local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
+
+ ClientSession session = getClientSession();
+ FactoryManager manager = session.getFactoryManager();
+ FileSystemFactory factory = manager.getFileSystemFactory();
+ FileSystem fs = factory.createFileSystem(session);
+ try {
+ download(remote, fs, fs.getPath(local), options);
+ } finally {
+ try {
+ fs.close();
+ } catch (UnsupportedOperationException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("download({}) {} => {} - failed ({}) to close file system={}: {}",
+ session, remote, local, e.getClass().getSimpleName(), fs, e.getMessage());
+ }
+ }
+ }
+ }
+
+ protected abstract void download(String remote, FileSystem fs, Path local, Collection<Option> options) throws IOException;
+
+ @Override
+ public void upload(String[] local, String remote, Collection<Option> options) throws IOException {
+ final Collection<String> paths = Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", (Object) local));
+ runUpload(remote, options, paths, (helper, local1, sendOptions) ->
+ helper.send(local1,
+ sendOptions.contains(Option.Recursive),
+ sendOptions.contains(Option.PreserveAttributes),
+ ScpHelper.DEFAULT_SEND_BUFFER_SIZE));
+ }
+
+ @Override
+ public void upload(Path[] local, String remote, Collection<Option> options) throws IOException {
+ final Collection<Path> paths = Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", (Object) local));
+ runUpload(remote, options, paths, (helper, local1, sendOptions) ->
+ helper.sendPaths(local1,
+ sendOptions.contains(Option.Recursive),
+ sendOptions.contains(Option.PreserveAttributes),
+ ScpHelper.DEFAULT_SEND_BUFFER_SIZE));
+ }
+
+ protected abstract <T> void runUpload(String remote, Collection<Option> options, Collection<T> local, AbstractScpClient.ScpOperationExecutor<T> executor) throws IOException;
+
+ /**
+ * Invoked by the various <code>upload/download</code> methods after having successfully
+ * completed the remote copy command and (optionally) having received an exit status
+ * from the remote server. If no exit status received within {@link FactoryManager#CHANNEL_CLOSE_TIMEOUT}
+ * the no further action is taken. Otherwise, the exit status is examined to ensure it
+ * is either OK or WARNING - if not, an {@link ScpException} is thrown
+ *
+ * @param cmd The attempted remote copy command
+ * @param channel The {@link ClientChannel} through which the command was sent - <B>Note:</B>
+ * then channel may be in the process of being closed
+ * @throws IOException If failed the command
+ * @see #handleCommandExitStatus(String, Integer)
+ */
+ protected void handleCommandExitStatus(String cmd, ClientChannel channel) throws IOException {
+ // give a chance for the exit status to be received
+ long timeout = channel.getLongProperty(SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT, DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT);
+ if (timeout <= 0L) {
+ handleCommandExitStatus(cmd, (Integer) null);
+ return;
+ }
+
+ long waitStart = System.nanoTime();
+ Collection<ClientChannelEvent> events = channel.waitFor(COMMAND_WAIT_EVENTS, timeout);
+ long waitEnd = System.nanoTime();
+ if (log.isDebugEnabled()) {
+ log.debug("handleCommandExitStatus({}) cmd='{}', waited={} nanos, events={}",
+ getClientSession(), cmd, waitEnd - waitStart, events);
+ }
+
+ /*
+ * There are sometimes race conditions in the order in which channels are closed and exit-status
+ * sent by the remote peer (if at all), thus there is no guarantee that we will have an exit
+ * status here
+ */
+ handleCommandExitStatus(cmd, channel.getExitStatus());
+ }
+
+ /**
+ * Invoked by the various <code>upload/download</code> methods after having successfully
+ * completed the remote copy command and (optionally) having received an exit status
+ * from the remote server
+ *
+ * @param cmd The attempted remote copy command
+ * @param exitStatus The exit status - if {@code null} then no status was reported
+ * @throws IOException If failed the command
+ */
+ protected void handleCommandExitStatus(String cmd, Integer exitStatus) throws IOException {
+ if (log.isDebugEnabled()) {
+ log.debug("handleCommandExitStatus({}) cmd='{}', exit-status={}", getClientSession(), cmd, ScpHelper.getExitStatusName(exitStatus));
+ }
+
+ if (exitStatus == null) {
+ return;
+ }
+
+ int statusCode = exitStatus;
+ switch (statusCode) {
+ case ScpHelper.OK: // do nothing
+ break;
+ case ScpHelper.WARNING:
+ log.warn("handleCommandExitStatus({}) cmd='{}' may have terminated with some problems", getClientSession(), cmd);
+ break;
+ default:
+ throw new ScpException("Failed to run command='" + cmd + "': " + ScpHelper.getExitStatusName(exitStatus), exitStatus);
+ }
+ }
+
+ protected Collection<Option> addTargetIsDirectory(Collection<Option> options) {
+ if (GenericUtils.isEmpty(options) || (!options.contains(Option.TargetIsDirectory))) {
+ // create a copy in case the original collection is un-modifiable
+ options = GenericUtils.isEmpty(options) ? EnumSet.noneOf(Option.class) : GenericUtils.of(options);
+ options.add(Option.TargetIsDirectory);
+ }
+
+ return options;
+ }
+
+ protected ChannelExec openCommandChannel(ClientSession session, String cmd) throws IOException {
+ long waitTimeout = session.getLongProperty(SCP_EXEC_CHANNEL_OPEN_TIMEOUT, DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT);
+ ChannelExec channel = session.createExecChannel(cmd);
+
+ long startTime = System.nanoTime();
+ try {
+ channel.open().verify(waitTimeout);
+ long endTime = System.nanoTime();
+ long nanosWait = endTime - startTime;
+ if (log.isTraceEnabled()) {
+ log.trace("openCommandChannel(" + session + ")[" + cmd + "]"
+ + " completed after " + nanosWait
+ + " nanos out of " + TimeUnit.MILLISECONDS.toNanos(waitTimeout));
+ }
+
+ return channel;
+ } catch (IOException | RuntimeException e) {
+ long endTime = System.nanoTime();
+ long nanosWait = endTime - startTime;
+ if (log.isTraceEnabled()) {
+ log.trace("openCommandChannel(" + session + ")[" + cmd + "]"
+ + " failed (" + e.getClass().getSimpleName() + ")"
+ + " to complete after " + nanosWait
+ + " nanos out of " + TimeUnit.MILLISECONDS.toNanos(waitTimeout)
+ + ": " + e.getMessage());
+ }
+
+ channel.close(false);
+ throw e;
+ }
+ }
+
+ @FunctionalInterface
+ public interface ScpOperationExecutor<T> {
+ void execute(ScpHelper helper, Collection<T> local, Collection<Option> options) throws IOException;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java
new file mode 100644
index 0000000..34ef7e5
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/AbstractScpClientCreator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.scp;
+
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractScpClientCreator extends AbstractLoggingBean implements ScpClientCreator {
+ private ScpFileOpener opener = DefaultScpFileOpener.INSTANCE;
+ private ScpTransferEventListener listener;
+
+ protected AbstractScpClientCreator() {
+ this("");
+ }
+
+ public AbstractScpClientCreator(String discriminator) {
+ super(discriminator);
+ }
+
+ @Override
+ public ScpFileOpener getScpFileOpener() {
+ return opener;
+ }
+
+ @Override
+ public void setScpFileOpener(ScpFileOpener opener) {
+ this.opener = opener;
+ }
+
+ @Override
+ public ScpTransferEventListener getScpTransferEventListener() {
+ return listener;
+ }
+
+ @Override
+ public void setScpTransferEventListener(ScpTransferEventListener listener) {
+ this.listener = listener;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
new file mode 100644
index 0000000..40afaf7
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/CloseableScpClient.java
@@ -0,0 +1,32 @@
+/*
+ * 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.scp;
+
+import java.nio.channels.Channel;
+
+/**
+ * An {@link ScpClient} wrapper that also closes the underlying session
+ * when closed
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface CloseableScpClient extends ScpClient, Channel {
+ // Marker interface
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
new file mode 100644
index 0000000..16d0cb2
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClient.java
@@ -0,0 +1,159 @@
+/*
+ * 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.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Objects;
+
+import org.apache.sshd.client.channel.ChannelExec;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.util.MockFileSystem;
+import org.apache.sshd.common.file.util.MockPath;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+import org.apache.sshd.common.scp.helpers.DefaultScpFileOpener;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpClient extends AbstractScpClient {
+ protected final ScpFileOpener opener;
+ protected final ScpTransferEventListener listener;
+ private final ClientSession clientSession;
+
+ public DefaultScpClient(
+ ClientSession clientSession, ScpFileOpener fileOpener, ScpTransferEventListener eventListener) {
+ this.clientSession = Objects.requireNonNull(clientSession, "No client session");
+ this.opener = (fileOpener == null) ? DefaultScpFileOpener.INSTANCE : fileOpener;
+ this.listener = (eventListener == null) ? ScpTransferEventListener.EMPTY : eventListener;
+ }
+
+ @Override
+ public ClientSession getClientSession() {
+ return clientSession;
+ }
+
+ @Override
+ public void download(String remote, OutputStream local) throws IOException {
+ String cmd = ScpClient.createReceiveCommand(remote, Collections.emptyList());
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ // NOTE: we use a mock file system since we expect no invocations for it
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
+ helper.receiveFileStream(local, ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+
+ @Override
+ protected void download(String remote, FileSystem fs, Path local, Collection<Option> options) throws IOException {
+ String cmd = ScpClient.createReceiveCommand(remote, options);
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
+ helper.receive(local,
+ options.contains(Option.Recursive),
+ options.contains(Option.TargetIsDirectory),
+ options.contains(Option.PreserveAttributes),
+ ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+
+ @Override
+ public void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ int namePos = ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified").lastIndexOf('/');
+ String name = (namePos < 0)
+ ? remote
+ : ValidateUtils.checkNotNullAndNotEmpty(remote.substring(namePos + 1), "No name value in remote=%s", remote);
+ Collection<Option> options = (time != null) ? EnumSet.of(Option.PreserveAttributes) : Collections.emptySet();
+ String cmd = ScpClient.createSendCommand(remote, options);
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ // NOTE: we use a mock file system since we expect no invocations for it
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
+ Path mockPath = new MockPath(remote);
+ helper.sendStream(new DefaultScpStreamResolver(name, mockPath, perms, time, size, local, cmd),
+ options.contains(Option.PreserveAttributes), ScpHelper.DEFAULT_SEND_BUFFER_SIZE);
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+
+ @Override
+ protected <T> void runUpload(String remote, Collection<Option> options, Collection<T> local, AbstractScpClient.ScpOperationExecutor<T> executor) throws IOException {
+ local = ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local);
+ remote = ValidateUtils.checkNotNullAndNotEmpty(remote, "Invalid argument remote: %s", remote);
+ if (local.size() > 1) {
+ options = addTargetIsDirectory(options);
+ }
+
+ String cmd = ScpClient.createSendCommand(remote, options);
+ ClientSession session = getClientSession();
+ ChannelExec channel = openCommandChannel(session, cmd);
+ try {
+ FactoryManager manager = session.getFactoryManager();
+ FileSystemFactory factory = manager.getFileSystemFactory();
+ FileSystem fs = factory.createFileSystem(session);
+
+ try (InputStream invOut = channel.getInvertedOut();
+ OutputStream invIn = channel.getInvertedIn()) {
+ ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
+ executor.execute(helper, local, options);
+ } finally {
+ try {
+ fs.close();
+ } catch (UnsupportedOperationException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("runUpload({}) {} => {} - failed ({}) to close file system={}: {}",
+ session, remote, local, e.getClass().getSimpleName(), fs, e.getMessage());
+ }
+ }
+ }
+ handleCommandExitStatus(cmd, channel);
+ } finally {
+ channel.close(false);
+ }
+ }
+}
+
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java
new file mode 100644
index 0000000..a23e0ba
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpClientCreator.java
@@ -0,0 +1,42 @@
+/*
+ * 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.scp;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpClientCreator extends AbstractScpClientCreator {
+ public static final DefaultScpClientCreator INSTANCE = new DefaultScpClientCreator();
+
+ public DefaultScpClientCreator() {
+ super();
+ }
+
+ @Override
+ public ScpClient createScpClient(ClientSession session, ScpFileOpener opener, ScpTransferEventListener listener) {
+ return new DefaultScpClient(session, opener, listener);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
new file mode 100644
index 0000000..e6362b8
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/DefaultScpStreamResolver.java
@@ -0,0 +1,88 @@
+/*
+ * 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.scp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+
+import org.apache.sshd.common.scp.ScpSourceStreamResolver;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.Session;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DefaultScpStreamResolver implements ScpSourceStreamResolver {
+ private final String name;
+ private final Path mockPath;
+ private final Collection<PosixFilePermission> perms;
+ private final ScpTimestamp time;
+ private final long size;
+ private final java.io.InputStream local;
+ private final String cmd;
+
+ public DefaultScpStreamResolver(String name, Path mockPath, Collection<PosixFilePermission> perms, ScpTimestamp time, long size, InputStream local, String cmd) {
+ this.name = name;
+ this.mockPath = mockPath;
+ this.perms = perms;
+ this.time = time;
+ this.size = size;
+ this.local = local;
+ this.cmd = cmd;
+ }
+
+ @Override
+ public String getFileName() throws java.io.IOException {
+ return name;
+ }
+
+ @Override
+ public Path getEventListenerFilePath() {
+ return mockPath;
+ }
+
+ @Override
+ public Collection<PosixFilePermission> getPermissions() throws IOException {
+ return perms;
+ }
+
+ @Override
+ public ScpTimestamp getTimestamp() throws IOException {
+ return time;
+ }
+
+ @Override
+ public long getSize() throws IOException {
+ return size;
+ }
+
+ @Override
+ public InputStream resolveSourceStream(Session session, OpenOption... options) throws IOException {
+ return local;
+ }
+
+ @Override
+ public String toString() {
+ return cmd;
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java
new file mode 100644
index 0000000..b2a6091
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClient.java
@@ -0,0 +1,174 @@
+/*
+ * 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.scp;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.session.ClientSessionHolder;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.common.scp.ScpTimestamp;
+import org.apache.sshd.common.session.SessionHolder;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpClient extends SessionHolder<ClientSession>, ClientSessionHolder {
+ enum Option {
+ Recursive,
+ PreserveAttributes,
+ TargetIsDirectory
+ }
+
+ /**
+ * Configurable value of the {@link org.apache.sshd.common.FactoryManager}
+ * for controlling the wait timeout for opening a channel for an SCP command
+ * in milliseconds. If not specified, then {@link #DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT}
+ * value is used
+ */
+ String SCP_EXEC_CHANNEL_OPEN_TIMEOUT = "scp-exec-channel-open-timeout";
+ long DEFAULT_EXEC_CHANNEL_OPEN_TIMEOUT = TimeUnit.SECONDS.toMillis(30L);
+
+ /**
+ * Configurable value of the {@link org.apache.sshd.common.FactoryManager}
+ * for controlling the wait timeout for waiting on a channel exit status'
+ * for an SCP command in milliseconds. If not specified, then
+ * {@link #DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT}
+ * value is used. If non-positive, then no wait is performed and the command
+ * is assumed to have completed successfully.
+ */
+ String SCP_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT = "scp-exec-channel-exit-status-timeout";
+ long DEFAULT_EXEC_CHANNEL_EXIT_STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(5L);
+
+ default void download(String remote, String local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ void download(String remote, String local, Collection<Option> options) throws IOException;
+
+ default void download(String remote, Path local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ void download(String remote, Path local, Collection<Option> options) throws IOException;
+
+ // NOTE: the remote location MUST be a file or an exception is generated
+ void download(String remote, OutputStream local) throws IOException;
+
+ default byte[] downloadBytes(String remote) throws IOException {
+ try (ByteArrayOutputStream local = new ByteArrayOutputStream()) {
+ download(remote, local);
+ return local.toByteArray();
+ }
+ }
+
+ default void download(String[] remote, String local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ default void download(String[] remote, Path local, Option... options) throws IOException {
+ download(remote, local, GenericUtils.of(options));
+ }
+
+ void download(String[] remote, String local, Collection<Option> options) throws IOException;
+
+ void download(String[] remote, Path local, Collection<Option> options) throws IOException;
+
+ default void upload(String local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ default void upload(String local, String remote, Collection<Option> options) throws IOException {
+ upload(new String[]{ValidateUtils.checkNotNullAndNotEmpty(local, "Invalid argument local: %s", local)}, remote, options);
+ }
+
+ default void upload(Path local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ default void upload(Path local, String remote, Collection<Option> options) throws IOException {
+ upload(new Path[]{ValidateUtils.checkNotNull(local, "Invalid local argument: %s", local)}, remote, GenericUtils.of(options));
+ }
+
+ default void upload(String[] local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ void upload(String[] local, String remote, Collection<Option> options) throws IOException;
+
+ default void upload(Path[] local, String remote, Option... options) throws IOException {
+ upload(local, remote, GenericUtils.of(options));
+ }
+
+ void upload(Path[] local, String remote, Collection<Option> options) throws IOException;
+
+ // NOTE: due to SCP command limitations, the amount of data to be uploaded must be known a-priori
+ // To upload a dynamic amount of data use SFTP
+ default void upload(byte[] data, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ upload(data, 0, data.length, remote, perms, time);
+ }
+
+ default void upload(byte[] data, int offset, int len, String remote, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ try (InputStream local = new ByteArrayInputStream(data, offset, len)) {
+ upload(local, remote, len, perms, time);
+ }
+ }
+
+ void upload(InputStream local, String remote, long size, Collection<PosixFilePermission> perms, ScpTimestamp time) throws IOException;
+
+ static String createSendCommand(String remote, Collection<Option> options) {
+ StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX);
+ if (options.contains(Option.Recursive)) {
+ sb.append(" -r");
+ }
+ if (options.contains(Option.TargetIsDirectory)) {
+ sb.append(" -d");
+ }
+ if (options.contains(Option.PreserveAttributes)) {
+ sb.append(" -p");
+ }
+
+ sb.append(" -t").append(" --").append(" ").append(remote);
+ return sb.toString();
+ }
+
+ static String createReceiveCommand(String remote, Collection<Option> options) {
+ ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified");
+ StringBuilder sb = new StringBuilder(remote.length() + Long.SIZE).append(ScpHelper.SCP_COMMAND_PREFIX);
+ if (options.contains(Option.Recursive)) {
+ sb.append(" -r");
+ }
+ if (options.contains(Option.PreserveAttributes)) {
+ sb.append(" -p");
+ }
+
+ sb.append(" -f").append(" --").append(' ').append(remote);
+ return sb.toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.java
new file mode 100644
index 0000000..a7a31cb
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/ScpClientCreator.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.scp;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.scp.ScpFileOpener;
+import org.apache.sshd.common.scp.ScpFileOpenerHolder;
+import org.apache.sshd.common.scp.ScpTransferEventListener;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpClientCreator extends ScpFileOpenerHolder {
+ static ScpClientCreator instance() {
+ return DefaultScpClientCreator.INSTANCE;
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @return An {@link ScpClient} instance. <B>Note:</B> uses the currently
+ * registered {@link ScpTransferEventListener} and {@link ScpFileOpener} if any
+ * @see #setScpFileOpener(ScpFileOpener)
+ * @see #setScpTransferEventListener(ScpTransferEventListener)
+ */
+ default ScpClient createScpClient(ClientSession session) {
+ return createScpClient(session, getScpFileOpener(), getScpTransferEventListener());
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @param listener A {@link ScpTransferEventListener} that can be used
+ * to receive information about the SCP operations - may be {@code null}
+ * to indicate no more events are required. <B>Note:</B> this listener
+ * is used <U>instead</U> of any listener set via {@link #setScpTransferEventListener(ScpTransferEventListener)}
+ * @return An {@link ScpClient} instance
+ */
+ default ScpClient createScpClient(ClientSession session, ScpTransferEventListener listener) {
+ return createScpClient(session, getScpFileOpener(), listener);
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @param opener The {@link ScpFileOpener} to use to control how local files
+ * are read/written. If {@code null} then a default opener is used.
+ * <B>Note:</B> this opener is used <U>instead</U> of any instance
+ * set via {@link #setScpFileOpener(ScpFileOpener)}
+ * @return An {@link ScpClient} instance
+ */
+ default ScpClient createScpClient(ClientSession session, ScpFileOpener opener) {
+ return createScpClient(session, opener, getScpTransferEventListener());
+ }
+
+ /**
+ * Create an SCP client from this session.
+ *
+ * @param session The {@link ClientSession}
+ * @param opener The {@link ScpFileOpener} to use to control how local files
+ * are read/written. If {@code null} then a default opener is used.
+ * <B>Note:</B> this opener is used <U>instead</U> of any instance
+ * set via {@link #setScpFileOpener(ScpFileOpener)}
+ * @param listener A {@link ScpTransferEventListener} that can be used
+ * to receive information about the SCP operations - may be {@code null}
+ * to indicate no more events are required. <B>Note:</B> this listener
+ * is used <U>instead</U> of any listener set via
+ * {@link #setScpTransferEventListener(ScpTransferEventListener)}
+ * @return An {@link ScpClient} instance
+ */
+ ScpClient createScpClient(ClientSession session, ScpFileOpener opener, ScpTransferEventListener listener);
+
+ /**
+ * @return The last {@link ScpTransferEventListener} set via
+ * {@link #setScpTransferEventListener(ScpTransferEventListener)}
+ */
+ ScpTransferEventListener getScpTransferEventListener();
+
+ /**
+ * @param listener A default {@link ScpTransferEventListener} that can be used
+ * to receive information about the SCP operations - may be {@code null}
+ * to indicate no more events are required
+ * @see #createScpClient(ScpTransferEventListener)
+ */
+ void setScpTransferEventListener(ScpTransferEventListener listener);
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java
new file mode 100644
index 0000000..e1a4c72
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClient.java
@@ -0,0 +1,178 @@
+/*
+ * 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.scp;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.channels.Channel;
+import java.security.KeyPair;
+import java.util.Objects;
+
+import org.apache.sshd.client.simple.SimpleClientConfigurator;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * A simplified <U>synchronous</U> API for obtaining SCP sessions.
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface SimpleScpClient extends Channel {
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, String username, String password) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param port The target port
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, int port, String username, String password) throws IOException {
+ return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, password);
+ }
+
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, String username, KeyPair identity) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host name or address
+ * @param port The target port
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(String host, int port, String username, KeyPair identity) throws IOException {
+ return scpLogin(InetAddress.getByName(ValidateUtils.checkNotNullAndNotEmpty(host, "No host")), port, username, identity);
+ }
+
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, String username, String password) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, password);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param port The target port
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, int port, String username, String password) throws IOException {
+ return scpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, password);
+ }
+
+ /**
+ * Creates an SCP session on the default port and logs in using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, String username, KeyPair identity) throws IOException {
+ return scpLogin(host, SimpleClientConfigurator.DEFAULT_PORT, username, identity);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param host The target host {@link InetAddress}
+ * @param port The target port
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ default CloseableScpClient scpLogin(InetAddress host, int port, String username, KeyPair identity) throws IOException {
+ return scpLogin(new InetSocketAddress(Objects.requireNonNull(host, "No host address"), port), username, identity);
+ }
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param target The target {@link SocketAddress}
+ * @param username Username
+ * @param password Password
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ CloseableScpClient scpLogin(SocketAddress target, String username, String password) throws IOException;
+
+ /**
+ * Creates an SCP session using the provided credentials
+ *
+ * @param target The target {@link SocketAddress}
+ * @param username Username
+ * @param identity The {@link KeyPair} identity
+ * @return Created {@link CloseableScpClient} - <B>Note:</B> closing the client also closes its
+ * underlying session
+ * @throws IOException If failed to login or authenticate
+ */
+ CloseableScpClient scpLogin(SocketAddress target, String username, KeyPair identity) throws IOException;
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java
new file mode 100644
index 0000000..c863c6c
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/client/scp/SimpleScpClientImpl.java
@@ -0,0 +1,153 @@
+/*
+ * 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.scp;
+
+import java.io.IOException;
+import java.lang.reflect.Proxy;
+import java.net.SocketAddress;
+import java.security.KeyPair;
+import java.util.Objects;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.simple.SimpleClient;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.functors.IOFunction;
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class SimpleScpClientImpl extends AbstractLoggingBean implements SimpleScpClient {
+ private SimpleClient clientInstance;
+ private ScpClientCreator scpClientCreator;
+
+ public SimpleScpClientImpl() {
+ this(null);
+ }
+
+ public SimpleScpClientImpl(SimpleClient client) {
+ this(client, null);
+ }
+
+ public SimpleScpClientImpl(SimpleClient client, ScpClientCreator scpClientCreator) {
+ this.clientInstance = client;
+ setScpClientCreator(scpClientCreator);
+ }
+
+ public SimpleClient getClient() {
+ return clientInstance;
+ }
+
+ public void setClient(SimpleClient client) {
+ this.clientInstance = client;
+ }
+
+ public ScpClientCreator getScpClientCreator() {
+ return scpClientCreator;
+ }
+
+ public void setScpClientCreator(ScpClientCreator scpClientCreator) {
+ this.scpClientCreator = (scpClientCreator == null) ? ScpClientCreator.instance() : scpClientCreator;
+ }
+
+ @Override
+ public CloseableScpClient scpLogin(SocketAddress target, String username, String password) throws IOException {
+ return createScpClient(client -> client.sessionLogin(target, username, password));
+ }
+
+ @Override
+ public CloseableScpClient scpLogin(SocketAddress target, String username, KeyPair identity) throws IOException {
+ return createScpClient(client -> client.sessionLogin(target, username, identity));
+ }
+
+ protected CloseableScpClient createScpClient(IOFunction<? super SimpleClient, ? extends ClientSession> sessionProvider) throws IOException {
+ SimpleClient client = getClient();
+ ClientSession session = sessionProvider.apply(client);
+ try {
+ CloseableScpClient scp = createScpClient(session);
+ session = null; // disable auto-close at finally block
+ return scp;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
+ protected CloseableScpClient createScpClient(ClientSession session) throws IOException {
+ try {
+ ScpClientCreator creator = getScpClientCreator();
+ ScpClient client = creator.createScpClient(Objects.requireNonNull(session, "No client session"));
+ return createScpClient(session, client);
+ } catch (Exception e) {
+ log.warn("createScpClient({}) failed ({}) to create proxy: {}",
+ session, e.getClass().getSimpleName(), e.getMessage());
+ try {
+ session.close();
+ } catch (Exception t) {
+ if (log.isDebugEnabled()) {
+ log.debug("createScpClient({}) failed ({}) to close session: {}",
+ session, t.getClass().getSimpleName(), t.getMessage());
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("createScpClient(" + session + ") session close failure details", t);
+ }
+ e.addSuppressed(t);
+ }
+
+ throw GenericUtils.toIOException(e);
+ }
+ }
+
+ protected CloseableScpClient createScpClient(ClientSession session, ScpClient client) throws IOException {
+ ClassLoader loader = getClass().getClassLoader();
+ Class<?>[] interfaces = {CloseableScpClient.class};
+ return (CloseableScpClient) Proxy.newProxyInstance(loader, interfaces, (proxy, method, args) -> {
+ String name = method.getName();
+ try {
+ // The Channel implementation is provided by the session
+ if (("close".equals(name) || "isOpen".equals(name)) && GenericUtils.isEmpty(args)) {
+ return method.invoke(session, args);
+ } else {
+ return method.invoke(client, args);
+ }
+ } catch (Throwable t) {
+ if (log.isTraceEnabled()) {
+ log.trace("invoke(CloseableScpClient#{}) failed ({}) to execute: {}",
+ name, t.getClass().getSimpleName(), t.getMessage());
+ }
+ throw t;
+ }
+ });
+ }
+
+ @Override
+ public boolean isOpen() {
+ return true;
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Do nothing
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
new file mode 100644
index 0000000..d929a07
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
@@ -0,0 +1,74 @@
+/*
+ * 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.common.scp;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * A no-op implementation of {@link ScpTransferEventListener} for those who wish to
+ * implement only a small number of methods. By default, all non-overridden methods
+ * simply log at TRACE level their invocation parameters
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractScpTransferEventListenerAdapter
+ extends AbstractLoggingBean
+ implements ScpTransferEventListener {
+ protected AbstractScpTransferEventListenerAdapter() {
+ super();
+ }
+
+ @Override
+ public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms)
+ throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("startFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")");
+ }
+ }
+
+ @Override
+ public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
+ throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("endFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")"
+ + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+ }
+ }
+
+ @Override
+ public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("startFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")");
+ }
+ }
+
+ @Override
+ public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
+ throws IOException {
+ if (log.isTraceEnabled()) {
+ log.trace("endFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")"
+ + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java
new file mode 100644
index 0000000..9ae17c7
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpException.java
@@ -0,0 +1,56 @@
+/*
+ * 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.common.scp;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class ScpException extends IOException {
+ private static final long serialVersionUID = 7734851624372451732L;
+ private final Integer exitStatus;
+
+ public ScpException(String message) {
+ this(message, null);
+ }
+
+ public ScpException(Integer exitStatus) {
+ this("Exit status=" + ScpHelper.getExitStatusName(Objects.requireNonNull(exitStatus, "No exit status")), exitStatus);
+ }
+
+ public ScpException(String message, Integer exitStatus) {
+ this(message, null, exitStatus);
+ }
+
+ public ScpException(Throwable cause, Integer exitStatus) {
+ this(Objects.requireNonNull(cause, "No cause").getMessage(), cause, exitStatus);
+ }
+
+ public ScpException(String message, Throwable cause, Integer exitStatus) {
+ super(message, cause);
+ this.exitStatus = exitStatus;
+ }
+
+ public Integer getExitStatus() {
+ return exitStatus;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
new file mode 100644
index 0000000..78e033f
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpener.java
@@ -0,0 +1,284 @@
+/*
+ * 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.common.scp;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.SelectorUtils;
+import org.apache.sshd.common.util.io.DirectoryScanner;
+import org.apache.sshd.common.util.io.IoUtils;
+
+/**
+ * Plug-in mechanism for users to intervene in the SCP process - e.g.,
+ * apply some kind of traffic shaping mechanism, display upload/download
+ * progress, etc...
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpFileOpener {
+ /**
+ * Invoked when receiving a new file to via a directory command
+ *
+ * @param localPath The target local path
+ * @param name The target file name
+ * @param preserve Whether requested to preserve the permissions and timestamp
+ * @param permissions The requested file permissions
+ * @param time The requested {@link ScpTimestamp} - may be {@code null} if nothing to update
+ * @return The actual target file path for the incoming file/directory
+ * @throws IOException If failed to resolve the file path
+ * @see #updateFileProperties(Path, Set, ScpTimestamp) updateFileProperties
+ */
+ default Path resolveIncomingFilePath(
+ Path localPath, String name, boolean preserve, Set<PosixFilePermission> permissions, ScpTimestamp time)
+ throws IOException {
+ LinkOption[] options = IoUtils.getLinkOptions(true);
+ Boolean status = IoUtils.checkFileExists(localPath, options);
+ if (status == null) {
+ throw new AccessDeniedException("Receive directory existence status cannot be determined: " + localPath);
+ }
+
+ Path file = null;
+ if (status && Files.isDirectory(localPath, options)) {
+ String localName = name.replace('/', File.separatorChar);
+ file = localPath.resolve(localName);
+ } else if (!status) {
+ Path parent = localPath.getParent();
+
+ status = IoUtils.checkFileExists(parent, options);
+ if (status == null) {
+ throw new AccessDeniedException("Receive directory parent (" + parent + ") existence status cannot be determined for " + localPath);
+ }
+
+ if (status && Files.isDirectory(parent, options)) {
+ file = localPath;
+ }
+ }
+
+ if (file == null) {
+ throw new IOException("Cannot write to " + localPath);
+ }
+
+ status = IoUtils.checkFileExists(file, options);
+ if (status == null) {
+ throw new AccessDeniedException("Receive directory file existence status cannot be determined: " + file);
+ }
+
+ if (!(status && Files.isDirectory(file, options))) {
+ Files.createDirectory(file);
+ }
+
+ if (preserve) {
+ updateFileProperties(file, permissions, time);
+ }
+
+ return file;
+ }
+
+ /**
+ * Invoked when required to send a pattern of files
+ *
+ * @param basedir The base directory - may be {@code null}/empty to indicate CWD
+ * @param pattern The required pattern
+ * @return The matching <U>relative paths</U> of the children to send
+ */
+ default Iterable<String> getMatchingFilesToSend(String basedir, String pattern) {
+ String[] matches = new DirectoryScanner(basedir, pattern).scan();
+ if (GenericUtils.isEmpty(matches)) {
+ return Collections.emptyList();
+ }
+
+ return Arrays.asList(matches);
+ }
+
+ /**
+ * Invoked on a local path in order to decide whether it should be sent
+ * as a file or as a directory
+ *
+ * @param path The local {@link Path}
+ * @param options The {@link LinkOption}-s
+ * @return Whether to send the file as a regular one - <B>Note:</B> if {@code false}
+ * then the {@link #sendAsDirectory(Path, LinkOption...)} is consulted.
+ * @throws IOException If failed to decide
+ */
+ default boolean sendAsRegularFile(Path path, LinkOption... options) throws IOException {
+ return Files.isRegularFile(path, options);
+ }
+
+ /**
+ * Invoked on a local path in order to decide whether it should be sent
+ * as a file or as a directory
+ *
+ * @param path The local {@link Path}
+ * @param options The {@link LinkOption}-s
+ * @return Whether to send the file as a directory - <B>Note:</B> if {@code true}
+ * then {@link #getLocalFolderChildren(Path)} is consulted
+ * @throws IOException If failed to decide
+ */
+ default boolean sendAsDirectory(Path path, LinkOption... options) throws IOException {
+ return Files.isDirectory(path, options);
+ }
+
+ /**
+ * Invoked when required to send all children of a local directory
+ *
+ * @param path The local folder {@link Path}{
+ * @return The {@link DirectoryStream} of children to send - <B>Note:</B> for each child
+ * the decision whether to send it as a file or a directory will be reached by consulting
+ * the respective {@link #sendAsRegularFile(Path, LinkOption...) sendAsRegularFile} and
+ * {@link #sendAsDirectory(Path, LinkOption...) sendAsDirectory} methods
+ * @throws IOException If failed to provide the children stream
+ * @see #sendAsDirectory(Path, LinkOption...) sendAsDirectory
+ */
+ default DirectoryStream<Path> getLocalFolderChildren(Path path) throws IOException {
+ return Files.newDirectoryStream(path);
+ }
+
+ default BasicFileAttributes getLocalBasicFileAttributes(Path path, LinkOption... options) throws IOException {
+ return Files.getFileAttributeView(path, BasicFileAttributeView.class, options).readAttributes();
+ }
+
+ default Set<PosixFilePermission> getLocalFilePermissions(Path path, LinkOption... options) throws IOException {
+ return IoUtils.getPermissions(path, options);
+ }
+
+ /**
+ * @param fileSystem The <U>local</U> {@link FileSystem} on which local file should reside
+ * @param commandPath The command path using the <U>local</U> file separator
+ * @return The resolved absolute and normalized local {@link Path}
+ * @throws IOException If failed to resolve the path
+ * @throws InvalidPathException If invalid local path value
+ */
+ default Path resolveLocalPath(FileSystem fileSystem, String commandPath) throws IOException, InvalidPathException {
+ String path = SelectorUtils.translateToLocalFileSystemPath(commandPath, File.separatorChar, fileSystem);
+ Path lcl = fileSystem.getPath(path);
+ Path abs = lcl.isAbsolute() ? lcl : lcl.toAbsolutePath();
+ return abs.normalize();
+ }
+
+ /**
+ * Invoked when a request to receive something is processed
+ *
+ * @param path The local target {@link Path} of the request
+ * @param recursive Whether the request is recursive
+ * @param shouldBeDir Whether target path is expected to be a directory
+ * @param preserve Whether target path is expected to preserve attributes (permissions, times)
+ * @return The effective target path - default=same as input
+ * @throws IOException If failed to resolve target location
+ */
+ default Path resolveIncomingReceiveLocation(
+ Path path, boolean recursive, boolean shouldBeDir, boolean preserve)
+ throws IOException {
+ if (!shouldBeDir) {
+ return path;
+ }
+ LinkOption[] options = IoUtils.getLinkOptions(true);
+ Boolean status = IoUtils.checkFileExists(path, options);
+ if (status == null) {
+ throw new SshException("Target directory " + path + " is most like inaccessible");
+ }
+ if (!status) {
+ throw new SshException("Target directory " + path + " does not exist");
+ }
+ if (!Files.isDirectory(path, options)) {
+ throw new SshException("Target directory " + path + " is not a directory");
+ }
+
+ return path;
+ }
+
+ /**
+ * Called when there is a candidate file/folder for sending
+ *
+ * @param localPath The original file/folder {@link Path} for sending
+ * @param options The {@link LinkOption}-s to use for validation
+ * @return The effective outgoing file path (default=same as input)
+ * @throws IOException If failed to resolve
+ */
+ default Path resolveOutgoingFilePath(Path localPath, LinkOption... options) throws IOException {
+ Boolean status = IoUtils.checkFileExists(localPath, options);
+ if (status == null) {
+ throw new AccessDeniedException("Send file existence status cannot be determined: " + localPath);
+ }
+ if (!status) {
+ throw new IOException(localPath + ": no such file or directory");
+ }
+
+ return localPath;
+ }
+
+ /**
+ * Create an input stream to read from a file
+ *
+ * @param session The {@link Session} requesting the access
+ * @param file The requested local file {@link Path}
+ * @param options The {@link OpenOption}s - may be {@code null}/empty
+ * @return The open {@link InputStream} never {@code null}
+ * @throws IOException If failed to open the file
+ */
+ InputStream openRead(Session session, Path file, OpenOption... options) throws IOException;
+
+ ScpSourceStreamResolver createScpSourceStreamResolver(Path path) throws IOException;
+
+ /**
+ * Create an output stream to write to a file
+ *
+ * @param session The {@link Session} requesting the access
+ * @param file The requested local file {@link Path}
+ * @param options The {@link OpenOption}s - may be {@code null}/empty
+ * @return The open {@link OutputStream} never {@code null}
+ * @throws IOException If failed to open the file
+ */
+ OutputStream openWrite(Session session, Path file, OpenOption... options) throws IOException;
+
+ ScpTargetStreamResolver createScpTargetStreamResolver(Path path) throws IOException;
+
+ static void updateFileProperties(Path file, Set<PosixFilePermission> perms, ScpTimestamp time) throws IOException {
+ IoUtils.setPermissions(file, perms);
+
+ if (time != null) {
+ BasicFileAttributeView view = Files.getFileAttributeView(file, BasicFileAttributeView.class);
+ FileTime lastModified = FileTime.from(time.getLastModifiedTime(), TimeUnit.MILLISECONDS);
+ FileTime lastAccess = FileTime.from(time.getLastAccessTime(), TimeUnit.MILLISECONDS);
+ view.setTimes(lastModified, lastAccess, null);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/af415e5f/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
----------------------------------------------------------------------
diff --git a/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
new file mode 100644
index 0000000..b492129
--- /dev/null
+++ b/sshd-scp/src/main/java/org/apache/sshd/common/scp/ScpFileOpenerHolder.java
@@ -0,0 +1,37 @@
+/*
+ * 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.common.scp;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface ScpFileOpenerHolder {
+ /**
+ * @return The last {@link ScpFileOpener} set via call
+ * to {@link #setScpFileOpener(ScpFileOpener)}
+ */
+ ScpFileOpener getScpFileOpener();
+
+ /**
+ * @param opener The default {@link ScpFileOpener} to use - if {@code null}
+ * then a default opener is used
+ */
+ void setScpFileOpener(ScpFileOpener opener);
+}