feat: Add archive extraction feature to file manager

Adds comprehensive archive extraction support to the file manager context menu:

Backend API (file-manager.ts):
- New POST /ssh/file_manager/ssh/extractArchive endpoint
- Supports multiple archive formats: .zip, .tar, .tar.gz, .tgz, .tar.bz2, .tbz2, .tar.xz, .gz, .bz2, .xz, .7z, .rar
- Automatically selects appropriate extraction command based on file extension
- Extracts to current directory by default, supports custom extraction path
- Proper error handling and logging for extraction operations

Frontend Implementation:
- Added extractSSHArchive() function in main-axios.ts for API calls
- Added handleExtractArchive() handler in FileManager.tsx
- FileManagerContextMenu displays 'Extract Archive' option for supported archive files
- FileArchive icon from lucide-react for visual clarity
- Keyboard shortcut: Ctrl+E

User Experience:
- Right-click on any supported archive file to see 'Extract Archive' option
- Toast notifications for progress and success/error states
- Automatic directory refresh after extraction to show extracted files
- Only shows extract option for recognized archive file types

i18n Support:
- English translations: extractArchive, extractingArchive, archiveExtractedSuccessfully, extractFailed
- Chinese translations: 解压文件, 正在解压, 解压成功, 解压失败

Supported Archive Formats:
- ZIP archives (.zip)
- TAR archives (.tar, .tar.gz, .tgz, .tar.bz2, .tbz2, .tar.xz)
- Compressed files (.gz, .bz2, .xz)
- 7-Zip archives (.7z)
- RAR archives (.rar)

This feature streamlines file management workflows by allowing users to extract archives directly from the file manager without switching to terminal.
This commit is contained in:
ZacharyZcR
2025-11-09 17:56:08 +08:00
parent 303386806f
commit c4c5be34f2
6 changed files with 239 additions and 0 deletions

View File

@@ -51,6 +51,7 @@ import {
getPinnedFiles,
logActivity,
changeSSHPermissions,
extractSSHArchive,
} from "@/ui/main-axios.ts";
import type { SidebarItem } from "./FileManagerSidebar";
@@ -1061,6 +1062,34 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
}
}
async function handleExtractArchive(file: FileItem) {
if (!sshSessionId) return;
try {
await ensureSSHConnection();
toast.info(t("fileManager.extractingArchive", { name: file.name }));
await extractSSHArchive(
sshSessionId,
file.path,
undefined,
currentHost?.id,
currentHost?.userId?.toString(),
);
toast.success(t("fileManager.archiveExtractedSuccessfully", { name: file.name }));
// Refresh directory to show extracted files
handleRefreshDirectory();
} catch (error: unknown) {
const err = error as { message?: string };
toast.error(
`${t("fileManager.extractFailed")}: ${err.message || t("fileManager.unknownError")}`,
);
}
}
async function handleUndo() {
if (undoHistory.length === 0) {
toast.info(t("fileManager.noUndoableActions"));
@@ -2000,6 +2029,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
isPinned={isPinnedFile}
currentPath={currentPath}
onProperties={handleOpenPermissionsDialog}
onExtractArchive={handleExtractArchive}
/>
</div>
</div>

View File

@@ -17,6 +17,7 @@ import {
Play,
Star,
Bookmark,
FileArchive,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { Kbd, KbdGroup } from "@/components/ui/kbd";
@@ -60,6 +61,7 @@ interface ContextMenuProps {
onAddShortcut?: (path: string) => void;
isPinned?: (file: FileItem) => boolean;
currentPath?: string;
onExtractArchive?: (file: FileItem) => void;
}
interface MenuItem {
@@ -99,6 +101,7 @@ export function FileManagerContextMenu({
onAddShortcut,
isPinned,
currentPath,
onExtractArchive,
}: ContextMenuProps) {
const { t } = useTranslation();
const [menuPosition, setMenuPosition] = useState({ x, y });
@@ -254,6 +257,33 @@ export function FileManagerContextMenu({
});
}
// Add extract option for archive files
if (isSingleFile && files[0].type === "file" && onExtractArchive) {
const fileName = files[0].name.toLowerCase();
const isArchive =
fileName.endsWith(".zip") ||
fileName.endsWith(".tar") ||
fileName.endsWith(".tar.gz") ||
fileName.endsWith(".tgz") ||
fileName.endsWith(".tar.bz2") ||
fileName.endsWith(".tbz2") ||
fileName.endsWith(".tar.xz") ||
fileName.endsWith(".gz") ||
fileName.endsWith(".bz2") ||
fileName.endsWith(".xz") ||
fileName.endsWith(".7z") ||
fileName.endsWith(".rar");
if (isArchive) {
menuItems.push({
icon: <FileArchive className="w-4 h-4" />,
label: t("fileManager.extractArchive"),
action: () => onExtractArchive(files[0]),
shortcut: "Ctrl+E",
});
}
}
if (isSingleFile && files[0].type === "file") {
const isCurrentlyPinned = isPinned ? isPinned(files[0]) : false;