157 lines
5.1 KiB
Rust
157 lines
5.1 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use directories::UserDirs;
|
|
use ely_browser_core::BrowserCore;
|
|
use ely_domain::{DownloadDestination, UrlText};
|
|
use url::Url;
|
|
|
|
pub(super) enum ActiveTabDownloadTarget {
|
|
Prompt { url: UrlText, directory: PathBuf, file_name: String },
|
|
Ready { url: UrlText, target_path: PathBuf },
|
|
}
|
|
|
|
pub(super) fn active_tab_download_target(
|
|
core: &BrowserCore,
|
|
) -> Result<ActiveTabDownloadTarget, String> {
|
|
let tab = core.active_tab().map_err(|error| error.to_string())?;
|
|
let parsed = Url::parse(tab.url().as_str()).map_err(|error| error.to_string())?;
|
|
match parsed.scheme() {
|
|
"http" | "https" => {}
|
|
_ => return Err("Only HTTP and HTTPS pages can be downloaded.".to_string()),
|
|
}
|
|
|
|
let snapshot = core.snapshot().map_err(|error| error.to_string())?;
|
|
let file_name = suggested_download_file_name(tab.url(), tab.title());
|
|
match snapshot.active_download_policy.destination() {
|
|
DownloadDestination::AskEveryTime => Ok(ActiveTabDownloadTarget::Prompt {
|
|
url: tab.url().clone(),
|
|
directory: default_download_directory()?,
|
|
file_name,
|
|
}),
|
|
DownloadDestination::FixedDirectory(directory) => Ok(ActiveTabDownloadTarget::Ready {
|
|
url: tab.url().clone(),
|
|
target_path: unique_download_target_path(directory, &file_name),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn default_download_directory() -> Result<PathBuf, String> {
|
|
UserDirs::new()
|
|
.and_then(|dirs| dirs.download_dir().map(Path::to_path_buf))
|
|
.ok_or_else(|| "Downloads directory is unavailable.".to_string())
|
|
}
|
|
|
|
fn suggested_download_file_name(url: &UrlText, title: &str) -> String {
|
|
let parsed = Url::parse(url.as_str()).ok();
|
|
let url_file_name = parsed
|
|
.as_ref()
|
|
.filter(|url| !url.path().ends_with('/'))
|
|
.and_then(|url| url.path_segments())
|
|
.and_then(|mut segments| segments.rfind(|segment| !segment.is_empty()))
|
|
.map(str::to_string);
|
|
let raw_file_name = url_file_name
|
|
.or_else(|| page_title_file_name(title))
|
|
.or_else(|| {
|
|
parsed.as_ref().and_then(|url| url.host_str().map(|host| format!("{host}.html")))
|
|
})
|
|
.unwrap_or_else(|| "download.html".to_string());
|
|
|
|
sanitize_download_file_name(&raw_file_name)
|
|
}
|
|
|
|
fn page_title_file_name(title: &str) -> Option<String> {
|
|
let title = title.trim();
|
|
(!title.is_empty()).then(|| format!("{title}.html"))
|
|
}
|
|
|
|
fn sanitize_download_file_name(file_name: &str) -> String {
|
|
let cleaned = file_name
|
|
.trim()
|
|
.chars()
|
|
.filter_map(|ch| match ch {
|
|
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => Some('-'),
|
|
ch if ch.is_control() => None,
|
|
ch => Some(ch),
|
|
})
|
|
.take(160)
|
|
.collect::<String>();
|
|
let cleaned = cleaned.trim_matches(|ch| ch == '.' || ch == ' ').to_string();
|
|
|
|
match cleaned.as_str() {
|
|
"" | "." | ".." => "download.html".to_string(),
|
|
_ => cleaned,
|
|
}
|
|
}
|
|
|
|
fn unique_download_target_path(directory: &Path, file_name: &str) -> PathBuf {
|
|
let candidate = directory.join(file_name);
|
|
if !candidate.exists() {
|
|
return candidate;
|
|
}
|
|
|
|
let (stem, extension) = split_file_name(file_name);
|
|
for suffix in 1..1_000 {
|
|
let file_name = match extension {
|
|
Some(extension) => format!("{stem} ({suffix}).{extension}"),
|
|
None => format!("{stem} ({suffix})"),
|
|
};
|
|
let candidate = directory.join(file_name);
|
|
if !candidate.exists() {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
directory.join(format!("{stem} (1000)"))
|
|
}
|
|
|
|
fn split_file_name(file_name: &str) -> (&str, Option<&str>) {
|
|
match file_name.rsplit_once('.') {
|
|
Some((stem, extension)) if !stem.is_empty() && !extension.is_empty() => {
|
|
(stem, Some(extension))
|
|
}
|
|
_ => (file_name, None),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::{error::Error, fs};
|
|
|
|
use ely_domain::UrlText;
|
|
|
|
use super::{suggested_download_file_name, unique_download_target_path};
|
|
|
|
#[test]
|
|
fn suggested_download_file_name_prefers_url_leaf() -> Result<(), Box<dyn Error>> {
|
|
let url = UrlText::parse("https://example.com/files/report.pdf?token=1")?;
|
|
|
|
assert_eq!(suggested_download_file_name(&url, "Example"), "report.pdf");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn suggested_download_file_name_uses_title_for_directory_urls() -> Result<(), Box<dyn Error>> {
|
|
let url = UrlText::parse("https://example.com/articles/")?;
|
|
|
|
assert_eq!(suggested_download_file_name(&url, "A/B: Research"), "A-B- Research.html");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn unique_download_target_path_preserves_existing_files() -> Result<(), Box<dyn Error>> {
|
|
let directory = std::env::temp_dir().join("ely-unique-download-test");
|
|
fs::create_dir_all(&directory)?;
|
|
let existing = directory.join("report.pdf");
|
|
fs::write(&existing, [])?;
|
|
|
|
assert_eq!(
|
|
unique_download_target_path(&directory, "report.pdf"),
|
|
directory.join("report (1).pdf")
|
|
);
|
|
|
|
fs::remove_file(existing)?;
|
|
fs::remove_dir(directory)?;
|
|
Ok(())
|
|
}
|
|
}
|