Skip to content

Commit b3035fc

Browse files
authored
perf: improve plugin download & load feature (#3001)
1 parent 6a1baaf commit b3035fc

File tree

6 files changed

+189
-210
lines changed

6 files changed

+189
-210
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zellij-server/src/plugins/wasm_bridge.rs

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ use zellij_utils::async_channel::Sender;
1717
use zellij_utils::async_std::task::{self, JoinHandle};
1818
use zellij_utils::consts::ZELLIJ_CACHE_DIR;
1919
use zellij_utils::data::{PermissionStatus, PermissionType};
20-
use zellij_utils::downloader::download::Download;
2120
use zellij_utils::downloader::Downloader;
2221
use zellij_utils::input::permission::PermissionCache;
2322
use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
@@ -166,22 +165,15 @@ impl WasmBridge {
166165
let mut loading_indication = LoadingIndication::new(plugin_name.clone());
167166

168167
if let RunPluginLocation::Remote(url) = &plugin.location {
169-
let download = Download::from(url);
170-
171-
let hash: String = PortableHash::default()
172-
.hash128(download.url.as_bytes())
168+
let file_name: String = PortableHash::default()
169+
.hash128(url.as_bytes())
173170
.iter()
174171
.map(ToString::to_string)
175172
.collect();
176173

177-
let plugin_directory = ZELLIJ_CACHE_DIR.join(hash);
178-
179-
// The plugin path is determined by the hash of the plugin URL in the cache directory.
180-
plugin.path = plugin_directory.join(&download.file_name);
181-
182-
let downloader = Downloader::new(plugin_directory);
183-
match downloader.fetch(&download).await {
184-
Ok(_) => {},
174+
let downloader = Downloader::new(ZELLIJ_CACHE_DIR.to_path_buf());
175+
match downloader.download(url, Some(&file_name)).await {
176+
Ok(_) => plugin.path = ZELLIJ_CACHE_DIR.join(&file_name),
185177
Err(e) => handle_plugin_loading_failure(
186178
&senders,
187179
plugin_id,

zellij-utils/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ termwiz = "0.20.0"
5151
log4rs = "1.2.0"
5252
signal-hook = "0.3"
5353
interprocess = "1.2.1"
54-
async-std = { version = "1.3.0", features = ["unstable"] }
54+
async-std = { version = "1.3.0", features = ["unstable", "attributes"] }
5555
notify-debouncer-full = "0.1.0"
5656
humantime = "2.1.0"
5757
futures = "0.3.28"

zellij-utils/src/downloader.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use async_std::{
2+
fs,
3+
io::{ReadExt, WriteExt},
4+
stream::StreamExt,
5+
};
6+
use std::path::PathBuf;
7+
use surf::Client;
8+
use thiserror::Error;
9+
use url::Url;
10+
11+
#[derive(Error, Debug)]
12+
pub enum DownloaderError {
13+
#[error("RequestError: {0}")]
14+
Request(surf::Error),
15+
#[error("IoError: {0}")]
16+
Io(#[source] std::io::Error),
17+
#[error("File name cannot be found in URL: {0}")]
18+
NotFoundFileName(String),
19+
}
20+
21+
#[derive(Debug)]
22+
pub struct Downloader {
23+
client: Client,
24+
location: PathBuf,
25+
}
26+
27+
impl Default for Downloader {
28+
fn default() -> Self {
29+
Self {
30+
client: surf::client().with(surf::middleware::Redirect::default()),
31+
location: PathBuf::from(""),
32+
}
33+
}
34+
}
35+
36+
impl Downloader {
37+
pub fn new(location: PathBuf) -> Self {
38+
Self {
39+
client: surf::client().with(surf::middleware::Redirect::default()),
40+
location,
41+
}
42+
}
43+
44+
pub async fn download(
45+
&self,
46+
url: &str,
47+
file_name: Option<&str>,
48+
) -> Result<(), DownloaderError> {
49+
let file_name = match file_name {
50+
Some(name) => name.to_string(),
51+
None => self.parse_name(url)?,
52+
};
53+
54+
let file_path = self.location.join(file_name.as_str());
55+
if file_path.exists() {
56+
log::debug!("File already exists: {:?}", file_path);
57+
return Ok(());
58+
}
59+
60+
let file_part_path = self.location.join(format!("{}.part", file_name));
61+
let (mut target, file_part_size) = {
62+
if file_part_path.exists() {
63+
let file_part = fs::OpenOptions::new()
64+
.append(true)
65+
.write(true)
66+
.open(&file_part_path)
67+
.await
68+
.map_err(|e| DownloaderError::Io(e))?;
69+
70+
let file_part_size = file_part
71+
.metadata()
72+
.await
73+
.map_err(|e| DownloaderError::Io(e))?
74+
.len();
75+
76+
log::debug!("Resuming download from {} bytes", file_part_size);
77+
78+
(file_part, file_part_size)
79+
} else {
80+
let file_part = fs::File::create(&file_part_path)
81+
.await
82+
.map_err(|e| DownloaderError::Io(e))?;
83+
84+
(file_part, 0)
85+
}
86+
};
87+
88+
let res = self
89+
.client
90+
.get(url)
91+
.header("Content-Type", "application/octet-stream")
92+
.header("Range", format!("bytes={}-", file_part_size))
93+
.await
94+
.map_err(|e| DownloaderError::Request(e))?;
95+
96+
let mut stream = res.bytes();
97+
while let Some(byte) = stream.next().await {
98+
let byte = byte.map_err(|e| DownloaderError::Io(e))?;
99+
target
100+
.write(&[byte])
101+
.await
102+
.map_err(|e| DownloaderError::Io(e))?;
103+
}
104+
105+
log::debug!("Download complete: {:?}", file_part_path);
106+
107+
fs::rename(file_part_path, file_path)
108+
.await
109+
.map_err(|e| DownloaderError::Io(e))?;
110+
111+
Ok(())
112+
}
113+
114+
fn parse_name(&self, url: &str) -> Result<String, DownloaderError> {
115+
Url::parse(url)
116+
.map_err(|_| DownloaderError::NotFoundFileName(url.to_string()))?
117+
.path_segments()
118+
.ok_or_else(|| DownloaderError::NotFoundFileName(url.to_string()))?
119+
.last()
120+
.ok_or_else(|| DownloaderError::NotFoundFileName(url.to_string()))
121+
.map(|s| s.to_string())
122+
}
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
129+
use tempfile::tempdir;
130+
131+
#[ignore]
132+
#[async_std::test]
133+
async fn test_download_ok() {
134+
let location = tempdir().expect("Failed to create temp directory");
135+
let location_path = location.path();
136+
137+
let downloader = Downloader::new(location_path.to_path_buf());
138+
let result = downloader
139+
.download(
140+
"https://github.com/imsnif/monocle/releases/download/0.39.0/monocle.wasm",
141+
Some("monocle.wasm"),
142+
)
143+
.await
144+
.is_ok();
145+
146+
assert!(result);
147+
assert!(location_path.join("monocle.wasm").exists());
148+
149+
location.close().expect("Failed to close temp directory");
150+
}
151+
152+
#[ignore]
153+
#[async_std::test]
154+
async fn test_download_without_file_name() {
155+
let location = tempdir().expect("Failed to create temp directory");
156+
let location_path = location.path();
157+
158+
let downloader = Downloader::new(location_path.to_path_buf());
159+
let result = downloader
160+
.download(
161+
"https://github.com/imsnif/multitask/releases/download/0.38.2v2/multitask.wasm",
162+
None,
163+
)
164+
.await
165+
.is_ok();
166+
167+
assert!(result);
168+
assert!(location_path.join("multitask.wasm").exists());
169+
170+
location.close().expect("Failed to close temp directory");
171+
}
172+
}

zellij-utils/src/downloader/download.rs

Lines changed: 0 additions & 49 deletions
This file was deleted.

0 commit comments

Comments
 (0)