mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: convert file manager into a plugin
This commit is contained in:
1 parent
b76da9ab52
commit
c85c1fdb86
259 files changed
+54155
-16791
No files matched your search
@@ -1,156 +0,0 @@
|
||||
import { authApi, handleApiError } from "@/main-axios";
|
||||
|
||||
/** Row shape shared by the recent / pinned / shortcut list endpoints. */
|
||||
export interface FileManagerEntry {
|
||||
id: number;
|
||||
name: string;
|
||||
path: string;
|
||||
lastOpened?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// FILE MANAGER DATA
|
||||
// ============================================================================
|
||||
|
||||
export async function getRecentFiles(
|
||||
hostId: number,
|
||||
): Promise<FileManagerEntry[]> {
|
||||
try {
|
||||
const response = await authApi.get("/host/file_manager/recent", {
|
||||
params: { hostId },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "get recent files");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addRecentFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
name?: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/host/file_manager/recent", {
|
||||
hostId,
|
||||
path,
|
||||
name,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "add recent file");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeRecentFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/host/file_manager/recent", {
|
||||
data: { hostId, path },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "remove recent file");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPinnedFiles(
|
||||
hostId: number,
|
||||
): Promise<FileManagerEntry[]> {
|
||||
try {
|
||||
const response = await authApi.get("/host/file_manager/pinned", {
|
||||
params: { hostId },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "get pinned files");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPinnedFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
name?: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/host/file_manager/pinned", {
|
||||
hostId,
|
||||
path,
|
||||
name,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "add pinned file");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removePinnedFile(
|
||||
hostId: number,
|
||||
path: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/host/file_manager/pinned", {
|
||||
data: { hostId, path },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "remove pinned file");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFolderShortcuts(
|
||||
hostId: number,
|
||||
): Promise<FileManagerEntry[]> {
|
||||
try {
|
||||
const response = await authApi.get("/host/file_manager/shortcuts", {
|
||||
params: { hostId },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "get folder shortcuts");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addFolderShortcut(
|
||||
hostId: number,
|
||||
path: string,
|
||||
name?: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.post("/host/file_manager/shortcuts", {
|
||||
hostId,
|
||||
path,
|
||||
name,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "add folder shortcut");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFolderShortcut(
|
||||
hostId: number,
|
||||
path: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await authApi.delete("/host/file_manager/shortcuts", {
|
||||
data: { hostId, path },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "remove folder shortcut");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1,126 +0,0 @@
|
||||
import { handleApiError, sshHostApi } from "@/main-axios";
|
||||
import type { FileManagerFile, FileManagerShortcut } from "@/types/index";
|
||||
|
||||
interface FileManagerOperation {
|
||||
name: string;
|
||||
path: string;
|
||||
isSSH: boolean;
|
||||
sshSessionId?: string;
|
||||
hostId: number;
|
||||
}
|
||||
|
||||
// FILE MANAGER METADATA (Recent, Pinned, Shortcuts)
|
||||
// ============================================================================
|
||||
|
||||
export async function getFileManagerRecent(
|
||||
hostId: number,
|
||||
): Promise<FileManagerFile[]> {
|
||||
try {
|
||||
const response = await sshHostApi.get(
|
||||
`/file_manager/recent?hostId=${hostId}`,
|
||||
);
|
||||
return response.data || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function addFileManagerRecent(
|
||||
file: FileManagerOperation,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post("/file_manager/recent", file);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "add recent file");
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileManagerRecent(
|
||||
file: FileManagerOperation,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete("/file_manager/recent", {
|
||||
data: file,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "remove recent file");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileManagerPinned(
|
||||
hostId: number,
|
||||
): Promise<FileManagerFile[]> {
|
||||
try {
|
||||
const response = await sshHostApi.get(
|
||||
`/file_manager/pinned?hostId=${hostId}`,
|
||||
);
|
||||
return response.data || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function addFileManagerPinned(
|
||||
file: FileManagerOperation,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post("/file_manager/pinned", file);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "add pinned file");
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileManagerPinned(
|
||||
file: FileManagerOperation,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete("/file_manager/pinned", {
|
||||
data: file,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "remove pinned file");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileManagerShortcuts(
|
||||
hostId: number,
|
||||
): Promise<FileManagerShortcut[]> {
|
||||
try {
|
||||
const response = await sshHostApi.get(
|
||||
`/file_manager/shortcuts?hostId=${hostId}`,
|
||||
);
|
||||
return response.data || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function addFileManagerShortcut(
|
||||
shortcut: FileManagerOperation,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.post("/file_manager/shortcuts", shortcut);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "add shortcut");
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeFileManagerShortcut(
|
||||
shortcut: FileManagerOperation,
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
const response = await sshHostApi.delete("/file_manager/shortcuts", {
|
||||
data: shortcut,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
handleApiError(error, "remove shortcut");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1,184 +0,0 @@
|
||||
// Local disk <-> remote SFTP transfers for the desktop app's dual-pane file
|
||||
// manager. The bytes never pass through the renderer: the Electron main
|
||||
// process streams them between disk and the backend's existing
|
||||
// `uploadFileStream` / `downloadFileStream` routes (see
|
||||
// electron/local-files.cjs), and we only orchestrate + relay progress here.
|
||||
//
|
||||
// The renderer deliberately does not choose the URL or the auth headers. It
|
||||
// names the origin that owns the SSH session ("local" embedded backend or the
|
||||
// configured Remote Sync server) and the main process resolves the rest, so a
|
||||
// compromised renderer cannot turn the transfer bridge into an HTTP client.
|
||||
|
||||
import type {
|
||||
LocalTransferOrigin,
|
||||
LocalTransferProgress,
|
||||
} from "@/types/electron";
|
||||
import { getSessionOrigin } from "@/main-axios";
|
||||
import { getDeviceId } from "@/lib/device-id";
|
||||
|
||||
export interface LocalTransferProgressEvent {
|
||||
transferred: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
type ProgressListener = (event: LocalTransferProgressEvent) => void;
|
||||
|
||||
/** Error from the main process; `code` is e.g. "EEXIST" or "EBUSY". */
|
||||
export class LocalTransferError extends Error {
|
||||
code?: string;
|
||||
constructor(message: string, code?: string) {
|
||||
super(message);
|
||||
this.name = "LocalTransferError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const progressListeners = new Map<string, ProgressListener>();
|
||||
let unsubscribeProgress: (() => void) | null = null;
|
||||
|
||||
function ensureProgressSubscription() {
|
||||
if (unsubscribeProgress) return;
|
||||
const api = window.electronAPI?.localTransfer;
|
||||
if (!api) return;
|
||||
unsubscribeProgress = api.onProgress((payload: LocalTransferProgress) => {
|
||||
const listener = progressListeners.get(payload.transferId);
|
||||
listener?.({ transferred: payload.transferred, total: payload.total });
|
||||
});
|
||||
}
|
||||
|
||||
function requireTransferApi() {
|
||||
const api = window.electronAPI?.localTransfer;
|
||||
if (!api) {
|
||||
throw new Error("Local transfers are only available in the desktop app");
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
function transferOriginFor(sessionId: string): LocalTransferOrigin {
|
||||
return getSessionOrigin(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* The token the desktop renderer uses for the embedded backend. The `jwt`
|
||||
* cookie the main process would otherwise rely on lasts only a day after an
|
||||
* interactive login, while this token (and every request the app makes with
|
||||
* it) keeps working, so transfers must carry it too. Remote-origin transfers
|
||||
* are authenticated by the main process from its own Remote Sync state.
|
||||
*/
|
||||
function localAuthTokenFor(origin: LocalTransferOrigin): string | undefined {
|
||||
if (origin !== "local") return undefined;
|
||||
try {
|
||||
return localStorage.getItem("jwt") || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function createLocalTransferId(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams a file from the local disk into `remoteDir` on the SSH session,
|
||||
* using the same multipart route the in-browser upload uses.
|
||||
*/
|
||||
export async function uploadLocalFileToSession(options: {
|
||||
sessionId: string;
|
||||
remoteDir: string;
|
||||
localPath: string;
|
||||
fileName: string;
|
||||
hostId?: number;
|
||||
transferId?: string;
|
||||
onProgress?: ProgressListener;
|
||||
}): Promise<void> {
|
||||
const api = requireTransferApi();
|
||||
ensureProgressSubscription();
|
||||
|
||||
const transferId =
|
||||
options.transferId ?? createLocalTransferId("local-upload");
|
||||
|
||||
const fields: Record<string, string> = {
|
||||
sessionId: options.sessionId,
|
||||
path: options.remoteDir,
|
||||
};
|
||||
if (options.hostId !== undefined) fields.hostId = String(options.hostId);
|
||||
|
||||
if (options.onProgress) {
|
||||
progressListeners.set(transferId, options.onProgress);
|
||||
}
|
||||
const origin = transferOriginFor(options.sessionId);
|
||||
try {
|
||||
const result = await api.upload({
|
||||
transferId,
|
||||
origin,
|
||||
deviceId: getDeviceId() ?? undefined,
|
||||
authToken: localAuthTokenFor(origin),
|
||||
fields,
|
||||
localPath: options.localPath,
|
||||
fileName: options.fileName,
|
||||
});
|
||||
if (result.success !== true) {
|
||||
throw new LocalTransferError(
|
||||
result.error || "Upload failed",
|
||||
result.code,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
progressListeners.delete(transferId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams a remote file from the SSH session straight to `destPath` on the
|
||||
* local disk. Refuses to replace an existing file (code "EEXIST") unless
|
||||
* `overwrite` is set.
|
||||
*/
|
||||
export async function downloadSessionFileToLocal(options: {
|
||||
sessionId: string;
|
||||
remotePath: string;
|
||||
destPath: string;
|
||||
/** The folder the user picked; the main process refuses any destination outside it. */
|
||||
rootPath: string;
|
||||
expectedSize?: number;
|
||||
overwrite?: boolean;
|
||||
transferId?: string;
|
||||
onProgress?: ProgressListener;
|
||||
}): Promise<void> {
|
||||
const api = requireTransferApi();
|
||||
ensureProgressSubscription();
|
||||
|
||||
const transferId =
|
||||
options.transferId ?? createLocalTransferId("local-download");
|
||||
|
||||
if (options.onProgress) {
|
||||
progressListeners.set(transferId, options.onProgress);
|
||||
}
|
||||
const origin = transferOriginFor(options.sessionId);
|
||||
try {
|
||||
const result = await api.download({
|
||||
transferId,
|
||||
origin,
|
||||
deviceId: getDeviceId() ?? undefined,
|
||||
authToken: localAuthTokenFor(origin),
|
||||
body: { sessionId: options.sessionId, path: options.remotePath },
|
||||
destPath: options.destPath,
|
||||
rootPath: options.rootPath,
|
||||
expectedSize: options.expectedSize,
|
||||
overwrite: options.overwrite === true,
|
||||
});
|
||||
if (result.success !== true) {
|
||||
throw new LocalTransferError(
|
||||
result.error || "Download failed",
|
||||
result.code,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
progressListeners.delete(transferId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelLocalTransfer(transferId: string): Promise<void> {
|
||||
const api = window.electronAPI?.localTransfer;
|
||||
if (!api) return;
|
||||
await api.cancel(transferId);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user