feat(feishu): 优化文件下载文件名推断逻辑,支持从响应头和内容中提取文件名
This commit is contained in:
parent
0dfa615ca9
commit
09ccd71cc7
@ -412,16 +412,19 @@ impl FeishuChannel {
|
|||||||
return Err(ChannelError::Other(format!("File download failed {}: {}", status, error_text)));
|
return Err(ChannelError::Other(format!("File download failed {}: {}", status, error_text)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let response_headers = resp.headers().clone();
|
||||||
|
|
||||||
let data = resp.bytes().await
|
let data = resp.bytes().await
|
||||||
.map_err(|e| ChannelError::Other(format!("Failed to read file data: {}", e)))?
|
.map_err(|e| ChannelError::Other(format!("Failed to read file data: {}", e)))?
|
||||||
.to_vec();
|
.to_vec();
|
||||||
|
|
||||||
let extension = match file_type {
|
let filename = infer_download_filename(
|
||||||
"audio" => "mp3",
|
content_json,
|
||||||
"video" => "mp4",
|
&response_headers,
|
||||||
_ => "bin",
|
message_id,
|
||||||
};
|
file_key,
|
||||||
let filename = format!("{}_{}.{}", message_id, &file_key[..8.min(file_key.len())], extension);
|
file_type,
|
||||||
|
);
|
||||||
let file_path = media_dir.join(&filename);
|
let file_path = media_dir.join(&filename);
|
||||||
|
|
||||||
tokio::fs::write(&file_path, &data).await
|
tokio::fs::write(&file_path, &data).await
|
||||||
@ -437,6 +440,15 @@ impl FeishuChannel {
|
|||||||
Ok((format!("[{}: {}]", file_type, filename), Some(media_item)))
|
Ok((format!("[{}: {}]", file_type, filename), Some(media_item)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fallback_download_filename(message_id: &str, file_key: &str, file_type: &str) -> String {
|
||||||
|
let extension = match file_type {
|
||||||
|
"audio" => "mp3",
|
||||||
|
"video" => "mp4",
|
||||||
|
_ => "bin",
|
||||||
|
};
|
||||||
|
format!("{}_{}.{}", message_id, &file_key[..8.min(file_key.len())], extension)
|
||||||
|
}
|
||||||
|
|
||||||
/// Upload image to Feishu and return the image_key
|
/// Upload image to Feishu and return the image_key
|
||||||
async fn upload_image(&self, file_path: &str) -> Result<String, ChannelError> {
|
async fn upload_image(&self, file_path: &str) -> Result<String, ChannelError> {
|
||||||
let token = self.get_tenant_access_token().await?;
|
let token = self.get_tenant_access_token().await?;
|
||||||
@ -1920,9 +1932,83 @@ impl FeishuChannel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn infer_download_filename(
|
||||||
|
content_json: &serde_json::Value,
|
||||||
|
headers: &reqwest::header::HeaderMap,
|
||||||
|
message_id: &str,
|
||||||
|
file_key: &str,
|
||||||
|
file_type: &str,
|
||||||
|
) -> String {
|
||||||
|
if let Some(file_name) = extract_original_file_name(content_json, headers) {
|
||||||
|
let sanitized = sanitize_download_file_name(&file_name);
|
||||||
|
if !sanitized.is_empty() {
|
||||||
|
return format!("{}_{}", message_id, sanitized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FeishuChannel::fallback_download_filename(message_id, file_key, file_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_original_file_name(
|
||||||
|
content_json: &serde_json::Value,
|
||||||
|
headers: &reqwest::header::HeaderMap,
|
||||||
|
) -> Option<String> {
|
||||||
|
let content_name = ["file_name", "filename", "name"]
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|key| content_json.get(key).and_then(|value| value.as_str()))
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
if content_name.is_some() {
|
||||||
|
return content_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_file_name_from_content_disposition(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_file_name_from_content_disposition(
|
||||||
|
headers: &reqwest::header::HeaderMap,
|
||||||
|
) -> Option<String> {
|
||||||
|
let header = headers
|
||||||
|
.get(reqwest::header::CONTENT_DISPOSITION)
|
||||||
|
.and_then(|value| value.to_str().ok())?;
|
||||||
|
|
||||||
|
for segment in header.split(';').map(str::trim) {
|
||||||
|
if let Some(value) = segment.strip_prefix("filename*=") {
|
||||||
|
let decoded = value.split("''").last().unwrap_or(value).trim_matches('"');
|
||||||
|
if !decoded.is_empty() {
|
||||||
|
return Some(decoded.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = segment.strip_prefix("filename=") {
|
||||||
|
let cleaned = value.trim_matches('"').trim();
|
||||||
|
if !cleaned.is_empty() {
|
||||||
|
return Some(cleaned.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_download_file_name(file_name: &str) -> String {
|
||||||
|
file_name
|
||||||
|
.chars()
|
||||||
|
.map(|ch| match ch {
|
||||||
|
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||||
|
_ => ch,
|
||||||
|
})
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_matches('.')
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{FeishuChannel, MsgFormat};
|
use super::{extract_file_name_from_content_disposition, infer_download_filename, sanitize_download_file_name, FeishuChannel, MsgFormat};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn markdown_post_uses_md_tag() {
|
fn markdown_post_uses_md_tag() {
|
||||||
@ -1945,6 +2031,65 @@ mod tests {
|
|||||||
let content = "intro\n## heading";
|
let content = "intro\n## heading";
|
||||||
assert_eq!(FeishuChannel::detect_msg_format(content), MsgFormat::Interactive);
|
assert_eq!(FeishuChannel::detect_msg_format(content), MsgFormat::Interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infer_download_filename_prefers_original_file_name() {
|
||||||
|
let content = serde_json::json!({
|
||||||
|
"file_key": "file_key_123",
|
||||||
|
"file_name": "demo-archive.zip"
|
||||||
|
});
|
||||||
|
let headers = reqwest::header::HeaderMap::new();
|
||||||
|
|
||||||
|
let filename = infer_download_filename(&content, &headers, "om_123", "file_key_123", "file");
|
||||||
|
|
||||||
|
assert_eq!(filename, "om_123_demo-archive.zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infer_download_filename_uses_content_disposition_when_message_lacks_name() {
|
||||||
|
let content = serde_json::json!({
|
||||||
|
"file_key": "file_key_123"
|
||||||
|
});
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::CONTENT_DISPOSITION,
|
||||||
|
reqwest::header::HeaderValue::from_static("attachment; filename=meeting-notes.zip"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let filename = infer_download_filename(&content, &headers, "om_123", "file_key_123", "file");
|
||||||
|
|
||||||
|
assert_eq!(filename, "om_123_meeting-notes.zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infer_download_filename_falls_back_to_bin_without_name() {
|
||||||
|
let content = serde_json::json!({
|
||||||
|
"file_key": "file_key_123"
|
||||||
|
});
|
||||||
|
let headers = reqwest::header::HeaderMap::new();
|
||||||
|
|
||||||
|
let filename = infer_download_filename(&content, &headers, "om_123", "file_key_123", "file");
|
||||||
|
|
||||||
|
assert_eq!(filename, "om_123_file_key.bin");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sanitize_download_file_name_replaces_path_separators() {
|
||||||
|
let sanitized = sanitize_download_file_name("../../demo/archive.zip");
|
||||||
|
assert_eq!(sanitized, "_.._demo_archive.zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_file_name_from_content_disposition_supports_filename_star() {
|
||||||
|
let mut headers = reqwest::header::HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
reqwest::header::CONTENT_DISPOSITION,
|
||||||
|
reqwest::header::HeaderValue::from_static("attachment; filename*=UTF-8''archive.zip"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let file_name = extract_file_name_from_content_disposition(&headers);
|
||||||
|
assert_eq!(file_name.as_deref(), Some("archive.zip"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user