Install plugin packages
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
pub mod download_checksums;
|
pub mod download_checksums;
|
||||||
pub mod download_files;
|
pub mod download_files;
|
||||||
|
pub mod plugin_packages;
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::{self, Read},
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ely_domain::PluginManifest;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PluginPackageError {
|
||||||
|
#[error("plugin package must use .rplug extension: {path}")]
|
||||||
|
InvalidPackageExtension { path: PathBuf },
|
||||||
|
|
||||||
|
#[error("plugin package is unavailable: {path}")]
|
||||||
|
PackageUnavailable { path: PathBuf },
|
||||||
|
|
||||||
|
#[error("plugin package must be a directory: {path}")]
|
||||||
|
PackageNotDirectory { path: PathBuf },
|
||||||
|
|
||||||
|
#[error("plugin package is missing {entry}: {path}")]
|
||||||
|
MissingEntry { entry: &'static str, path: PathBuf },
|
||||||
|
|
||||||
|
#[error("failed to read plugin package entry {entry}: {path}")]
|
||||||
|
ReadFailed { entry: &'static str, path: PathBuf, source: io::Error },
|
||||||
|
|
||||||
|
#[error("plugin package checksum mismatch for component.wasm")]
|
||||||
|
ChecksumMismatch,
|
||||||
|
|
||||||
|
#[error("plugin package signature mismatch for signatures/ed25519.sig")]
|
||||||
|
SignatureMismatch,
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Manifest(#[from] ely_domain::DomainError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PluginPackageReader;
|
||||||
|
|
||||||
|
impl PluginPackageReader {
|
||||||
|
pub fn read_directory_package(path: &Path) -> Result<PluginManifest, PluginPackageError> {
|
||||||
|
require_rplug_directory(path)?;
|
||||||
|
|
||||||
|
let manifest_path = path.join("plugin.toml");
|
||||||
|
require_file("plugin.toml", &manifest_path)?;
|
||||||
|
let manifest_text = read_text("plugin.toml", &manifest_path)?;
|
||||||
|
let manifest = PluginManifest::from_toml(manifest_text.as_str())?;
|
||||||
|
|
||||||
|
let component_path = path.join("component.wasm");
|
||||||
|
require_file("component.wasm", &component_path)?;
|
||||||
|
let component_checksum = sha256_file(&component_path)?;
|
||||||
|
if component_checksum != manifest.checksum() {
|
||||||
|
return Err(PluginPackageError::ChecksumMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature_path = path.join("signatures").join("ed25519.sig");
|
||||||
|
require_file("signatures/ed25519.sig", &signature_path)?;
|
||||||
|
let signature = read_text("signatures/ed25519.sig", &signature_path)?;
|
||||||
|
if signature.trim().to_ascii_lowercase() != manifest.signature().value() {
|
||||||
|
return Err(PluginPackageError::SignatureMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(manifest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_rplug_directory(path: &Path) -> Result<(), PluginPackageError> {
|
||||||
|
if path.extension().and_then(|value| value.to_str()) != Some("rplug") {
|
||||||
|
return Err(PluginPackageError::InvalidPackageExtension { path: path.to_path_buf() });
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = fs::metadata(path)
|
||||||
|
.map_err(|_| PluginPackageError::PackageUnavailable { path: path.to_path_buf() })?;
|
||||||
|
if !metadata.is_dir() {
|
||||||
|
return Err(PluginPackageError::PackageNotDirectory { path: path.to_path_buf() });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_file(entry: &'static str, path: &Path) -> Result<(), PluginPackageError> {
|
||||||
|
let metadata = fs::metadata(path)
|
||||||
|
.map_err(|_| PluginPackageError::MissingEntry { entry, path: path.to_path_buf() })?;
|
||||||
|
if !metadata.is_file() {
|
||||||
|
return Err(PluginPackageError::MissingEntry { entry, path: path.to_path_buf() });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(entry: &'static str, path: &Path) -> Result<String, PluginPackageError> {
|
||||||
|
fs::read_to_string(path).map_err(|source| PluginPackageError::ReadFailed {
|
||||||
|
entry,
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sha256_file(path: &Path) -> Result<String, PluginPackageError> {
|
||||||
|
let mut file = fs::File::open(path).map_err(|source| PluginPackageError::ReadFailed {
|
||||||
|
entry: "component.wasm",
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
let mut buffer = [0_u8; 8192];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let read = file.read(&mut buffer).map_err(|source| PluginPackageError::ReadFailed {
|
||||||
|
entry: "component.wasm",
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasher.update(&buffer[..read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!("{:x}", hasher.finalize()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fs,
|
||||||
|
path::PathBuf,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use super::PluginPackageReader;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_verified_directory_package() -> Result<(), Box<dyn Error>> {
|
||||||
|
let package = write_package("verified", b"wasm component")?;
|
||||||
|
|
||||||
|
let manifest = PluginPackageReader::read_directory_package(&package)?;
|
||||||
|
|
||||||
|
assert_eq!(manifest.id().as_str(), "com.elydora.verified");
|
||||||
|
assert_eq!(manifest.name(), "Verified Plugin");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_package_checksum_mismatch() -> Result<(), Box<dyn Error>> {
|
||||||
|
let package = write_package("checksum", b"wasm component")?;
|
||||||
|
fs::write(package.join("component.wasm"), b"changed")?;
|
||||||
|
|
||||||
|
let error = PluginPackageReader::read_directory_package(&package)
|
||||||
|
.err()
|
||||||
|
.ok_or_else(|| std::io::Error::other("package read succeeded"))?;
|
||||||
|
|
||||||
|
assert!(matches!(error, super::PluginPackageError::ChecksumMismatch));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_package_signature_mismatch() -> Result<(), Box<dyn Error>> {
|
||||||
|
let package = write_package("signature", b"wasm component")?;
|
||||||
|
fs::write(package.join("signatures").join("ed25519.sig"), "aa")?;
|
||||||
|
|
||||||
|
let error = PluginPackageReader::read_directory_package(&package)
|
||||||
|
.err()
|
||||||
|
.ok_or_else(|| std::io::Error::other("package read succeeded"))?;
|
||||||
|
|
||||||
|
assert!(matches!(error, super::PluginPackageError::SignatureMismatch));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_package_directory() -> Result<(), Box<dyn Error>> {
|
||||||
|
let package = temp_root()?.join("invalid");
|
||||||
|
fs::create_dir_all(&package)?;
|
||||||
|
|
||||||
|
let error = PluginPackageReader::read_directory_package(&package)
|
||||||
|
.err()
|
||||||
|
.ok_or_else(|| std::io::Error::other("package read succeeded"))?;
|
||||||
|
|
||||||
|
assert!(matches!(error, super::PluginPackageError::InvalidPackageExtension { .. }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_package(name: &str, component: &[u8]) -> Result<PathBuf, Box<dyn Error>> {
|
||||||
|
let package = temp_root()?.join(format!("{name}.rplug"));
|
||||||
|
fs::create_dir_all(package.join("signatures"))?;
|
||||||
|
fs::write(package.join("component.wasm"), component)?;
|
||||||
|
|
||||||
|
let checksum = sha256_bytes(component);
|
||||||
|
let signature = "b".repeat(128);
|
||||||
|
fs::write(package.join("signatures").join("ed25519.sig"), &signature)?;
|
||||||
|
fs::write(
|
||||||
|
package.join("plugin.toml"),
|
||||||
|
manifest_toml(name, checksum.as_str(), signature.as_str()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(package)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn manifest_toml(name: &str, checksum: &str, signature: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
id = "com.elydora.{name}"
|
||||||
|
name = "Verified Plugin"
|
||||||
|
description = "Exports verified content."
|
||||||
|
author = "Elydora"
|
||||||
|
homepage = "https://elydora.com/plugins/{name}"
|
||||||
|
permissions = ["page:metadata"]
|
||||||
|
contributes = ["command-bar-command"]
|
||||||
|
min_ely_build = "0.1.0"
|
||||||
|
checksum = "{checksum}"
|
||||||
|
|
||||||
|
[signature]
|
||||||
|
algorithm = "ed25519"
|
||||||
|
value = "{signature}"
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sha256_bytes(bytes: &[u8]) -> String {
|
||||||
|
format!("{:x}", Sha256::digest(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temp_root() -> Result<PathBuf, Box<dyn Error>> {
|
||||||
|
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
||||||
|
let path = std::env::temp_dir().join(format!("ely-plugin-package-{nanos}"));
|
||||||
|
fs::create_dir_all(&path)?;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ impl ElyShell {
|
|||||||
"ely://downloads" => self.render_downloads_page(snapshot, cx),
|
"ely://downloads" => self.render_downloads_page(snapshot, cx),
|
||||||
"ely://history" => self.render_history_page(snapshot, cx),
|
"ely://history" => self.render_history_page(snapshot, cx),
|
||||||
"ely://archive" => self.render_archive_page(snapshot, cx),
|
"ely://archive" => self.render_archive_page(snapshot, cx),
|
||||||
"ely://settings/plugins" => self.render_plugins_page(snapshot),
|
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
|
||||||
_ => render_default_page(tab),
|
_ => render_default_page(tab),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
use ely_browser_core::{BrowserSnapshot, InstalledPlugin, PluginAuditAction, PluginAuditEvent};
|
use ely_browser_core::{BrowserSnapshot, InstalledPlugin, PluginAuditAction, PluginAuditEvent};
|
||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb};
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui_component::{StyledExt, scroll::ScrollableElement};
|
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||||
|
use gpui_component::{
|
||||||
|
IconName, Sizable, StyledExt,
|
||||||
|
button::{Button, ButtonVariants},
|
||||||
|
scroll::ScrollableElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::super::plugins::PendingPluginInstall;
|
||||||
use super::{ElyShell, render_canvas_surface};
|
use super::{ElyShell, render_canvas_surface};
|
||||||
|
|
||||||
impl ElyShell {
|
impl ElyShell {
|
||||||
pub(super) fn render_plugins_page(&mut self, snapshot: &BrowserSnapshot) -> AnyElement {
|
pub(super) fn render_plugins_page(
|
||||||
|
&mut self,
|
||||||
|
snapshot: &BrowserSnapshot,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
render_canvas_surface(
|
render_canvas_surface(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -51,12 +61,115 @@ impl ElyShell {
|
|||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child("Install signed .rplug packages from local disk."),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("install-plugin-package")
|
||||||
|
.primary()
|
||||||
|
.small()
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.label("Install")
|
||||||
|
.tooltip("Install Plugin from File")
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.choose_plugin_package(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.plugin_install_error.clone(), |this, message| {
|
||||||
|
this.child(render_plugin_install_error(message))
|
||||||
|
})
|
||||||
|
.when_some(self.pending_plugin_install.clone(), |this, pending| {
|
||||||
|
this.child(render_plugin_install_confirmation(&pending, cx))
|
||||||
|
})
|
||||||
.child(render_plugin_list(snapshot))
|
.child(render_plugin_list(snapshot))
|
||||||
.child(render_plugin_audit_list(snapshot)),
|
.child(render_plugin_audit_list(snapshot)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_plugin_install_error(message: String) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.rounded_md()
|
||||||
|
.border_1()
|
||||||
|
.border_color(rgb(colors::ERROR))
|
||||||
|
.px_3()
|
||||||
|
.py_2()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(rgb(colors::ERROR))
|
||||||
|
.child(message)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_plugin_install_confirmation(
|
||||||
|
pending: &PendingPluginInstall,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.rounded_md()
|
||||||
|
.border_1()
|
||||||
|
.border_color(rgb(colors::ERROR))
|
||||||
|
.px_3()
|
||||||
|
.py_2()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.gap_3()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(rgb(colors::ERROR))
|
||||||
|
.child(format!("Confirm install for {}", pending.manifest().name())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.truncate()
|
||||||
|
.text_color(rgb(colors::MUTED))
|
||||||
|
.child(high_risk_permissions_label(pending)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Button::new("cancel-plugin-install").ghost().xsmall().label("Cancel").on_click(
|
||||||
|
cx.listener(|shell, _, _, cx| {
|
||||||
|
shell.cancel_plugin_install(cx);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("confirm-plugin-install")
|
||||||
|
.danger()
|
||||||
|
.xsmall()
|
||||||
|
.label("Confirm")
|
||||||
|
.on_click(cx.listener(|shell, _, _, cx| {
|
||||||
|
shell.confirm_plugin_install(cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_plugin_list(snapshot: &BrowserSnapshot) -> AnyElement {
|
fn render_plugin_list(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||||
if snapshot.installed_plugins.is_empty() {
|
if snapshot.installed_plugins.is_empty() {
|
||||||
return div()
|
return div()
|
||||||
@@ -180,3 +293,13 @@ fn plugin_audit_action_label(action: &PluginAuditAction) -> &'static str {
|
|||||||
PluginAuditAction::Disabled => "Disabled",
|
PluginAuditAction::Disabled => "Disabled",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn high_risk_permissions_label(pending: &PendingPluginInstall) -> String {
|
||||||
|
let permissions = pending
|
||||||
|
.high_risk_permissions()
|
||||||
|
.iter()
|
||||||
|
.map(|permission| permission.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
format!("High risk permissions: {permissions}")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod downloads;
|
mod downloads;
|
||||||
mod internal_pages;
|
mod internal_pages;
|
||||||
|
mod plugins;
|
||||||
mod render;
|
mod render;
|
||||||
|
|
||||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||||
@@ -8,6 +9,7 @@ use gpui::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscriptio
|
|||||||
use gpui_component::input::{InputEvent, InputState, SelectAll};
|
use gpui_component::input::{InputEvent, InputState, SelectAll};
|
||||||
|
|
||||||
use downloads::PendingDownloadFileAction;
|
use downloads::PendingDownloadFileAction;
|
||||||
|
use plugins::PendingPluginInstall;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
|
||||||
@@ -28,6 +30,8 @@ pub struct ElyShell {
|
|||||||
download_action_error: Option<String>,
|
download_action_error: Option<String>,
|
||||||
download_clear_confirmation: bool,
|
download_clear_confirmation: bool,
|
||||||
download_security_confirmation: Option<PendingDownloadFileAction>,
|
download_security_confirmation: Option<PendingDownloadFileAction>,
|
||||||
|
plugin_install_error: Option<String>,
|
||||||
|
pending_plugin_install: Option<PendingPluginInstall>,
|
||||||
_command_subscription: Subscription,
|
_command_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +90,8 @@ impl ElyShell {
|
|||||||
download_action_error: None,
|
download_action_error: None,
|
||||||
download_clear_confirmation: false,
|
download_clear_confirmation: false,
|
||||||
download_security_confirmation: None,
|
download_security_confirmation: None,
|
||||||
|
plugin_install_error: None,
|
||||||
|
pending_plugin_install: None,
|
||||||
_command_subscription: command_subscription,
|
_command_subscription: command_subscription,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use ely_domain::{PluginManifest, PluginPermission};
|
||||||
|
use gpui::{Context, PathPromptOptions, Window};
|
||||||
|
|
||||||
|
use crate::services::plugin_packages::{PluginPackageError, PluginPackageReader};
|
||||||
|
|
||||||
|
use super::{ElyShell, ShellState};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(super) struct PendingPluginInstall {
|
||||||
|
manifest: PluginManifest,
|
||||||
|
high_risk_permissions: Vec<PluginPermission>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PendingPluginInstall {
|
||||||
|
fn new(manifest: PluginManifest, high_risk_permissions: Vec<PluginPermission>) -> Self {
|
||||||
|
Self { manifest, high_risk_permissions }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn manifest(&self) -> &PluginManifest {
|
||||||
|
&self.manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn high_risk_permissions(&self) -> &[PluginPermission] {
|
||||||
|
&self.high_risk_permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ElyShell {
|
||||||
|
pub(super) fn choose_plugin_package(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
multiple: false,
|
||||||
|
prompt: Some("Select .rplug package".into()),
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |shell, window| {
|
||||||
|
let selected_path = match prompt.await {
|
||||||
|
Ok(Ok(Some(paths))) => paths.into_iter().next(),
|
||||||
|
Ok(Ok(None)) => None,
|
||||||
|
Ok(Err(error)) => {
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.plugin_install_error = Some(error.to_string());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.plugin_install_error = Some(error.to_string());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(path) = selected_path else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let result =
|
||||||
|
window.background_executor().spawn(async move { load_plugin_package(path) }).await;
|
||||||
|
_ = shell.update_in(window, |shell, _, cx| {
|
||||||
|
shell.handle_plugin_package_result(result, cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn confirm_plugin_install(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(pending) = self.pending_plugin_install.take() else {
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.install_plugin_manifest(pending.manifest, true, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cancel_plugin_install(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.pending_plugin_install = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_plugin_package_result(
|
||||||
|
&mut self,
|
||||||
|
result: Result<PluginManifest, PluginPackageError>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
match result {
|
||||||
|
Ok(manifest) => self.install_plugin_manifest(manifest, false, cx),
|
||||||
|
Err(error) => {
|
||||||
|
self.plugin_install_error = Some(error.to_string());
|
||||||
|
self.pending_plugin_install = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_plugin_manifest(
|
||||||
|
&mut self,
|
||||||
|
manifest: PluginManifest,
|
||||||
|
high_risk_confirmed: bool,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let high_risk_permissions = manifest.high_risk_permissions().cloned().collect::<Vec<_>>();
|
||||||
|
if !high_risk_confirmed && !high_risk_permissions.is_empty() {
|
||||||
|
self.pending_plugin_install =
|
||||||
|
Some(PendingPluginInstall::new(manifest, high_risk_permissions));
|
||||||
|
self.plugin_install_error = None;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match &mut self.state {
|
||||||
|
ShellState::Ready(core) => core
|
||||||
|
.install_plugin(manifest, high_risk_confirmed)
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|error| error.to_string()),
|
||||||
|
ShellState::StartupError(message) => Err(message.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.plugin_install_error = result.err();
|
||||||
|
if self.plugin_install_error.is_none() {
|
||||||
|
self.pending_plugin_install = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_plugin_package(path: PathBuf) -> Result<PluginManifest, PluginPackageError> {
|
||||||
|
PluginPackageReader::read_directory_package(&path)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user