Fix SSH encryption and add file download functionality
- Fix SSH authentication by ensuring all database operations use EncryptedDBOperations for automatic encryption/decryption - Resolve SSH connection failures caused by encrypted password data being passed to authentication - Add comprehensive file download functionality for SSH file manager (Issue #228) - Update database migration to add require_password column for SSH sessions - Enhance debugging and logging for SSH connection troubleshooting 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Card } from "@/components/ui/card.tsx";
|
||||
import { Folder, File, Trash2, Pin } from "lucide-react";
|
||||
import { Folder, File, Trash2, Pin, Download } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface SSHConnection {
|
||||
@@ -32,6 +32,7 @@ interface FileManagerLeftSidebarVileViewerProps {
|
||||
onOpenFile: (file: FileItem) => void;
|
||||
onOpenFolder: (folder: FileItem) => void;
|
||||
onStarFile: (file: FileItem) => void;
|
||||
onDownloadFile?: (file: FileItem) => void;
|
||||
onDeleteFile: (file: FileItem) => void;
|
||||
isLoading?: boolean;
|
||||
error?: string;
|
||||
@@ -47,6 +48,7 @@ export function FileManagerLeftSidebarFileViewer({
|
||||
onOpenFile,
|
||||
onOpenFolder,
|
||||
onStarFile,
|
||||
onDownloadFile,
|
||||
onDeleteFile,
|
||||
isLoading,
|
||||
error,
|
||||
@@ -104,6 +106,17 @@ export function FileManagerLeftSidebarFileViewer({
|
||||
className={`w-4 h-4 ${item.isStarred ? "text-yellow-400" : "text-muted-foreground"}`}
|
||||
/>
|
||||
</Button>
|
||||
{item.type === "file" && onDownloadFile && (
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-7 w-7"
|
||||
onClick={() => onDownloadFile(item)}
|
||||
title={t("fileManager.downloadFile")}
|
||||
>
|
||||
<Download className="w-4 h-4 text-blue-400" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/card.tsx";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import {
|
||||
Upload,
|
||||
Download,
|
||||
FilePlus,
|
||||
FolderPlus,
|
||||
Trash2,
|
||||
@@ -27,12 +28,14 @@ export function FileManagerOperations({
|
||||
}: FileManagerOperationsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showDownload, setShowDownload] = useState(false);
|
||||
const [showCreateFile, setShowCreateFile] = useState(false);
|
||||
const [showCreateFolder, setShowCreateFolder] = useState(false);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [showRename, setShowRename] = useState(false);
|
||||
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [downloadPath, setDownloadPath] = useState("");
|
||||
const [newFileName, setNewFileName] = useState("");
|
||||
const [newFolderName, setNewFolderName] = useState("");
|
||||
const [deletePath, setDeletePath] = useState("");
|
||||
@@ -154,6 +157,66 @@ export function FileManagerOperations({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!downloadPath.trim() || !sshSessionId) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const { toast } = await import("sonner");
|
||||
const fileName = downloadPath.split('/').pop() || 'download';
|
||||
const loadingToast = toast.loading(
|
||||
t("fileManager.downloadingFile", { name: fileName }),
|
||||
);
|
||||
|
||||
try {
|
||||
const { downloadSSHFile } = await import("@/ui/main-axios.ts");
|
||||
|
||||
const response = await downloadSSHFile(
|
||||
sshSessionId,
|
||||
downloadPath.trim(),
|
||||
);
|
||||
|
||||
toast.dismiss(loadingToast);
|
||||
|
||||
if (response?.content) {
|
||||
// Convert base64 to blob and trigger download
|
||||
const byteCharacters = atob(response.content);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: response.mimeType || 'application/octet-stream' });
|
||||
|
||||
// Create download link
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = response.fileName || fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
onSuccess(
|
||||
t("fileManager.fileDownloadedSuccessfully", { name: response.fileName || fileName }),
|
||||
);
|
||||
} else {
|
||||
onError(t("fileManager.noFileContent"));
|
||||
}
|
||||
|
||||
setShowDownload(false);
|
||||
setDownloadPath("");
|
||||
} catch (error: any) {
|
||||
toast.dismiss(loadingToast);
|
||||
onError(
|
||||
error?.response?.data?.error || t("fileManager.failedToDownloadFile"),
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateFolder = async () => {
|
||||
if (!newFolderName.trim() || !sshSessionId) return;
|
||||
|
||||
@@ -344,7 +407,7 @@ export function FileManagerOperations({
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="p-4 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -357,6 +420,18 @@ export function FileManagerOperations({
|
||||
<span className="truncate">{t("fileManager.uploadFile")}</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowDownload(true)}
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover"
|
||||
title={t("fileManager.downloadFile")}
|
||||
>
|
||||
<Download className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")} />
|
||||
{showTextLabels && (
|
||||
<span className="truncate">{t("fileManager.downloadFile")}</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -397,7 +472,7 @@ export function FileManagerOperations({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowDelete(true)}
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover col-span-2"
|
||||
className="h-10 bg-dark-bg border-2 border-dark-border hover:border-dark-border-hover hover:bg-dark-hover col-span-3"
|
||||
title={t("fileManager.deleteItem")}
|
||||
>
|
||||
<Trash2 className={cn("w-4 h-4", showTextLabels ? "mr-2" : "")} />
|
||||
@@ -516,6 +591,64 @@ export function FileManagerOperations({
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showDownload && (
|
||||
<Card className="bg-dark-bg border-2 border-dark-border p-3 sm:p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base sm:text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Download className="w-5 h-5 sm:w-6 sm:h-6 flex-shrink-0" />
|
||||
<span className="break-words">
|
||||
{t("fileManager.downloadFile")}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowDownload(false)}
|
||||
className="h-8 w-8 p-0 flex-shrink-0 ml-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-white mb-2 block">
|
||||
{t("fileManager.filePath")}
|
||||
</label>
|
||||
<Input
|
||||
value={downloadPath}
|
||||
onChange={(e) => setDownloadPath(e.target.value)}
|
||||
placeholder={t("placeholders.fullPath")}
|
||||
className="bg-dark-bg-button border-2 border-dark-border-hover text-white text-sm"
|
||||
onKeyDown={(e) => e.key === "Enter" && handleDownload()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
disabled={!downloadPath.trim() || isLoading}
|
||||
className="w-full text-sm h-9"
|
||||
>
|
||||
{isLoading
|
||||
? t("fileManager.downloading")
|
||||
: t("fileManager.downloadFile")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDownload(false)}
|
||||
disabled={isLoading}
|
||||
className="w-full text-sm h-9"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showCreateFile && (
|
||||
<Card className="bg-dark-bg border-2 border-dark-border p-3 sm:p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
|
||||
Reference in New Issue
Block a user