mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
fix(file-manager): cancel uploads and stop remote writes (#1477)
* fix(file-manager): cancel uploads and stop remote writes * test(css): exercise the configured PostCSS plugin chain
This commit is contained in:
1 parent
26d52c7631
commit
2a2ba2f790
6 files changed
+283
-16
No files matched your search
@@ -1244,7 +1244,7 @@ export function registerFileContentRoutes(
|
||||
let destroyUpload: (() => void) | undefined;
|
||||
|
||||
const abortUpload = () => {
|
||||
if (req.complete || cleanupStarted) return;
|
||||
if (resolved || cleanupStarted) return;
|
||||
requestAborted = true;
|
||||
cleanupStarted = true;
|
||||
destroyUpload?.();
|
||||
@@ -1252,7 +1252,12 @@ export function registerFileContentRoutes(
|
||||
|
||||
req.once("aborted", abortUpload);
|
||||
req.once("error", abortUpload);
|
||||
req.once("close", abortUpload);
|
||||
req.once("close", () => {
|
||||
if (!req.complete) abortUpload();
|
||||
});
|
||||
res.once("close", () => {
|
||||
if (!res.writableFinished) abortUpload();
|
||||
});
|
||||
|
||||
const bb = Busboy({ headers: req.headers });
|
||||
|
||||
@@ -1311,6 +1316,10 @@ export function registerFileContentRoutes(
|
||||
|
||||
getSessionSftp(sshConn)
|
||||
.then((sftp) => {
|
||||
if (requestAborted) {
|
||||
fileStream.resume();
|
||||
return;
|
||||
}
|
||||
const writeStream = sftp.createWriteStream(fullPath);
|
||||
const removePartialFile = () => {
|
||||
writeStream.destroy();
|
||||
@@ -1325,11 +1334,6 @@ export function registerFileContentRoutes(
|
||||
};
|
||||
destroyUpload = removePartialFile;
|
||||
|
||||
if (requestAborted) {
|
||||
removePartialFile();
|
||||
return;
|
||||
}
|
||||
|
||||
writeStream.on("error", (err) => {
|
||||
ctx.log.error("SFTP write stream error during upload:", err);
|
||||
if (!resolved && !requestAborted) {
|
||||
@@ -1460,12 +1464,24 @@ export function registerFileContentRoutes(
|
||||
let bytesWritten = 0;
|
||||
const uploadStartTime = Date.now();
|
||||
|
||||
let destroyUpload: (() => void) | undefined;
|
||||
res.once("close", () => {
|
||||
if (resolved || res.writableFinished) return;
|
||||
resolved = true;
|
||||
destroyUpload?.();
|
||||
});
|
||||
|
||||
getSessionSftp(sshConn)
|
||||
.then((sftp) => {
|
||||
if (resolved) return;
|
||||
const writeStream = sftp.createWriteStream(fullPath, {
|
||||
flags: offset === 0 ? "w" : "r+",
|
||||
start: offset,
|
||||
});
|
||||
destroyUpload = () => {
|
||||
req.unpipe(writeStream as unknown as NodeJS.WritableStream);
|
||||
writeStream.destroy();
|
||||
};
|
||||
|
||||
const fail = (status: number, error: string) => {
|
||||
if (resolved) return;
|
||||
|
||||
@@ -1185,9 +1185,14 @@ function FileManagerContent({
|
||||
|
||||
if (files.length === 0 && emptyDirs.length === 0) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
const cancel = {
|
||||
label: t("fileManager.cancel"),
|
||||
onClick: () => controller.abort(),
|
||||
};
|
||||
const progressToast = toast.loading(
|
||||
t("fileManager.uploadingFolderFiles", { count: files.length }),
|
||||
{ duration: Infinity },
|
||||
{ duration: Infinity, cancel },
|
||||
);
|
||||
|
||||
const failed: string[] = [];
|
||||
@@ -1214,6 +1219,7 @@ function FileManagerContent({
|
||||
a.split("/").length - b.split("/").length || a.localeCompare(b),
|
||||
);
|
||||
for (const dir of sortedDirs) {
|
||||
controller.signal.throwIfAborted();
|
||||
const parentDir = dir.split("/").slice(0, -1).join("/");
|
||||
const targetPath = parentDir ? `${base}${parentDir}/` : base;
|
||||
const folderName = dir.split("/").pop()!;
|
||||
@@ -1226,6 +1232,7 @@ function FileManagerContent({
|
||||
}
|
||||
|
||||
for (const { file, relativePath } of files) {
|
||||
controller.signal.throwIfAborted();
|
||||
const dirPart = relativePath.includes("/")
|
||||
? relativePath.substring(0, relativePath.lastIndexOf("/"))
|
||||
: "";
|
||||
@@ -1238,13 +1245,18 @@ function FileManagerContent({
|
||||
file.name,
|
||||
file,
|
||||
currentHost?.id,
|
||||
undefined,
|
||||
undefined,
|
||||
controller.signal,
|
||||
);
|
||||
} catch (error) {
|
||||
controller.signal.throwIfAborted();
|
||||
failed.push(relativePath);
|
||||
console.error(`Failed to upload ${relativePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
controller.signal.throwIfAborted();
|
||||
toast.dismiss(progressToast);
|
||||
if (failed.length === 0) {
|
||||
toast.success(
|
||||
@@ -1263,6 +1275,11 @@ function FileManagerContent({
|
||||
handleRefreshDirectory();
|
||||
} catch (error) {
|
||||
toast.dismiss(progressToast);
|
||||
if (controller.signal.aborted) {
|
||||
toast.info(t("fileManager.localTransferCancelled"));
|
||||
handleRefreshDirectory();
|
||||
return;
|
||||
}
|
||||
toast.error(t("fileManager.failedToUploadFile"));
|
||||
console.error("Folder upload failed:", error);
|
||||
}
|
||||
@@ -1280,12 +1297,17 @@ function FileManagerContent({
|
||||
async function handleUploadFile(file: File) {
|
||||
if (!sshSessionId) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
const cancel = {
|
||||
label: t("fileManager.cancel"),
|
||||
onClick: () => controller.abort(),
|
||||
};
|
||||
const progressToast = toast.loading(
|
||||
t("fileManager.uploadingFile", {
|
||||
name: file.name,
|
||||
size: formatFileSize(file.size),
|
||||
}),
|
||||
{ duration: Infinity },
|
||||
{ duration: Infinity, cancel },
|
||||
);
|
||||
|
||||
const updateProgress = (p: {
|
||||
@@ -1294,13 +1316,14 @@ function FileManagerContent({
|
||||
bytesSent: number;
|
||||
totalBytes: number;
|
||||
}) => {
|
||||
if (controller.signal.aborted) return;
|
||||
const percent = Math.min(
|
||||
100,
|
||||
Math.round((p.bytesSent / p.totalBytes) * 100),
|
||||
);
|
||||
toast.loading(
|
||||
`Uploading ${file.name} — ${percent}% (chunk ${p.chunkIndex + 1}/${p.totalChunks})`,
|
||||
{ id: progressToast, duration: Infinity },
|
||||
{ id: progressToast, duration: Infinity, cancel },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1315,7 +1338,9 @@ function FileManagerContent({
|
||||
currentHost?.id,
|
||||
undefined,
|
||||
updateProgress,
|
||||
controller.signal,
|
||||
);
|
||||
controller.signal.throwIfAborted();
|
||||
|
||||
toast.dismiss(progressToast);
|
||||
|
||||
@@ -1325,6 +1350,11 @@ function FileManagerContent({
|
||||
handleRefreshDirectory();
|
||||
} catch (error: unknown) {
|
||||
toast.dismiss(progressToast);
|
||||
if (controller.signal.aborted) {
|
||||
toast.info(t("fileManager.localTransferCancelled"));
|
||||
handleRefreshDirectory();
|
||||
return;
|
||||
}
|
||||
const uploadErr = error instanceof Error ? error : null;
|
||||
if (
|
||||
uploadErr?.message?.includes("connection") ||
|
||||
|
||||
@@ -358,6 +358,7 @@ export async function uploadSSHFile(
|
||||
bytesSent: number;
|
||||
totalBytes: number;
|
||||
}) => void,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Record<string, unknown>> {
|
||||
// Browser-side safety: any single multipart body approaching 2^31 bytes (~2.14GB)
|
||||
// crashes the XHR/ArrayBuffer pipeline in both Chromium (Electron) and Firefox,
|
||||
@@ -368,6 +369,7 @@ export async function uploadSSHFile(
|
||||
const CHUNK_SIZE_BYTES = 8 * 1024 * 1024; // 8 MiB
|
||||
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
if (file.size > CHUNK_THRESHOLD_BYTES) {
|
||||
const totalChunks = Math.ceil(file.size / CHUNK_SIZE_BYTES);
|
||||
fileLogger.info("Starting chunked upload", {
|
||||
@@ -380,6 +382,7 @@ export async function uploadSSHFile(
|
||||
|
||||
let bytesSent = 0;
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
signal?.throwIfAborted();
|
||||
const start = i * CHUNK_SIZE_BYTES;
|
||||
const end = Math.min(start + CHUNK_SIZE_BYTES, file.size);
|
||||
const chunkBlob = file.slice(start, end);
|
||||
@@ -397,6 +400,7 @@ export async function uploadSSHFile(
|
||||
},
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
timeout: 0,
|
||||
signal,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -433,6 +437,7 @@ export async function uploadSSHFile(
|
||||
form,
|
||||
{
|
||||
timeout: 0,
|
||||
signal,
|
||||
onUploadProgress: (event) => {
|
||||
const totalBytes =
|
||||
typeof event.total === "number" && event.total > 0
|
||||
@@ -449,6 +454,7 @@ export async function uploadSSHFile(
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (signal?.aborted) throw error;
|
||||
handleApiError(error, "upload SSH file");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user