summaryrefslogtreecommitdiffstats
path: root/zellij-utils
diff options
context:
space:
mode:
authorJae-Heon Ji <32578710+jaeheonji@users.noreply.github.com>2023-11-02 21:09:18 +0900
committerGitHub <noreply@github.com>2023-11-02 13:09:18 +0100
commitc87ff8cb2e6667648fc773dd2a811c25579c4ca0 (patch)
tree8757e0a0e69390e9ec6fea67b4eedcce44ff9c8b /zellij-utils
parent0e12f770cbf962673368d8b3b2ad7647cf641145 (diff)
feat: load plugins from the web (#2863)
* feat: add basic downloader * feat: add download progress bar * feat: move crate location and some fix * feat: add downloader in layout * chore: remove comment * Revert "feat: add downloader in layout" This reverts commit ac4efb937e88cdb31fe7f18919f9fbe3857054b0. * feat: change http request module to surf * feat: add some function * feat: add plugin download in wasm * feat: add error handling * test: update unittest * feat: change hash library from ring to highway * fix: openssl-sys issue for surf-client * minor adjustments * style(fmt): rustfmt * move openssl-sys back to dependencies --------- Co-authored-by: Aram Drevekenin <aram@poor.dev>
Diffstat (limited to 'zellij-utils')
-rw-r--r--zellij-utils/Cargo.toml6
-rw-r--r--zellij-utils/src/cli.rs26
-rw-r--r--zellij-utils/src/downloader/download.rs49
-rw-r--r--zellij-utils/src/downloader/mod.rs147
-rw-r--r--zellij-utils/src/input/layout.rs6
-rw-r--r--zellij-utils/src/input/plugins.rs9
-rw-r--r--zellij-utils/src/lib.rs2
7 files changed, 242 insertions, 3 deletions
diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml
index 8845b64f8..bb7d03087 100644
--- a/zellij-utils/Cargo.toml
+++ b/zellij-utils/Cargo.toml
@@ -54,7 +54,11 @@ interprocess = "1.2.1"
async-std = { version = "1.3.0", features = ["unstable"] }
notify-debouncer-full = "0.1.0"
humantime = "2.1.0"
-surf = { version = "2.3.2", default-features = false, features = ["h1-client-rustls"] }
+futures = "0.3.28"
+surf = { version = "2.3.2", default-features = false, features = [
+ "curl-client",
+] }
+openssl-sys = { version = "0.9.93", features = ["vendored"] }
[dev-dependencies]
insta = { version = "1.6.0", features = ["backtrace"] }
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 79aad9947..c196c17d1 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -217,6 +217,32 @@ pub enum Sessions {
#[clap(short, long, value_parser, default_value("false"), takes_value(false))]
start_suspended: bool,
},
+ /// Load a plugin
+ #[clap(visible_alias = "r")]
+ Plugin {
+ /// Plugin URL, can either start with http(s), file: or zellij:
+ #[clap(last(true), required(true))]
+ url: String,
+
+ /// Plugin configuration
+ #[clap(short, long, value_parser)]
+ configuration: Option<PluginUserConfiguration>,
+
+ /// Open the new pane in floating mode
+ #[clap(short, long, value_parser, default_value("false"), takes_value(false))]
+ floating: bool,
+
+ /// Open the new pane in place of the current pane, temporarily suspending it
+ #[clap(
+ short,
+ long,
+ value_parser,
+ default_value("false"),
+ takes_value(false),
+ conflicts_with("floating")
+ )]
+ in_place: bool,
+ },
/// Edit file with default $EDITOR / $VISUAL
#[clap(visible_alias = "e")]
Edit {
diff --git a/zellij-utils/src/downloader/download.rs b/zellij-utils/src/downloader/download.rs
new file mode 100644
index 000000000..d665f7e54
--- /dev/null
+++ b/zellij-utils/src/downloader/download.rs
@@ -0,0 +1,49 @@
+use serde::{Deserialize, Serialize};
+use surf::Url;
+
+#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
+pub struct Download {
+ pub url: String,
+ pub file_name: String,
+}
+
+impl Download {
+ pub fn from(url: &str) -> Self {
+ match Url::parse(url) {
+ Ok(u) => u
+ .path_segments()
+ .map_or_else(Download::default, |segments| {
+ let file_name = segments.last().unwrap_or("").to_string();
+
+ Download {
+ url: url.to_string(),
+ file_name,
+ }
+ }),
+ Err(_) => Download::default(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_from_download() {
+ let download = Download::from("https://github.com/example/plugin.wasm");
+ assert_eq!(download.url, "https://github.com/example/plugin.wasm");
+ assert_eq!(download.file_name, "plugin.wasm");
+ }
+
+ #[test]
+ fn test_empty_download() {
+ let d1 = Download::from("https://example.com");
+ assert_eq!(d1.url, "https://example.com");
+ assert_eq!(d1.file_name, "");
+
+ let d2 = Download::from("github.com");
+ assert_eq!(d2.url, "");
+ assert_eq!(d2.file_name, "");
+ }
+}
diff --git a/zellij-utils/src/downloader/mod.rs b/zellij-utils/src/downloader/mod.rs
new file mode 100644
index 000000000..b0b2771dd
--- /dev/null
+++ b/zellij-utils/src/downloader/mod.rs
@@ -0,0 +1,147 @@
+pub mod download;
+
+use async_std::{
+ fs::{create_dir_all, File},
+ io::{ReadExt, WriteExt},
+ stream, task,
+};
+use futures::{StreamExt, TryStreamExt};
+use std::path::PathBuf;
+use surf::Client;
+use thiserror::Error;
+
+use self::download::Download;
+
+#[derive(Error, Debug)]
+pub enum DownloaderError {
+ #[error("RequestError: {0}")]
+ Request(surf::Error),
+ #[error("StatusError: {0}, StatusCode: {1}")]
+ Status(String, surf::StatusCode),
+ #[error("IoError: {0}")]
+ Io(#[source] std::io::Error),
+ #[error("IoPathError: {0}, File: {1}")]
+ IoPath(std::io::Error, PathBuf),
+}
+
+#[derive(Default, Debug)]
+pub struct Downloader {
+ client: Client,
+ directory: PathBuf,
+}
+
+impl Downloader {
+ pub fn new(directory: PathBuf) -> Self {
+ Self {
+ client: surf::client().with(surf::middleware::Redirect::default()),
+ directory,
+ }
+ }
+
+ pub fn set_directory(&mut self, directory: PathBuf) {
+ self.directory = directory;
+ }
+
+ pub fn download(&self, downloads: &[Download]) -> Vec<Result<(), DownloaderError>> {
+ task::block_on(async {
+ stream::from_iter(downloads)
+ .map(|download| self.fetch(download))
+ .buffer_unordered(4)
+ .collect::<Vec<_>>()
+ .await
+ })
+ }
+
+ pub async fn fetch(&self, download: &Download) -> Result<(), DownloaderError> {
+ let mut file_size: usize = 0;
+
+ let file_path = self.directory.join(&download.file_name);
+
+ if file_path.exists() {
+ file_size = match file_path.metadata() {
+ Ok(metadata) => metadata.len() as usize,
+ Err(e) => return Err(DownloaderError::IoPath(e, file_path)),
+ }
+ }
+
+ let response = self
+ .client
+ .get(&download.url)
+ .await
+ .map_err(|e| DownloaderError::Request(e))?;
+ let status = response.status();
+
+ if status.is_client_error() || status.is_server_error() {
+ return Err(DownloaderError::Status(
+ status.canonical_reason().to_string(),
+ status,
+ ));
+ }
+
+ let length = response.len().unwrap_or(0);
+ if length > 0 && length == file_size {
+ return Ok(());
+ }
+
+ let mut dest = {
+ create_dir_all(&self.directory)
+ .await
+ .map_err(|e| DownloaderError::IoPath(e, self.directory.clone()))?;
+ File::create(&file_path)
+ .await
+ .map_err(|e| DownloaderError::IoPath(e, file_path))?
+ };
+
+ let mut bytes = response.bytes();
+ while let Some(byte) = bytes.try_next().await.map_err(DownloaderError::Io)? {
+ dest.write_all(&[byte]).await.map_err(DownloaderError::Io)?;
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use tempfile::tempdir;
+
+ #[test]
+ #[ignore]
+ fn test_fetch_plugin() {
+ let dir = tempdir().expect("could not get temp dir");
+ let dir_path = dir.path();
+
+ let downloader = Downloader::new(dir_path.to_path_buf());
+ let dl = Download::from(
+ "https://github.com/imsnif/monocle/releases/download/0.37.2/monocle.wasm",
+ );
+
+ let result = task::block_on(downloader.fetch(&dl));
+
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ #[ignore]
+ fn test_download_plugins() {
+ let dir = tempdir().expect("could not get temp dir");
+ let dir_path = dir.path();
+
+ let downloader = Downloader::new(dir_path.to_path_buf());
+ let downloads = vec![
+ Download::from(
+ "https://github.com/imsnif/monocle/releases/download/0.37.2/monocle.wasm",
+ ),
+ Download::from(
+ "https://github.com/imsnif/multitask/releases/download/0.38.2/multitask.wasm",
+ ),
+ ];
+
+ let results = downloader.download(&downloads);
+ for result in results {
+ assert!(result.is_ok())
+ }
+ }
+}
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index ace471f8f..312286ec0 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -287,6 +287,7 @@ impl FromStr for PluginUserConfiguration {
pub enum RunPluginLocation {
File(PathBuf),
Zellij(PluginTag),
+ Remote(String),
}
impl Default for RunPluginLocation {
@@ -332,6 +333,7 @@ impl RunPluginLocation {
};
Ok(Self::File(path))
},
+ "https" | "http" => Ok(Self::Remote(url.as_str().to_owned())),
_ => Err(PluginsConfigError::InvalidUrlScheme(url)),
}
}
@@ -339,6 +341,7 @@ impl RunPluginLocation {
match self {
RunPluginLocation::File(pathbuf) => format!("file:{}", pathbuf.display()),
RunPluginLocation::Zellij(plugin_tag) => format!("zellij:{}", plugin_tag),
+ RunPluginLocation::Remote(url) => format!("remote:{}", url),
}
}
}
@@ -351,6 +354,7 @@ impl From<&RunPluginLocation> for Url {
path.clone().into_os_string().into_string().unwrap()
),
RunPluginLocation::Zellij(tag) => format!("zellij:{}", tag),
+ RunPluginLocation::Remote(url) => format!("remote:{}", url),
};
Self::parse(&url).unwrap()
}
@@ -364,8 +368,8 @@ impl fmt::Display for RunPluginLocation {
"{}",
path.clone().into_os_string().into_string().unwrap()
),
-
Self::Zellij(tag) => write!(f, "{}", tag),
+ Self::Remote(url) => write!(f, "{}", url),
}
}
}
diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs
index f12aba2a2..2a262222f 100644
--- a/zellij-utils/src/input/plugins.rs
+++ b/zellij-utils/src/input/plugins.rs
@@ -55,6 +55,13 @@ impl PluginsConfig {
userspace_configuration: run.configuration.clone(),
..plugin
}),
+ RunPluginLocation::Remote(_) => Some(PluginConfig {
+ path: PathBuf::new(),
+ run: PluginType::Pane(None),
+ _allow_exec_host_cmd: run._allow_exec_host_cmd,
+ location: run.location.clone(),
+ userspace_configuration: run.configuration.clone(),
+ }),
}
}
@@ -231,7 +238,7 @@ pub enum PluginsConfigError {
DuplicatePlugins(PluginTag),
#[error("Failed to parse url: {0:?}")]
InvalidUrl(#[from] url::ParseError),
- #[error("Only 'file:' and 'zellij:' url schemes are supported for plugin lookup. '{0}' does not match either.")]
+ #[error("Only 'file:', 'http(s):' and 'zellij:' url schemes are supported for plugin lookup. '{0}' does not match either.")]
InvalidUrlScheme(Url),
#[error("Could not find plugin at the path: '{0:?}'")]
InvalidPluginLocation(PathBuf),
diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs
index 5438f3238..05752e21a 100644
--- a/zellij-utils/src/lib.rs
+++ b/zellij-utils/src/lib.rs
@@ -17,6 +17,8 @@ pub mod shared;
#[cfg(not(target_family = "wasm"))]
pub mod channels; // Requires async_std
#[cfg(not(target_family = "wasm"))]
+pub mod downloader; // Requires async_std
+#[cfg(not(target_family = "wasm"))]
pub mod ipc; // Requires interprocess
#[cfg(not(target_family = "wasm"))]
pub mod logging; // Requires log4rs