mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
release-2.7.1 (#1296)
* Add Helm and GitOps deployment setup * fix: build better-sqlite3 from source in Docker (#1267) * fix: preserve runtime SSL settings (#1268) * fix: support forwarding from the memory SSH agent (#1269) * fix: support forwarding from the memory agent * style: format memory agent test * fix: prompt for encrypted SFTP key passphrases (#1270) * fix: prompt for SFTP key passphrases * style: format SSH key utility test * fix: include host context in automation notifications (#1271) * fix: include host context in automation notifications * style: format automation notification changes * fix: reserve sidebar height for host tags (#1272) * fix: keep host action rows stable at large font sizes (#1273) * fix: honor certificate setting during server probe (#1274) * fix: package standard Linux icon sizes (#1275) * fix: avoid duplicate Docker HTTPS listener (#1276) * Fix host status without metrics collection (#1277) * fix: allow eight-digit secure auth codes (#1263) Allow TOTP prompts to accept secure auth codes longer than six digits without blocking valid authentication attempts. Generated with Codebuff 🤖 Co-authored-by: Chetan <chetan.development@gmail.com> Co-authored-by: Codebuff <noreply@codebuff.com> * Harden Helm deployment defaults * Update Helm workflow action * Exclude Helm templates from Prettier * Fix browser RDP file drops (#1279) * Fix Proxmox guest credential usernames (#1280) * Add WSL local terminal option (#1281) * refactor: split the transfer engine into focused modules (#1282) * refactor: extract SFTP promisify helpers into sftp-promisify module * refactor: extract transfer timing and rate stats into transfer-stats module * refactor: extract transfer error classes and recovery checks into transfer-errors module * refactor: extract host/path utility helpers into transfer-host-utils module * refactor: extract SFTP directory tree helpers into transfer-sftp-dir module * refactor: extract segment copy job builder into transfer-segment-copy module * refactor: extract file scan and sample helpers into transfer-scan module * refactor: move throttled progress helper into transfer-stats module * style: format transfer modules * perf: optimize tmux monitor aggregation (#1283) * fix: reserve credential tag row height (#1284) * feat: edit AI provider model settings (#1285) * fix: clarify click-to-expand host setting (#1286) * fix: allow portable imports on remote databases (#1287) * fix: allow HTTPS to share the configured port (#1288) * fix: resolve synced jump hosts on the server (#1289) * fix: make terminal clipboard shortcuts layout independent (#1290) * fix: use compatible fetch dispatcher for Tailscale (#1291) * fix: add OIDC environment recovery override (#1292) * fix: coalesce rapid mobile terminal input (#1293) * fix: coalesce rapid mobile terminal input * fix: support clean xterm patch installs * fix: resolve synced remote desktop host IDs (#1295) * feat: make the SFTP file manager path bar editable (#1294) Co-authored-by: Maxime Bonillo <257463937+dropafterfree@users.noreply.github.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> * feat: add passkey sign in to the login screen * fix: remove rounded corners from the host list search bar * fix: stop image storage settings text wrapping to one word per line * fix: prevent malformed websocket messages from crashing the server * chore: increment version * fix: remove gaps between host rows in the sidebar list Keep sub-pixel row measurements and stop wiping the size cache on hover. * fix: Failed to connect through jump hosts (#1180) https://github.com/Termix-SSH/Support/issues/1180 * feat: Progress bar for file downloads in the file manager (#1158) https://github.com/Termix-SSH/Support/issues/1158 * feat: Allow setting Silent OIDC Login via ENV var (#1174) https://github.com/Termix-SSH/Support/issues/1174 * feat: `IdentityFile` to limit the number of attempts by agents (#1165) https://github.com/Termix-SSH/Support/issues/1165 * feat: Credentials clone (#1159) https://github.com/Termix-SSH/Support/issues/1159 * chore: update release notes * docs: move helm setup guide to the docs site * fix: type errors in FilteredAgent agent identity handling * fix: remove stale better-sqlite3 prebuilds so the source build is used * fix: actually build better-sqlite3 from source so arm64 docker images work * fix: credential edit pencil in host editor and add clone action to credential list * fix: clear editingHost so the credential pencil actually opens the editor * chore: run format and lint * fix: folder drag and drop upload failing in the file manager * chore: sync Crowdin translations for 2.7.1 --------- Co-authored-by: alex-ctms <alex-ctms@users.noreply.github.com> Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com> Co-authored-by: Chetan Kumar <74929596+ckloop@users.noreply.github.com> Co-authored-by: Chetan <chetan.development@gmail.com> Co-authored-by: Codebuff <noreply@codebuff.com> Co-authored-by: ZacharyZcR <payasonorahc@protonmail.com> Co-authored-by: dropafterfree <maxime.bonillo@gmail.com> Co-authored-by: Maxime Bonillo <257463937+dropafterfree@users.noreply.github.com>
This commit is contained in:
co-authored by
Chetan
Codebuff
Maxime Bonillo
ZacharyZcR
alex-ctms
Chetan Kumar
ZacharyZcR
dropafterfree
parent
566b908daf
commit
76fd9eedbf
@@ -19,6 +19,7 @@ import {
|
||||
useWindowManager,
|
||||
} from "./components/WindowManager.tsx";
|
||||
import { FileWindow } from "./components/FileWindow.tsx";
|
||||
import { DownloadProgressToast } from "./components/DownloadProgressToast.tsx";
|
||||
import { DiffWindow } from "./components/DiffWindow.tsx";
|
||||
import { useDragToDesktop } from "@/features/file-manager/hooks/useDragToDesktop";
|
||||
import { useDragToSystemDesktop } from "@/features/file-manager/hooks/useDragToSystemDesktop";
|
||||
@@ -976,19 +977,14 @@ function FileManagerContent({
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, [currentPath]);
|
||||
|
||||
async function handleItemsDropped(items: DataTransferItemList) {
|
||||
async function handleItemsDropped(entries: FileSystemEntry[]) {
|
||||
if (!sshSessionId) {
|
||||
toast.error(t("fileManager.noSSHConnection"));
|
||||
return;
|
||||
}
|
||||
|
||||
const entries: FileSystemEntry[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = items[i].webkitGetAsEntry?.();
|
||||
if (entry) entries.push(entry);
|
||||
}
|
||||
|
||||
const files: { file: File; relativePath: string }[] = [];
|
||||
const emptyDirs: string[] = [];
|
||||
|
||||
async function readEntry(
|
||||
entry: FileSystemEntry,
|
||||
@@ -999,51 +995,77 @@ function FileManagerContent({
|
||||
(entry as FileSystemFileEntry).file(resolve, reject),
|
||||
);
|
||||
files.push({ file, relativePath: path });
|
||||
} else if (entry.isDirectory) {
|
||||
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
||||
let batch: FileSystemEntry[];
|
||||
do {
|
||||
batch = await new Promise<FileSystemEntry[]>((resolve, reject) =>
|
||||
reader.readEntries(resolve, reject),
|
||||
);
|
||||
for (const child of batch) {
|
||||
await readEntry(child, `${path}/${child.name}`);
|
||||
}
|
||||
} while (batch.length > 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entry.isDirectory) return;
|
||||
|
||||
// readEntries only hands back a page at a time and signals the end with an
|
||||
// empty batch, so drain it fully before walking into the children.
|
||||
const reader = (entry as FileSystemDirectoryEntry).createReader();
|
||||
const children: FileSystemEntry[] = [];
|
||||
for (;;) {
|
||||
const batch = await new Promise<FileSystemEntry[]>((resolve, reject) =>
|
||||
reader.readEntries(resolve, reject),
|
||||
);
|
||||
if (batch.length === 0) break;
|
||||
children.push(...batch);
|
||||
}
|
||||
|
||||
if (children.length === 0) {
|
||||
emptyDirs.push(path);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const child of children) {
|
||||
await readEntry(child, `${path}/${child.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
await readEntry(entry, entry.name);
|
||||
try {
|
||||
for (const entry of entries) {
|
||||
await readEntry(entry, entry.name);
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(t("fileManager.failedToUploadFile"));
|
||||
console.error("Failed to read dropped folder:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (files.length === 0) return;
|
||||
if (files.length === 0 && emptyDirs.length === 0) return;
|
||||
|
||||
const progressToast = toast.loading(
|
||||
`Uploading ${files.length} file(s)...`,
|
||||
t("fileManager.uploadingFolderFiles", { count: files.length }),
|
||||
{ duration: Infinity },
|
||||
);
|
||||
|
||||
const failed: string[] = [];
|
||||
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
|
||||
const base = currentPath.endsWith("/") ? currentPath : currentPath + "/";
|
||||
|
||||
const dirs = new Set<string>();
|
||||
for (const { relativePath } of files) {
|
||||
for (const relativePath of [
|
||||
...files.map((f) => f.relativePath),
|
||||
...emptyDirs.map((d) => `${d}/`),
|
||||
]) {
|
||||
const parts = relativePath.split("/");
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
dirs.add(parts.slice(0, i).join("/"));
|
||||
}
|
||||
}
|
||||
|
||||
const sortedDirs = Array.from(dirs).sort();
|
||||
// Shallowest first so each parent exists before its children.
|
||||
const sortedDirs = Array.from(dirs).sort(
|
||||
(a, b) =>
|
||||
a.split("/").length - b.split("/").length || a.localeCompare(b),
|
||||
);
|
||||
for (const dir of sortedDirs) {
|
||||
const parentPath = currentPath.endsWith("/")
|
||||
? currentPath + dir.split("/").slice(0, -1).join("/")
|
||||
: currentPath + "/" + dir.split("/").slice(0, -1).join("/");
|
||||
const parentDir = dir.split("/").slice(0, -1).join("/");
|
||||
const targetPath = parentDir ? `${base}${parentDir}/` : base;
|
||||
const folderName = dir.split("/").pop()!;
|
||||
const targetPath = parentPath.endsWith("/")
|
||||
? parentPath
|
||||
: parentPath + "/";
|
||||
try {
|
||||
await createSSHFolder(
|
||||
sshSessionId,
|
||||
@@ -1060,23 +1082,37 @@ function FileManagerContent({
|
||||
const dirPart = relativePath.includes("/")
|
||||
? relativePath.substring(0, relativePath.lastIndexOf("/"))
|
||||
: "";
|
||||
const uploadPath = dirPart
|
||||
? (currentPath.endsWith("/") ? currentPath : currentPath + "/") +
|
||||
dirPart +
|
||||
"/"
|
||||
: currentPath;
|
||||
const uploadPath = dirPart ? `${base}${dirPart}/` : currentPath;
|
||||
|
||||
await uploadSSHFile(
|
||||
sshSessionId,
|
||||
uploadPath,
|
||||
file.name,
|
||||
file,
|
||||
currentHost?.id,
|
||||
);
|
||||
try {
|
||||
await uploadSSHFile(
|
||||
sshSessionId,
|
||||
uploadPath,
|
||||
file.name,
|
||||
file,
|
||||
currentHost?.id,
|
||||
);
|
||||
} catch (error) {
|
||||
failed.push(relativePath);
|
||||
console.error(`Failed to upload ${relativePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
toast.dismiss(progressToast);
|
||||
toast.success(`Uploaded ${files.length} file(s) successfully`);
|
||||
if (failed.length === 0) {
|
||||
toast.success(
|
||||
t("fileManager.uploadedFolderFiles", { count: files.length }),
|
||||
);
|
||||
} else if (failed.length === files.length) {
|
||||
toast.error(t("fileManager.failedToUploadFile"));
|
||||
} else {
|
||||
toast.warning(
|
||||
t("fileManager.uploadedFolderPartial", {
|
||||
uploaded: files.length - failed.length,
|
||||
failed: failed.length,
|
||||
}),
|
||||
);
|
||||
}
|
||||
handleRefreshDirectory();
|
||||
} catch (error) {
|
||||
toast.dismiss(progressToast);
|
||||
@@ -1166,14 +1202,51 @@ function FileManagerContent({
|
||||
async function handleDownloadFile(file: FileItem) {
|
||||
if (!sshSessionId) return;
|
||||
|
||||
const toastId = `download-${file.path}-${Date.now()}`;
|
||||
let lastLoaded = 0;
|
||||
let lastTime = Date.now();
|
||||
let mbPerSec: number | undefined;
|
||||
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
|
||||
const { downloadSSHFileStream } = await import("@/main-axios.ts");
|
||||
await downloadSSHFileStream(sshSessionId, file.path);
|
||||
|
||||
toast.loading(<DownloadProgressToast fileName={file.name} loaded={0} />, {
|
||||
id: toastId,
|
||||
duration: Infinity,
|
||||
});
|
||||
|
||||
await downloadSSHFileStream(
|
||||
sshSessionId,
|
||||
file.path,
|
||||
({ loaded, total }) => {
|
||||
const now = Date.now();
|
||||
const deltaMs = now - lastTime;
|
||||
if (deltaMs > 200) {
|
||||
const deltaBytes = loaded - lastLoaded;
|
||||
if (deltaBytes >= 0) {
|
||||
mbPerSec = (deltaBytes / deltaMs / 1024 / 1024) * 1000;
|
||||
}
|
||||
lastLoaded = loaded;
|
||||
lastTime = now;
|
||||
}
|
||||
|
||||
toast.loading(
|
||||
<DownloadProgressToast
|
||||
fileName={file.name}
|
||||
loaded={loaded}
|
||||
total={total}
|
||||
mbPerSec={mbPerSec}
|
||||
/>,
|
||||
{ id: toastId, duration: Infinity },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
toast.success(
|
||||
t("fileManager.fileDownloadedSuccessfully", { name: file.name }),
|
||||
{ id: toastId },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const err = error instanceof Error ? error : null;
|
||||
@@ -1187,9 +1260,10 @@ function FileManagerContent({
|
||||
ip: currentHost?.ip,
|
||||
port: currentHost?.port,
|
||||
}),
|
||||
{ id: toastId },
|
||||
);
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToDownloadFile"));
|
||||
toast.error(t("fileManager.failedToDownloadFile"), { id: toastId });
|
||||
}
|
||||
console.error("Download failed:", error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user