/*
* 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.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import org.apache.sshd.client.SshClient;
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.ScpLocation;
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.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.io.NoCloseInputStream;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class DefaultScpClient extends AbstractScpClient {
/**
* Command line option used to indicate a non-default port
*/
public static final String SCP_PORT_OPTION = "-P";
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(final InputStream local, final String remote, final long size, final Collection<PosixFilePermission> perms, final ScpTimestamp time) throws IOException {
int namePos = ValidateUtils.checkNotNullAndNotEmpty(remote, "No remote location specified").lastIndexOf('/');
final String name = (namePos < 0)
? remote
: ValidateUtils.checkNotNullAndNotEmpty(remote.substring(namePos + 1), "No name value in remote=%s", remote);
final String cmd = ScpClient.createSendCommand(remote, (time != null) ? EnumSet.of(Option.PreserveAttributes) : Collections.emptySet());
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);
final Path mockPath = new MockPath(remote);
helper.sendStream(new DefaultScpStreamResolver(name, mockPath, perms, time, size, local, cmd),
time != null, 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);
}
}
//////////////////////////////////////////////////////////////////////////
private static boolean showError(PrintStream stderr, String message) {
stderr.println(message);
return true;
}
private static String[] normalizeCommandArguments(PrintStream stdout, PrintStream stderr, String... args) {
int numArgs = GenericUtils.length(args);
if (numArgs <= 0) {
return args;
}
List<String> effective = new ArrayList<>(numArgs);
boolean error = false;
for (int index = 0; (index < numArgs) && (!error); index++) {
String argName = args[index];
// handled by 'setupClientSession'
if (SshClient.isArgumentedOption(SCP_PORT_OPTION, argName)) {
if ((index + 1) >= numArgs) {
error = showError(stderr, "option requires an argument: " + argName);
break;
}
effective.add(argName);
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)) {
effective.add(argName);
} else if (argName.charAt(0) == '-') {
error = showError(stderr, "Unknown option: " + argName);
break;
} else {
if ((index + 1) >= numArgs) {
error = showError(stderr, "Not enough arguments");
break;
}
ScpLocation source = new ScpLocation(argName);
ScpLocation target = new ScpLocation(args[++index]);
if (index < (numArgs - 1)) {
error = showError(stderr, "Unexpected extra arguments");
break;
}
if (source.isLocal() == target.isLocal()) {
error = showError(stderr, "Both targets are either remote or local");
break;
}
ScpLocation remote = source.isLocal() ? target : source;
effective.add(remote.resolveUsername() + "@" + remote.getHost());
effective.add(source.toString());
effective.add(target.toString());
break;
}
}
if (error) {
return null;
}
return effective.toArray(new String[effective.size()]);
}
public static void main(String[] args) throws Exception {
final PrintStream stdout = System.out;
final PrintStream stderr = System.err;
OutputStream logStream = stdout;
try (BufferedReader stdin = new BufferedReader(
new InputStreamReader(new NoCloseInputStream(System.in), Charset.defaultCharset()))) {
args = normalizeCommandArguments(stdout, stderr, args);
int numArgs = GenericUtils.length(args);
// see the way normalizeCommandArguments works...
if (numArgs >= 2) {
Level level = SshClient.resolveLoggingVerbosity(args, numArgs - 2);
logStream = SshClient.resolveLoggingTargetStream(stdout, stderr, args, numArgs - 2);
if (logStream != null) {
SshClient.setupLogging(level, stdout, stderr, logStream);
}
}
ClientSession session = (logStream == null) || GenericUtils.isEmpty(args)
? null : SshClient.setupClientSession(SCP_PORT_OPTION, stdin, stdout, stderr, args);
if (session == null) {
stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i identity]"
+ " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] [-o option=value]"
+ " [-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");
stderr.println("NOTE: exactly ONE of the source or target must be remote and the other one local");
System.exit(-1);
return; // not that we really need it...
}
try {
// see the way normalizeCommandArguments works...
Collection<Option> options = EnumSet.noneOf(Option.class);
boolean quiet = false;
for (int index = 0; index < numArgs; index++) {
String argName = args[index];
if ("-r".equals(argName)) {
options.add(Option.Recursive);
} else if ("-p".equals(argName)) {
options.add(Option.PreserveAttributes);
} else if ("-q".equals(argName)) {
quiet = true;
}
}
if (!quiet) {
session.setScpTransferEventListener(new ScpTransferEventListener() {
@Override
public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) {
logEvent("startFolderEvent", op, file, -1L, perms, null);
}
@Override
public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) {
logEvent("endFolderEvent", op, file, -1L, perms, thrown);
}
@Override
public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) {
logEvent("startFileEvent", op, file, length, perms, null);
}
@Override
public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) {
logEvent("endFileEvent", op, file, length, perms, thrown);
}
private void logEvent(String name, FileOperation op, Path file, long length, Collection<PosixFilePermission> perms, Throwable thrown) {
PrintStream ps = (thrown == null) ? stdout : stderr;
ps.append('\t').append(name).append('[').append(op.name()).append(']').append(' ').append(file.toString());
if (length > 0L) {
ps.append(' ').append("length=").append(Long.toString(length));
}
ps.append(' ').append(String.valueOf(perms));
if (thrown != null) {
ps.append(" - ").append(thrown.getClass().getSimpleName()).append(": ").append(thrown.getMessage());
}
ps.println();
}
});
}
ScpClient client = session.createScpClient();
ScpLocation source = new ScpLocation(args[numArgs - 2]);
ScpLocation target = new ScpLocation(args[numArgs - 1]);
if (source.isLocal()) {
client.upload(source.getPath(), target.getPath(), options);
} else {
client.download(source.getPath(), target.getPath(), options);
}
} finally {
session.close();
}
} finally {
if ((logStream != stdout) && (logStream != stderr)) {
logStream.close();
}
}
}
}