diff --git a/src/main/java/net/lamgc/oracle/sentry/oci/compute/ssh/SftpSession.java b/src/main/java/net/lamgc/oracle/sentry/oci/compute/ssh/SftpSession.java
new file mode 100644
index 0000000..2b24ea3
--- /dev/null
+++ b/src/main/java/net/lamgc/oracle/sentry/oci/compute/ssh/SftpSession.java
@@ -0,0 +1,250 @@
+package net.lamgc.oracle.sentry.oci.compute.ssh;
+
+import org.apache.sshd.sftp.client.SftpClient;
+import org.apache.sshd.sftp.common.SftpConstants;
+import org.apache.sshd.sftp.common.SftpException;
+
+import java.io.*;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.NoSuchFileException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Sftp 会话.
+ *
可通过会话访问远程服务器的文件.
+ * @author LamGC
+ */
+@SuppressWarnings("unused")
+public class SftpSession implements Closeable {
+
+ /**
+ * 排除的文件名.
+ */
+ private final static Set EXCLUDED_FILE_NAMES;
+
+ static {
+ EXCLUDED_FILE_NAMES = new HashSet<>();
+ EXCLUDED_FILE_NAMES.add(".");
+ EXCLUDED_FILE_NAMES.add("..");
+ }
+
+ private final SftpClient sftpClient;
+
+ /**
+ * 创建 Sftp 会话.
+ * @param sftpClient Sftp 客户端.
+ */
+ SftpSession(SftpClient sftpClient) {
+ this.sftpClient = sftpClient;
+ }
+
+ /**
+ * 获取指定文件夹内的所有文件.
+ * @param path 文件夹路径.
+ * @return 返回该目录下所有文件的文件名, 文件名不带路径.
+ * @throws IOException 执行失败时抛出异常.
+ */
+ public Set listFiles(String path) throws IOException {
+ SftpClient.CloseableHandle handle = sftpClient.openDir(path);
+ Set paths = new HashSet<>();
+ for (SftpClient.DirEntry entry : sftpClient.listDir(handle)) {
+ if (EXCLUDED_FILE_NAMES.contains(entry.getFilename())) {
+ continue;
+ }
+ paths.add(entry.getFilename());
+ }
+ sftpClient.close(handle);
+ return paths;
+ }
+
+ /**
+ * 读取指定路径的问题.
+ * @param path 文件所在路径.
+ * @return 返回文件输入流.
+ * @throws FileNotFoundException 当文件不存在时抛出该异常.
+ * @throws IOException 当操作执行失败时抛出异常.
+ */
+ public InputStream read(String path) throws IOException {
+ if (!exists(path)) {
+ throw new FileNotFoundException(path);
+ }
+ return sftpClient.read(path);
+ }
+
+ /**
+ * 写入数据到指定目录.
+ * @param path 待写入的路径.
+ * @return 返回数据输出流.
+ * @throws FileNotFoundException 当文件不存在时抛出该异常.
+ * @throws IOException 如果操作失败则抛出异常.
+ */
+ public OutputStream write(String path) throws IOException {
+ if (!exists(path)) {
+ throw new FileNotFoundException(path);
+ }
+ return sftpClient.write(path, SftpClient.OpenMode.Write);
+ }
+
+ /**
+ * 检查指定路径是否存在.
+ * @param path 待检查的路径.
+ * @return 如果存在, 返回 {@code true}, 如果文件不存在, 返回 {@code false}.
+ * @throws IOException 执行失败时抛出.
+ */
+ public boolean exists(String path) throws IOException {
+ try {
+ return getAttributes(path) != null;
+ } catch (IOException e) {
+ if (e instanceof SftpException sftpException) {
+ if (sftpException.getStatus() == SftpConstants.SSH_FX_NO_SUCH_FILE) {
+ return false;
+ }
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * 是否为一个目录.
+ * @param path 待检查路径.
+ * @return 如果是一个目录, 返回 {@code true}.
+ * @throws FileNotFoundException 当路径不存在时抛出该异常.
+ * @throws IOException 如果执行失败则抛出异常.
+ */
+ public boolean isDirectory(String path) throws IOException {
+ if (!exists(path)) {
+ throw new FileNotFoundException(path);
+ }
+ return getAttributes(path).isDirectory();
+ }
+
+ /**
+ * 是否为一个文件.
+ * @param path 待检查路径.
+ * @return 如果是一个文件, 返回 {@code true}.
+ * @throws FileNotFoundException 当路径不存在时抛出该异常.
+ * @throws IOException 如果执行失败则抛出异常.
+ */
+ public boolean isFile(String path) throws IOException {
+ if (!exists(path)) {
+ throw new FileNotFoundException(path);
+ }
+ return getAttributes(path).isRegularFile();
+ }
+
+ /**
+ * 获取文件大小.
+ * @param path 待获取的路径.
+ * @return 返回文件大小, 单位 b.
+ * @throws NoSuchFileException 当指定路径不是一个文件(或符号链接)时抛出.
+ * @throws FileNotFoundException 当指定路径不存在时抛出.
+ * @throws IOException 当操作执行失败时抛出.
+ */
+ public long getFileSize(String path) throws IOException {
+ if (!exists(path)) {
+ throw new FileNotFoundException(path);
+ }
+ SftpClient.Attributes attributes = getAttributes(path);
+ if (!attributes.isRegularFile()) {
+ if (attributes.isSymbolicLink()) {
+ return getFileSize(sftpClient.readLink(path));
+ }
+ throw new NoSuchFileException("Not a file: " + path);
+ }
+
+ return attributes.getSize();
+ }
+
+ /**
+ * 获取指定路径的属性.
+ * @param path 待获取属性的路径.
+ * @return 返回路径所属属性.
+ * @throws IOException 如果执行失败则抛出异常.
+ */
+ public SftpClient.Attributes getAttributes(String path) throws IOException {
+ return sftpClient.stat(path);
+ }
+
+ /**
+ * 创建文件夹.
+ * @param path 待创建的目录.
+ * @return 当文件夹已存在时返回 {@code false}, 不存在且创建成功则返回 {@code true}.
+ * @throws IOException 如果操作执行失败则抛出异常.
+ */
+ public boolean mkdir(String path) throws IOException {
+ if (exists(path)) {
+ return false;
+ }
+ sftpClient.mkdir(path);
+ return true;
+ }
+
+ /**
+ * 创建新文件.
+ * @param path 待创建的文件路径.
+ * @return 当文件已存在时返回 {@code false}, 不存在且创建成功则返回 {@code true}.
+ * @throws IOException 如果操作失败啧抛出异常.
+ */
+ public boolean createNewFile(String path) throws IOException {
+ if (exists(path)) {
+ return false;
+ }
+ SftpClient.CloseableHandle handle = sftpClient.open(path, SftpClient.OpenMode.Create);
+ sftpClient.close(handle);
+ return true;
+ }
+
+ /**
+ * 删除指定路径.
+ * 该方法内部做了适配, 可兼容文件与文件夹两种类型.
+ * @param path 待删除的路径.
+ * @throws IOException 如果删除失败, 抛出异常.
+ * @throws FileNotFoundException 当指定路径不存在时抛出该异常.
+ * @throws DirectoryNotEmptyException 当路径为目录且目录不为空时抛出该异常.
+ */
+ public void delete(String path) throws IOException {
+ if (!exists(path)) {
+ throw new FileNotFoundException(path);
+ }
+ if (isDirectory(path)) {
+ if (!listFiles(path).isEmpty()) {
+ throw new DirectoryNotEmptyException(path);
+ }
+ sftpClient.rmdir(path);
+ } else {
+ sftpClient.remove(path);
+ }
+ }
+
+ /**
+ * 强制删除文件夹.
+ *
如果删除文件夹, 将会对文件夹执行遍历删除, 文件夹及子文件夹内的所有内容都会被删除.
+ * @param path 待删除的路径.
+ * @return 如果目标路径为文件夹且删除成功, 返回 {@code true}, 如果是文件, 则不会执行操作并返回 {@code false}.
+ * 该行为是防止文件遭到误删.
+ * @throws IOException 如果操作执行失败, 则抛出异常.
+ */
+ public boolean forceDeleteDir(String path) throws IOException {
+ if (isDirectory(path)) {
+ for (String filePath : listFiles(path)) {
+ String fullFilePath = path + "/" + filePath;
+ if (isDirectory(fullFilePath)) {
+ forceDeleteDir(fullFilePath);
+ } else {
+ sftpClient.remove(fullFilePath);
+ }
+ }
+ sftpClient.rmdir(path);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ sftpClient.close();
+ }
+
+}
diff --git a/src/main/java/net/lamgc/oracle/sentry/oci/compute/ssh/SshSession.java b/src/main/java/net/lamgc/oracle/sentry/oci/compute/ssh/SshSession.java
index e43e125..0adf13c 100644
--- a/src/main/java/net/lamgc/oracle/sentry/oci/compute/ssh/SshSession.java
+++ b/src/main/java/net/lamgc/oracle/sentry/oci/compute/ssh/SshSession.java
@@ -1,6 +1,7 @@
package net.lamgc.oracle.sentry.oci.compute.ssh;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.sftp.client.SftpClientFactory;
import java.io.Closeable;
import java.io.IOException;
@@ -33,6 +34,17 @@ public class SshSession implements Closeable {
return new CommandExecSession(clientSession.createExecChannel(command));
}
+ /**
+ * 创建 Sftp 会话.
+ *
可通过会话操作 Sftp.
+ * @return 返回 Sftp 会话.
+ * @throws IOException 如果创建失败, 将抛出异常.
+ */
+ public SftpSession createSftpSession() throws IOException {
+ SftpClientFactory factory = SftpClientFactory.instance();
+ return new SftpSession(factory.createSftpClient(clientSession));
+ }
+
/**
* 关闭 SSH 连接会话, 该连接会话所属的其他会话将会一同被关闭.
* @throws IOException 关闭失败时抛出异常,