use crate::zfile;
use crate::zfile::ZPath;
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Method {
GET,
POST,
PUT,
DELETE,
HEAD,
OPTIONS,
PATCH,
}
impl Method {
pub(crate) fn as_str(&self) -> &str {
match self {
Method::GET => "GET",
Method::POST => "POST",
Method::PUT => "PUT",
Method::DELETE => "DELETE",
Method::HEAD => "HEAD",
Method::OPTIONS => "OPTIONS",
Method::PATCH => "PATCH",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum BodyFormat {
FormUrlEncoded,
Json,
Text,
FormData(String),
Custom(String),
}
impl BodyFormat {
pub(crate) fn content_type(&self) -> String {
match self {
BodyFormat::FormUrlEncoded => "application/x-www-form-urlencoded".to_string(),
BodyFormat::Json => "application/json".to_string(),
BodyFormat::Text => "text/plain".to_string(),
BodyFormat::FormData(boundary) => format!("multipart/form-data; boundary={}", boundary),
BodyFormat::Custom(content_type) => content_type.clone(),
}
}
}
#[derive(Debug)]
pub struct Response {
pub status: u16,
pub(crate) headers: HashMap<String, String>,
pub(crate) body: Vec<u8>,
}
impl Response {
pub fn text(&self) -> Result<String, Error> {
String::from_utf8(self.body.clone()).map_err(|e| Error::Custom(e.to_string()))
}
#[cfg(feature = "json")]
pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, Error> {
serde_json::from_slice(&self.body).map_err(|e| Error::Custom(e.to_string()))
}
pub fn bytes(&self) -> &[u8] {
&self.body
}
pub fn header(&self, name: &str) -> Option<&String> {
self.headers.iter().find_map(|(key, value)| {
if key.eq_ignore_ascii_case(name) {
Some(value)
} else {
None
}
})
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub fn is_success(&self) -> bool {
(200..300).contains(&self.status)
}
pub fn is_redirect(&self) -> bool {
(300..400).contains(&self.status)
}
pub fn is_client_error(&self) -> bool {
(400..500).contains(&self.status)
}
pub fn is_server_error(&self) -> bool {
(500..600).contains(&self.status)
}
pub fn content_length(&self) -> usize {
self.body.len()
}
pub fn content_type(&self) -> Option<&String> {
self.header("Content-Type")
}
#[cfg(feature = "zfile")]
pub fn save_to_file<P: AsRef<std::path::Path>>(&self, path: P) -> Result<ZPath, Error> {
zfile::write_file(
&path.as_ref().to_string_lossy(),
&String::from_utf8_lossy(&self.body),
false,
)
.map_err(|e| Error::Custom(format!("保存文件失败: {}", e)))
.map(|path| ZPath::new(&path.as_ref().to_string_lossy()))
}
#[cfg(feature = "zfile")]
pub fn save_to_dir<P: AsRef<std::path::Path>>(
&self,
dir: P,
) -> Result<std::path::PathBuf, Error> {
use crate::zfile;
use std::path::PathBuf;
zfile::create_dir(&dir.as_ref().to_string_lossy())
.map_err(|e| Error::Custom(format!("创建目录失败: {}", e)))?;
let filename = self
.get_filename_from_disposition()
.or_else(|| self.get_filename_from_url())
.unwrap_or_else(|| "downloaded_file".to_string());
let path = PathBuf::from(dir.as_ref()).join(filename);
zfile::write_file(
&path.to_string_lossy(),
&String::from_utf8_lossy(&self.body),
false,
)
.map_err(|e| Error::Custom(format!("保存文件失败: {}", e)))?;
Ok(path)
}
fn get_filename_from_disposition(&self) -> Option<String> {
self.header("Content-Disposition").and_then(|cd| {
cd.split(';')
.find(|part| part.trim().starts_with("filename="))
.and_then(|filename_part| {
filename_part
.trim()
.strip_prefix("filename=")
.map(|filename| filename.trim_matches('"').trim_matches('\'').to_string())
})
})
}
fn get_filename_from_url(&self) -> Option<String> {
self.header("Location")
.or_else(|| self.header("X-Original-URL"))
.and_then(|url| {
url.split('/')
.last()
.map(|s| s.split('?').next().unwrap_or(s).to_string())
})
}
}
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub timeout: Duration,
pub user_agent: String,
pub max_retries: u32,
pub retry_interval: Duration,
pub allow_redirects: bool,
pub max_redirects: u32,
pub verify_ssl: bool,
pub debug: bool,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36".to_string(),
max_retries: 3,
retry_interval: Duration::from_secs(1),
allow_redirects: true,
max_redirects: 10,
verify_ssl: true,
debug: false,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("请求超时")]
Timeout,
#[error("网络错误: {0}")]
Network(#[from] std::io::Error),
#[error("URL 解析错误: {0}")]
UrlParse(#[from] url::ParseError),
#[error("响应解析错误: {0}")]
ResponseParse(String),
#[error("重定向次数过多")]
TooManyRedirects,
#[error("SSL 证书验证失败")]
SslVerification,
#[error("无效的状态码")]
InvalidStatus,
#[error("自定义错误: {0}")]
Custom(String),
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::Custom(err.to_string())
}
}