Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.

Commit 33e1d91

Browse files
committed
prioritize filename from response header if not provided
1 parent fba8698 commit 33e1d91

File tree

3 files changed

+50
-25
lines changed

3 files changed

+50
-25
lines changed

src/downloader.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88

99
use futures::{future::join_all, StreamExt};
1010
use regex::Regex;
11-
use reqwest::header::USER_AGENT;
11+
use reqwest::header::{CONTENT_DISPOSITION, USER_AGENT};
1212
use tokio::{
1313
fs::{self, OpenOptions},
1414
io::AsyncWriteExt,
@@ -20,7 +20,7 @@ use url::Url;
2020
use crate::{
2121
error::DownloadError,
2222
oci::{OciClient, OciLayer, OciManifest, Reference},
23-
utils::{extract_filename, is_elf, matches_pattern},
23+
utils::{extract_filename, extract_filename_from_url, is_elf, matches_pattern},
2424
};
2525

2626
#[derive(Debug, Clone)]
@@ -83,13 +83,25 @@ impl Downloader {
8383

8484
let filename = options
8585
.output_path
86-
.unwrap_or_else(|| extract_filename(&options.url));
86+
.or_else(|| {
87+
response
88+
.headers()
89+
.get(CONTENT_DISPOSITION)
90+
.and_then(|header| header.to_str().ok())
91+
.and_then(|header| extract_filename(header))
92+
})
93+
.or_else(|| extract_filename_from_url(&options.url))
94+
.ok_or(DownloadError::FileNameNotFound)?;
95+
8796
let filename = if filename.ends_with('/') {
88-
format!(
89-
"{}/{}",
90-
filename.trim_end_matches('/'),
91-
extract_filename(&options.url)
92-
)
97+
let extracted_name = response
98+
.headers()
99+
.get(CONTENT_DISPOSITION)
100+
.and_then(|header| header.to_str().ok())
101+
.and_then(|header| extract_filename(header))
102+
.or_else(|| extract_filename_from_url(&options.url))
103+
.ok_or(DownloadError::FileNameNotFound)?;
104+
format!("{}/{}", filename.trim_end_matches('/'), extracted_name)
93105
} else {
94106
filename
95107
};

src/error.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub enum DownloadError {
1717
InvalidResponse,
1818
LayersNotFound,
1919
ChunkError,
20+
FileNameNotFound,
2021
}
2122

2223
impl Display for DownloadError {
@@ -31,6 +32,12 @@ impl Display for DownloadError {
3132
DownloadError::LayersNotFound => write!(f, "No downloadable layers found"),
3233
DownloadError::InvalidResponse => write!(f, "Failed to parse response"),
3334
DownloadError::ChunkError => write!(f, "Failed to read chunk"),
35+
DownloadError::FileNameNotFound => {
36+
write!(
37+
f,
38+
"Couldn't find filename. Please provide filename explicitly."
39+
)
40+
}
3441
}
3542
}
3643
}
@@ -41,10 +48,7 @@ impl Error for DownloadError {
4148
DownloadError::IoError(err) => Some(err),
4249
DownloadError::InvalidUrl { source, .. } => Some(source),
4350
DownloadError::NetworkError { source } => Some(source),
44-
DownloadError::ResourceError { .. } => None,
45-
DownloadError::LayersNotFound => None,
46-
DownloadError::InvalidResponse => None,
47-
DownloadError::ChunkError => None,
51+
_ => None,
4852
}
4953
}
5054
}

src/utils.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
1-
use std::{
2-
path::Path,
3-
time::{SystemTime, UNIX_EPOCH},
4-
};
1+
use std::path::Path;
52

63
use regex::Regex;
74
use reqwest::StatusCode;
85
use tokio::{
96
fs::File,
107
io::{AsyncReadExt, BufReader},
118
};
9+
use url::Url;
1210

1311
pub const ELF_MAGIC_BYTES: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46];
1412

15-
pub fn extract_filename(url: &str) -> String {
16-
Path::new(url)
13+
pub fn extract_filename_from_url(url: &str) -> Option<String> {
14+
let url = Url::parse(url)
15+
.map(|u| u.path().to_string())
16+
.unwrap_or_else(|_| url.to_string());
17+
Path::new(&url)
1718
.file_name()
1819
.map(|name| name.to_string_lossy().to_string())
19-
.unwrap_or_else(|| {
20-
let dt = SystemTime::now()
21-
.duration_since(UNIX_EPOCH)
22-
.unwrap()
23-
.as_millis();
24-
dt.to_string()
25-
})
20+
}
21+
22+
pub fn extract_filename(header_value: &str) -> Option<String> {
23+
header_value.split(';').find_map(|part| {
24+
let part = part.trim();
25+
if part.starts_with("filename=") {
26+
Some(
27+
part.trim_start_matches("filename=")
28+
.trim_matches('"')
29+
.to_string(),
30+
)
31+
} else {
32+
None
33+
}
34+
})
2635
}
2736

2837
pub async fn is_elf<P: AsRef<Path>>(file_path: P) -> bool {

0 commit comments

Comments
 (0)