diff --git a/Cargo.toml b/Cargo.toml index 717d761..29eaecf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,10 @@ description = "A complex toolset that include various small functions." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +futures-util = { version = "0.3" } +indicatif = { version = "0.17" } +path-absolutize = { version = "3" } +reqwest = { version = "0.12", features = ["stream"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", features = ["env-filter", "local-time"] } diff --git a/src/file.rs b/src/file.rs new file mode 100644 index 0000000..938ee45 --- /dev/null +++ b/src/file.rs @@ -0,0 +1,197 @@ +use std::path::{Path, PathBuf}; +use tokio::fs::{File, OpenOptions}; +use tokio::io::{AsyncSeekExt, AsyncWriteExt, SeekFrom}; + +use futures_util::StreamExt; +use indicatif::{ProgressBar, ProgressStyle}; +use path_absolutize::Absolutize; +use reqwest::Client; +use tracing::{debug, info}; + +/// 从给定的 URL 下载文件并将其保存到指定的目标路径。 +/// +/// 该函数初始化一个异步运行时,并阻塞在异步下载函数上,直到下载完成。 +/// +/// # 参数 +/// - `url`: 要下载文件的 URL。 +/// - `target`: 一个可选参数,指定下载文件保存的路径。如果为 `None`,函数会自动确定保存路径。 +/// +/// # 返回值 +/// 该函数返回一个 `Result` 类型,如果下载成功则为 `Ok(())`,如果发生错误则返回错误信息。 +/// +/// # 错误 +/// 如果异步运行时初始化失败或者下载失败,该函数会返回一个错误。 +/// +/// # 示例 +/// ``` +/// let url = "https://example.com/file.zip"; +/// let target_path = Some("/path/to/save/file.zip"); +/// download(url, target_path)?; +/// ``` +/// +/// # 备注 +/// `crate::async_runtime` 函数是一个辅助函数,用于返回一个异步运行时的引用。`async_download` 函数是执行实际异步下载过程的实现。 +pub fn download>(url: &str, target: Option

) -> crate::Result<()> { + // 使用库中的辅助函数初始化异步运行时。 + let rt = crate::async_runtime()?; + // 阻塞在异步下载函数上,直到下载完成。 + rt.block_on(async_download(url, target)) +} + +/// 异步下载文件并保存到指定的目标路径。 +/// +/// 该函数使用异步 HTTP 客户端从指定的 URL 下载文件,并将其保存到指定的目标路径。 +/// 如果目标路径为空,则使用 URL 的文件名作为本地文件名。 +/// 支持断点续传功能。 +/// +/// # 参数 +/// - `url`: 要下载文件的 URL。 +/// - `target`: 一个可选参数,指定下载文件保存的路径。如果为空,则使用 URL 的文件名。 +/// +/// # 返回值 +/// 该函数返回一个 `Result` 类型,如果下载成功则为 `Ok(())`,如果发生错误则返回错误信息。 +/// +/// # 错误 +/// 如果下载文件的父目录不存在、无法创建或写入文件、或者下载过程中出现任何问题,该函数会返回一个错误。 +/// +/// # 示例 +/// ```rust +/// let url = "https://example.com/file.zip"; +/// let target_path = Some("/path/to/save/file.zip"); +/// async_download(url, target_path).await?; +/// ``` +/// +/// # 备注 +/// 使用 `Client` 进行 HTTP 请求,`OpenOptions` 和 `File` 进行文件操作,支持断点续传。 +pub async fn async_download>(url: &str, target: Option

) -> crate::Result<()> { + debug!("Download url: {}", url); + // 创建一个新的 HTTP 客户端 + let client = Client::new(); + // 发送 GET 请求获取响应 + let response = client.get(url).send().await?; + + // 确定下载文件的路径 + let download_file = match target { + Some(file) => { + let file = abs_path(Path::new(file.as_ref()))?; + // 检查下载文件的父目录是否存在 + if !parent_exists(&file) { + return Err("下载文件的父级目录不存在!".into()); + } + file + } + None => { + // 如果未指定目标路径,则使用 URL 的文件名 + let _remote = Path::new(url); + let _local = match _remote.file_name() { + Some(file) => file, + None => return Err("无效的文件名!".into()), + }; + debug!("Get raw name by url: {:?}", _local); + abs_path(Path::new(_local))? + } + }; + debug!("Download file: {:?}", download_file); + + // 打开或创建文件 + let mut file = if download_file.exists() { + info!("File exists, continue downloading."); + OpenOptions::new() + .read(true) + .write(true) + .open(&download_file) + .await? + } else { + File::create(&download_file).await? + }; + + // 获取当前文件的大小和总大小 + let mut current_size = file.metadata().await?.len(); + let total_size = response.content_length().unwrap(); + // 如果文件已经完全下载,则返回 + if current_size == total_size { + info!("File already downloaded: {:?}.", download_file); + return Ok(()); + } + + // 如果文件部分下载过,则进行断点续传 + let mut request = client.get(url); + if current_size > 0 { + request = request.header("Range", format!("bytes={}-", current_size)); + file.seek(SeekFrom::Start(current_size)).await?; + } + + // 发送请求并获取响应 + let response = request.send().await?; + let mut stream = response.bytes_stream(); + + // 设置下载进度条 + let pb = ProgressBar::new(total_size); + pb.set_style( + ProgressStyle::default_bar() + .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta_precise})").unwrap() + .progress_chars("#>-"), + ); + + // 读取响应数据并写入文件 + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + file.write_all(&chunk).await?; + current_size += chunk.len() as u64; + + // 更新下载进度 + pb.set_position(current_size); + } + + Ok(()) +} + +/// 检查给定路径的父目录是否存在。 +/// +/// 该函数获取路径的父目录并检查该目录是否存在。 +/// +/// # 参数 +/// - `path`: 要检查的路径。 +/// +/// # 返回值 +/// 如果父目录存在则返回 `true`,否则返回 `false`。 +/// +/// # 示例 +/// ``` +/// use std::path::Path; +/// let path = Path::new("/some/directory/file.txt"); +/// assert_eq!(parent_exists(&path), true); +/// ``` +pub fn parent_exists(path: &Path) -> bool { + // 获取路径的父目录,并检查该目录是否存在。 + match path.parent() { + Some(parent) => parent.exists(), + None => false, + } +} + +/// 获取给定路径的绝对路径。 +/// +/// 该函数接收一个路径并返回其绝对路径。 +/// +/// # 参数 +/// - `path`: 要转换为绝对路径的相对路径或绝对路径。 +/// +/// # 返回值 +/// 该函数返回一个 `Result` 类型,如果转换成功则为 `Ok(PathBuf)`,如果发生错误则返回错误信息。 +/// +/// # 错误 +/// 如果路径无法转换为绝对路径,该函数会返回一个错误。 +/// +/// # 示例 +/// ```rust +/// use std::path::Path; +/// let relative_path = Path::new("some/relative/path"); +/// let absolute_path = abs_path(&relative_path)?; +/// println!("Absolute path: {:?}", absolute_path); +/// ``` +pub fn abs_path(path: &Path) -> crate::Result { + // 将路径转换为绝对路径,并将其转换为 PathBuf 类型 + let abs = path.absolutize()?.to_path_buf(); + Ok(abs) +} diff --git a/src/lib.rs b/src/lib.rs index a6c11b3..49aa67a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,57 @@ +use tokio::runtime::Runtime; +use tracing_subscriber::{fmt, EnvFilter}; + +/// 包含了一些常用的文件操作函数 +pub mod file; + +/// 定义 crate::Error +/// 大部分函数返回的错误 pub type Error = Box; + +/// 定义 crate::Result pub type Result = std::result::Result; + +/// 创建一个 `tokio` 异步运行时,可用于在同步函数中 block 调用异步方法,例: +/// +/// ```no_run +/// let rt = ccutils::async_runtime()?; +/// +/// rt.block_on(async { +/// println!("Run as Async"); +/// }) +/// ``` +pub fn async_runtime() -> crate::Result { + Ok(Runtime::new()?) +} + +/// 设置日志记录配置。 +/// +/// 该函数配置日志记录格式、时间戳、行号和环境过滤器。 +/// 使用 `tracing` 和 `tracing_subscriber` 库进行日志记录设置。 +/// +/// # 返回值 +/// 该函数返回一个 `Result` 类型,如果日志记录设置成功则为 `Ok(())`,如果发生错误则返回错误信息。 +/// +/// # 错误 +/// 如果日志记录初始化失败,该函数会返回一个错误。 +/// +/// # 示例 +/// ```rust +/// set_up_logging()?; +/// ``` +pub fn set_up_logging() -> crate::Result<()> { + // 配置日志格式化器 + fmt() + // 禁用 ANSI 转义序列(颜色) + .with_ansi(false) + // 设置时间戳格式为 RFC 3339 + .with_timer(fmt::time::OffsetTime::local_rfc_3339().unwrap()) + // 启用行号 + .with_line_number(true) + // 使用环境变量过滤日志级别 + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + // 尝试初始化日志记录 + .try_init() +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..7de2dde --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +use ccutils::file; + +fn main() -> ccutils::Result<()> { + println!("请运行 `cargo doc --open` 查看文档"); + Ok(()) +}