diff options
Diffstat (limited to 'zellij-utils/src')
-rw-r--r-- | zellij-utils/src/cli.rs | 26 | ||||
-rw-r--r-- | zellij-utils/src/downloader/download.rs | 49 | ||||
-rw-r--r-- | zellij-utils/src/downloader/mod.rs | 147 | ||||
-rw-r--r-- | zellij-utils/src/input/layout.rs | 6 | ||||
-rw-r--r-- | zellij-utils/src/input/plugins.rs | 9 | ||||
-rw-r--r-- | zellij-utils/src/lib.rs | 2 |
6 files changed, 237 insertions, 2 deletions
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 |