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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user