* feat(cli): allow loading layouts directly from a url * feat(plugins): allow loading layouts directly from a url * style(fmt): rustfmt
197 lines
5.8 KiB
Rust
197 lines
5.8 KiB
Rust
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),
|
|
#[error("Failed to parse URL body: {0}")]
|
|
InvalidUrlBody(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(())
|
|
}
|
|
pub async fn download_without_cache(url: &str) -> Result<String, DownloaderError> {
|
|
// result is the stringified body
|
|
let client = surf::client().with(surf::middleware::Redirect::default());
|
|
|
|
let res = client
|
|
.get(url)
|
|
.header("Content-Type", "application/octet-stream")
|
|
.await
|
|
.map_err(|e| DownloaderError::Request(e))?;
|
|
|
|
let mut downloaded_bytes: Vec<u8> = vec![];
|
|
let mut stream = res.bytes();
|
|
while let Some(byte) = stream.next().await {
|
|
let byte = byte.map_err(|e| DownloaderError::Io(e))?;
|
|
downloaded_bytes.push(byte);
|
|
}
|
|
|
|
log::debug!("Download complete");
|
|
let stringified = String::from_utf8(downloaded_bytes)
|
|
.map_err(|e| DownloaderError::InvalidUrlBody(format!("{}", e)))?;
|
|
|
|
Ok(stringified)
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|