mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
fix: finish 2.9.0 plugin moves and get every check green
This commit is contained in:
1 parent
8c1e02af89
commit
6489b62ece
335 files changed
+29878
-17749
No files matched your search
@@ -9,6 +9,12 @@ import type { PluginContext, PluginSshHost } from "@termix/plugin-sdk/backend";
|
||||
import { getErrorMessage } from "./error-message.js";
|
||||
import { setPluginSsh, pluginSsh } from "./ssh.js";
|
||||
import { setPluginCtx } from "./plugin-ctx.js";
|
||||
import {
|
||||
flushTransferProfiles,
|
||||
setTransferProfileStore,
|
||||
} from "./transfer-tuning.js";
|
||||
|
||||
const TRANSFER_PROFILES_KEY = "transfer-profiles";
|
||||
import { AuthManager } from "../../../../src/backend/utils/auth-manager.js";
|
||||
import { logger } from "../../../../src/backend/utils/logger.js";
|
||||
import {
|
||||
@@ -113,6 +119,14 @@ export async function activate(ctx: PluginContext) {
|
||||
setPluginCtx(ctx);
|
||||
ctx.disposables.add(() => setPluginSsh(null));
|
||||
ctx.disposables.add(() => setPluginCtx(null));
|
||||
setTransferProfileStore({
|
||||
read: () => ctx.kv.get(TRANSFER_PROFILES_KEY),
|
||||
write: (value) => ctx.kv.set(TRANSFER_PROFILES_KEY, value),
|
||||
});
|
||||
ctx.disposables.add(async () => {
|
||||
await flushTransferProfiles();
|
||||
setTransferProfileStore(null);
|
||||
});
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// ctx.db.define hands back an untyped table object; typed at this module's
|
||||
@@ -605,7 +619,11 @@ export async function activate(ctx: PluginContext) {
|
||||
};
|
||||
resolvedTerminalConfig = resolvedHost.terminalConfig as unknown as
|
||||
Record<string, unknown> | undefined;
|
||||
resolvedScpLegacy = resolvedHost.scpLegacy ?? false;
|
||||
resolvedScpLegacy =
|
||||
(await ctx.settings.getHost<boolean>(
|
||||
Number(resolvedHost.id),
|
||||
"scpLegacy",
|
||||
)) === true;
|
||||
if (resolvedHost.useSocks5) {
|
||||
resolvedUseSocks5 = resolvedHost.useSocks5;
|
||||
resolvedSocks5Host = resolvedHost.socks5Host;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// TODO(file-manager): move this to ctx.kv, see FINISH-LIST.md
|
||||
import { createHash } from "node:crypto";
|
||||
import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export interface TransferPerformanceProfile {
|
||||
throughputBps: number;
|
||||
@@ -32,15 +29,32 @@ const MB = 1024 * 1024;
|
||||
const PROFILE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const MAX_PROFILES = 128;
|
||||
const STORE_VERSION = 1;
|
||||
const STORE_FILENAME = "adaptive-transfer-profiles.json";
|
||||
const profiles = new Map<string, TransferPerformanceProfile>();
|
||||
const directRoutes = new Map<string, DirectRouteProfile>();
|
||||
let loadedPath: string | undefined;
|
||||
|
||||
/** Where the learned profiles live: ctx.kv, set on activate. */
|
||||
export interface TransferProfileStore {
|
||||
read: () => Promise<unknown>;
|
||||
write: (value: PersistedProfiles) => Promise<void>;
|
||||
}
|
||||
|
||||
let store: TransferProfileStore | null = null;
|
||||
let loadedStore: TransferProfileStore | null = null;
|
||||
let loadPromise: Promise<void> | undefined;
|
||||
|
||||
export function setTransferProfileStore(
|
||||
next: TransferProfileStore | null,
|
||||
): void {
|
||||
store = next;
|
||||
loadedStore = null;
|
||||
loadPromise = undefined;
|
||||
profiles.clear();
|
||||
directRoutes.clear();
|
||||
}
|
||||
let persistTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let persistPromise = Promise.resolve();
|
||||
|
||||
interface PersistedProfiles {
|
||||
export interface PersistedProfiles {
|
||||
version: typeof STORE_VERSION;
|
||||
profiles: Record<string, TransferPerformanceProfile>;
|
||||
directRoutes: Record<string, DirectRouteProfile>;
|
||||
@@ -68,12 +82,6 @@ function validDirectRoute(value: unknown): value is DirectRouteProfile {
|
||||
);
|
||||
}
|
||||
|
||||
function storePath(): string {
|
||||
const dataDir =
|
||||
process.env.DATA_DIR || path.join(process.cwd(), "db", "data");
|
||||
return path.join(dataDir, STORE_FILENAME);
|
||||
}
|
||||
|
||||
function profileId(key: string): string {
|
||||
return createHash("sha256").update(key).digest("hex");
|
||||
}
|
||||
@@ -117,22 +125,15 @@ function trimProfiles(now = Date.now()): void {
|
||||
|
||||
async function persistProfiles(): Promise<void> {
|
||||
trimProfiles();
|
||||
const target = storePath();
|
||||
const temporary = `${target}.${process.pid}.tmp`;
|
||||
const payload: PersistedProfiles = {
|
||||
version: STORE_VERSION,
|
||||
profiles: Object.fromEntries(profiles),
|
||||
directRoutes: Object.fromEntries(directRoutes),
|
||||
};
|
||||
if (!store) return;
|
||||
try {
|
||||
await fs.mkdir(path.dirname(target), { recursive: true });
|
||||
await fs.writeFile(temporary, JSON.stringify(payload), {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
await store.write({
|
||||
version: STORE_VERSION,
|
||||
profiles: Object.fromEntries(profiles),
|
||||
directRoutes: Object.fromEntries(directRoutes),
|
||||
});
|
||||
await fs.rename(temporary, target);
|
||||
} catch {
|
||||
await fs.rm(temporary, { force: true }).catch(() => {});
|
||||
// Learning data is a nicety; a failed write must not affect transfers.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,16 +147,16 @@ function queuePersist(): void {
|
||||
}
|
||||
|
||||
export async function initializeTransferProfiles(): Promise<void> {
|
||||
const target = storePath();
|
||||
if (loadedPath === target) return loadPromise;
|
||||
loadedPath = target;
|
||||
if (loadPromise && loadedStore === store) return loadPromise;
|
||||
loadedStore = store;
|
||||
const current = store;
|
||||
loadPromise = (async () => {
|
||||
profiles.clear();
|
||||
directRoutes.clear();
|
||||
if (!current) return;
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
await fs.readFile(target, "utf8"),
|
||||
) as Partial<PersistedProfiles>;
|
||||
const parsed = ((await current.read()) ??
|
||||
{}) as Partial<PersistedProfiles>;
|
||||
if (parsed.version !== STORE_VERSION || !parsed.profiles) return;
|
||||
for (const [key, profile] of Object.entries(parsed.profiles)) {
|
||||
if (/^[a-f0-9]{64}$/.test(key) && validProfile(profile)) {
|
||||
@@ -381,7 +382,7 @@ export function getRecentDirectRouteDecision(
|
||||
export function clearTransferProfiles(): void {
|
||||
if (persistTimer) clearTimeout(persistTimer);
|
||||
persistTimer = undefined;
|
||||
loadedPath = undefined;
|
||||
loadedStore = null;
|
||||
loadPromise = undefined;
|
||||
profiles.clear();
|
||||
directRoutes.clear();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fileManagerHostSetting } from "./host-settings";
|
||||
import { getErrorMessage } from "@/lib/error-message.js";
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, {
|
||||
@@ -44,7 +45,11 @@ import {
|
||||
ConnectionScreen,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { ConnectionLogPanel } from "@/components/connection/ConnectionLogPanel.tsx";
|
||||
import { invokeAction, useConnectionRetry } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
invokeAction,
|
||||
useConnectionRetry,
|
||||
logActivity,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { copyToClipboard } from "@/lib/clipboard.ts";
|
||||
import {
|
||||
listSSHFiles,
|
||||
@@ -77,7 +82,6 @@ import {
|
||||
getPinnedFiles,
|
||||
} from "./api/file-manager-data-api";
|
||||
import {
|
||||
logActivity,
|
||||
transferToHost,
|
||||
addTransferRecent,
|
||||
type TransferMethodPreference,
|
||||
@@ -145,15 +149,21 @@ function FileManagerContent({
|
||||
|
||||
const [currentHost] = useState<SSHHost | null>(initialHost || null);
|
||||
const [currentPath, setCurrentPath] = useState(
|
||||
initialPath || initialHost?.defaultPath || "/",
|
||||
initialPath ||
|
||||
fileManagerHostSetting(initialHost, "defaultPath", "") ||
|
||||
"/",
|
||||
);
|
||||
const currentPathRef = useRef(currentPath);
|
||||
currentPathRef.current = currentPath;
|
||||
const lastSuccessfulPathRef = useRef(
|
||||
initialPath || initialHost?.defaultPath || "/",
|
||||
initialPath ||
|
||||
fileManagerHostSetting(initialHost, "defaultPath", "") ||
|
||||
"/",
|
||||
);
|
||||
const [navHistory, setNavHistory] = useState<string[]>([
|
||||
initialPath || initialHost?.defaultPath || "/",
|
||||
initialPath ||
|
||||
fileManagerHostSetting(initialHost, "defaultPath", "") ||
|
||||
"/",
|
||||
]);
|
||||
const [navIndex, setNavIndex] = useState(0);
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
Input,
|
||||
SectionCard,
|
||||
SettingRow,
|
||||
FakeSwitch,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { FolderSearch } from "lucide-react";
|
||||
import type { HostEditorForm } from "@/sidebar/HostEditorData";
|
||||
|
||||
type SetHostField = <K extends keyof HostEditorForm>(
|
||||
key: K,
|
||||
value: HostEditorForm[K],
|
||||
) => void;
|
||||
|
||||
export function HostFilesTab({
|
||||
form,
|
||||
setField,
|
||||
}: {
|
||||
form: HostEditorForm;
|
||||
setField: SetHostField;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("hosts.fileManager")}
|
||||
icon={<FolderSearch className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-4 py-3">
|
||||
<SettingRow
|
||||
label={t("hosts.enableFileManagerMonitor")}
|
||||
description={t("hosts.enableFileManagerMonitorDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.enableFileManager}
|
||||
onChange={(v) => setField("enableFileManager", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("hosts.scpLegacyLabel")}
|
||||
description={t("hosts.scpLegacyDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.scpLegacy}
|
||||
onChange={(v) => setField("scpLegacy", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("hosts.defaultPathLabel")}
|
||||
</label>
|
||||
<Input
|
||||
placeholder="/"
|
||||
value={form.defaultPath}
|
||||
onChange={(e) => setField("defaultPath", e.target.value)}
|
||||
/>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("hosts.fileManagerPathHint")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fileManagerHostSetting } from "./host-settings";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -372,11 +373,11 @@ function RemotePane({
|
||||
...current,
|
||||
connectionState: "ready",
|
||||
sessionId: result.sessionId || String(host.id),
|
||||
path: host.defaultPath || "/",
|
||||
path: fileManagerHostSetting(host, "defaultPath", "") || "/",
|
||||
}));
|
||||
await loadRemotePath(
|
||||
result.sessionId || String(host.id),
|
||||
host.defaultPath || "/",
|
||||
fileManagerHostSetting(host, "defaultPath", "") || "/",
|
||||
);
|
||||
},
|
||||
[loadRemotePath, setPane],
|
||||
@@ -585,7 +586,7 @@ export function SftpTransferTab() {
|
||||
setHosts(
|
||||
data.filter(
|
||||
(host) =>
|
||||
host.enableFileManager !== false &&
|
||||
fileManagerHostSetting(host, "enableFileManager", true) &&
|
||||
host.connectionType !== "rdp" &&
|
||||
host.connectionType !== "vnc",
|
||||
),
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import React, { useState } from "react";
|
||||
import { Document, Page } from "react-pdf";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
import { AlertCircle, Download } from "lucide-react";
|
||||
import { Button } from "@termix/plugin-sdk/ui";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import pdfjsWorkerUrl from "pdfjs-dist/build/pdf.worker.min.mjs?url";
|
||||
|
||||
// No workerSrc: the termix-plugin CLI's esbuild step has no asset-copy
|
||||
// pipeline for a worker file the way core's Vite build does with `?url`, so
|
||||
// pdfjs falls back to parsing on the main thread instead. Fine for the
|
||||
// config/log-sized files this preview usually opens; a real worker needs the
|
||||
// CLI to gain static-asset output first (see FINISH-LIST.md).
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl;
|
||||
|
||||
interface PdfPreviewProps {
|
||||
content: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fileManagerHostSetting } from "../host-settings";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -236,7 +237,7 @@ export function TransferToHostDialog({
|
||||
const candidates = hosts.filter(
|
||||
(h) =>
|
||||
h.id !== sourceHost.id &&
|
||||
h.enableFileManager !== false &&
|
||||
fileManagerHostSetting(h, "enableFileManager", true) &&
|
||||
h.connectionType !== "rdp" &&
|
||||
h.connectionType !== "vnc",
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fileManagerHostSetting } from "../host-settings";
|
||||
import { useHosts, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Select2 } from "@termix/plugin-sdk/ui";
|
||||
import type {
|
||||
@@ -12,7 +13,9 @@ export function FileManagerWidgetEditForm({
|
||||
const { t } = useTranslation();
|
||||
const { hosts } = useHosts();
|
||||
const options = hosts.filter(
|
||||
(host) => host.enableSsh !== false && host.enableFileManager !== false,
|
||||
(host) =>
|
||||
host.enableSsh !== false &&
|
||||
fileManagerHostSetting(host, "enableFileManager", true),
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/** One of this plugin's host settings, from the host payload. */
|
||||
export function fileManagerHostSetting<T>(
|
||||
host: object | null | undefined,
|
||||
key: "enableFileManager" | "defaultPath" | "scpLegacy",
|
||||
fallback: T,
|
||||
): T {
|
||||
const settings = (
|
||||
host as { pluginSettings?: Record<string, Record<string, unknown>> } | null
|
||||
)?.pluginSettings;
|
||||
const value = settings?.["file-manager"]?.[key];
|
||||
return value === undefined || value === null || value === ""
|
||||
? fallback
|
||||
: (value as T);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { fileManagerHostSetting } from "./host-settings";
|
||||
import type { ComponentType } from "react";
|
||||
import { FolderSearch, ArrowLeftRight } from "lucide-react";
|
||||
import {
|
||||
@@ -11,7 +12,6 @@ import { GRID_SIZE } from "./homepage/homepage.js";
|
||||
import { FileManager } from "./FileManager.tsx";
|
||||
import FileManagerApp from "./FileManagerApp.tsx";
|
||||
import { SftpTransferTab } from "./SftpTransferTab.tsx";
|
||||
import { HostFilesTab } from "./HostFilesTab.tsx";
|
||||
import { FileManagerWidget } from "./homepage/FileManagerWidget.tsx";
|
||||
import { FileManagerWidgetEditForm } from "./homepage/FileManagerWidgetEditForm.tsx";
|
||||
import { startTransferMonitor } from "./TransferMonitor.tsx";
|
||||
@@ -104,16 +104,9 @@ export function activate(app: TermixApp): void {
|
||||
order: 20,
|
||||
tabType: "files",
|
||||
copyUrlView: "file-manager",
|
||||
when: (host) => !!host.enableSsh && host.enableFileManager !== false,
|
||||
});
|
||||
|
||||
app.registerHostEditorSection({
|
||||
id: "files",
|
||||
group: "ssh",
|
||||
titleKey: "hosts.tabFiles",
|
||||
icon: FolderSearch,
|
||||
order: 60,
|
||||
component: HostFilesTab as never,
|
||||
when: (host) =>
|
||||
!!host.enableSsh &&
|
||||
fileManagerHostSetting(host, "enableFileManager", true),
|
||||
});
|
||||
|
||||
app.registerHomepageWidget({
|
||||
|
||||
Reference in new issue
Block a user