/* * 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.io.InputStream; import java.io.OutputStream; import java.nio.channels.Channel; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.AclEntry; import java.nio.file.attribute.FileTime; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.client.subsystem.SubsystemClient; import org.apache.sshd.client.subsystem.sftp.extensions.SftpClientExtension; import org.apache.sshd.common.subsystem.sftp.SftpConstants; import org.apache.sshd.common.subsystem.sftp.SftpHelper; import org.apache.sshd.common.subsystem.sftp.SftpUniversalOwnerAndGroup; import org.apache.sshd.common.util.GenericUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.buffer.BufferUtils; /** * @author <a href="http://mina.apache.org">Apache MINA Project</a> */ public interface SftpClient extends SubsystemClient { enum OpenMode { Read, Write, Append, Create, Truncate, Exclusive; /** * The {@link Set} of {@link OpenOption}-s supported by {@link #fromOpenOptions(Collection)} */ public static final Set<OpenOption> SUPPORTED_OPTIONS = Collections.unmodifiableSet( EnumSet.of( StandardOpenOption.READ, StandardOpenOption.APPEND, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, StandardOpenOption.SPARSE)); /** * Converts {@link StandardOpenOption}-s into {@link OpenMode}-s * * @param options The original options - ignored if {@code null}/empty * @return A {@link Set} of the equivalent modes * @throws IllegalArgumentException If an unsupported option is requested * @see #SUPPORTED_OPTIONS */ public static Set<OpenMode> fromOpenOptions(Collection<? extends OpenOption> options) { if (GenericUtils.isEmpty(options)) { return Collections.emptySet(); } Set<OpenMode> modes = EnumSet.noneOf(OpenMode.class); for (OpenOption option : options) { if (option == StandardOpenOption.READ) { modes.add(Read); } else if (option == StandardOpenOption.APPEND) { modes.add(Append); } else if (option == StandardOpenOption.CREATE) { modes.add(Create); } else if (option == StandardOpenOption.TRUNCATE_EXISTING) { modes.add(Truncate); } else if (option == StandardOpenOption.WRITE) { modes.add(Write); } else if (option == StandardOpenOption.CREATE_NEW) { modes.add(Create); modes.add(Exclusive); } else if (option == StandardOpenOption.SPARSE) { /* * As per the Javadoc: * * The option is ignored when the file system does not * support the creation of sparse files */ continue; } else { throw new IllegalArgumentException("Unsupported open option: " + option); } } return modes; } } enum CopyMode { Atomic, Overwrite } enum Attribute { Size, UidGid, Perms, OwnerGroup, AccessTime, ModifyTime, CreateTime, Acl, Extensions } class Handle { private final String path; private final byte[] id; Handle(String path, byte[] id) { // clone the original so the handle is immutable this.path = ValidateUtils.checkNotNullAndNotEmpty(path, "No remote path"); this.id = ValidateUtils.checkNotNullAndNotEmpty(id, "No handle ID").clone(); } /** * @return The remote path represented by this handle */ public String getPath() { return path; } public int length() { return id.length; } /** * @return A <U>cloned</U> instance of the identifier in order to * avoid inadvertent modifications to the handle contents */ public byte[] getIdentifier() { return id.clone(); } @Override public int hashCode() { return Arrays.hashCode(id); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } // we do not ask getClass() == obj.getClass() in order to allow for derived classes equality if (!(obj instanceof Handle)) { return false; } return Arrays.equals(id, ((Handle) obj).id); } @Override public String toString() { return getPath() + ": " + BufferUtils.toHex(BufferUtils.EMPTY_HEX_SEPARATOR, id); } } // CHECKSTYLE:OFF abstract class CloseableHandle extends Handle implements Channel, Closeable { protected CloseableHandle(String path, byte[] id) { super(path, id); } } // CHECKSTYLE:ON class Attributes { private Set<Attribute> flags = EnumSet.noneOf(Attribute.class); private int type = SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN; private int perms; private int uid; private int gid; private String owner; private String group; private long size; private FileTime accessTime; private FileTime createTime; private FileTime modifyTime; private List<AclEntry> acl; private Map<String, byte[]> extensions = Collections.emptyMap(); public Attributes() { super(); } public Set<Attribute> getFlags() { return flags; } public Attributes addFlag(Attribute flag) { flags.add(flag); return this; } public int getType() { return type; } public void setType(int type) { this.type = type; } public long getSize() { return size; } public Attributes size(long size) { setSize(size); return this; } public void setSize(long size) { this.size = size; addFlag(Attribute.Size); } public String getOwner() { return owner; } public Attributes owner(String owner) { setOwner(owner); return this; } public void setOwner(String owner) { this.owner = ValidateUtils.checkNotNullAndNotEmpty(owner, "No owner"); addFlag(Attribute.OwnerGroup); if (GenericUtils.isEmpty(getGroup())) { setGroup(SftpUniversalOwnerAndGroup.Group.getName()); } } public String getGroup() { return group; } public Attributes group(String group) { setGroup(group); return this; } public void setGroup(String group) { this.group = ValidateUtils.checkNotNullAndNotEmpty(group, "No group"); addFlag(Attribute.OwnerGroup); if (GenericUtils.isEmpty(getOwner())) { setOwner(SftpUniversalOwnerAndGroup.Owner.getName()); } } public int getUserId() { return uid; } public int getGroupId() { return gid; } public Attributes owner(int uid, int gid) { this.uid = uid; this.gid = gid; addFlag(Attribute.UidGid); return this; } public int getPermissions() { return perms; } public Attributes perms(int perms) { setPermissions(perms); return this; } public void setPermissions(int perms) { this.perms = perms; addFlag(Attribute.Perms); } public FileTime getAccessTime() { return accessTime; } public Attributes accessTime(long atime) { return accessTime(atime, TimeUnit.SECONDS); } public Attributes accessTime(long atime, TimeUnit unit) { return accessTime(FileTime.from(atime, unit)); } public Attributes accessTime(FileTime atime) { setAccessTime(atime); return this; } public void setAccessTime(FileTime atime) { accessTime = Objects.requireNonNull(atime, "No access time"); addFlag(Attribute.AccessTime); } public FileTime getCreateTime() { return createTime; } public Attributes createTime(long ctime) { return createTime(ctime, TimeUnit.SECONDS); } public Attributes createTime(long ctime, TimeUnit unit) { return createTime(FileTime.from(ctime, unit)); } public Attributes createTime(FileTime ctime) { setCreateTime(ctime); return this; } public void setCreateTime(FileTime ctime) { createTime = Objects.requireNonNull(ctime, "No create time"); addFlag(Attribute.CreateTime); } public FileTime getModifyTime() { return modifyTime; } public Attributes modifyTime(long mtime) { return modifyTime(mtime, TimeUnit.SECONDS); } public Attributes modifyTime(long mtime, TimeUnit unit) { return modifyTime(FileTime.from(mtime, unit)); } public Attributes modifyTime(FileTime mtime) { setModifyTime(mtime); return this; } public void setModifyTime(FileTime mtime) { modifyTime = Objects.requireNonNull(mtime, "No modify time"); addFlag(Attribute.ModifyTime); } public List<AclEntry> getAcl() { return acl; } public Attributes acl(List<AclEntry> acl) { setAcl(acl); return this; } public void setAcl(List<AclEntry> acl) { this.acl = Objects.requireNonNull(acl, "No ACLs"); addFlag(Attribute.Acl); } public Map<String, byte[]> getExtensions() { return extensions; } public Attributes extensions(Map<String, byte[]> extensions) { setExtensions(extensions); return this; } public void setStringExtensions(Map<String, String> extensions) { setExtensions(SftpHelper.toBinaryExtensions(extensions)); } public void setExtensions(Map<String, byte[]> extensions) { this.extensions = Objects.requireNonNull(extensions, "No extensions"); addFlag(Attribute.Extensions); } public boolean isRegularFile() { return (getPermissions() & SftpConstants.S_IFMT) == SftpConstants.S_IFREG; } public boolean isDirectory() { return (getPermissions() & SftpConstants.S_IFMT) == SftpConstants.S_IFDIR; } public boolean isSymbolicLink() { return (getPermissions() & SftpConstants.S_IFMT) == SftpConstants.S_IFLNK; } public boolean isOther() { return !isRegularFile() && !isDirectory() && !isSymbolicLink(); } @Override public String toString() { return "type=" + getType() + ";size=" + getSize() + ";uid=" + getUserId() + ";gid=" + getGroupId() + ";perms=0x" + Integer.toHexString(getPermissions()) + ";flags=" + getFlags() + ";owner=" + getOwner() + ";group=" + getGroup() + ";aTime=" + getAccessTime() + ";cTime=" + getCreateTime() + ";mTime=" + getModifyTime() + ";extensions=" + getExtensions().keySet(); } } class DirEntry { private final String filename; private final String longFilename; private final Attributes attributes; DirEntry(String filename, String longFilename, Attributes attributes) { this.filename = filename; this.longFilename = longFilename; this.attributes = attributes; } public String getFilename() { return filename; } public String getLongFilename() { return longFilename; } public Attributes getAttributes() { return attributes; } @Override public String toString() { return getFilename() + "[" + getLongFilename() + "]: " + getAttributes(); } } // default values used if none specified int MIN_BUFFER_SIZE = Byte.MAX_VALUE; int MIN_READ_BUFFER_SIZE = MIN_BUFFER_SIZE; int MIN_WRITE_BUFFER_SIZE = MIN_BUFFER_SIZE; int IO_BUFFER_SIZE = 32 * 1024; int DEFAULT_READ_BUFFER_SIZE = IO_BUFFER_SIZE; int DEFAULT_WRITE_BUFFER_SIZE = IO_BUFFER_SIZE; long DEFAULT_WAIT_TIMEOUT = TimeUnit.SECONDS.toMillis(15L); /** * Property that can be used on the {@link org.apache.sshd.common.FactoryManager} * to control the internal timeout used by the client to open a channel. * If not specified then {@link #DEFAULT_CHANNEL_OPEN_TIMEOUT} value * is used */ String SFTP_CHANNEL_OPEN_TIMEOUT = "sftp-channel-open-timeout"; long DEFAULT_CHANNEL_OPEN_TIMEOUT = DEFAULT_WAIT_TIMEOUT; /** * Default modes for opening a channel if no specific modes specified */ Set<OpenMode> DEFAULT_CHANNEL_MODES = Collections.unmodifiableSet(EnumSet.of(OpenMode.Read, OpenMode.Write)); /** * @return The negotiated SFTP protocol version */ int getVersion(); @Override default String getName() { return SftpConstants.SFTP_SUBSYSTEM_NAME; } /** * @return An (unmodifiable) {@link Map} of the reported server extensions. */ Map<String, byte[]> getServerExtensions(); boolean isClosing(); // // Low level API // /** * Opens a remote file for read * * @param path The remote path * @return The file's {@link CloseableHandle} * @throws IOException If failed to open the remote file * @see #open(String, Collection) */ default CloseableHandle open(String path) throws IOException { return open(path, Collections.emptySet()); } /** * Opens a remote file with the specified mode(s) * * @param path The remote path * @param options The desired mode - if none specified * then {@link OpenMode#Read} is assumed * @return The file's {@link CloseableHandle} * @throws IOException If failed to open the remote file * @see #open(String, Collection) */ default CloseableHandle open(String path, OpenMode... options) throws IOException { return open(path, GenericUtils.of(options)); } /** * Opens a remote file with the specified mode(s) * * @param path The remote path * @param options The desired mode - if none specified * then {@link OpenMode#Read} is assumed * @return The file's {@link CloseableHandle} * @throws IOException If failed to open the remote file */ CloseableHandle open(String path, Collection<OpenMode> options) throws IOException; /** * Close the handle obtained from one of the {@code open} methods * * @param handle The {@code Handle} to close * @throws IOException If failed to execute */ void close(Handle handle) throws IOException; /** * @param path The remote path to remove * @throws IOException If failed to execute */ void remove(String path) throws IOException; default void rename(String oldPath, String newPath) throws IOException { rename(oldPath, newPath, Collections.emptySet()); } default void rename(String oldPath, String newPath, CopyMode... options) throws IOException { rename(oldPath, newPath, GenericUtils.of(options)); } void rename(String oldPath, String newPath, Collection<CopyMode> options) throws IOException; /** * Reads data from the open (file) handle * * @param handle The file {@link Handle} to read from * @param fileOffset The file offset to read from * @param dst The destination buffer * @return Number of read bytes - {@code -1} if EOF reached * @throws IOException If failed to read the data * @see #read(Handle, long, byte[], int, int) */ default int read(Handle handle, long fileOffset, byte[] dst) throws IOException { return read(handle, fileOffset, dst, null); } /** * Reads data from the open (file) handle * * @param handle The file {@link Handle} to read from * @param fileOffset The file offset to read from * @param dst The destination buffer * @param eofSignalled If not {@code null} then upon return holds a value indicating * whether EOF was reached due to the read. If {@code null} indicator * value then this indication is not available * @return Number of read bytes - {@code -1} if EOF reached * @throws IOException If failed to read the data * @see #read(Handle, long, byte[], int, int, AtomicReference) * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A> */ default int read(Handle handle, long fileOffset, byte[] dst, AtomicReference<Boolean> eofSignalled) throws IOException { return read(handle, fileOffset, dst, 0, dst.length, eofSignalled); } default int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len) throws IOException { return read(handle, fileOffset, dst, dstOffset, len, null); } /** * Reads data from the open (file) handle * * @param handle The file {@link Handle} to read from * @param fileOffset The file offset to read from * @param dst The destination buffer * @param dstOffset Offset in destination buffer to place the read data * @param len Available destination buffer size to read * @param eofSignalled If not {@code null} then upon return holds a value indicating * whether EOF was reached due to the read. If {@code null} indicator * value then this indication is not available * @return Number of read bytes - {@code -1} if EOF reached * @throws IOException If failed to read the data * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.3">SFTP v6 - section 9.3</A> */ int read(Handle handle, long fileOffset, byte[] dst, int dstOffset, int len, AtomicReference<Boolean> eofSignalled) throws IOException; default void write(Handle handle, long fileOffset, byte[] src) throws IOException { write(handle, fileOffset, src, 0, src.length); } /** * Write data to (open) file handle * * @param handle The file {@link Handle} * @param fileOffset Zero-based offset to write in file * @param src Data buffer * @param srcOffset Offset of valid data in buffer * @param len Number of bytes to write * @throws IOException If failed to write the data */ void write(Handle handle, long fileOffset, byte[] src, int srcOffset, int len) throws IOException; /** * Create remote directory * * @param path Remote directory path * @throws IOException If failed to execute */ void mkdir(String path) throws IOException; /** * Remove remote directory * * @param path Remote directory path * @throws IOException If failed to execute */ void rmdir(String path) throws IOException; /** * Obtain a handle for a directory * * @param path Remote directory path * @return The associated directory {@link Handle} * @throws IOException If failed to execute */ CloseableHandle openDir(String path) throws IOException; /** * @param handle Directory {@link Handle} to read from * @return A {@link List} of entries - {@code null} to indicate no more entries * <B>Note:</B> the list may be <U>incomplete</U> since the client and * server have some internal imposed limit on the number of entries they * can process. Therefore several calls to this method may be required * (until {@code null}). In order to iterate over all the entries use * {@link #readDir(String)} * @throws IOException If failed to access the remote site */ default List<DirEntry> readDir(Handle handle) throws IOException { return readDir(handle, null); } /** * @param handle Directory {@link Handle} to read from * @return A {@link List} of entries - {@code null} to indicate no more entries * @param eolIndicator An indicator that can be used to get information * whether end of list has been reached - ignored if {@code null}. Upon * return, set value indicates whether all entries have been exhausted - a {@code null} * value means that this information cannot be provided and another call to * {@code readDir} is necessary in order to verify that no more entries are pending * @throws IOException If failed to access the remote site * @see <A HREF="https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4">SFTP v6 - section 9.4</A> */ List<DirEntry> readDir(Handle handle, AtomicReference<Boolean> eolIndicator) throws IOException; /** * @param handle A directory {@link Handle} * @return An {@link Iterable} that can be used to iterate over all the * directory entries (like {@link #readDir(String)}). <B>Note:</B> the * iterable instance is not re-usable - i.e., files can be iterated * only <U>once</U> * @throws IOException If failed to access the directory */ default Iterable<DirEntry> listDir(Handle handle) throws IOException { if (!isOpen()) { throw new IOException("listDir(" + handle + ") client is closed"); } return new StfpIterableDirHandle(this, handle); } /** * The effective "normalized" remote path * * @param path The requested path - may be relative, and/or contain * dots - e.g., ".", "..", "./foo", "../bar" * * @return The effective "normalized" remote path * @throws IOException If failed to execute */ String canonicalPath(String path) throws IOException; /** * Retrieve remote path meta-data - follow symbolic links if encountered * * @param path The remote path * @return The associated {@link Attributes} * @throws IOException If failed to execute */ Attributes stat(String path) throws IOException; /** * Retrieve remote path meta-data - do <B>not</B> follow symbolic links * * @param path The remote path * @return The associated {@link Attributes} * @throws IOException If failed to execute */ Attributes lstat(String path) throws IOException; /** * Retrieve file/directory handle meta-data * * @param handle The {@link Handle} obtained via one of the {@code open} calls * @return The associated {@link Attributes} * @throws IOException If failed to execute */ Attributes stat(Handle handle) throws IOException; /** * Update remote node meta-data * * @param path The remote path * @param attributes The {@link Attributes} to update * @throws IOException If failed to execute */ void setStat(String path, Attributes attributes) throws IOException; /** * Update remote node meta-data * * @param handle The {@link Handle} obtained via one of the {@code open} calls * @param attributes The {@link Attributes} to update * @throws IOException If failed to execute */ void setStat(Handle handle, Attributes attributes) throws IOException; /** * Retrieve target of a link * * @param path Remote path that represents a link * @return The link target * @throws IOException If failed to execute */ String readLink(String path) throws IOException; /** * Create symbolic link * * @param linkPath The link location * @param targetPath The referenced target by the link * @throws IOException If failed to execute * @see #link(String, String, boolean) */ default void symLink(String linkPath, String targetPath) throws IOException { link(linkPath, targetPath, true); } /** * Create a link * * @param linkPath The link location * @param targetPath The referenced target by the link * @param symbolic If {@code true} then make this a symbolic link, otherwise a hard one * @throws IOException If failed to execute */ void link(String linkPath, String targetPath, boolean symbolic) throws IOException; // see SSH_FXP_BLOCK / SSH_FXP_UNBLOCK for byte range locks void lock(Handle handle, long offset, long length, int mask) throws IOException; void unlock(Handle handle, long offset, long length) throws IOException; // // High level API // default SftpRemotePathChannel openRemotePathChannel(String path, OpenOption... options) throws IOException { return openRemotePathChannel(path, GenericUtils.isEmpty(options) ? Collections.emptyList() : Arrays.asList(options)); } default SftpRemotePathChannel openRemotePathChannel(String path, Collection<? extends OpenOption> options) throws IOException { return openRemoteFileChannel(path, OpenMode.fromOpenOptions(options)); } default SftpRemotePathChannel openRemoteFileChannel(String path, OpenMode... modes) throws IOException { return openRemoteFileChannel(path, GenericUtils.isEmpty(modes) ? Collections.emptyList() : Arrays.asList(modes)); } /** * Opens an {@link SftpRemotePathChannel} on the specified remote path * * @param path The remote path * @param modes The access mode(s) - if {@code null}/empty then the {@link #DEFAULT_CHANNEL_MODES} are used * @return The open {@link SftpRemotePathChannel} - <B>Note:</B> do not close this * owner client instance until the channel is no longer needed since it uses the client * for providing the channel's functionality. * @throws IOException If failed to open the channel * @see java.nio.channels.Channels#newInputStream(java.nio.channels.ReadableByteChannel) * @see java.nio.channels.Channels#newOutputStream(java.nio.channels.WritableByteChannel) */ default SftpRemotePathChannel openRemoteFileChannel(String path, Collection<OpenMode> modes) throws IOException { return new SftpRemotePathChannel(path, this, false, GenericUtils.isEmpty(modes) ? DEFAULT_CHANNEL_MODES : modes); } /** * @param path The remote directory path * @return An {@link Iterable} that can be used to iterate over all the * directory entries (unlike {@link #readDir(Handle)}) * @throws IOException If failed to access the remote site * @see #readDir(Handle) */ default Iterable<DirEntry> readDir(String path) throws IOException { if (!isOpen()) { throw new IOException("readDir(" + path + ") client is closed"); } return new SftpIterableDirEntry(this, path); } default InputStream read(String path) throws IOException { return read(path, DEFAULT_READ_BUFFER_SIZE); } default InputStream read(String path, int bufferSize) throws IOException { return read(path, bufferSize, EnumSet.of(OpenMode.Read)); } default InputStream read(String path, OpenMode... mode) throws IOException { return read(path, DEFAULT_READ_BUFFER_SIZE, mode); } default InputStream read(String path, int bufferSize, OpenMode... mode) throws IOException { return read(path, bufferSize, GenericUtils.of(mode)); } default InputStream read(String path, Collection<OpenMode> mode) throws IOException { return read(path, DEFAULT_READ_BUFFER_SIZE, mode); } /** * Read a remote file's data via an input stream * * @param path The remote file path * @param bufferSize The internal read buffer size * @param mode The remote file {@link OpenMode}s * @return An {@link InputStream} for reading the remote file data * @throws IOException If failed to execute */ default InputStream read(String path, int bufferSize, 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); } default OutputStream write(String path) throws IOException { return write(path, DEFAULT_WRITE_BUFFER_SIZE); } default OutputStream write(String path, int bufferSize) throws IOException { return write(path, bufferSize, EnumSet.of(OpenMode.Write, OpenMode.Create, OpenMode.Truncate)); } default OutputStream write(String path, OpenMode... mode) throws IOException { return write(path, DEFAULT_WRITE_BUFFER_SIZE, mode); } default OutputStream write(String path, int bufferSize, OpenMode... mode) throws IOException { return write(path, bufferSize, GenericUtils.of(mode)); } default OutputStream write(String path, Collection<OpenMode> mode) throws IOException { return write(path, DEFAULT_WRITE_BUFFER_SIZE, mode); } /** * Write to a remote file via an output stream * * @param path The remote file path * @param bufferSize The internal write buffer size * @param mode The remote file {@link OpenMode}s * @return An {@link OutputStream} for writing the data * @throws IOException If failed to execute */ default OutputStream write(String path, int bufferSize, 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); } /** * @param <E> The generic extension type * @param extensionType The extension type * @return The extension instance - <B>Note:</B> it is up to the caller * to invoke {@link SftpClientExtension#isSupported()} - {@code null} if * this extension type is not implemented by the client * @see #getServerExtensions() */ <E extends SftpClientExtension> E getExtension(Class<? extends E> extensionType); /** * @param extensionName The extension name * @return The extension instance - <B>Note:</B> it is up to the caller * to invoke {@link SftpClientExtension#isSupported()} - {@code null} if * this extension type is not implemented by the client * @see #getServerExtensions() */ SftpClientExtension getExtension(String extensionName); }