Jsch 是 Java 實作的 ssh, sftp library,這個函式庫原本是由 com.jcraft:jsch 實作,但在 2018 就停止更新,故現在要改使用 com.github.mwiede:jsch
只需要在 maven 加入 library 即可,最低可使用 Java 8
<dependency>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
<version>2.27.0</version>
</dependency>
exec
jsch 最基本是使用 exec channel,一次執行一個指令,沒有 context,單獨執行的指令。
以下程式另外用 Scanner 包裝一個簡單的互動介面
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class JSchExecTest {
private Session session;
public void connect(String host, int port, String user, String pwd) throws JSchException {
disconnect();
JSch jsch = new JSch();
Session session = jsch.getSession(user, host, port);
session.setPassword(pwd);
// yes / no / ask
session.setConfig("StrictHostKeyChecking", "no");
session.connect();
this.session = session;
}
public void disconnect() throws JSchException {
if( session!=null ) {
session.disconnect();
session = null;
}
}
public void execCmd(String command) throws JSchException, IOException {
if( session==null ) {
return;
}
// ChannelExec 單指令,單獨執行(適合非交互式)
// ChannelShell 多指令、有狀態、有上下文需求(如 cd)
ChannelExec channelExec = (ChannelExec) session.openChannel("exec");
channelExec.setCommand(command);
// set error output stream
// channelExec.setErrStream(System.err);
ByteArrayOutputStream errorOutputStream = new ByteArrayOutputStream();
channelExec.setErrStream(errorOutputStream);
// get command output
InputStream in = channelExec.getInputStream();
// execute command
channelExec.connect();
//// get output content
// BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
// String line;
// while ((line = reader.readLine()) != null) {
// System.out.println(line);
// }
// reader.close();
String output = IOUtils.toString(in, StandardCharsets.UTF_8);
if (!output.isEmpty()) {
System.out.println("Exec Output: " + command + "\r\n" + output);
}
// error
String errorMsg = errorOutputStream.toString();
if (!errorMsg.isEmpty()) {
System.err.println("Error Output: " + errorMsg);
}
channelExec.disconnect();
}
public void printhelp() {
System.out.println("connect host port username password");
System.out.println("exec command");
System.out.println("disconnect");
System.out.println("");
}
public static void main(String[] args) {
JSchExecTest jSchExecTest = new JSchExecTest();
try {
// session = JschTest1.connect("192.168.1.89", 22, "user", "pass");
// JschTest1.execCmd(session, "ls -al");
// JschTest1.execCmd(session, "ls -al ggg");
// JschTest1.disconnect(session);
Scanner scanner = new Scanner(System.in);
System.out.println("jsch CLI: Enter a command (type 'exit' to quit)");
while (true) {
System.out.print("> ");
String input = scanner.nextLine().trim();
if (input.isEmpty()) continue;
// 分割整行輸入,第一個為指令,其餘為參數
String[] parts = input.split("\\s+", 2); // 最多分兩段:指令 和 其餘參數
String command = parts[0];
String arguments = parts.length > 1 ? parts[1] : "";
if ("exit".equalsIgnoreCase(command)) {
System.out.println("Goodbye!");
break;
}
switch (command.toLowerCase()) {
case "h":
jSchExecTest.printhelp();
break;
case "help":
jSchExecTest.printhelp();
break;
case "?":
jSchExecTest.printhelp();
break;
case "connect":
String[] argparts = arguments.split("\\s+");
if( argparts.length == 4 ) {
try {
jSchExecTest.connect(argparts[0], Integer.parseInt(argparts[1]), argparts[2], argparts[3]);
} catch (JSchException je) {
}
} else {
System.out.println("connect host port username password");
}
break;
case "disconnect":
System.out.println("Current time: " + java.time.LocalTime.now());
jSchExecTest.disconnect();
break;
case "exec":
jSchExecTest.execCmd(arguments);
break;
default:
System.out.println("Unknown command: " + input);
}
}
scanner.close();
} catch (JSchException e) {
e.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
測試
jsch CLI: Enter a command (type 'exit' to quit)
> connect 192.168.1.89 22 user pass
> ls -al
Unknown command: ls -al
> exec ls -al
Exec Output: ls -al
總計 24
drwx------. 5 larzio larzio 159 6月 11 16:31 .
.....
> disconnect
Current time: 17:50:59.521429
> exit
Goodbye!
shell
像是 cd 指令,需要記錄目錄,就需要改用 shell channell
import com.jcraft.jsch.*;
import java.io.*;
public class JSchInteractiveShell {
public static void main(String[] args) throws Exception {
String user = "user";
String host = "192.168.1.89";
int port = 22;
String password = "s2papago";
String sudoPassword = "pass";
JSch jsch = new JSch();
Session session = jsch.getSession(user, host, port);
session.setPassword(password);
// 不驗證 host key(開發用,正式環境請換成嚴格檢查)
// session.setConfig("StrictHostKeyChecking", "no");
// 指定 known_hosts 檔案位置 (通常是 ~/.ssh/known_hosts)
String knownHostsPath = System.getProperty("user.home") + "/.ssh/known_hosts";
jsch.setKnownHosts(knownHostsPath);
session.setConfig("StrictHostKeyChecking", "yes");
session.connect();
ChannelShell channel = (ChannelShell) session.openChannel("shell");
InputStream in = channel.getInputStream();
OutputStream out = channel.getOutputStream();
PrintWriter writer = new PrintWriter(out, true);
channel.connect();
// thread 持續讀取遠端輸出
Thread readerThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
// // 判斷是否有 sudo 密碼提示 (常見字串,可視環境調整)
// if (line.toLowerCase().contains("[sudo] password") ||
// line.toLowerCase().contains("password for " + user.toLowerCase()) || line.toLowerCase().contains("密碼:")) {
// System.out.println("[INFO] Detected sudo password prompt, sending password...");
// writer.println(sudoPassword); // 自動送 sudo 密碼
// }
}
} catch (IOException e) {
e.printStackTrace();
}
});
readerThread.start();
Thread.sleep(1000);
// main thread: 從 System.in 讀使用者輸入,傳給遠端 shell
try (BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)) ) {
String command;
System.out.print("$ "); // prompt
while ((command = consoleReader.readLine()) != null) {
writer.println(command);
if ("exit".equalsIgnoreCase(command.trim())) {
break;
}
System.out.print("$ ");
}
}
readerThread.join();
channel.disconnect();
session.disconnect();
}
}
測試
$ ls -al
[larzio@lzstg2 ~]$ ls -al
總計 24
drwx------. 5 larzio larzio 159 6月 11 16:31 .
drwxr-xr-x. 3 root root 20 9月 11 2024 ..
.............
cd download
$ [larzio@lzstg2 ~]$ cd download
l s-al
$ [larzio@lzstg2 download]$ l s-al
-bash: l:命令找不到
ls -al
$ [larzio@lzstg2 download]$ ls -al
總計 16
drwxr-xr-x 2 root root 70 6月 11 16:29 .
drwx------. 5 larzio larzio 159 6月 11 16:31 ..
.............
exit
[larzio@lzstg2 download]$ exit
登出
sftp
上傳或下載檔案,如果要能支援目錄,要使用 sftp channel
import com.jcraft.jsch.*;
import java.io.*;
import java.util.Vector;
public class JSchSftpUtil {
private Session session;
private ChannelSftp sftp;
public JSchSftpUtil(Session session) throws JSchException {
this.session = session;
Channel channel = session.openChannel("sftp");
channel.connect();
sftp = (ChannelSftp) channel;
}
public void disconnect() {
if (sftp != null && sftp.isConnected()) sftp.disconnect();
if (session != null && session.isConnected()) session.disconnect();
}
public void upload(String localPath, String remotePath, int permission) throws Exception {
File localFile = new File(localPath);
if (!localFile.exists()) throw new FileNotFoundException(localPath);
if (localFile.isFile()) {
uploadFileWithResume(localFile, remotePath, permission);
} else {
uploadDirectory(localFile, remotePath, permission);
}
}
public void download(String remotePath, String localPath) throws Exception {
SftpATTRS attrs = null;
try {
attrs = sftp.stat(remotePath);
} catch (Exception e) {
throw new FileNotFoundException("Remote path not found: " + remotePath);
}
File localFile = new File(localPath);
if (attrs.isDir()) {
downloadDirectory(remotePath, localFile);
} else {
downloadFileWithResume(remotePath, localFile);
}
}
// 進度監控
private static class MyProgressMonitor implements SftpProgressMonitor {
private long max = 0;
private long count = 0;
private long percent = -1;
public MyProgressMonitor(long max) {
this.max = max;
}
@Override
public void init(int op, String src, String dest, long max) {
System.out.println("Start transfer: " + src + " -> " + dest);
}
@Override
public boolean count(long count) {
this.count += count;
long newPercent = this.count * 100 / max;
if (newPercent != percent) {
percent = newPercent;
System.out.print("\rProgress: " + percent + "%");
}
return true;
}
@Override
public void end() {
System.out.println("\nTransfer complete");
}
}
// 斷點續傳上傳單檔
private void uploadFileWithResume(File localFile, String remoteFilePath, int permission) throws Exception {
long localFileSize = localFile.length();
long remoteFileSize = 0;
try {
SftpATTRS attrs = sftp.stat(remoteFilePath);
remoteFileSize = attrs.getSize();
} catch (SftpException e) {
// 檔案不存在
remoteFileSize = 0;
}
if (remoteFileSize == localFileSize) {
System.out.println("Remote file already complete, skipping upload: " + remoteFilePath);
return;
} else if (remoteFileSize > localFileSize) {
System.out.println("Remote file is larger than local file, overwriting");
remoteFileSize = 0;
}
try (RandomAccessFile raf = new RandomAccessFile(localFile, "r")) {
raf.seek(remoteFileSize);
InputStream fis = new InputStream() {
@Override
public int read() throws IOException {
return raf.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return raf.read(b, off, len);
}
};
sftp.put(fis, remoteFilePath, new MyProgressMonitor(localFileSize), ChannelSftp.APPEND);
fis.close();
}
// 設定遠端檔案權限
sftp.chmod(permission, remoteFilePath);
}
// 遞迴上傳目錄
private void uploadDirectory(File localDir, String remoteDir, int permission) throws Exception {
try {
sftp.cd(remoteDir);
} catch (SftpException e) {
sftp.mkdir(remoteDir);
sftp.cd(remoteDir);
}
for (File file : localDir.listFiles()) {
if (file.isFile()) {
uploadFileWithResume(file, remoteDir + "/" + file.getName(), permission);
} else if (file.isDirectory()) {
uploadDirectory(file, remoteDir + "/" + file.getName(), permission);
}
}
sftp.cd("..");
}
// 斷點續傳下載單檔
private void downloadFileWithResume(String remoteFilePath, File localFile) throws Exception {
long remoteFileSize = sftp.stat(remoteFilePath).getSize();
long localFileSize = 0;
if (localFile.exists()) {
localFileSize = localFile.length();
if (localFileSize > remoteFileSize) {
System.out.println("Local file larger than remote, overwrite");
localFileSize = 0;
} else if (localFileSize == remoteFileSize) {
System.out.println("Local file already complete, skipping download: " + localFile.getAbsolutePath());
return;
}
}
OutputStream os;
if (localFileSize > 0) {
os = new FileOutputStream(localFile, true);
} else {
os = new FileOutputStream(localFile);
}
try (OutputStream outputStream = os) {
sftp.get(remoteFilePath, outputStream, new MyProgressMonitor(remoteFileSize), ChannelSftp.RESUME, localFileSize);
}
}
// 遞迴下載目錄
private void downloadDirectory(String remoteDir, File localDir) throws Exception {
if (!localDir.exists()) {
localDir.mkdirs();
}
Vector<ChannelSftp.LsEntry> list = sftp.ls(remoteDir);
for (ChannelSftp.LsEntry entry : list) {
String filename = entry.getFilename();
if (".".equals(filename) || "..".equals(filename)) continue;
String remoteFilePath = remoteDir + "/" + filename;
File localFilePath = new File(localDir, filename);
if (entry.getAttrs().isDir()) {
downloadDirectory(remoteFilePath, localFilePath);
} else {
downloadFileWithResume(remoteFilePath, localFilePath);
}
}
}
public static void main(String[] args) {
String user = "user";
String host = "192.168.1.89";
int port = 22;
String password = "pass";
String localUploadPath = "/Users/charley/Downloads/temp";
String remoteUploadPath = "/home/larzio/temp";
String remoteDownloadPath = "/home/larzio/download";
String localDownloadPath = "/Users/charley/Downloads/testdownload";
int permission = 0644;
JSch jsch = new JSch();
Session session = null;
JSchSftpUtil sftpUtil = null;
try {
session = jsch.getSession(user, host, port);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
session.connect();
sftpUtil = new JSchSftpUtil(session);
// 上傳
sftpUtil.upload(localUploadPath, remoteUploadPath, permission);
System.out.println("Upload done.");
// 下載
sftpUtil.download(remoteDownloadPath, localDownloadPath);
System.out.println("Download done.");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (sftpUtil != null) sftpUtil.disconnect();
}
}
}
沒有留言:
張貼留言