Initial commit: SSH Manager (backend + frontend)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
liu
2026-02-03 09:10:06 +08:00
commit 1c5a44ff71
63 changed files with 6946 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
package com.sshmanager.controller;
import com.sshmanager.dto.SftpFileInfo;
import com.sshmanager.entity.Connection;
import com.sshmanager.entity.User;
import com.sshmanager.repository.UserRepository;
import com.sshmanager.service.ConnectionService;
import com.sshmanager.service.SftpService;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/sftp")
public class SftpController {
private final ConnectionService connectionService;
private final UserRepository userRepository;
private final SftpService sftpService;
private final Map<String, SftpService.SftpSession> sessions = new ConcurrentHashMap<>();
public SftpController(ConnectionService connectionService,
UserRepository userRepository,
SftpService sftpService) {
this.connectionService = connectionService;
this.userRepository = userRepository;
this.sftpService = sftpService;
}
private Long getCurrentUserId(Authentication auth) {
User user = userRepository.findByUsername(auth.getName()).orElseThrow(() -> new IllegalStateException("User not found"));
return user.getId();
}
private String sessionKey(Long userId, Long connectionId) {
return userId + ":" + connectionId;
}
private SftpService.SftpSession getOrCreateSession(Long connectionId, Long userId) throws Exception {
String key = sessionKey(userId, connectionId);
SftpService.SftpSession session = sessions.get(key);
if (session == null || !session.isConnected()) {
Connection conn = connectionService.getConnectionForSsh(connectionId, userId);
String password = connectionService.getDecryptedPassword(conn);
String privateKey = connectionService.getDecryptedPrivateKey(conn);
String passphrase = connectionService.getDecryptedPassphrase(conn);
session = sftpService.connect(conn, password, privateKey, passphrase);
sessions.put(key, session);
}
return session;
}
@GetMapping("/list")
public ResponseEntity<List<SftpFileInfo>> list(
@RequestParam Long connectionId,
@RequestParam(required = false, defaultValue = ".") String path,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
List<SftpService.FileInfo> files = sftpService.listFiles(session, path);
List<SftpFileInfo> dtos = files.stream()
.map(f -> new SftpFileInfo(f.name, f.directory, f.size, f.mtime))
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
@GetMapping("/pwd")
public ResponseEntity<Map<String, String>> pwd(
@RequestParam Long connectionId,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
String pwd = sftpService.pwd(session);
Map<String, String> result = new HashMap<>();
result.put("path", pwd);
return ResponseEntity.ok(result);
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
@GetMapping("/download")
public ResponseEntity<byte[]> download(
@RequestParam Long connectionId,
@RequestParam String path,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
byte[] data = sftpService.download(session, path);
String filename = path.contains("/") ? path.substring(path.lastIndexOf('/') + 1) : path;
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(data);
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
@PostMapping("/upload")
public ResponseEntity<Map<String, String>> upload(
@RequestParam Long connectionId,
@RequestParam String path,
@RequestParam("file") MultipartFile file,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
String remotePath = (path == null || path.isEmpty() || path.equals("/"))
? "/" + file.getOriginalFilename()
: (path.endsWith("/") ? path + file.getOriginalFilename() : path + "/" + file.getOriginalFilename());
sftpService.upload(session, remotePath, file.getBytes());
Map<String, String> result = new HashMap<>();
result.put("message", "Uploaded");
return ResponseEntity.ok(result);
} catch (Exception e) {
Map<String, String> error = new HashMap<>();
error.put("error", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
@DeleteMapping("/delete")
public ResponseEntity<Map<String, String>> delete(
@RequestParam Long connectionId,
@RequestParam String path,
@RequestParam boolean directory,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
sftpService.delete(session, path, directory);
Map<String, String> result = new HashMap<>();
result.put("message", "Deleted");
return ResponseEntity.ok(result);
} catch (Exception e) {
Map<String, String> error = new HashMap<>();
error.put("error", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
@PostMapping("/mkdir")
public ResponseEntity<Map<String, String>> mkdir(
@RequestParam Long connectionId,
@RequestParam String path,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
sftpService.mkdir(session, path);
Map<String, String> result = new HashMap<>();
result.put("message", "Created");
return ResponseEntity.ok(result);
} catch (Exception e) {
Map<String, String> error = new HashMap<>();
error.put("error", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
@PostMapping("/rename")
public ResponseEntity<Map<String, String>> rename(
@RequestParam Long connectionId,
@RequestParam String oldPath,
@RequestParam String newPath,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
SftpService.SftpSession session = getOrCreateSession(connectionId, userId);
sftpService.rename(session, oldPath, newPath);
Map<String, String> result = new HashMap<>();
result.put("message", "Renamed");
return ResponseEntity.ok(result);
} catch (Exception e) {
Map<String, String> error = new HashMap<>();
error.put("error", e.getMessage());
return ResponseEntity.status(500).body(error);
}
}
@PostMapping("/transfer-remote")
public ResponseEntity<Map<String, String>> transferRemote(
@RequestParam Long sourceConnectionId,
@RequestParam String sourcePath,
@RequestParam Long targetConnectionId,
@RequestParam String targetPath,
Authentication authentication) {
try {
Long userId = getCurrentUserId(authentication);
if (sourcePath == null || sourcePath.trim().isEmpty()) {
Map<String, String> err = new HashMap<>();
err.put("error", "sourcePath is required");
return ResponseEntity.badRequest().body(err);
}
if (targetPath == null || targetPath.trim().isEmpty()) {
Map<String, String> err = new HashMap<>();
err.put("error", "targetPath is required");
return ResponseEntity.badRequest().body(err);
}
SftpService.SftpSession sourceSession = getOrCreateSession(sourceConnectionId, userId);
SftpService.SftpSession targetSession = getOrCreateSession(targetConnectionId, userId);
if (sourceConnectionId.equals(targetConnectionId)) {
sftpService.rename(sourceSession, sourcePath.trim(), targetPath.trim());
} else {
sftpService.transferRemote(sourceSession, sourcePath.trim(), targetSession, targetPath.trim());
}
Map<String, String> result = new HashMap<>();
result.put("message", "Transferred");
return ResponseEntity.ok(result);
} catch (Exception e) {
Map<String, String> error = new HashMap<>();
error.put("error", e.getMessage() != null ? e.getMessage() : "Transfer failed");
return ResponseEntity.status(500).body(error);
}
}
@PostMapping("/disconnect")
public ResponseEntity<Map<String, String>> disconnect(
@RequestParam Long connectionId,
Authentication authentication) {
Long userId = getCurrentUserId(authentication);
String key = sessionKey(userId, connectionId);
SftpService.SftpSession session = sessions.remove(key);
if (session != null) {
session.disconnect();
}
Map<String, String> result = new HashMap<>();
result.put("message", "Disconnected");
return ResponseEntity.ok(result);
}
}