Skip to content

Commit

Permalink
perf: improve plugin download & load feature (zellij-org#3001)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaeheonji authored Dec 12, 2023
1 parent 6a1baaf commit b3035fc
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 210 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 5 additions & 13 deletions zellij-server/src/plugins/wasm_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use zellij_utils::async_channel::Sender;
use zellij_utils::async_std::task::{self, JoinHandle};
use zellij_utils::consts::ZELLIJ_CACHE_DIR;
use zellij_utils::data::{PermissionStatus, PermissionType};
use zellij_utils::downloader::download::Download;
use zellij_utils::downloader::Downloader;
use zellij_utils::input::permission::PermissionCache;
use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
Expand Down Expand Up @@ -166,22 +165,15 @@ impl WasmBridge {
let mut loading_indication = LoadingIndication::new(plugin_name.clone());

if let RunPluginLocation::Remote(url) = &plugin.location {
let download = Download::from(url);

let hash: String = PortableHash::default()
.hash128(download.url.as_bytes())
let file_name: String = PortableHash::default()
.hash128(url.as_bytes())
.iter()
.map(ToString::to_string)
.collect();

let plugin_directory = ZELLIJ_CACHE_DIR.join(hash);

// The plugin path is determined by the hash of the plugin URL in the cache directory.
plugin.path = plugin_directory.join(&download.file_name);

let downloader = Downloader::new(plugin_directory);
match downloader.fetch(&download).await {
Ok(_) => {},
let downloader = Downloader::new(ZELLIJ_CACHE_DIR.to_path_buf());
match downloader.download(url, Some(&file_name)).await {
Ok(_) => plugin.path = ZELLIJ_CACHE_DIR.join(&file_name),
Err(e) => handle_plugin_loading_failure(
&senders,
plugin_id,
Expand Down
2 changes: 1 addition & 1 deletion zellij-utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ termwiz = "0.20.0"
log4rs = "1.2.0"
signal-hook = "0.3"
interprocess = "1.2.1"
async-std = { version = "1.3.0", features = ["unstable"] }
async-std = { version = "1.3.0", features = ["unstable", "attributes"] }
notify-debouncer-full = "0.1.0"
humantime = "2.1.0"
futures = "0.3.28"
Expand Down
172 changes: 172 additions & 0 deletions zellij-utils/src/downloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use async_std::{
fs,
io::{ReadExt, WriteExt},
stream::StreamExt,
};
use std::path::PathBuf;
use surf::Client;
use thiserror::Error;
use url::Url;

#[derive(Error, Debug)]
pub enum DownloaderError {
#[error("RequestError: {0}")]
Request(surf::Error),
#[error("IoError: {0}")]
Io(#[source] std::io::Error),
#[error("File name cannot be found in URL: {0}")]
NotFoundFileName(String),
}

#[derive(Debug)]
pub struct Downloader {
client: Client,
location: PathBuf,
}

impl Default for Downloader {
fn default() -> Self {
Self {
client: surf::client().with(surf::middleware::Redirect::default()),
location: PathBuf::from(""),
}
}
}

impl Downloader {
pub fn new(location: PathBuf) -> Self {
Self {
client: surf::client().with(surf::middleware::Redirect::default()),
location,
}
}

pub async fn download(
&self,
url: &str,
file_name: Option<&str>,
) -> Result<(), DownloaderError> {
let file_name = match file_name {
Some(name) => name.to_string(),
None => self.parse_name(url)?,
};

let file_path = self.location.join(file_name.as_str());
if file_path.exists() {
log::debug!("File already exists: {:?}", file_path);
return Ok(());
}

let file_part_path = self.location.join(format!("{}.part", file_name));
let (mut target, file_part_size) = {
if file_part_path.exists() {
let file_part = fs::OpenOptions::new()
.append(true)
.write(true)
.open(&file_part_path)
.await
.map_err(|e| DownloaderError::Io(e))?;

let file_part_size = file_part
.metadata()
.await
.map_err(|e| DownloaderError::Io(e))?
.len();

log::debug!("Resuming download from {} bytes", file_part_size);

(file_part, file_part_size)
} else {
let file_part = fs::File::create(&file_part_path)
.await
.map_err(|e| DownloaderError::Io(e))?;

(file_part, 0)
}
};

let res = self
.client
.get(url)
.header("Content-Type", "application/octet-stream")
.header("Range", format!("bytes={}-", file_part_size))
.await
.map_err(|e| DownloaderError::Request(e))?;

let mut stream = res.bytes();
while let Some(byte) = stream.next().await {
let byte = byte.map_err(|e| DownloaderError::Io(e))?;
target
.write(&[byte])
.await
.map_err(|e| DownloaderError::Io(e))?;
}

log::debug!("Download complete: {:?}", file_part_path);

fs::rename(file_part_path, file_path)
.await
.map_err(|e| DownloaderError::Io(e))?;

Ok(())
}

fn parse_name(&self, url: &str) -> Result<String, DownloaderError> {
Url::parse(url)
.map_err(|_| DownloaderError::NotFoundFileName(url.to_string()))?
.path_segments()
.ok_or_else(|| DownloaderError::NotFoundFileName(url.to_string()))?
.last()
.ok_or_else(|| DownloaderError::NotFoundFileName(url.to_string()))
.map(|s| s.to_string())
}
}

#[cfg(test)]
mod tests {
use super::*;

use tempfile::tempdir;

#[ignore]
#[async_std::test]
async fn test_download_ok() {
let location = tempdir().expect("Failed to create temp directory");
let location_path = location.path();

let downloader = Downloader::new(location_path.to_path_buf());
let result = downloader
.download(
"https://github.com/imsnif/monocle/releases/download/0.39.0/monocle.wasm",
Some("monocle.wasm"),
)
.await
.is_ok();

assert!(result);
assert!(location_path.join("monocle.wasm").exists());

location.close().expect("Failed to close temp directory");
}

#[ignore]
#[async_std::test]
async fn test_download_without_file_name() {
let location = tempdir().expect("Failed to create temp directory");
let location_path = location.path();

let downloader = Downloader::new(location_path.to_path_buf());
let result = downloader
.download(
"https://github.com/imsnif/multitask/releases/download/0.38.2v2/multitask.wasm",
None,
)
.await
.is_ok();

assert!(result);
assert!(location_path.join("multitask.wasm").exists());

location.close().expect("Failed to close temp directory");
}
}
49 changes: 0 additions & 49 deletions zellij-utils/src/downloader/download.rs

This file was deleted.

Loading

0 comments on commit b3035fc

Please sign in to comment.