Install plugins from Yaak plugin registry (#230)

This commit is contained in:
Gregory Schier
2025-06-23 08:55:38 -07:00
committed by GitHub
parent b5620fcdf3
commit cb7c44cc65
27 changed files with 421 additions and 218 deletions

View File

@@ -1,10 +1,17 @@
use crate::error::Error::ApiErr;
use crate::commands::{PluginSearchResponse, PluginVersion};
use crate::error::Result;
use crate::plugin_meta::get_plugin_meta;
use log::{info, warn};
use reqwest::{Response, Url};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::str::FromStr;
use log::info;
use tauri::{AppHandle, Runtime, is_dev};
use ts_rs::TS;
use yaak_common::api_client::yaak_api_client;
use yaak_models::query_manager::QueryManagerExt;
use crate::error::Error::ApiErr;
pub async fn get_plugin<R: Runtime>(
@@ -44,6 +51,38 @@ pub async fn download_plugin_archive<R: Runtime>(
Ok(resp)
}
pub async fn check_plugin_updates<R: Runtime>(
app_handle: &AppHandle<R>,
) -> Result<PluginUpdatesResponse> {
let name_versions: Vec<PluginNameVersion> = app_handle
.db()
.list_plugins()?
.into_iter()
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
Ok(m) => Some(PluginNameVersion {
name: m.name,
version: m.version,
}),
Err(e) => {
warn!("Failed to get plugin metadata: {}", e);
None
}
})
.collect();
let url = base_url("/updates");
let body = serde_json::to_vec(&PluginUpdatesResponse {
plugins: name_versions,
})?;
let resp = yaak_api_client(app_handle)?.post(url.clone()).body(body).send().await?;
if !resp.status().is_success() {
return Err(ApiErr(format!("{} response to {}", resp.status(), url.to_string())));
}
let results: PluginUpdatesResponse = resp.json().await?;
Ok(results)
}
pub async fn search_plugins<R: Runtime>(
app_handle: &AppHandle<R>,
query: &str,
@@ -65,3 +104,41 @@ fn base_url(path: &str) -> Url {
};
Url::from_str(&format!("{base_url}{path}")).unwrap()
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_search.ts")]
pub struct PluginVersion {
pub id: String,
pub version: String,
pub description: Option<String>,
pub name: String,
pub display_name: String,
pub homepage_url: Option<String>,
pub repository_url: Option<String>,
pub checksum: String,
pub readme: Option<String>,
pub yanked: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_api.ts")]
pub struct PluginSearchResponse {
pub plugins: Vec<PluginVersion>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_api.ts")]
pub struct PluginNameVersion {
name: String,
version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_api.ts")]
pub struct PluginUpdatesResponse {
pub plugins: Vec<PluginNameVersion>,
}

View File

@@ -1,9 +1,9 @@
use crate::api::search_plugins;
use crate::api::{
PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates, search_plugins,
};
use crate::error::Result;
use crate::install::download_and_install;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Runtime, WebviewWindow, command};
use ts_rs::TS;
#[command]
pub(crate) async fn search<R: Runtime>(
@@ -16,30 +16,14 @@ pub(crate) async fn search<R: Runtime>(
#[command]
pub(crate) async fn install<R: Runtime>(
window: WebviewWindow<R>,
plugin: PluginVersion,
) -> Result<String> {
download_and_install(&window, &plugin).await
name: &str,
version: Option<String>,
) -> Result<()> {
download_and_install(&window, name, version).await?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_search.ts")]
pub struct PluginSearchResponse {
pub results: Vec<PluginVersion>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_search.ts")]
pub struct PluginVersion {
pub id: String,
pub version: String,
pub description: Option<String>,
pub name: String,
pub display_name: String,
pub homepage_url: Option<String>,
pub repository_url: Option<String>,
pub checksum: String,
pub readme: Option<String>,
pub yanked: bool,
#[command]
pub(crate) async fn updates<R: Runtime>(app_handle: AppHandle<R>) -> Result<PluginUpdatesResponse> {
check_plugin_updates(&app_handle).await
}

View File

@@ -1,23 +1,25 @@
use crate::api::download_plugin_archive;
use crate::api::{PluginVersion, download_plugin_archive, get_plugin};
use crate::checksum::compute_checksum;
use crate::commands::PluginVersion;
use crate::error::Error::PluginErr;
use crate::error::Result;
use crate::events::PluginWindowContext;
use crate::manager::PluginManager;
use chrono::Utc;
use log::info;
use std::fs::create_dir_all;
use std::fs::{create_dir_all, remove_dir_all};
use std::io::Cursor;
use tauri::{Manager, Runtime, WebviewWindow};
use yaak_models::models::Plugin;
use yaak_models::query_manager::QueryManagerExt;
use yaak_models::util::{UpdateSource, generate_id};
use yaak_models::util::UpdateSource;
pub async fn download_and_install<R: Runtime>(
window: &WebviewWindow<R>,
plugin_version: &PluginVersion,
) -> Result<String> {
name: &str,
version: Option<String>,
) -> Result<PluginVersion> {
let plugin_manager = window.state::<PluginManager>();
let plugin_version = get_plugin(window.app_handle(), name, version).await?;
let resp = download_plugin_archive(window.app_handle(), &plugin_version).await?;
let bytes = resp.bytes().await?;
@@ -32,17 +34,19 @@ pub async fn download_and_install<R: Runtime>(
info!("Checksum matched {}", checksum);
let plugin_dir = window.path().app_data_dir()?.join("plugins").join(generate_id());
let plugin_dir = plugin_manager.installed_plugin_dir.join(name);
let plugin_dir_str = plugin_dir.to_str().unwrap().to_string();
// Re-create the plugin directory
let _ = remove_dir_all(&plugin_dir);
create_dir_all(&plugin_dir)?;
zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?;
info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str);
let plugin_manager = window.state::<PluginManager>();
plugin_manager.add_plugin_by_dir(&PluginWindowContext::new(&window), &plugin_dir_str).await?;
let p = window.db().upsert_plugin(
window.db().upsert_plugin(
&Plugin {
id: plugin_version.id.clone(),
checked_at: Some(Utc::now().naive_utc()),
@@ -56,5 +60,5 @@ pub async fn download_and_install<R: Runtime>(
info!("Installed plugin {} to {}", plugin_version.id, plugin_dir_str);
Ok(p.id)
Ok(plugin_version)
}

View File

@@ -1,4 +1,4 @@
use crate::commands::{install, search};
use crate::commands::{install, search, updates};
use crate::manager::PluginManager;
use log::info;
use std::process::exit;
@@ -18,10 +18,11 @@ mod util;
mod checksum;
pub mod api;
pub mod install;
pub mod plugin_meta;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-plugins")
.invoke_handler(generate_handler![search, install])
.invoke_handler(generate_handler![search, install, updates])
.setup(|app_handle, _| {
let manager = PluginManager::new(app_handle.clone());
app_handle.manage(manager.clone());

View File

@@ -39,7 +39,7 @@ pub struct PluginManager {
kill_tx: tokio::sync::watch::Sender<bool>,
ws_service: Arc<PluginRuntimeServerWebsocket>,
vendored_plugin_dir: PathBuf,
installed_plugin_dir: PathBuf,
pub(crate) installed_plugin_dir: PathBuf,
}
#[derive(Clone)]
@@ -62,8 +62,11 @@ impl PluginManager {
.resolve("vendored/plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
let installed_plugin_dir =
app_handle.path().app_data_dir().expect("failed to get app data dir");
let installed_plugin_dir = app_handle
.path()
.app_data_dir()
.expect("failed to get app data dir")
.join("installed-plugins");
let plugin_manager = PluginManager {
plugins: Default::default(),
@@ -209,7 +212,7 @@ impl PluginManager {
None => return Err(ClientNotInitializedErr),
Some(tx) => tx,
};
let plugin_handle = PluginHandle::new(dir, tx.clone());
let plugin_handle = PluginHandle::new(dir, tx.clone())?;
let dir_path = Path::new(dir);
let is_vendored = dir_path.starts_with(self.vendored_plugin_dir.as_path());
let is_installed = dir_path.starts_with(self.installed_plugin_dir.as_path());
@@ -231,14 +234,11 @@ impl PluginManager {
// Add the new plugin
self.plugins.lock().await.push(plugin_handle.clone());
let resp = match event.payload {
let _ = match event.payload {
InternalEventPayload::BootResponse(resp) => resp,
_ => return Err(UnknownEventErr),
};
// Set the boot response
plugin_handle.set_boot_response(&resp).await;
Ok(())
}
@@ -317,7 +317,7 @@ impl PluginManager {
pub async fn get_plugin_by_name(&self, name: &str) -> Option<PluginHandle> {
for plugin in self.plugins.lock().await.iter().cloned() {
let info = plugin.info().await;
let info = plugin.info();
if info.name == name {
return Some(plugin);
}

View File

@@ -1,38 +1,35 @@
use crate::error::Result;
use crate::events::{BootResponse, InternalEvent, InternalEventPayload, PluginWindowContext};
use crate::events::{InternalEvent, InternalEventPayload, PluginWindowContext};
use crate::plugin_meta::{PluginMetadata, get_plugin_meta};
use crate::util::gen_id;
use log::info;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tokio::sync::{Mutex, mpsc};
#[derive(Clone)]
pub struct PluginHandle {
pub ref_id: String,
pub dir: String,
pub(crate) to_plugin_tx: Arc<Mutex<mpsc::Sender<InternalEvent>>>,
pub(crate) boot_resp: Arc<Mutex<BootResponse>>,
pub(crate) metadata: PluginMetadata,
}
impl PluginHandle {
pub fn new(dir: &str, tx: mpsc::Sender<InternalEvent>) -> Self {
pub fn new(dir: &str, tx: mpsc::Sender<InternalEvent>) -> Result<Self> {
let ref_id = gen_id();
let metadata = get_plugin_meta(&Path::new(dir))?;
PluginHandle {
Ok(PluginHandle {
ref_id: ref_id.clone(),
dir: dir.to_string(),
to_plugin_tx: Arc::new(Mutex::new(tx)),
boot_resp: Arc::new(Mutex::new(BootResponse::default())),
}
metadata,
})
}
pub async fn name(&self) -> String {
self.boot_resp.lock().await.name.clone()
}
pub async fn info(&self) -> BootResponse {
let resp = &*self.boot_resp.lock().await;
resp.clone()
pub fn info(&self) -> PluginMetadata {
self.metadata.clone()
}
pub fn build_event_to_send(
@@ -72,9 +69,4 @@ impl PluginHandle {
self.to_plugin_tx.lock().await.send(event.to_owned()).await?;
Ok(())
}
pub async fn set_boot_response(&self, resp: &BootResponse) {
let mut boot_resp = self.boot_resp.lock().await;
*boot_resp = resp.clone();
}
}

View File

@@ -0,0 +1,64 @@
use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_search.ts")]
pub struct PluginMetadata {
pub version: String,
pub name: String,
pub display_name: String,
pub description: Option<String>,
pub homepage_url: Option<String>,
pub repository_url: Option<String>,
}
pub(crate) fn get_plugin_meta(plugin_dir: &Path) -> Result<PluginMetadata> {
let package_json = fs::File::open(plugin_dir.join("package.json"))?;
let package_json: PackageJson = serde_json::from_reader(package_json)?;
let display_name = match package_json.display_name {
None => {
let display_name = package_json.name.to_string();
let display_name = display_name.split('/').last().unwrap_or(&package_json.name);
let display_name = display_name.strip_prefix("yaak-plugin-").unwrap_or(&display_name);
let display_name = display_name.strip_prefix("yaak-").unwrap_or(&display_name);
display_name.to_string()
}
Some(n) => n,
};
Ok(PluginMetadata {
version: package_json.version,
description: package_json.description,
name: package_json.name,
display_name,
homepage_url: package_json.homepage,
repository_url: match package_json.repository {
None => None,
Some(RepositoryField::Object { url }) => Some(url),
Some(RepositoryField::String(url)) => Some(url),
},
})
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PackageJson {
pub name: String,
pub display_name: Option<String>,
pub version: String,
pub repository: Option<RepositoryField>,
pub homepage: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum RepositoryField {
String(String),
Object { url: String },
}