增加 file 模块

添加功能函数:
  abs_path	获取给定路径的绝对路径。
  async_download	异步下载文件并保存到指定的目标路径。
  download	从给定的 URL 下载文件并将其保存到指定的目标路径。
  parent_exists	检查给定路径的父目录是否存在。

Signed-off-by: Jia Chao <jiac13@chinaunicom.cn>
This commit is contained in:
Jia Chao 2024-07-04 14:46:06 +08:00
parent 4e2268eb46
commit 3544ad5256
4 changed files with 265 additions and 0 deletions

View File

@ -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"] }

197
src/file.rs Normal file
View File

@ -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<P: AsRef<Path>>(url: &str, target: Option<P>) -> 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<P: AsRef<Path>>(url: &str, target: Option<P>) -> 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> {
// 将路径转换为绝对路径,并将其转换为 PathBuf 类型
let abs = path.absolutize()?.to_path_buf();
Ok(abs)
}

View File

@ -1,2 +1,57 @@
use tokio::runtime::Runtime;
use tracing_subscriber::{fmt, EnvFilter};
/// 包含了一些常用的文件操作函数
pub mod file;
/// 定义 crate::Error
/// 大部分函数返回的错误
pub type Error = Box<dyn std::error::Error + Send + Sync>;
/// 定义 crate::Result
pub type Result<T> = std::result::Result<T, Error>;
/// 创建一个 `tokio` 异步运行时,可用于在同步函数中 block 调用异步方法,例:
///
/// ```no_run
/// let rt = ccutils::async_runtime()?;
///
/// rt.block_on(async {
/// println!("Run as Async");
/// })
/// ```
pub fn async_runtime() -> crate::Result<Runtime> {
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()
}

6
src/main.rs Normal file
View File

@ -0,0 +1,6 @@
use ccutils::file;
fn main() -> ccutils::Result<()> {
println!("请运行 `cargo doc --open` 查看文档");
Ok(())
}