package com.sshmanager.service; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.Session; import com.sshmanager.entity.Connection; import org.springframework.stereotype.Service; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Vector; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @Service public class SftpService { public SftpSession connect(Connection conn, String password, String privateKey, String passphrase) throws Exception { JSch jsch = new JSch(); if (conn.getAuthType() == Connection.AuthType.PRIVATE_KEY && privateKey != null && !privateKey.isEmpty()) { byte[] keyBytes = privateKey.getBytes(StandardCharsets.UTF_8); byte[] passphraseBytes = (passphrase != null && !passphrase.isEmpty()) ? passphrase.getBytes(StandardCharsets.UTF_8) : null; jsch.addIdentity("key", keyBytes, null, passphraseBytes); } Session session = jsch.getSession(conn.getUsername(), conn.getHost(), conn.getPort()); session.setConfig("StrictHostKeyChecking", "no"); if (conn.getAuthType() == Connection.AuthType.PASSWORD && password != null) { session.setPassword(password); } session.connect(10000); ChannelSftp channel = (ChannelSftp) session.openChannel("sftp"); channel.connect(5000); return new SftpSession(session, channel); } public static class SftpSession { private final Session session; private final ChannelSftp channel; public SftpSession(Session session, ChannelSftp channel) { this.session = session; this.channel = channel; } public ChannelSftp getChannel() { return channel; } public void disconnect() { if (channel != null && channel.isConnected()) { channel.disconnect(); } if (session != null && session.isConnected()) { session.disconnect(); } } public boolean isConnected() { return channel != null && channel.isConnected(); } } public static class FileInfo { public String name; public boolean directory; public long size; public long mtime; public FileInfo(String name, boolean directory, long size, long mtime) { this.name = name; this.directory = directory; this.size = size; this.mtime = mtime; } } public List listFiles(SftpSession sftpSession, String path) throws Exception { Vector entries = sftpSession.getChannel().ls(path); List result = new ArrayList<>(); for (Object obj : entries) { ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) obj; String name = entry.getFilename(); if (".".equals(name) || "..".equals(name)) continue; result.add(new FileInfo( name, entry.getAttrs().isDir(), entry.getAttrs().getSize(), entry.getAttrs().getMTime() * 1000L )); } return result; } public byte[] download(SftpSession sftpSession, String remotePath) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); sftpSession.getChannel().get(remotePath, out); return out.toByteArray(); } public void upload(SftpSession sftpSession, String remotePath, byte[] data) throws Exception { sftpSession.getChannel().put(new ByteArrayInputStream(data), remotePath); } public void delete(SftpSession sftpSession, String remotePath, boolean isDir) throws Exception { if (isDir) { sftpSession.getChannel().rmdir(remotePath); } else { sftpSession.getChannel().rm(remotePath); } } public void mkdir(SftpSession sftpSession, String remotePath) throws Exception { sftpSession.getChannel().mkdir(remotePath); } public void rename(SftpSession sftpSession, String oldPath, String newPath) throws Exception { sftpSession.getChannel().rename(oldPath, newPath); } public String pwd(SftpSession sftpSession) throws Exception { return sftpSession.getChannel().pwd(); } public void cd(SftpSession sftpSession, String path) throws Exception { sftpSession.getChannel().cd(path); } /** * Transfer a single file from source session to target session (streaming, no full file in memory). * Fails if sourcePath is a directory. */ public void transferRemote(SftpSession source, String sourcePath, SftpSession target, String targetPath) throws Exception { if (source.getChannel().stat(sourcePath).isDir()) { throw new IllegalArgumentException("Source path is a directory; only single file transfer is supported"); } final int pipeBufferSize = 65536; PipedOutputStream pos = new PipedOutputStream(); PipedInputStream pis = new PipedInputStream(pos, pipeBufferSize); ExecutorService executor = Executors.newSingleThreadExecutor(); try { Future putFuture = executor.submit(() -> { try { target.getChannel().put(pis, targetPath); } catch (Exception e) { throw new RuntimeException(e); } }); source.getChannel().get(sourcePath, pos); pos.close(); putFuture.get(5, TimeUnit.MINUTES); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof RuntimeException && cause.getCause() instanceof Exception) { throw (Exception) cause.getCause(); } if (cause instanceof Exception) { throw (Exception) cause; } throw new RuntimeException(cause); } catch (TimeoutException e) { throw new RuntimeException("Transfer timeout", e); } finally { executor.shutdownNow(); try { pis.close(); } catch (Exception ignored) { } } } }