mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 04:43:36 +00:00
fix: use streaming download for large files in file manager
This commit is contained in:
@@ -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
|
* @openapi
|
||||||
* /ssh/file_manager/ssh/copyItem:
|
* /ssh/file_manager/ssh/copyItem:
|
||||||
|
|||||||
@@ -864,38 +864,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
|||||||
try {
|
try {
|
||||||
await ensureSSHConnection();
|
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;
|
toast.success(
|
||||||
const mimeType = response?.mimeType as string | undefined;
|
t("fileManager.fileDownloadedSuccessfully", { name: file.name }),
|
||||||
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"));
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error instanceof Error ? error : null;
|
const err = error instanceof Error ? error : null;
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -2082,6 +2082,25 @@ export async function downloadSSHFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadSSHFileStream(
|
||||||
|
sessionId: string,
|
||||||
|
filePath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
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(
|
export async function createSSHFile(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
path: string,
|
path: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user