diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 4bed2271..cb4fee3e 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -4870,6 +4870,55 @@ app.post("/ssh/file_manager/ssh/downloadFile", async (req, res) => { }); }); +app.post("/ssh/file_manager/ssh/downloadFileStream", async (req, res) => { + const { sessionId, path: filePath } = req.body; + const userId = (req as AuthenticatedRequest).userId; + + if (!sessionId || !filePath) { + return res.status(400).json({ error: "Missing download parameters" }); + } + + const sshConn = sshSessions[sessionId]; + if (!sshConn?.isConnected) { + return res.status(400).json({ error: "SSH session not found or not connected" }); + } + if (!verifySessionOwnership(sshConn, userId)) { + return res.status(403).json({ error: "Session access denied" }); + } + + sshConn.lastActive = Date.now(); + + try { + const sftp = await getSessionSftp(sshConn); + const stats = await new Promise<{ size: number; isFile: () => boolean }>((resolve, reject) => { + sftp.stat(filePath, (err, s) => (err ? reject(err) : resolve(s))); + }); + + if (!stats.isFile()) { + return res.status(400).json({ error: "Cannot download directories" }); + } + + const fileName = filePath.split("/").pop() || "download"; + res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(fileName)}"`); + res.setHeader("Content-Length", String(stats.size)); + + const readStream = sftp.createReadStream(filePath); + readStream.on("error", (err) => { + if (!res.headersSent) { + res.status(500).json({ error: `Download failed: ${err.message}` }); + } else { + res.destroy(); + } + }); + readStream.pipe(res); + } catch (err) { + if (!res.headersSent) { + res.status(500).json({ error: `Download failed: ${(err as Error).message}` }); + } + } +}); + /** * @openapi * /ssh/file_manager/ssh/copyItem: diff --git a/src/ui/features/file-manager/FileManager.tsx b/src/ui/features/file-manager/FileManager.tsx index 41a142db..b8ed87b9 100644 --- a/src/ui/features/file-manager/FileManager.tsx +++ b/src/ui/features/file-manager/FileManager.tsx @@ -864,38 +864,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { try { await ensureSSHConnection(); - const response = await downloadSSHFile(sshSessionId, file.path); + const { downloadSSHFileStream } = await import("@/main-axios.ts"); + await downloadSSHFileStream(sshSessionId, file.path); - const content = response?.content as string | undefined; - const mimeType = response?.mimeType as string | undefined; - const fileName = response?.fileName as string | undefined; - - if (content) { - const byteCharacters = atob(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: mimeType || "application/octet-stream", - }); - - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = fileName || file.name; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - toast.success( - t("fileManager.fileDownloadedSuccessfully", { name: file.name }), - ); - } else { - toast.error(t("fileManager.failedToDownloadFile")); - } + toast.success( + t("fileManager.fileDownloadedSuccessfully", { name: file.name }), + ); } catch (error: unknown) { const err = error instanceof Error ? error : null; if ( diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 04010ac8..d55c1e38 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -2082,6 +2082,25 @@ export async function downloadSSHFile( } } +export async function downloadSSHFileStream( + sessionId: string, + filePath: string, +): Promise { + const response = await fileManagerApi.post( + "/ssh/downloadFileStream", + { sessionId, path: filePath }, + { responseType: "blob" }, + ); + const blob = response.data as Blob; + const fileName = filePath.split("/").pop() || "download"; + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName; + a.click(); + URL.revokeObjectURL(url); +} + export async function createSSHFile( sessionId: string, path: string,