Merge pull request #227

* Search and install plugins PoC

* Checksum

* Tab sidebar for settings

* Fix nested tabs, and tweaks

* Table for plugin results

* Deep links working

* Focus window during deep links

* Merge branch 'master' into plugin-directory

* More stuff
This commit is contained in:
Gregory Schier
2025-06-22 07:06:43 -07:00
committed by GitHub
parent b8e6dbc7c7
commit b5620fcdf3
56 changed files with 1222 additions and 444 deletions

View File

@@ -0,0 +1,67 @@
use crate::commands::{PluginSearchResponse, PluginVersion};
use crate::error::Result;
use reqwest::{Response, Url};
use std::str::FromStr;
use log::info;
use tauri::{AppHandle, Runtime, is_dev};
use yaak_common::api_client::yaak_api_client;
use crate::error::Error::ApiErr;
pub async fn get_plugin<R: Runtime>(
app_handle: &AppHandle<R>,
name: &str,
version: Option<String>,
) -> Result<PluginVersion> {
info!("Getting plugin: {name} {version:?}");
let mut url = base_url(&format!("/{name}"));
if let Some(version) = version {
let mut query_pairs = url.query_pairs_mut();
query_pairs.append_pair("version", &version);
};
let resp = yaak_api_client(app_handle)?.get(url.clone()).send().await?;
if !resp.status().is_success() {
return Err(ApiErr(format!("{} response to {}", resp.status(), url.to_string())));
}
Ok(resp.json().await?)
}
pub async fn download_plugin_archive<R: Runtime>(
app_handle: &AppHandle<R>,
plugin_version: &PluginVersion,
) -> Result<Response> {
let name = plugin_version.name.clone();
let version = plugin_version.version.clone();
info!("Downloading plugin: {name} {version}");
let mut url = base_url(&format!("/{}/download", name));
{
let mut query_pairs = url.query_pairs_mut();
query_pairs.append_pair("version", &version);
};
let resp = yaak_api_client(app_handle)?.get(url.clone()).send().await?;
if !resp.status().is_success() {
return Err(ApiErr(format!("{} response to {}", resp.status(), url.to_string())));
}
Ok(resp)
}
pub async fn search_plugins<R: Runtime>(
app_handle: &AppHandle<R>,
query: &str,
) -> Result<PluginSearchResponse> {
let mut url = base_url("/search");
{
let mut query_pairs = url.query_pairs_mut();
query_pairs.append_pair("query", query);
};
let resp = yaak_api_client(app_handle)?.get(url).send().await?;
Ok(resp.json().await?)
}
fn base_url(path: &str) -> Url {
let base_url = if is_dev() {
"http://localhost:9444/api/v1/plugins"
} else {
"https://api.yaak.app/api/v1/plugins"
};
Url::from_str(&format!("{base_url}{path}")).unwrap()
}

View File

@@ -0,0 +1,8 @@
use sha2::{Digest, Sha256};
pub(crate) fn compute_checksum(bytes: impl AsRef<[u8]>) -> String {
let mut hasher = Sha256::new();
hasher.update(&bytes);
let hash = hasher.finalize();
hex::encode(hash)
}

View File

@@ -0,0 +1,45 @@
use crate::api::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>(
app_handle: AppHandle<R>,
query: &str,
) -> Result<PluginSearchResponse> {
search_plugins(&app_handle, query).await
}
#[command]
pub(crate) async fn install<R: Runtime>(
window: WebviewWindow<R>,
plugin: PluginVersion,
) -> Result<String> {
download_and_install(&window, &plugin).await
}
#[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,
}

View File

