use std::path::{Path, PathBuf};
use crate::downloads::DownloadError;
pub struct DownloadDirectoryPreset;
impl DownloadDirectoryPreset {
pub const USER_DOWNLOADS: &'static str = "user-downloads";
pub const TEMPORARY: &'static str = "temporary";
}
pub const ARTIFACT_FILE_MODE: u32 = 0o600;
pub const ARTIFACT_DIRECTORY_MODE: u32 = 0o700;
pub(crate) fn restrict(path: &Path, mode: u32) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
}
#[cfg(not(unix))]
{
let _ = (path, mode);
}
Ok(())
}
pub fn resolve_download_directory(directory: Option<&str>) -> Result<PathBuf, DownloadError> {
let directory = directory.unwrap_or(DownloadDirectoryPreset::USER_DOWNLOADS);
if directory == DownloadDirectoryPreset::TEMPORARY {
return Ok(std::env::temp_dir().join("browser-commander-downloads"));
}
if directory == DownloadDirectoryPreset::USER_DOWNLOADS {
return Ok(dirs::download_dir().unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(std::env::temp_dir)
.join("Downloads")
}));
}
if directory.is_empty() || !Path::new(directory).is_absolute() {
return Err(DownloadError::RelativeDirectory {
directory: directory.to_string(),
});
}
Ok(PathBuf::from(directory))
}
pub fn prepare_download_directory(root: &Path) -> Result<PathBuf, DownloadError> {
let existed = root.is_dir();
std::fs::create_dir_all(root).map_err(|source| DownloadError::DirectoryNotCreated {
root: root.to_path_buf(),
source,
})?;
if !existed {
let _ = restrict(root, ARTIFACT_DIRECTORY_MODE);
}
let probe = root.join(format!(".browser-commander-probe-{}", std::process::id()));
let written = std::fs::write(&probe, b"");
let _ = std::fs::remove_file(&probe);
written.map_err(|source| DownloadError::DirectoryNotWritable {
root: root.to_path_buf(),
source,
})?;
Ok(root.to_path_buf())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::downloads::test_support::TempDir;
#[test]
fn resolves_the_temporary_preset_under_the_system_temp_folder() {
let resolved =
resolve_download_directory(Some(DownloadDirectoryPreset::TEMPORARY)).unwrap();
assert_eq!(
resolved,
std::env::temp_dir().join("browser-commander-downloads")
);
}
#[test]
fn resolves_the_user_folder_by_default() {
let implicit = resolve_download_directory(None).unwrap();
let explicit =
resolve_download_directory(Some(DownloadDirectoryPreset::USER_DOWNLOADS)).unwrap();
assert_eq!(implicit, explicit);
assert!(implicit.is_absolute(), "{implicit:?} is not absolute");
}
#[test]
fn keeps_an_absolute_path_the_caller_chose() {
let chosen = std::env::temp_dir().join("bc-explicit");
let resolved = resolve_download_directory(Some(chosen.to_str().unwrap())).unwrap();
assert_eq!(resolved, chosen);
}
#[test]
fn refuses_a_relative_directory() {
let error = resolve_download_directory(Some("downloads")).unwrap_err();
assert!(
error.to_string().contains("must be absolute"),
"unexpected message: {error}"
);
}
#[test]
fn creates_the_directory_and_proves_it_is_writable() {
let temp = TempDir::new("bc-destination");
let root = temp.path().join("nested/downloads");
let prepared = prepare_download_directory(&root).unwrap();
assert_eq!(prepared, root);
assert!(root.is_dir());
let leftovers: Vec<_> = std::fs::read_dir(&root)
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect();
assert!(leftovers.is_empty(), "left behind {leftovers:?}");
}
#[cfg(unix)]
#[test]
fn reports_a_directory_it_cannot_write_into() {
let temp = TempDir::new("bc-unwritable");
let root = temp.path().join("read-only");
std::fs::create_dir_all(&root).unwrap();
restrict(&root, 0o500).unwrap();
if std::fs::write(root.join("probe"), b"").is_ok() {
restrict(&root, 0o700).unwrap();
return;
}
let error = prepare_download_directory(&root).unwrap_err();
restrict(&root, 0o700).unwrap();
assert!(
error.to_string().contains("is not writable"),
"unexpected message: {error}"
);
}
}