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)));
|
||||
}
|
||||
|
||||
let response_headers = resp.headers().clone();
|
||||
|
||||
let data = resp.bytes().await
|
||||
.map_err(|e| ChannelError::Other(format!("Failed to read file data: {}", e)))?
|
||||
.to_vec();
|
||||
|
||||
let extension = match file_type {
|
||||
"audio" => "mp3",
|
||||
"video" => "mp4",
|
||||
_ => "bin",
|
||||
};
|
||||
let filename = format!("{}_{}.{}", message_id, &file_key[..8.min(file_key.len())], extension);
|
||||
let filename = infer_download_filename(
|
||||
content_json,
|
||||
&response_headers,
|
||||
message_id,
|
||||
file_key,
|
||||
file_type,
|
||||
);
|
||||
let file_path = media_dir.join(&filename);
|
||||
|
||||
tokio::fs::write(&file_path, &data).await
|
||||
@ -437,6 +440,15 @@ impl FeishuChannel {
|
||||
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
|
||||
async fn upload_image(&self, file_path: &str) -> Result<String, ChannelError> {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::{FeishuChannel, MsgFormat};
|
||||
use super::{extract_file_name_from_content_disposition, infer_download_filename, sanitize_download_file_name, FeishuChannel, MsgFormat};
|
||||
|
||||
#[test]
|
||||
fn markdown_post_uses_md_tag() {
|
||||
@ -1945,6 +2031,65 @@ mod tests {
|
||||
let content = "intro\n## heading";
|
||||
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]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user