mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-29 21:51:59 +02:00
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:
67
src-tauri/yaak-plugins/src/api.rs
Normal file
67
src-tauri/yaak-plugins/src/api.rs
Normal 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()
|
||||
}
|
||||
8
src-tauri/yaak-plugins/src/checksum.rs
Normal file
8
src-tauri/yaak-plugins/src/checksum.rs
Normal 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)
|
||||
}
|
||||
45
src-tauri/yaak-plugins/src/commands.rs
Normal file
45
src-tauri/yaak-plugins/src/commands.rs
Normal 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,
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -67,7 +67,7 @@ pub enum InternalEventPayload {
|
||||
BootResponse(BootResponse),
|
||||
|
||||
ReloadRequest(EmptyPayload),
|
||||
ReloadResponse(EmptyPayload),
|
||||
ReloadResponse(BootResponse),
|
||||
|
||||
TerminateRequest,
|
||||
TerminateResponse,
|
||||
|
||||
60
src-tauri/yaak-plugins/src/install.rs
Normal file
60
src-tauri/yaak-plugins/src/install.rs
Normal 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)
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user