Add profile download policy
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
use std::time::SystemTime;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use crate::{DomainError, DownloadId, ProfileId, UrlText};
|
||||
|
||||
@@ -11,23 +14,126 @@ pub enum DownloadState {
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum DownloadDestination {
|
||||
AskEveryTime,
|
||||
FixedDirectory(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DownloadPolicy {
|
||||
destination: DownloadDestination,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum DownloadSecurity {
|
||||
Standard,
|
||||
DangerousExtension,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DownloadEntry {
|
||||
id: DownloadId,
|
||||
profile_id: ProfileId,
|
||||
source_url: UrlText,
|
||||
file_name: String,
|
||||
destination: DownloadDestination,
|
||||
security: DownloadSecurity,
|
||||
state: DownloadState,
|
||||
received_bytes: u64,
|
||||
total_bytes: Option<u64>,
|
||||
started_at: SystemTime,
|
||||
}
|
||||
|
||||
const DANGEROUS_DOWNLOAD_EXTENSIONS: &[&str] = &[
|
||||
"app",
|
||||
"applescript",
|
||||
"bat",
|
||||
"bash",
|
||||
"cmd",
|
||||
"com",
|
||||
"command",
|
||||
"dmg",
|
||||
"exe",
|
||||
"fish",
|
||||
"jar",
|
||||
"js",
|
||||
"jse",
|
||||
"msi",
|
||||
"msp",
|
||||
"pkg",
|
||||
"ps1",
|
||||
"psm1",
|
||||
"reg",
|
||||
"scr",
|
||||
"scpt",
|
||||
"sh",
|
||||
"terminal",
|
||||
"vbe",
|
||||
"vbs",
|
||||
"workflow",
|
||||
"zsh",
|
||||
];
|
||||
|
||||
impl DownloadDestination {
|
||||
pub fn fixed_directory(path: impl Into<PathBuf>) -> Result<Self, DomainError> {
|
||||
let path = path.into();
|
||||
if path.as_os_str().is_empty() {
|
||||
return Err(DomainError::EmptyField { field: "download_directory" });
|
||||
}
|
||||
if !path.is_absolute() {
|
||||
return Err(DomainError::InvalidDownloadDirectory { path: path.display().to_string() });
|
||||
}
|
||||
|
||||
Ok(Self::FixedDirectory(path))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn as_path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::AskEveryTime => None,
|
||||
Self::FixedDirectory(path) => Some(path.as_path()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadPolicy {
|
||||
#[must_use]
|
||||
pub fn ask_every_time() -> Self {
|
||||
Self { destination: DownloadDestination::AskEveryTime }
|
||||
}
|
||||
|
||||
pub fn fixed_directory(path: impl Into<PathBuf>) -> Result<Self, DomainError> {
|
||||
Ok(Self { destination: DownloadDestination::fixed_directory(path)? })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn destination(&self) -> &DownloadDestination {
|
||||
&self.destination
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadSecurity {
|
||||
#[must_use]
|
||||
pub fn for_file_name(file_name: &str) -> Self {
|
||||
match download_extension(file_name) {
|
||||
Some(extension) if is_dangerous_extension(extension) => Self::DangerousExtension,
|
||||
_ => Self::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn requires_prompt(&self) -> bool {
|
||||
matches!(self, Self::DangerousExtension)
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadEntry {
|
||||
pub fn started(
|
||||
profile_id: ProfileId,
|
||||
source_url: UrlText,
|
||||
file_name: impl Into<String>,
|
||||
destination: DownloadDestination,
|
||||
total_bytes: Option<u64>,
|
||||
started_at: SystemTime,
|
||||
) -> Result<Self, DomainError> {
|
||||
@@ -36,12 +142,15 @@ impl DownloadEntry {
|
||||
if file_name.is_empty() {
|
||||
return Err(DomainError::EmptyField { field: "file_name" });
|
||||
}
|
||||
validate_file_name(file_name)?;
|
||||
|
||||
Ok(Self {
|
||||
id: DownloadId::new(),
|
||||
profile_id,
|
||||
source_url,
|
||||
file_name: file_name.to_string(),
|
||||
destination,
|
||||
security: DownloadSecurity::for_file_name(file_name),
|
||||
state: DownloadState::InProgress,
|
||||
received_bytes: 0,
|
||||
total_bytes,
|
||||
@@ -115,6 +224,16 @@ impl DownloadEntry {
|
||||
&self.file_name
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn destination(&self) -> &DownloadDestination {
|
||||
&self.destination
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn security(&self) -> &DownloadSecurity {
|
||||
&self.security
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn state(&self) -> &DownloadState {
|
||||
&self.state
|
||||
@@ -170,3 +289,28 @@ impl DownloadState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_file_name(file_name: &str) -> Result<(), DomainError> {
|
||||
let has_path_separator = file_name.chars().any(|ch| ch == '/' || ch == '\\');
|
||||
let is_parent_reference = file_name == "." || file_name == "..";
|
||||
let has_control_character = file_name.chars().any(char::is_control);
|
||||
|
||||
if has_path_separator || is_parent_reference || has_control_character {
|
||||
return Err(DomainError::InvalidFileName { value: file_name.to_string() });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_extension(file_name: &str) -> Option<&str> {
|
||||
let (_, extension) = file_name.rsplit_once('.')?;
|
||||
if extension.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(extension)
|
||||
}
|
||||
|
||||
fn is_dangerous_extension(extension: &str) -> bool {
|
||||
DANGEROUS_DOWNLOAD_EXTENSIONS.iter().any(|candidate| extension.eq_ignore_ascii_case(candidate))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user