fix(plugins): install plugin packages from a .zip (#896)

Root cause: Install Plugin from Disk only opened a folder picker and the host could only copy folders, so a zipped plugin could not be installed. The picker now accepts a .zip or a package's plugin.toml, and the host's install_from_path unpacks a zip to a temporary folder and runs it through the existing install_from_dir validation. The package may sit at the archive root or in its only folder (archiver metadata such as __MACOSX is ignored). Archives with unsafe paths or symbolic links, more than 10,000 entries or over 1 GB unpacked are refused, and a zip without plugin.toml (e.g. an IntelliJ plugin) gets a clear error.

Fixes #892

Co-authored-by: florentvdry <[email protected]>
Цей коміт міститься в:
Florent Vтаflorentvdry authored and GitHub committed 2026-11-03 01:44:28 +08:00
1 джерело eb633f6ca6
коміт db5251efe6
12 files changed
+208 -21

No files matched your search

+2 -2
Переглянути файл
@@ -853,7 +853,7 @@ export const commands = {
*/
pluginList: () => typedError<PluginsSnapshot, IdeError>(__TAURI_INVOKE("plugin_list")),
pluginSetEnabled: (id: string, enabled: boolean) => typedError<null, IdeError>(__TAURI_INVOKE("plugin_set_enabled", { id, enabled })),
/** Installs the plugin package in `path` (a folder with `plugin.toml`). */
/** Installs the plugin package at `path`: a folder, its `plugin.toml`, or a `.zip` of the folder. */
pluginInstallDir: (path: string) => typedError<PluginInfo, IdeError>(__TAURI_INVOKE("plugin_install_dir", { path })),
/**
* Plugin packages available locally for installation: `<resources>/plugins`
@@ -1688,7 +1688,7 @@ export type CatalogPlugin = {
authors: string[],
homepage: string | null,
changeNotes: string | null,
/** Package directory; install with `install_from_dir(path)`. */
/** Package directory; install with `install_from_path(path)`. */
path: IpcPath,
hasCode: boolean,
permissions: Permission[],
+10 -4
Переглянути файл
@@ -5,6 +5,7 @@
*/
import { Channel } from "@tauri-apps/api/core";
import type { ActiveTheme, CatalogPlugin, PluginEvent, PluginInfo, PluginsSnapshot } from "@/bindings";
import { tx } from "@/platform/i18n";
import { call, commands, IpcError, isTauri } from "@/platform/ipc";
export interface PluginsBackend {
@@ -21,7 +22,8 @@ export interface PluginsBackend {
/** The selected theme's resolved frame colour (`#rrggbb[aa]`), which new windows open in. */
setThemeFrame(color: string | null): Promise<void>;
/** Folder picker; `null` when cancelled. */
pickFolder(): Promise<string | null>;
/** A package to install: a `.zip`, or the `plugin.toml` of a folder. */
pickPackage(): Promise<string | null>;
/** Subscribes to backend plugin events (this window only). */
onEvent(handler: (e: PluginEvent) => void): () => void;
}
@@ -45,9 +47,13 @@ export const tauriPluginsBackend: PluginsBackend = {
setThemeFrame: async (color) => {
await call(commands.pluginSetThemeFrame(color));
},
pickFolder: async () => {
pickPackage: async () => {
const { open } = await import("@tauri-apps/plugin-dialog");
const picked = await open({ directory: true, multiple: false, title: "Install Plugin from Disk" });
const picked = await open({
multiple: false,
title: tx("Install Plugin from Disk"),
filters: [{ name: tx("Plugin package"), extensions: ["zip", "toml"] }],
});
return (Array.isArray(picked) ? picked[0] : picked) ?? null;
},
onEvent: (handler) => {
@@ -247,7 +253,7 @@ export function createMemoryPluginsBackend(
return t;
},
setThemeFrame: async () => {},
pickFolder: async () => null,
pickPackage: async () => null,
onEvent: (handler) => {
listeners.add(handler);
return () => void listeners.delete(handler);
+6 -6
Переглянути файл
@@ -124,25 +124,25 @@ export function compareVersions(a: string, b: string): number {
return 0;
}
/** Asks for a plugin folder (containing plugin.toml) and installs it. Returns the new plugin id. */
/** Asks for a plugin package (a `.zip` or a `plugin.toml`) and installs it. Returns the new plugin id. */
export async function installFromDisk(path?: string): Promise<string | null> {
const backend = pluginsBackend();
let dir: string | null = path ?? null;
let source: string | null = path ?? null;
try {
dir ??= await backend.pickFolder();
source ??= await backend.pickPackage();
} catch (e) {
notifications().notify({
group: "plugins",
level: "error",
title: "Could not open the folder picker",
title: "Could not open the file picker",
body: errorText(e),
});
return null;
}
if (!dir) return null;
if (!source) return null;
usePlugins.setState({ installing: true });
try {
const info = await backend.installDir(dir);
const info = await backend.installDir(source);
notifications().notify({
group: "plugins",
level: info.status === "failed" ? "warning" : "info",
+2 -1
Переглянути файл
@@ -523,7 +523,8 @@
"Could not install plugin": "无法安装插件",
"Could not invalidate caches": "无法清除缓存",
"Could not open project": "无法打开项目",
"Could not open the folder picker": "无法打开文件夹选择器",
"Could not open the file picker": "无法打开文件选择器",
"Plugin package": "插件包",
"Could not reach GitHub": "无法连接到 GitHub",
"Could not save": "无法保存",
"Coverage": "覆盖率",