/*
* 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(false);
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;
}
}