blob: 6c20da305ce631ff1154389907e6b69d80102ee6 [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.client.subsystem.sftp;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.sshd.client.channel.ClientChannel;
import org.apache.sshd.client.subsystem.AbstractSubsystemClient;
import org.apache.sshd.client.subsystem.sftp.extensions.BuiltinSftpClientExtensions;
import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension;
import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtensionFactory;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.channel.Channel;
import org.apache.sshd.common.subsystem.sftp.SftpConstants;
import org.apache.sshd.common.subsystem.sftp.SftpException;
import org.apache.sshd.common.subsystem.sftp.SftpHelper;
import org.apache.sshd.common.subsystem.sftp.SftpUniversalOwnerAndGroup;
import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public abstract class AbstractSftpClient extends AbstractSubsystemClient implements SftpClient, RawSftpClient {
private final Attributes fileOpenAttributes = new Attributes();
private final AtomicReference<Map<String, Object>> parsedExtensionsHolder = new AtomicReference<>(null);
protected AbstractSftpClient() {
fileOpenAttributes.setType(SftpConstants.SSH_FILEXFER_TYPE_REGULAR);
}
@Override
public Channel getChannel() {
return getClientChannel();
}
@Override
public String getName() {
return SftpConstants.SFTP_SUBSYSTEM_NAME;
}
@Override
public CloseableHandle open(String path) throws IOException {
return open(path, Collections.<OpenMode>emptySet());
}
@Override
public CloseableHandle open(String path, OpenMode... options) throws IOException {
return open(path, GenericUtils.of(options));
}
@Override
public void rename(String oldPath, String newPath) throws IOException {
rename(oldPath, newPath, Collections.<CopyMode>emptySet());
}
@Override
public void rename(String oldPath, String newPath, CopyMode... options) throws IOException {
rename(oldPath, newPath, GenericUtils.of(options));
}
@Override
public InputStream read(String path) throws IOException {
return read(path, DEFAULT_READ_BUFFER_SIZE);
}
@Override
public InputStream read(String path, int bufferSize) throws IOException {
return read(path, bufferSize, EnumSet.of(OpenMode.Read));
}
@Override
public InputStream read(String path, OpenMode... mode) throws IOException {
return read(path, DEFAULT_READ_BUFFER_SIZE, mode);
}
@Override
public InputStream read(String path, int bufferSize, OpenMode... mode) throws IOException {
return read(path, bufferSize, GenericUtils.of(mode));
}
@Override
public InputStream read(String path, Collection<OpenMode> mode) throws IOException {
return read(path, DEFAULT_READ_BUFFER_SIZE, mode);
}
@Override
public OutputStream write(String path) throws IOException {
return write(path, DEFAULT_WRITE_BUFFER_SIZE);
}
@Override
public OutputStream write(String path, int bufferSize) throws IOException {
return write(path, bufferSize, EnumSet.of(OpenMode.Write, OpenMode.Create, OpenMode.Truncate));
}
@Override
public OutputStream write(String path, OpenMode... mode) throws IOException {
return write(path, DEFAULT_WRITE_BUFFER_SIZE, mode);
}
@Override
public OutputStream write(String path, Collection<OpenMode> mode) throws IOException {
return write(path, DEFAULT_WRITE_BUFFER_SIZE, mode);
}
@Override
public OutputStream write(String path, int bufferSize, OpenMode... mode) throws IOException {
return write(path, bufferSize, GenericUtils.of(mode));
}
@Override
public void write(Handle handle, long fileOffset, byte[] src) throws IOException {
write(handle, fileOffset, src, 0, src.length);
}
@Override
public void symLink(String linkPath, String targetPath) throws IOException {
link(linkPath, targetPath, true);
}
@Override
public <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType) {
Object instance = getExtension(BuiltinSftpClientExtensions.fromType(extensionType));
if (instance == null) {
return null;
} else {
return extensionType.cast(instance);
}
}
@Override
public SftpClientExtension getExtension(String extensionName) {
return getExtension(BuiltinSftpClientExtensions.fromName(extensionName));
}
protected SftpClientExtension getExtension(SftpClientExtensionFactory factory) {
if (factory == null) {
return null;
}
Map<String, byte[]> extensions = getServerExtensions();
Map<String, Object> parsed = getParsedServerExtensions(extensions);
return factory.create(this, this, extensions, parsed);
}
protected Map<String, Object> getParsedServerExtensions() {
return getParsedServerExtensions(getServerExtensions());
}
protected Map<String, Object> getParsedServerExtensions(Map<String, byte[]> extensions) {
Map<String, Object> parsed = parsedExtensionsHolder.get();
if (parsed == null) {
parsed = ParserUtils.parse(extensions);
if (parsed == null) {
parsed = Collections.emptyMap();
}
parsedExtensionsHolder.set(parsed);
}
return parsed;
}
/**
* Sends the specified command, waits for the response and then invokes {@link #checkResponseStatus(int, Buffer)}
* @param cmd The command to send
* @param request The request {@link Buffer}
* @throws IOException If failed to send, receive or check the returned status
* @see #send(int, Buffer)
* @see #receive(int)
* @see #checkResponseStatus(int, Buffer)
*/
protected void checkCommandStatus(int cmd, Buffer request) throws IOException {
int reqId = send(cmd, request);
Buffer response = receive(reqId);
checkResponseStatus(cmd, response);
}
/**
* Checks if the incoming response is an {@code SSH_FXP_STATUS} one,
* and if so whether the substatus is {@code SSH_FX_OK}.
*
* @param cmd The sent command opcode
* @param buffer The received response {@link Buffer}
* @throws IOException If response does not carry a status or carries
* a bad status code
* @see #checkResponseStatus(int, int, int, String, String)
*/
protected void checkResponseStatus(int cmd, Buffer buffer) throws IOException {
int length = buffer.getInt();
int type = buffer.getUByte();
int id = buffer.getInt();
if (type == SftpConstants.SSH_FXP_STATUS) {
int substatus = buffer.getInt();
String msg = buffer.getString();
String lang = buffer.getString();
checkResponseStatus(cmd, id, substatus, msg, lang);
} else {
handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_STATUS, id, type, length, buffer);
}
}
/**
* @param cmd The sent command opcode
* @param id The request id
* @param substatus The sub-status value
* @param msg The message
* @param lang The language
* @throws IOException if the sub-status is not {@code SSH_FX_OK}
* @see #throwStatusException(int, int, int, String, String)
*/
protected void checkResponseStatus(int cmd, int id, int substatus, String msg, String lang) throws IOException {
if (log.isTraceEnabled()) {
log.trace("checkResponseStatus({})[id={}] cmd={} status={} lang={} msg={}",
getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
SftpConstants.getStatusName(substatus), lang, msg);
}
if (substatus != SftpConstants.SSH_FX_OK) {
throwStatusException(cmd, id, substatus, msg, lang);
}
}
protected void throwStatusException(int cmd, int id, int substatus, String msg, String lang) throws IOException {
throw new SftpException(substatus, msg);
}
/**
* @param cmd Command to be sent
* @param request The {@link Buffer} containing the request
* @return The received handle identifier
* @throws IOException If failed to send/receive or process the response
* @see #send(int, Buffer)
* @see #receive(int)
* @see #checkHandleResponse(int, Buffer)
*/
protected byte[] checkHandle(int cmd, Buffer request) throws IOException {
int reqId = send(cmd, request);
Buffer response = receive(reqId);
return checkHandleResponse(cmd, response);
}
protected byte[] checkHandleResponse(int cmd, Buffer buffer) throws IOException {
int length = buffer.getInt();
int type = buffer.getUByte();
int id = buffer.getInt();
if (type == SftpConstants.SSH_FXP_HANDLE) {
return ValidateUtils.checkNotNullAndNotEmpty(buffer.getBytes(), "Null/empty handle in buffer", GenericUtils.EMPTY_OBJECT_ARRAY);
}
if (type == SftpConstants.SSH_FXP_STATUS) {
int substatus = buffer.getInt();
String msg = buffer.getString();
String lang = buffer.getString();
if (log.isTraceEnabled()) {
log.trace("checkHandleResponse({})[id={}] {} - status: {} [{}] {}",
getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
SftpConstants.getStatusName(substatus), lang, msg);
}
throwStatusException(cmd, id, substatus, msg, lang);
}
return handleUnexpectedHandlePacket(cmd, id, type, length, buffer);
}
protected byte[] handleUnexpectedHandlePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_HANDLE, id, type, length, buffer);
throw new SshException("No handling for unexpected handle packet id=" + id
+ ", type=" + SftpConstants.getCommandMessageName(type) + ", length=" + length);
}
/**
* @param cmd Command to be sent
* @param request Request {@link Buffer}
* @return The decoded response {@code Attributes}
* @throws IOException If failed to send/receive or process the response
* @see #send(int, Buffer)
* @see #receive(int)
* @see #checkAttributesResponse(int, Buffer)
*/
protected Attributes checkAttributes(int cmd, Buffer request) throws IOException {
int reqId = send(cmd, request);
Buffer response = receive(reqId);
return checkAttributesResponse(cmd, response);
}
protected Attributes checkAttributesResponse(int cmd, Buffer buffer) throws IOException {
int length = buffer.getInt();
int type = buffer.getUByte();
int id = buffer.getInt();
if (type == SftpConstants.SSH_FXP_ATTRS) {
return readAttributes(buffer);
}
if (type == SftpConstants.SSH_FXP_STATUS) {
int substatus = buffer.getInt();
String msg = buffer.getString();
String lang = buffer.getString();
if (log.isTraceEnabled()) {
log.trace("checkAttributesResponse()[id={}] {} - status: {} [{}] {}",
getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
SftpConstants.getStatusName(substatus), lang, msg);
}
throwStatusException(cmd, id, substatus, msg, lang);
}
return handleUnexpectedAttributesPacket(cmd, id, type, length, buffer);
}
protected Attributes handleUnexpectedAttributesPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_ATTRS, id, type, length, buffer);
if (err != null) {
throw err;
}
return null;
}
/**
* @param cmd Command to be sent
* @param request The request {@link Buffer}
* @return The retrieved name
* @throws IOException If failed to send/receive or process the response
* @see #send(int, Buffer)
* @see #receive(int)
* @see #checkOneNameResponse(int, Buffer)
*/
protected String checkOneName(int cmd, Buffer request) throws IOException {
int reqId = send(cmd, request);
Buffer response = receive(reqId);
return checkOneNameResponse(cmd, response);
}
protected String checkOneNameResponse(int cmd, Buffer buffer) throws IOException {
int length = buffer.getInt();
int type = buffer.getUByte();
int id = buffer.getInt();
if (type == SftpConstants.SSH_FXP_NAME) {
int len = buffer.getInt();
if (len != 1) {
throw new SshException("SFTP error: received " + len + " names instead of 1");
}
String name = buffer.getString();
String longName = null;
int version = getVersion();
if (version == SftpConstants.SFTP_V3) {
longName = buffer.getString();
}
Attributes attrs = readAttributes(buffer);
Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
// TODO decide what to do if not-null and not TRUE
if (log.isTraceEnabled()) {
log.trace("checkOneNameResponse({})[id={}] {} ({})[{}] eol={}: {}",
getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
name, longName, indicator, attrs);
}
return name;
}
if (type == SftpConstants.SSH_FXP_STATUS) {
int substatus = buffer.getInt();
String msg = buffer.getString();
String lang = buffer.getString();
if (log.isTraceEnabled()) {
log.trace("checkOneNameResponse({})[id={}] {} status: {} [{}] {}",
getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
SftpConstants.getStatusName(substatus), lang, msg);
}
throwStatusException(cmd, id, substatus, msg, lang);
}
return handleUnknownOneNamePacket(cmd, id, type, length, buffer);
}
protected String handleUnknownOneNamePacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer);
if (err != null) {
throw err;
}
return null;
}
protected Attributes readAttributes(Buffer buffer) throws IOException {
Attributes attrs = new Attributes();
int flags = buffer.getInt();
int version = getVersion();
if (version == SftpConstants.SFTP_V3) {
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
attrs.setSize(buffer.getLong());
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
attrs.owner(buffer.getInt(), buffer.getInt());
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
int perms = buffer.getInt();
attrs.setPermissions(perms);
attrs.setType(SftpHelper.permissionsToFileType(perms));
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags));
attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags));
}
} else if (version >= SftpConstants.SFTP_V4) {
attrs.setType(buffer.getUByte());
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
attrs.setSize(buffer.getLong());
}
if ((version >= SftpConstants.SFTP_V6) && ((flags & SftpConstants.SSH_FILEXFER_ATTR_ALLOCATION_SIZE) != 0)) {
@SuppressWarnings("unused")
long allocSize = buffer.getLong(); // TODO handle allocation size
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
attrs.setOwner(buffer.getString());
attrs.setGroup(buffer.getString());
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
attrs.setPermissions(buffer.getInt());
}
// update the permissions according to the type
int perms = attrs.getPermissions();
perms |= SftpHelper.fileTypeToPermission(attrs.getType());
attrs.setPermissions(perms);
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
attrs.setAccessTime(SftpHelper.readTime(buffer, version, flags));
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
attrs.setCreateTime(SftpHelper.readTime(buffer, version, flags));
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
attrs.setModifyTime(SftpHelper.readTime(buffer, version, flags));
}
if ((version >= SftpConstants.SFTP_V6) && (flags & SftpConstants.SSH_FILEXFER_ATTR_CTIME) != 0) {
@SuppressWarnings("unused")
FileTime attrsChangedTime = SftpHelper.readTime(buffer, version, flags); // TODO the last time the file attributes were changed
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
attrs.setAcl(SftpHelper.readACLs(buffer, version));
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_BITS) != 0) {
@SuppressWarnings("unused")
int bits = buffer.getInt();
@SuppressWarnings("unused")
int valid = 0xffffffff;
if (version >= SftpConstants.SFTP_V6) {
valid = buffer.getInt();
}
// TODO: handle attrib bits
}
if (version >= SftpConstants.SFTP_V6) {
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_TEXT_HINT) != 0) {
@SuppressWarnings("unused")
boolean text = buffer.getBoolean(); // TODO: handle text
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_MIME_TYPE) != 0) {
@SuppressWarnings("unused")
String mimeType = buffer.getString(); // TODO: handle mime-type
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_LINK_COUNT) != 0) {
@SuppressWarnings("unused")
int nlink = buffer.getInt(); // TODO: handle link-count
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_UNTRANSLATED_NAME) != 0) {
@SuppressWarnings("unused")
String untranslated = buffer.getString(); // TODO: handle untranslated-name
}
}
} else {
throw new IllegalStateException("readAttributes - unsupported version: " + version);
}
if ((flags & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
attrs.setExtensions(SftpHelper.readExtensions(buffer));
}
return attrs;
}
protected void writeAttributes(Buffer buffer, Attributes attributes) throws IOException {
int version = getVersion();
int flagsMask = 0;
Collection<Attribute> flags = ValidateUtils.checkNotNull(attributes, "No attributes").getFlags();
if (version == SftpConstants.SFTP_V3) {
for (Attribute a : flags) {
switch (a) {
case Size:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
break;
case UidGid:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_UIDGID;
break;
case Perms:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
break;
case AccessTime:
if (flags.contains(Attribute.ModifyTime)) {
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
}
break;
case ModifyTime:
if (flags.contains(Attribute.AccessTime)) {
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME;
}
break;
case Extensions:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
break;
default: // do nothing
}
}
buffer.putInt(flagsMask);
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
buffer.putLong(attributes.getSize());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_UIDGID) != 0) {
buffer.putInt(attributes.getUserId());
buffer.putInt(attributes.getGroupId());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
buffer.putInt(attributes.getPermissions());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACMODTIME) != 0) {
SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime());
SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime());
}
} else if (version >= SftpConstants.SFTP_V4) {
for (Attribute a : flags) {
switch (a) {
case Size:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_SIZE;
break;
case OwnerGroup:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP;
break;
case Perms:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS;
break;
case AccessTime:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME;
break;
case ModifyTime:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME;
break;
case CreateTime:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_CREATETIME;
break;
case Acl:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_ACL;
break;
case Extensions:
flagsMask |= SftpConstants.SSH_FILEXFER_ATTR_EXTENDED;
break;
default: // do nothing
}
}
buffer.putInt(flagsMask);
buffer.putByte((byte) attributes.getType());
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
buffer.putLong(attributes.getSize());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_OWNERGROUP) != 0) {
String owner = attributes.getOwner();
buffer.putString(GenericUtils.isEmpty(owner) ? SftpUniversalOwnerAndGroup.Owner.getName() : owner);
String group = attributes.getGroup();
buffer.putString(GenericUtils.isEmpty(group) ? SftpUniversalOwnerAndGroup.Group.getName() : group);
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS) != 0) {
buffer.putInt(attributes.getPermissions());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME) != 0) {
SftpHelper.writeTime(buffer, version, flagsMask, attributes.getAccessTime());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_CREATETIME) != 0) {
SftpHelper.writeTime(buffer, version, flagsMask, attributes.getCreateTime());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME) != 0) {
SftpHelper.writeTime(buffer, version, flagsMask, attributes.getModifyTime());
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_ACL) != 0) {
SftpHelper.writeACLs(buffer, version, attributes.getAcl());
}
// TODO: for v6+ add CTIME (see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#page-21)
} else {
throw new UnsupportedOperationException("writeAttributes(" + attributes + ") unsupported version: " + version);
}
if ((flagsMask & SftpConstants.SSH_FILEXFER_ATTR_EXTENDED) != 0) {
SftpHelper.writeExtensions(buffer, attributes.getExtensions());
}
}
@Override
public CloseableHandle open(String path, Collection<OpenMode> options) throws IOException {
if (!isOpen()) {
throw new IOException("open(" + path + ")[" + options + "] client is closed");
}
/*
* Be consistent with FileChannel#open - if no mode specified then READ is assumed
*/
if (GenericUtils.isEmpty(options)) {
options = EnumSet.of(OpenMode.Read);
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
buffer.putString(path);
int version = getVersion();
int mode = 0;
if (version == SftpConstants.SFTP_V3) {
for (OpenMode m : options) {
switch (m) {
case Read:
mode |= SftpConstants.SSH_FXF_READ;
break;
case Write:
mode |= SftpConstants.SSH_FXF_WRITE;
break;
case Append:
mode |= SftpConstants.SSH_FXF_APPEND;
break;
case Create:
mode |= SftpConstants.SSH_FXF_CREAT;
break;
case Truncate:
mode |= SftpConstants.SSH_FXF_TRUNC;
break;
case Exclusive:
mode |= SftpConstants.SSH_FXF_EXCL;
break;
default: // do nothing
}
}
} else {
if (version >= SftpConstants.SFTP_V5) {
int access = 0;
if (options.contains(OpenMode.Read)) {
access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES;
}
if (options.contains(OpenMode.Write)) {
access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES;
}
if (options.contains(OpenMode.Append)) {
access |= SftpConstants.ACE4_APPEND_DATA;
}
buffer.putInt(access);
}
if (options.contains(OpenMode.Create) && options.contains(OpenMode.Exclusive)) {
mode |= SftpConstants.SSH_FXF_CREATE_NEW;
} else if (options.contains(OpenMode.Create) && options.contains(OpenMode.Truncate)) {
mode |= SftpConstants.SSH_FXF_CREATE_TRUNCATE;
} else if (options.contains(OpenMode.Create)) {
mode |= SftpConstants.SSH_FXF_OPEN_OR_CREATE;
} else if (options.contains(OpenMode.Truncate)) {
mode |= SftpConstants.SSH_FXF_TRUNCATE_EXISTING;
} else {
mode |= SftpConstants.SSH_FXF_OPEN_EXISTING;
}
}
buffer.putInt(mode);
writeAttributes(buffer, fileOpenAttributes);
CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPEN, buffer));
if (log.isTraceEnabled()) {
log.trace("open({})[{}] options={}: {}", getClientSession(), path, options, handle);
}
return handle;
}
@Override
public void close(Handle handle) throws IOException {
if (!isOpen()) {
throw new IOException("close(" + handle + ") client is closed");
}
if (log.isTraceEnabled()) {
log.trace("close({}) {}", getClientSession(), handle);
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false);
buffer.putBytes(id);
checkCommandStatus(SftpConstants.SSH_FXP_CLOSE, buffer);
}
@Override
public void remove(String path) throws IOException {
if (!isOpen()) {
throw new IOException("remove(" + path + ") client is closed");
}
if (log.isDebugEnabled()) {
log.debug("remove({}) {}", getClientSession(), path);
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
buffer.putString(path);
checkCommandStatus(SftpConstants.SSH_FXP_REMOVE, buffer);
}
@Override
public void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException {
if (!isOpen()) {
throw new IOException("rename(" + oldPath + " => " + newPath + ")[" + options + "] client is closed");
}
if (log.isDebugEnabled()) {
log.debug("rename({}) {} => {}", getClientSession(), oldPath, newPath);
}
Buffer buffer = new ByteArrayBuffer(oldPath.length() + newPath.length() + Long.SIZE /* some extra fields */, false);
buffer.putString(oldPath);
buffer.putString(newPath);
int numOptions = GenericUtils.size(options);
int version = getVersion();
if (version >= SftpConstants.SFTP_V5) {
int opts = 0;
if (numOptions > 0) {
for (CopyMode opt : options) {
switch (opt) {
case Atomic:
opts |= SftpConstants.SSH_FXP_RENAME_ATOMIC;
break;
case Overwrite:
opts |= SftpConstants.SSH_FXP_RENAME_OVERWRITE;
break;
default: // do nothing
}
}
}
buffer.putInt(opts);
} else if (numOptions > 0) {
throw new UnsupportedOperationException("rename(" + oldPath + " => " + newPath + ")"
+ " - copy options can not be used with this SFTP version: " + options);
}
checkCommandStatus(SftpConstants.SSH_FXP_RENAME, buffer);
}
@Override // TODO make this a default method in Java 8
public int read(Handle handle, long fileOffset, byte[] dst) throws IOException {
return read(handle, fileOffset, dst, null);
}
@Override // TODO make this a default method in Java 8
public int read(Handle handle, long fileOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
return read(handle, fileOffset, dst, 0, dst.length, eofSignalled);
}
@Override // TODO make this a default method in Java 8
public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len) throws IOException {
return read(handle, fileOffset, dst, dstOffset, len, null);
}
@Override
public int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException {
if (eofSignalled != null) {
eofSignalled.set(null);
}
if (!isOpen()) {
throw new IOException("read(" + handle + "/" + fileOffset + ")[" + dstOffset + "/" + len + "] client is closed");
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* some extra fields */, false);
buffer.putBytes(id);
buffer.putLong(fileOffset);
buffer.putInt(len);
return checkData(SftpConstants.SSH_FXP_READ, buffer, dstOffset, dst, eofSignalled);
}
protected int checkData(int cmd, Buffer request, int dstOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
if (eofSignalled != null) {
eofSignalled.set(null);
}
int reqId = send(cmd, request);
Buffer response = receive(reqId);
return checkDataResponse(cmd, response, dstOffset, dst, eofSignalled);
}
protected int checkDataResponse(int cmd, Buffer buffer, int dstoff, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException {
if (eofSignalled != null) {
eofSignalled.set(null);
}
int length = buffer.getInt();
int type = buffer.getUByte();
int id = buffer.getInt();
if (type == SftpConstants.SSH_FXP_DATA) {
int len = buffer.getInt();
buffer.getRawBytes(dst, dstoff, len);
Boolean indicator = SftpHelper.getEndOfFileIndicatorValue(buffer, getVersion());
if (log.isTraceEnabled()) {
log.trace("checkDataResponse({}][id={}] {} offset={}, len={}, EOF={}",
getClientChannel(), SftpConstants.getCommandMessageName(cmd),
id, dstoff, len, indicator);
}
if (eofSignalled != null) {
eofSignalled.set(indicator);
}
return len;
}
if (type == SftpConstants.SSH_FXP_STATUS) {
int substatus = buffer.getInt();
String msg = buffer.getString();
String lang = buffer.getString();
if (log.isTraceEnabled()) {
log.trace("checkDataResponse({})[id={}] {} status: {} [{}] {}",
getClientChannel(), id, SftpConstants.getCommandMessageName(cmd),
SftpConstants.getStatusName(substatus), lang, msg);
}
if (substatus == SftpConstants.SSH_FX_EOF) {
return -1;
}
throwStatusException(cmd, id, substatus, msg, lang);
}
return handleUnknownDataPacket(cmd, id, type, length, buffer);
}
protected int handleUnknownDataPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_DATA, id, type, length, buffer);
if (err != null) {
throw err;
}
return 0;
}
@Override
public void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException {
// do some bounds checking first
if ((fileOffset < 0) || (srcOffset < 0) || (len < 0)) {
throw new IllegalArgumentException("write(" + handle + ") please ensure all parameters "
+ " are non-negative values: file-offset=" + fileOffset
+ ", src-offset=" + srcOffset + ", len=" + len);
}
if ((srcOffset + len) > src.length) {
throw new IllegalArgumentException("write(" + handle + ")"
+ " cannot read bytes " + srcOffset + " to " + (srcOffset + len)
+ " when array is only of length " + src.length);
}
if (!isOpen()) {
throw new IOException("write(" + handle + "/" + fileOffset + ")[" + srcOffset + "/" + len + "] client is closed");
}
if (log.isTraceEnabled()) {
log.trace("write({}) handle={}, file-offset={}, buf-offset={}, len={}",
getClientChannel(), handle, fileOffset, srcOffset, len);
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + len + Long.SIZE /* some extra fields */, false);
buffer.putBytes(id);
buffer.putLong(fileOffset);
buffer.putBytes(src, srcOffset, len);
checkCommandStatus(SftpConstants.SSH_FXP_WRITE, buffer);
}
@Override
public void mkdir(String path) throws IOException {
if (!isOpen()) {
throw new IOException("mkdir(" + path + ") client is closed");
}
if (log.isDebugEnabled()) {
log.debug("mkdir({}) {}", getClientSession(), path);
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
buffer.putString(path);
buffer.putInt(0);
int version = getVersion();
if (version != SftpConstants.SFTP_V3) {
buffer.putByte((byte) 0);
}
checkCommandStatus(SftpConstants.SSH_FXP_MKDIR, buffer);
}
@Override
public void rmdir(String path) throws IOException {
if (!isOpen()) {
throw new IOException("rmdir(" + path + ") client is closed");
}
if (log.isDebugEnabled()) {
log.debug("rmdir({}) {}", getClientSession(), path);
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
buffer.putString(path);
checkCommandStatus(SftpConstants.SSH_FXP_RMDIR, buffer);
}
@Override
public CloseableHandle openDir(String path) throws IOException {
if (!isOpen()) {
throw new IOException("openDir(" + path + ") client is closed");
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
buffer.putString(path);
CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPENDIR, buffer));
if (log.isTraceEnabled()) {
log.trace("openDir({})[{}}: {}", getClientSession(), path, handle);
}
return handle;
}
@Override // TODO in JDK-8 make this a default method
public List<DirEntry> readDir(Handle handle) throws IOException {
return readDir(handle, null);
}
@Override
public List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException {
if (eolIndicator != null) {
eolIndicator.set(null); // assume unknown information
}
if (!isOpen()) {
throw new IOException("readDir(" + handle + ") client is closed");
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* some extra fields */, false);
buffer.putBytes(id);
int cmdId = send(SftpConstants.SSH_FXP_READDIR, buffer);
Buffer response = receive(cmdId);
return checkDirResponse(SftpConstants.SSH_FXP_READDIR, response, eolIndicator);
}
protected List<DirEntry> checkDirResponse(int cmd, Buffer buffer, AtomicReference<Boolean> eolIndicator) throws IOException {
if (eolIndicator != null) {
eolIndicator.set(null); // assume unknown
}
int length = buffer.getInt();
int type = buffer.getUByte();
int id = buffer.getInt();
if (type == SftpConstants.SSH_FXP_NAME) {
int len = buffer.getInt();
int version = getVersion();
ClientChannel channel = getClientChannel();
if (log.isDebugEnabled()) {
log.debug("checkDirResponse({}}[id={}] reading {} entries", channel, id, len);
}
List<DirEntry> entries = new ArrayList<DirEntry>(len);
for (int i = 0; i < len; i++) {
String name = buffer.getString();
String longName = (version == SftpConstants.SFTP_V3) ? buffer.getString() : null;
Attributes attrs = readAttributes(buffer);
if (log.isTraceEnabled()) {
log.trace("checkDirResponse({})[id={}][{}] ({})[{}]: {}",
channel, id, i, name, longName, attrs);
}
entries.add(new DirEntry(name, longName, attrs));
}
Boolean indicator = SftpHelper.getEndOfListIndicatorValue(buffer, version);
if (eolIndicator != null) {
eolIndicator.set(indicator);
}
if (log.isDebugEnabled()) {
log.debug("checkDirResponse({}}[id={}] read count={}, eol={}", channel, entries.size(), indicator);
}
return entries;
}
if (type == SftpConstants.SSH_FXP_STATUS) {
int substatus = buffer.getInt();
String msg = buffer.getString();
String lang = buffer.getString();
if (log.isTraceEnabled()) {
log.trace("checkDirResponse({})[id={}] - status: {} [{}] {}",
getClientChannel(), id, SftpConstants.getStatusName(substatus), lang, msg);
}
if (substatus == SftpConstants.SSH_FX_EOF) {
return null;
}
throwStatusException(cmd, id, substatus, msg, lang);
}
return handleUnknownDirListingPacket(cmd, id, type, length, buffer);
}
protected List<DirEntry> handleUnknownDirListingPacket(int cmd, int id, int type, int length, Buffer buffer) throws IOException {
IOException err = handleUnexpectedPacket(cmd, SftpConstants.SSH_FXP_NAME, id, type, length, buffer);
if (err != null) {
throw err;
}
return Collections.emptyList();
}
protected IOException handleUnexpectedPacket(int cmd, int expected, int id, int type, int length, Buffer buffer) throws IOException {
throw new SshException("Unexpected SFTP packet received while awaiting " + SftpConstants.getCommandMessageName(expected)
+ " response to " + SftpConstants.getCommandMessageName(cmd)
+ ": type=" + SftpConstants.getCommandMessageName(type) + ", id=" + id + ", length=" + length);
}
@Override
public String canonicalPath(String path) throws IOException {
if (!isOpen()) {
throw new IOException("canonicalPath(" + path + ") client is closed");
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
buffer.putString(path);
return checkOneName(SftpConstants.SSH_FXP_REALPATH, buffer);
}
@Override
public Attributes stat(String path) throws IOException {
if (!isOpen()) {
throw new IOException("stat(" + path + ") client is closed");
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
buffer.putString(path);
int version = getVersion();
if (version >= SftpConstants.SFTP_V4) {
buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
}
return checkAttributes(SftpConstants.SSH_FXP_STAT, buffer);
}
@Override
public Attributes lstat(String path) throws IOException {
if (!isOpen()) {
throw new IOException("lstat(" + path + ") client is closed");
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE, false);
buffer.putString(path);
int version = getVersion();
if (version >= SftpConstants.SFTP_V4) {
buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
}
return checkAttributes(SftpConstants.SSH_FXP_LSTAT, buffer);
}
@Override
public Attributes stat(Handle handle) throws IOException {
if (!isOpen()) {
throw new IOException("stat(" + handle + ") client is closed");
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + Byte.SIZE /* a bit extra */, false);
buffer.putBytes(id);
int version = getVersion();
if (version >= SftpConstants.SFTP_V4) {
buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_ALL);
}
return checkAttributes(SftpConstants.SSH_FXP_FSTAT, buffer);
}
@Override
public void setStat(String path, Attributes attributes) throws IOException {
if (!isOpen()) {
throw new IOException("setStat(" + path + ")[" + attributes + "] client is closed");
}
if (log.isDebugEnabled()) {
log.debug("setStat({})[{}]: {}", getClientSession(), path, attributes);
}
Buffer buffer = new ByteArrayBuffer();
buffer.putString(path);
writeAttributes(buffer, attributes);
checkCommandStatus(SftpConstants.SSH_FXP_SETSTAT, buffer);
}
@Override
public void setStat(Handle handle, Attributes attributes) throws IOException {
if (!isOpen()) {
throw new IOException("setStat(" + handle + ")[" + attributes + "] client is closed");
}
if (log.isDebugEnabled()) {
log.debug("setStat({})[{}]: {}", getClientSession(), handle, attributes);
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + (2 * Long.SIZE) /* some extras */, false);
buffer.putBytes(id);
writeAttributes(buffer, attributes);
checkCommandStatus(SftpConstants.SSH_FXP_FSETSTAT, buffer);
}
@Override
public String readLink(String path) throws IOException {
if (!isOpen()) {
throw new IOException("readLink(" + path + ") client is closed");
}
Buffer buffer = new ByteArrayBuffer(path.length() + Long.SIZE /* some extra fields */, false);
buffer.putString(path);
return checkOneName(SftpConstants.SSH_FXP_READLINK, buffer);
}
@Override
public void link(String linkPath, String targetPath, boolean symbolic) throws IOException {
if (!isOpen()) {
throw new IOException("link(" + linkPath + " => " + targetPath + ")[symbolic=" + symbolic + "] client is closed");
}
if (log.isDebugEnabled()) {
log.debug("link({})[symbolic={}] {} => {}", getClientSession(), symbolic, linkPath, targetPath);
}
Buffer buffer = new ByteArrayBuffer(linkPath.length() + targetPath.length() + Long.SIZE /* some extra fields */, false);
int version = getVersion();
if (version < SftpConstants.SFTP_V6) {
if (!symbolic) {
throw new UnsupportedOperationException("Hard links are not supported in sftp v" + version);
}
buffer.putString(targetPath);
buffer.putString(linkPath);
checkCommandStatus(SftpConstants.SSH_FXP_SYMLINK, buffer);
} else {
buffer.putString(targetPath);
buffer.putString(linkPath);
buffer.putBoolean(symbolic);
checkCommandStatus(SftpConstants.SSH_FXP_LINK, buffer);
}
}
@Override
public void lock(Handle handle, long offset, long length, int mask) throws IOException {
if (!isOpen()) {
throw new IOException("lock(" + handle + ")[offset=" + offset + ", length=" + length + ", mask=0x" + Integer.toHexString(mask) + "] client is closed");
}
if (log.isDebugEnabled()) {
log.debug("lock({})[{}] offset={}, length={}, mask=0x{}",
getClientSession(), handle, offset, length, Integer.toHexString(mask));
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false);
buffer.putBytes(id);
buffer.putLong(offset);
buffer.putLong(length);
buffer.putInt(mask);
checkCommandStatus(SftpConstants.SSH_FXP_BLOCK, buffer);
}
@Override
public void unlock(Handle handle, long offset, long length) throws IOException {
if (!isOpen()) {
throw new IOException("unlock" + handle + ")[offset=" + offset + ", length=" + length + "] client is closed");
}
if (log.isDebugEnabled()) {
log.debug("unlock({})[{}] offset={}, length={}", getClientSession(), handle, offset, length);
}
byte[] id = ValidateUtils.checkNotNull(handle, "No handle").getIdentifier();
Buffer buffer = new ByteArrayBuffer(id.length + Long.SIZE /* a bit extra */, false);
buffer.putBytes(id);
buffer.putLong(offset);
buffer.putLong(length);
checkCommandStatus(SftpConstants.SSH_FXP_UNBLOCK, buffer);
}
@Override
public Iterable<DirEntry> readDir(final String path) throws IOException {
if (!isOpen()) {
throw new IOException("readDir(" + path + ") client is closed");
}
return new SftpIterableDirEntry(this, path);
}
@Override
public InputStream read(final String path, final int bufferSize, final Collection<OpenMode> mode) throws IOException {
if (bufferSize < MIN_READ_BUFFER_SIZE) {
throw new IllegalArgumentException("Insufficient read buffer size: " + bufferSize + ", min.=" + MIN_READ_BUFFER_SIZE);
}
if (!isOpen()) {
throw new IOException("read(" + path + ")[" + mode + "] size=" + bufferSize + ": client is closed");
}
return new SftpInputStreamWithChannel(this, bufferSize, path, mode);
}
@Override
public OutputStream write(final String path, final int bufferSize, final Collection<OpenMode> mode) throws IOException {
if (bufferSize < MIN_WRITE_BUFFER_SIZE) {
throw new IllegalArgumentException("Insufficient write buffer size: " + bufferSize + ", min.=" + MIN_WRITE_BUFFER_SIZE);
}
if (!isOpen()) {
throw new IOException("write(" + path + ")[" + mode + "] size=" + bufferSize + ": client is closed");
}
return new SftpOutputStreamWithChannel(this, bufferSize, path, mode);
}
}