blob: 0973df7808c50b7220f5ef41e3c712aff51a2665 [file] [log] [blame]
/*
* 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.scp.common;
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.Charset;
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 org.apache.sshd.common.file.util.MockPath;
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.input.LimitInputStream;
import org.apache.sshd.common.util.logging.AbstractLoggingBean;
import org.apache.sshd.scp.ScpModuleProperties;
import org.apache.sshd.scp.common.ScpTransferEventListener.FileOperation;
import org.apache.sshd.scp.common.helpers.DefaultScpFileOpener;
import org.apache.sshd.scp.common.helpers.ScpAckInfo;
import org.apache.sshd.scp.common.helpers.ScpDirEndCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpIoUtils;
import org.apache.sshd.scp.common.helpers.ScpPathCommandDetailsSupport;
import org.apache.sshd.scp.common.helpers.ScpReceiveDirCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpReceiveFileCommandDetails;
import org.apache.sshd.scp.common.helpers.ScpTimestampCommandDetails;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Session> {
/**
* Command prefix used to identify SCP commands
*/
public static final String SCP_COMMAND_PREFIX = "scp";
/**
* 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;
protected final InputStream in;
protected final Charset csIn;
protected final OutputStream out;
protected final Charset csOut;
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(session, in, ScpModuleProperties.SCP_INCOMING_ENCODING.getRequired(session),
out, ScpModuleProperties.SCP_OUTGOING_ENCODING.getRequired(session),
fileSystem, opener, eventListener);
}
public ScpHelper(Session session,
InputStream in, Charset csIn, OutputStream out, Charset csOut,
FileSystem fileSystem, ScpFileOpener opener, ScpTransferEventListener eventListener) {
this.sessionInstance = Objects.requireNonNull(session, "No session");
this.in = Objects.requireNonNull(in, "No input stream");
this.csIn = Objects.requireNonNull(csIn, "No input charset");
this.out = Objects.requireNonNull(out, "No output stream");
this.csOut = Objects.requireNonNull(csOut, "No output charset");
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((session, 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") // see
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=537593
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") // see
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=537593
public void postProcessReceivedData(
String name, boolean preserve, Set<PosixFilePermission> perms,
ScpTimestampCommandDetails 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(getSession(), localPath, recursive, shouldBeDir, preserve);
receive((session, line, isDir, time) -> {
if (recursive && isDir) {
receiveDir(line, path, time, preserve, bufferSize);
} else {
receiveFile(line, path, time, preserve, bufferSize);
}
});
}
/**
* Reads command line(s) and invokes the handler until EOF or and &quot;E&quot; command is received
*
* @param handler The {@link ScpReceiveLineHandler} to invoke when a command has been read
* @throws IOException If failed to read/write
*/
protected void receive(ScpReceiveLineHandler handler) throws IOException {
sendOk();
boolean debugEnabled = log.isDebugEnabled();
Session session = getSession();
for (ScpTimestampCommandDetails time = null;; debugEnabled = log.isDebugEnabled()) {
String line;
boolean isDir = false;
int c = receiveNextCmd();
switch (c) {
case -1:
return;
case ScpReceiveDirCommandDetails.COMMAND_NAME:
line = ScpIoUtils.readLine(in, csIn);
line = Character.toString((char) c) + line;
isDir = true;
if (debugEnabled) {
log.debug("receive({}) - Received 'D' header: {}", this, line);
}
break;
case ScpReceiveFileCommandDetails.COMMAND_NAME:
line = ScpIoUtils.readLine(in, csIn);
line = Character.toString((char) c) + line;
if (debugEnabled) {
log.debug("receive({}) - Received 'C' header: {}", this, line);
}
break;
case ScpTimestampCommandDetails.COMMAND_NAME:
line = ScpIoUtils.readLine(in, csIn);
line = Character.toString((char) c) + line;
if (debugEnabled) {
log.debug("receive({}) - Received 'T' header: {}", this, line);
}
time = ScpTimestampCommandDetails.parse(line);
sendOk();
continue;
case ScpDirEndCommandDetails.COMMAND_NAME:
line = ScpIoUtils.readLine(in, csIn);
line = Character.toString((char) c) + line;
if (debugEnabled) {
log.debug("receive({}) - Received 'E' header: {}", this, line);
}
sendOk();
return;
default:
// a real ack that has been acted upon already
continue;
}
try {
handler.process(session, line, isDir, time);
} finally {
time = null;
}
}
}
// NOTE: we rely on the fact that an SCP command does not start with an ACK code
protected int receiveNextCmd() throws IOException {
int c = in.read();
if (c == -1) {
return c;
}
if (c == ScpAckInfo.OK) {
return c;
}
if ((c == ScpAckInfo.WARNING) || (c == ScpAckInfo.ERROR)) {
String line = ScpIoUtils.readLine(in, csIn, true);
if (log.isDebugEnabled()) {
log.debug("receiveNextCmd - ACK={}", new ScpAckInfo(c, line));
}
}
return c;
}
public void receiveDir(String header, Path local, ScpTimestampCommandDetails time, boolean preserve, int bufferSize)
throws IOException {
Path path = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
boolean debugEnabled = log.isDebugEnabled();
if (debugEnabled) {
log.debug("receiveDir({})[{}] Receiving directory {} - preserve={}, time={}, buffer-size={}", this, header,
path, preserve, time, bufferSize);
}
ScpReceiveDirCommandDetails details = new ScpReceiveDirCommandDetails(header);
String name = details.getName();
long length = details.getLength();
if (length != 0L) {
throw new IOException("Expected 0 length for directory=" + name + " but got " + length);
}
Session session = getSession();
Set<PosixFilePermission> perms = details.getPermissions();
Path file = opener.resolveIncomingFilePath(session, path, name, preserve, perms, time);
sendOk();
time = null;
listener.startFolderEvent(session, FileOperation.RECEIVE, path, perms);
try {
for (;;) {
header = readLine();
if (debugEnabled) {
log.debug("receiveDir({})[{}] Received header: {}", this, file, header);
}
char cmdChar = header.charAt(0);
if (cmdChar == ScpReceiveFileCommandDetails.COMMAND_NAME) {
receiveFile(header, file, time, preserve, bufferSize);
time = null;
} else if (cmdChar == ScpReceiveDirCommandDetails.COMMAND_NAME) {
receiveDir(header, file, time, preserve, bufferSize);
time = null;
} else if (cmdChar == ScpDirEndCommandDetails.COMMAND_NAME) {
sendOk();
break;
} else if (cmdChar == ScpTimestampCommandDetails.COMMAND_NAME) {
time = ScpTimestampCommandDetails.parse(header);
sendOk();
} else {
throw new IOException("Unexpected message: '" + header + "'");
}
}
} catch (IOException | RuntimeException e) {
listener.endFolderEvent(session, FileOperation.RECEIVE, path, perms, e);
throw e;
}
listener.endFolderEvent(session, FileOperation.RECEIVE, path, perms, null);
}
public void receiveFile(String header, Path local, ScpTimestampCommandDetails 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);
}
ScpTargetStreamResolver targetStreamResolver = opener.createScpTargetStreamResolver(getSession(), path);
receiveStream(header, targetStreamResolver, time, preserve, bufferSize);
}
public void receiveStream(
String header, ScpTargetStreamResolver resolver, ScpTimestampCommandDetails time, boolean preserve,
int bufferSize)
throws IOException {
if (bufferSize < MIN_RECEIVE_BUFFER_SIZE) {
throw new IOException("receiveStream(" + resolver + ") buffer size (" + bufferSize + ") below minimum ("
+ MIN_RECEIVE_BUFFER_SIZE + ")");
}
ScpReceiveFileCommandDetails details = new ScpReceiveFileCommandDetails(header);
long length = details.getLength();
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;
}
Session session = getSession();
String name = details.getName();
Set<PosixFilePermission> perms = details.getPermissions();
Path file;
try (InputStream is = new LimitInputStream(this.in, length);
OutputStream os = resolver.resolveTargetStream(session, name, length, perms,
IoUtils.EMPTY_OPEN_OPTIONS)) {
file = resolver.getEventListenerFilePath();
listener.startFileEvent(session, FileOperation.RECEIVE, file, length, perms);
sendOk();
try {
IoUtils.copy(is, os, bufSize);
} catch (IOException | RuntimeException e) {
listener.endFileEvent(session, FileOperation.RECEIVE, file, length, perms, e);
throw e;
}
listener.endFileEvent(session, FileOperation.RECEIVE, file, length, perms, null);
resolver.closeTargetStream(session, name, length, perms, os);
}
resolver.postProcessReceivedData(name, preserve, perms, time);
sendOk();
ScpAckInfo ackInfo = readAck(false);
if (debugEnabled) {
log.debug("receiveStream({})[{}] ACK={}", this, resolver, ackInfo);
}
validateFileOperationAckReplyCode(header, session, FileOperation.RECEIVE, file, length, perms, ackInfo);
}
public String readLine() throws IOException {
return readLine(false);
}
public String readLine(boolean canEof) throws IOException {
return ScpIoUtils.readLine(in, csIn, canEof);
}
public void send(Collection<String> paths, boolean recursive, boolean preserve, int bufferSize) throws IOException {
ScpAckInfo ackInfo = readAck(false);
boolean debugEnabled = log.isDebugEnabled();
if (debugEnabled) {
log.debug("send({}) ACK={}", paths, ackInfo);
}
validateOperationReadyCode("send", "Paths", ackInfo);
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);
}
Session session = getSession();
Path basePath = resolveLocalPath(basedir);
Iterable<Path> included = opener.getMatchingFilesToSend(session, basePath, pattern);
for (Path file : included) {
if (opener.sendAsRegularFile(session, file, options)) {
sendFile(file, preserve, bufferSize);
} else if (opener.sendAsDirectory(session, file, options)) {
if (!recursive) {
if (debugEnabled) {
log.debug("send({}) {}: not a regular file", this, file);
}
String path = basePath.relativize(file).toString();
sendWarning(path.replace(File.separatorChar, '/') + " not a regular file");
} else {
sendDir(file, preserve, bufferSize);
}
} else {
if (debugEnabled) {
log.debug("send({}) {}: unknown file type", this, file);
}
String path = basePath.relativize(file).toString();
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 {
ScpAckInfo ackInfo = readAck(false);
if (log.isDebugEnabled()) {
log.debug("sendPaths({}) ACK={}", paths, ackInfo);
}
validateOperationReadyCode("sendPaths", "Paths", ackInfo);
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();
Session session = getSession();
Path file = opener.resolveOutgoingFilePath(session, localPath, options);
if (opener.sendAsRegularFile(session, file, options)) {
sendFile(file, preserve, bufferSize);
} else if (opener.sendAsDirectory(session, 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(getSession(), 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);
}
ScpSourceStreamResolver sourceStreamResolver = opener.createScpSourceStreamResolver(getSession(), path);
sendStream(sourceStreamResolver, 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;
}
ScpTimestampCommandDetails time = resolver.getTimestamp();
if (preserve && (time != null)) {
ScpAckInfo ackInfo = ScpIoUtils.sendAcknowledgedCommand(time, in, csIn, out, csOut);
String cmd = time.toHeader();
if (debugEnabled) {
log.debug("sendStream({})[{}] command='{}' ACK={}", this, resolver, cmd, ackInfo);
}
validateAckReplyCode(cmd, resolver, ackInfo);
}
Set<PosixFilePermission> perms = EnumSet.copyOf(resolver.getPermissions());
String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms))
? ScpReceiveFileCommandDetails.DEFAULT_FILE_OCTAL_PERMISSIONS
: ScpPathCommandDetailsSupport.getOctalPermissions(perms);
String fileName = resolver.getFileName();
String cmd = ScpReceiveFileCommandDetails.COMMAND_NAME + octalPerms + " " + fileSize + " " + fileName;
if (debugEnabled) {
log.debug("sendStream({})[{}] send 'C' command: {}", this, resolver, cmd);
}
ScpAckInfo ackInfo = sendAcknowledgedCommand(cmd);
if (debugEnabled) {
log.debug("sendStream({})[{}] command='{}' ACK={}", this, resolver,
cmd.substring(0, cmd.length() - 1), ackInfo);
}
validateAckReplyCode(cmd, resolver, ackInfo);
Session session = getSession();
Path path;
try (InputStream in = resolver.resolveSourceStream(session, fileSize, perms, IoUtils.EMPTY_OPEN_OPTIONS)) {
path = resolver.getEventListenerFilePath();
listener.startFileEvent(session, FileOperation.SEND, path, fileSize, perms);
try {
IoUtils.copy(in, out, bufSize);
} catch (IOException | RuntimeException e) {
listener.endFileEvent(session, FileOperation.SEND, path, fileSize, perms, e);
throw e;
}
listener.endFileEvent(session, FileOperation.SEND, path, fileSize, perms, null);
resolver.closeSourceStream(session, fileSize, perms, in);
}
sendOk();
ackInfo = readAck(false);
if (debugEnabled) {
log.debug("sendStream({})[{}] command='{}' ACK={}", this, resolver, cmd, ackInfo);
}
validateFileOperationAckReplyCode(cmd, session, FileOperation.SEND, path, fileSize, perms, ackInfo);
}
protected void validateOperationReadyCode(String command, Object location, ScpAckInfo ackInfo)
throws IOException {
validateCommandStatusCode(command, location, ackInfo, false);
}
protected void validateFileOperationAckReplyCode(
String command, Session session, FileOperation op, Path file,
long fileSize, Set<PosixFilePermission> perms, ScpAckInfo ackInfo)
throws IOException {
listener.handleFileEventAckInfo(session, op, file, fileSize, perms, ackInfo);
validateAckReplyCode(command, file, ackInfo);
}
protected void validateAckReplyCode(String command, Object location, ScpAckInfo ackInfo)
throws IOException {
validateCommandStatusCode(command, location, ackInfo, false);
}
protected void validateCommandStatusCode(String command, Object location, ScpAckInfo ackInfo, boolean eofAllowed)
throws IOException {
if (ackInfo == null) {
if (eofAllowed) {
return;
}
log.error("validateCommandStatusCode({})[{}] unexpected EOF while waiting on ACK for command={}",
this, location, command);
throw new EOFException("EOF while waiting on ACK for command=" + command + " at " + location);
}
int statusCode = ackInfo.getStatusCode();
switch (statusCode) {
case ScpAckInfo.OK:
break;
case ScpAckInfo.WARNING:
log.warn("validateCommandStatusCode({})[{}] advisory ACK={} for command={}",
this, location, ackInfo, command);
break;
default:
log.error("validateCommandStatusCode({})[{}] bad ACK={} for command={}",
this, location, ackInfo, command);
ackInfo.validateCommandStatusCode(command, location); // this actually throws an SCPException
}
}
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);
Session session = getSession();
if (preserve) {
BasicFileAttributes basic = opener.getLocalBasicFileAttributes(session, path, options);
FileTime lastModified = basic.lastModifiedTime();
FileTime lastAccess = basic.lastAccessTime();
ScpTimestampCommandDetails time = new ScpTimestampCommandDetails(lastModified, lastAccess);
String cmd = time.toHeader();
if (debugEnabled) {
log.debug("sendDir({})[{}] send last-modified={}, last-access={} command: {}", this, path, lastModified,
lastAccess, cmd);
}
ScpAckInfo ackInfo = sendAcknowledgedCommand(cmd);
if (debugEnabled) {
log.debug("sendDir({})[{}] command='{}' ACK={}", this, path, cmd, ackInfo);
}
validateAckReplyCode(cmd, path, ackInfo);
}
Set<PosixFilePermission> perms = opener.getLocalFilePermissions(session, path, options);
String octalPerms = ((!preserve) || GenericUtils.isEmpty(perms))
? ScpReceiveDirCommandDetails.DEFAULT_DIR_OCTAL_PERMISSIONS
: ScpPathCommandDetailsSupport.getOctalPermissions(perms);
String cmd = ScpReceiveDirCommandDetails.COMMAND_NAME + octalPerms + " " + "0" + " "
+ Objects.toString(path.getFileName(), null);
if (debugEnabled) {
log.debug("sendDir({})[{}] send 'D' command: {}", this, path, cmd);
}
ScpAckInfo ackInfo = sendAcknowledgedCommand(cmd);
if (debugEnabled) {
log.debug("sendDir({})[{}] command='{}' ACK={}", this, path, cmd, ackInfo);
}
validateAckReplyCode(cmd, path, ackInfo);
try (DirectoryStream<Path> children = opener.getLocalFolderChildren(session, path)) {
listener.startFolderEvent(session, FileOperation.SEND, path, perms);
try {
for (Path child : children) {
if (opener.sendAsRegularFile(session, child, options)) {
sendFile(child, preserve, bufferSize);
} else if (opener.sendAsDirectory(session, child, options)) {
sendDir(child, preserve, bufferSize);
}
}
} catch (IOException | RuntimeException e) {
listener.endFolderEvent(session, FileOperation.SEND, path, perms, e);
throw e;
}
listener.endFolderEvent(session, FileOperation.SEND, path, perms, null);
}
if (debugEnabled) {
log.debug("sendDir({})[{}] send 'E' command", this, path);
}
ackInfo = sendAcknowledgedCommand(ScpDirEndCommandDetails.HEADER);
if (debugEnabled) {
log.debug("sendDir({})[{}] 'E' command ACK={}", this, path, ackInfo);
}
validateAckReplyCode(cmd, path, ackInfo);
}
protected ScpAckInfo sendAcknowledgedCommand(String cmd) throws IOException {
return ScpIoUtils.sendAcknowledgedCommand(cmd, in, csIn, out, csOut);
}
public void sendOk() throws IOException {
sendResponseMessage(ScpAckInfo.OK, null /* ignored */);
}
protected void sendWarning(String message) throws IOException {
sendResponseMessage(ScpAckInfo.WARNING, message);
}
protected void sendError(String message) throws IOException {
sendResponseMessage(ScpAckInfo.ERROR, message);
}
protected void sendResponseMessage(int level, String message) throws IOException {
ScpAckInfo.sendAck(out, csOut, level, message);
}
public ScpAckInfo readAck(boolean canEof) throws IOException {
return ScpAckInfo.readAck(in, csIn, canEof);
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + getSession() + "]";
}
}