@@ -1,4 +1,5 @@
use crate::events::InternalEvent;
use serde::{Serialize, Serializer};
use thiserror::Error;
use tokio::io;
use tokio::sync::mpsc::error::SendError;
@@ -8,9 +9,12 @@ pub enum Error {
#[error(transparent)]
CryptoErr(#[from] yaak_crypto::error::Error),
#[error(transparent)]
DbErr(#[from] yaak_models::error::Error),
#[error(transparent)]
TemplateErr(#[from] yaak_templates::error::Error),
#[error("IO error: {0}")]
IoErr(#[from] io::Error),
@@ -23,8 +27,17 @@ pub enum Error {
#[error("Grpc send error: {0}")]
GrpcSendErr(#[from] SendError<InternalEvent>),
#[error("Failed to send request: {0}")]
RequestError(#[from] reqwest::Error),
#[error("JSON error: {0}")]
JsonErr(#[from] serde_json::Error),
#[error("API Error: {0}")]
ApiErr(String),
#[error(transparent)]
CommonError(#[from] yaak_common::error::Error),
#[error("Timeout elapsed: {0}")]
TimeoutElapsed(#[from] tokio::time::error::Elapsed),
@@ -38,6 +51,9 @@ pub enum Error {
#[error("Plugin error: {0}")]
PluginErr(String),
#[error("zip error: {0}")]
ZipError(#[from] zip_extract::ZipExtractError),
#[error("Client not initialized error")]
ClientNotInitializedErr,
@@ -45,4 +61,13 @@ pub enum Error {
UnknownEventErr,
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -67,7 +67,7 @@ pub enum InternalEventPayload {
BootResponse(BootResponse),
ReloadRequest(EmptyPayload),
ReloadResponse(EmptyPayload),
ReloadResponse(BootResponse),
TerminateRequest,
TerminateResponse,

View File

@@ -0,0 +1,60 @@
use crate::api::download_plugin_archive;
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::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};
pub async fn download_and_install<R: Runtime>(
window: &WebviewWindow<R>,
plugin_version: &PluginVersion,
) -> Result<String> {
let resp = download_plugin_archive(window.app_handle(), &plugin_version).await?;
let bytes = resp.bytes().await?;
let checksum = compute_checksum(&bytes);
if checksum != plugin_version.checksum {
return Err(PluginErr(format!(
"Checksum mismatch {}b {checksum} != {}",
bytes.len(),
plugin_version.checksum
)));
}
info!("Checksum matched {}", checksum);
let plugin_dir = window.path().app_data_dir()?.join("plugins").join(generate_id());
let plugin_dir_str = plugin_dir.to_str().unwrap().to_string();
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(
&Plugin {
id: plugin_version.id.clone(),
checked_at: Some(Utc::now().naive_utc()),
directory: plugin_dir_str.clone(),
enabled: true,
url: None,
..Default::default()
},
&UpdateSource::Background,
)?;
info!("Installed plugin {} to {}", plugin_version.id, plugin_dir_str);
Ok(p.id)
}

View File

@@ -1,9 +1,11 @@
use crate::commands::{install, search};
use crate::manager::PluginManager;
use log::info;
use std::process::exit;
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, RunEvent, Runtime, State};
use tauri::{Manager, RunEvent, Runtime, State, generate_handler};
mod commands;
pub mod error;
pub mod events;
pub mod manager;
@@ -13,9 +15,13 @@ pub mod plugin_handle;
mod server_ws;
pub mod template_callback;
mod util;
mod checksum;
pub mod api;
pub mod install;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-plugins")
.invoke_handler(generate_handler![search, install])
.setup(|app_handle, _| {
let manager = PluginManager::new(app_handle.clone());
app_handle.manage(manager.clone());

View File

@@ -18,7 +18,7 @@ use crate::server_ws::PluginRuntimeServerWebsocket;
use log::{error, info, warn};
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tauri::path::BaseDirectory;
@@ -38,12 +38,13 @@ pub struct PluginManager {
plugins: Arc<Mutex<Vec<PluginHandle>>>,
kill_tx: tokio::sync::watch::Sender<bool>,
ws_service: Arc<PluginRuntimeServerWebsocket>,
vendored_plugin_dir: PathBuf,
installed_plugin_dir: PathBuf,
}
#[derive(Clone)]
struct PluginCandidate {
dir: String,
watch: bool,
}
impl PluginManager {
@@ -56,11 +57,21 @@ impl PluginManager {
let ws_service =
PluginRuntimeServerWebsocket::new(events_tx, client_disconnect_tx, client_connect_tx);
let vendored_plugin_dir = app_handle
.path()
.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 plugin_manager = PluginManager {
plugins: Default::default(),
subscribers: Default::default(),
ws_service: Arc::new(ws_service.clone()),
kill_tx: kill_server_tx,
vendored_plugin_dir,
installed_plugin_dir,
};
// Forward events to subscribers
@@ -135,18 +146,13 @@ impl PluginManager {
&self,
app_handle: &AppHandle<R>,
) -> Vec<PluginCandidate> {
let bundled_plugins_dir = &app_handle
.path()
.resolve("vendored/plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
let plugins_dir = if is_dev() {
// Use plugins directly for easy development
env::current_dir()
.map(|cwd| cwd.join("../plugins").canonicalize().unwrap())
.unwrap_or_else(|_| bundled_plugins_dir.clone())
.unwrap_or_else(|_| self.vendored_plugin_dir.to_path_buf())
} else {
bundled_plugins_dir.clone()
self.vendored_plugin_dir.to_path_buf()
};
info!("Loading bundled plugins from {plugins_dir:?}");
@@ -155,13 +161,7 @@ impl PluginManager {
.await
.expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str())
.iter()
.map(|d| {
let is_vendored = plugins_dir.starts_with(bundled_plugins_dir);
PluginCandidate {
dir: d.into(),
watch: !is_vendored,
}
})
.map(|d| PluginCandidate { dir: d.into() })
.collect();
let plugins = app_handle.db().list_plugins().unwrap_or_default();
@@ -169,7 +169,6 @@ impl PluginManager {
.iter()
.map(|p| PluginCandidate {
dir: p.directory.to_owned(),
watch: true,
})
.collect();
@@ -203,7 +202,6 @@ impl PluginManager {
&self,
window_context: &PluginWindowContext,
dir: &str,
watch: bool,
) -> Result<()> {
info!("Adding plugin by dir {dir}");
let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await;
@@ -212,6 +210,9 @@ impl PluginManager {
Some(tx) => tx,
};
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());
// Boot the plugin
let event = timeout(
@@ -221,7 +222,7 @@ impl PluginManager {
&plugin_handle,
&InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(),
watch,
watch: !is_vendored && !is_installed,
}),
),
)
@@ -256,10 +257,7 @@ impl PluginManager {
continue;
}
}
if let Err(e) = self
.add_plugin_by_dir(window_context, candidate.dir.as_str(), candidate.watch)
.await
{
if let Err(e) = self.add_plugin_by_dir(window_context, candidate.dir.as_str()).await {
warn!("Failed to add plugin {} {e:?}", candidate.dir);
}
}