Implement complete file manager keyboard shortcuts and copy functionality

Core Features:
- Full Ctrl+C/X/V/Z keyboard shortcuts system for file operations
- Real SSH file copy functionality supporting both files and directories
- Smart filename conflict resolution with timestamp-based naming
- Enhanced UX with detailed toast feedback and operation status

Technical Improvements:
- Remove complex file existence checks to prevent SSH connection hanging
- Optimize cp command with -fpr flags for non-interactive execution
- 20-second timeout mechanism for quick error feedback
- Comprehensive error handling and logging system

Keyboard Shortcuts System:
- Ctrl+A: Select all files (fixed text selection conflicts)
- Ctrl+C: Copy files to clipboard
- Ctrl+X: Cut files to clipboard
- Ctrl+V: Paste files (supports both copy and move operations)
- Ctrl+Z: Undo operations (basic framework)
- Delete: Delete selected files
- F2: Rename files

User Experience Enhancements:
- Smart focus management ensuring shortcuts work properly
- Fixed multi-select right-click delete functionality
- Copy operations with auto-rename: file_copy_12345678.txt
- Detailed operation feedback and error messages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZcR
2025-09-16 22:13:37 +08:00
parent bf166d602f
commit cae9097034
4 changed files with 378 additions and 10 deletions

View File

@@ -1521,6 +1521,155 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => {
});
});
// Copy SSH file/directory
app.post("/ssh/file_manager/ssh/copyItem", async (req, res) => {
const { sessionId, sourcePath, targetDir, hostId, userId } = req.body;
if (!sessionId || !sourcePath || !targetDir) {
return res.status(400).json({ error: "Missing required parameters" });
}
const sshConn = sshSessions[sessionId];
if (!sshConn || !sshConn.isConnected) {
return res.status(400).json({ error: "SSH session not found or not connected" });
}
sshConn.lastActive = Date.now();
scheduleSessionCleanup(sessionId);
try {
// Extract source name
const sourceName = sourcePath.split('/').pop() || 'copied_item';
// Skip file existence check to avoid SSH hanging - just use timestamp for uniqueness
const timestamp = Date.now().toString().slice(-8);
const nameWithoutExt = sourceName.includes('.')
? sourceName.substring(0, sourceName.lastIndexOf('.'))
: sourceName;
const extension = sourceName.includes('.')
? sourceName.substring(sourceName.lastIndexOf('.'))
: '';
// Always use timestamp suffix to ensure uniqueness without SSH calls
const uniqueName = `${nameWithoutExt}_copy_${timestamp}${extension}`;
fileLogger.info("Using timestamp-based unique name", { originalName: sourceName, uniqueName });
const targetPath = `${targetDir}/${uniqueName}`;
// Escape paths for shell commands
const escapedSource = sourcePath.replace(/'/g, "'\"'\"'");
const escapedTarget = targetPath.replace(/'/g, "'\"'\"'");
// Use cp with explicit flags to avoid hanging on prompts
// -f: force overwrite without prompting
// -r: recursive for directories
// -p: preserve timestamps, permissions
const copyCommand = `cp -fpr '${escapedSource}' '${escapedTarget}' 2>&1`;
fileLogger.info("Starting file copy operation", {
operation: "file_copy_start",
sessionId,
sourcePath,
targetPath,
uniqueName,
command: copyCommand.substring(0, 200) + "..." // Log truncated command
});
// Add timeout to prevent hanging
const commandTimeout = setTimeout(() => {
fileLogger.error("Copy command timed out after 20 seconds", {
sourcePath,
targetPath,
command: copyCommand
});
if (!res.headersSent) {
res.status(500).json({
error: "Copy operation timed out",
toast: { type: "error", message: "Copy operation timed out. SSH connection may be unstable." }
});
}
}, 20000); // 20 second timeout for better responsiveness
sshConn.client.exec(copyCommand, (err, stream) => {
if (err) {
clearTimeout(commandTimeout);
fileLogger.error("SSH copyItem error:", err);
if (!res.headersSent) {
return res.status(500).json({ error: err.message });
}
return;
}
let errorData = "";
let stdoutData = "";
// Monitor both stdout and stderr
stream.on("data", (data: Buffer) => {
const output = data.toString();
stdoutData += output;
fileLogger.info("Copy command stdout", { output: output.substring(0, 200) });
});
stream.stderr.on("data", (data: Buffer) => {
const output = data.toString();
errorData += output;
fileLogger.info("Copy command stderr", { output: output.substring(0, 200) });
});
stream.on("close", (code) => {
clearTimeout(commandTimeout);
fileLogger.info("Copy command completed", { code, errorData, hasError: errorData.length > 0 });
if (code !== 0) {
fileLogger.error(`SSH copyItem command failed with code ${code}: ${errorData}`);
if (!res.headersSent) {
return res.status(500).json({
error: `Copy failed: ${errorData}`,
toast: { type: "error", message: `Copy failed: ${errorData}` }
});
}
return;
}
fileLogger.success("Item copied successfully", {
operation: "file_copy",
sessionId,
sourcePath,
targetPath,
uniqueName,
hostId,
userId,
});
if (!res.headersSent) {
res.json({
message: "Item copied successfully",
sourcePath,
targetPath,
uniqueName,
toast: {
type: "success",
message: `Successfully copied to: ${uniqueName}`,
},
});
}
});
stream.on("error", (streamErr) => {
clearTimeout(commandTimeout);
fileLogger.error("SSH copyItem stream error:", streamErr);
if (!res.headersSent) {
res.status(500).json({ error: `Stream error: ${streamErr.message}` });
}
});
});
} catch (error: any) {
fileLogger.error("Copy operation error:", error);
res.status(500).json({ error: error.message });
}
});
// Helper function to determine MIME type based on file extension
function getMimeType(fileName: string): string {
const ext = fileName.split('.').pop()?.toLowerCase();