Add download lifecycle controls
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use ely_domain::{DomainError, ProfileId, SpaceId, TabId};
|
use ely_domain::{DomainError, DownloadId, ProfileId, SpaceId, TabId};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
#[derive(Clone, Debug, Error, Eq, PartialEq)]
|
||||||
@@ -15,6 +15,9 @@ pub enum CoreError {
|
|||||||
#[error("profile not found: {id}")]
|
#[error("profile not found: {id}")]
|
||||||
ProfileNotFound { id: ProfileId },
|
ProfileNotFound { id: ProfileId },
|
||||||
|
|
||||||
|
#[error("download not found: {id}")]
|
||||||
|
DownloadNotFound { id: DownloadId },
|
||||||
|
|
||||||
#[error("favorite limit reached: {limit}")]
|
#[error("favorite limit reached: {limit}")]
|
||||||
FavoriteLimitReached { limit: usize },
|
FavoriteLimitReached { limit: usize },
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,49 @@ impl BrowserCore {
|
|||||||
Ok(download_id)
|
Ok(download_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn pause_download(&mut self, download_id: &DownloadId) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.pause()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume_download(&mut self, download_id: &DownloadId) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.resume()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel_download(&mut self, download_id: &DownloadId) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.cancel()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retry_download(&mut self, download_id: &DownloadId) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.retry()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_download_progress(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
received_bytes: u64,
|
||||||
|
) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.update_progress(received_bytes)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete_download(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
received_bytes: u64,
|
||||||
|
) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.complete(received_bytes)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fail_download(&mut self, download_id: &DownloadId) -> Result<(), CoreError> {
|
||||||
|
self.download_entry_mut(download_id)?.fail()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn visible_downloads(&self) -> Vec<DownloadEntry> {
|
pub(super) fn visible_downloads(&self) -> Vec<DownloadEntry> {
|
||||||
self.download_entries
|
self.download_entries
|
||||||
.iter()
|
.iter()
|
||||||
@@ -32,4 +75,14 @@ impl BrowserCore {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn download_entry_mut(
|
||||||
|
&mut self,
|
||||||
|
download_id: &DownloadId,
|
||||||
|
) -> Result<&mut DownloadEntry, CoreError> {
|
||||||
|
self.download_entries
|
||||||
|
.iter_mut()
|
||||||
|
.find(|entry| entry.id() == download_id)
|
||||||
|
.ok_or_else(|| CoreError::DownloadNotFound { id: download_id.clone() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
||||||
use ely_domain::{ProfileKind, UrlText};
|
use ely_domain::{DomainError, DownloadId, DownloadState, ProfileKind, UrlText};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn download_entries_stay_with_active_profile() -> Result<(), Box<dyn Error>> {
|
fn download_entries_stay_with_active_profile() -> Result<(), Box<dyn Error>> {
|
||||||
@@ -36,3 +36,118 @@ fn download_entries_stay_with_active_profile() -> Result<(), Box<dyn Error>> {
|
|||||||
assert_eq!(default_snapshot.download_entries[0].file_name(), "report.pdf");
|
assert_eq!(default_snapshot.download_entries[0].file_name(), "report.pdf");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn controls_download_lifecycle() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = core.record_download_started(
|
||||||
|
UrlText::parse("https://example.com/report.pdf")?,
|
||||||
|
"report.pdf",
|
||||||
|
Some(2048),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
core.update_download_progress(&download_id, 1024)?;
|
||||||
|
assert_eq!(active_download(&core)?.received_bytes(), 1024);
|
||||||
|
|
||||||
|
core.pause_download(&download_id)?;
|
||||||
|
assert_eq!(active_download(&core)?.state(), &DownloadState::Paused);
|
||||||
|
|
||||||
|
core.resume_download(&download_id)?;
|
||||||
|
assert_eq!(active_download(&core)?.state(), &DownloadState::InProgress);
|
||||||
|
|
||||||
|
core.complete_download(&download_id, 2048)?;
|
||||||
|
let completed = active_download(&core)?;
|
||||||
|
assert_eq!(completed.state(), &DownloadState::Completed);
|
||||||
|
assert_eq!(completed.received_bytes(), 2048);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retries_cancelled_download_from_zero_bytes() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = core.record_download_started(
|
||||||
|
UrlText::parse("https://example.com/archive.zip")?,
|
||||||
|
"archive.zip",
|
||||||
|
Some(4096),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
core.update_download_progress(&download_id, 1024)?;
|
||||||
|
core.cancel_download(&download_id)?;
|
||||||
|
core.retry_download(&download_id)?;
|
||||||
|
|
||||||
|
let retried = active_download(&core)?;
|
||||||
|
assert_eq!(retried.state(), &DownloadState::InProgress);
|
||||||
|
assert_eq!(retried.received_bytes(), 0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_invalid_download_transition() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = core.record_download_started(
|
||||||
|
UrlText::parse("https://example.com/report.pdf")?,
|
||||||
|
"report.pdf",
|
||||||
|
Some(2048),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
core.complete_download(&download_id, 2048)?;
|
||||||
|
let error = match core.pause_download(&download_id) {
|
||||||
|
Ok(()) => return Err("completed download should reject pause".into()),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
CoreError::Domain(DomainError::InvalidDownloadTransition {
|
||||||
|
action: "pause",
|
||||||
|
state: "completed"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_progress_above_total_bytes() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = core.record_download_started(
|
||||||
|
UrlText::parse("https://example.com/report.pdf")?,
|
||||||
|
"report.pdf",
|
||||||
|
Some(2048),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let error = match core.update_download_progress(&download_id, 4096) {
|
||||||
|
Ok(()) => return Err("progress above total should be rejected".into()),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
CoreError::Domain(DomainError::InvalidDownloadProgress {
|
||||||
|
received_bytes: 4096,
|
||||||
|
total_bytes: 2048,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unknown_download_id() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
let download_id = DownloadId::new();
|
||||||
|
|
||||||
|
let error = match core.cancel_download(&download_id) {
|
||||||
|
Ok(()) => return Err("unknown download should be rejected".into()),
|
||||||
|
Err(error) => error,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(error, CoreError::DownloadNotFound { id: download_id });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_download(core: &BrowserCore) -> Result<ely_domain::DownloadEntry, Box<dyn Error>> {
|
||||||
|
core.snapshot()?
|
||||||
|
.download_entries
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "download entry should exist".into())
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,6 +49,52 @@ impl DownloadEntry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn pause(&mut self) -> Result<(), DomainError> {
|
||||||
|
self.require_state("pause", &[DownloadState::InProgress])?;
|
||||||
|
self.state = DownloadState::Paused;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume(&mut self) -> Result<(), DomainError> {
|
||||||
|
self.require_state("resume", &[DownloadState::Paused])?;
|
||||||
|
self.state = DownloadState::InProgress;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel(&mut self) -> Result<(), DomainError> {
|
||||||
|
self.require_state("cancel", &[DownloadState::InProgress, DownloadState::Paused])?;
|
||||||
|
self.state = DownloadState::Cancelled;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retry(&mut self) -> Result<(), DomainError> {
|
||||||
|
self.require_state("retry", &[DownloadState::Cancelled, DownloadState::Failed])?;
|
||||||
|
self.state = DownloadState::InProgress;
|
||||||
|
self.received_bytes = 0;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_progress(&mut self, received_bytes: u64) -> Result<(), DomainError> {
|
||||||
|
self.require_state("update progress", &[DownloadState::InProgress])?;
|
||||||
|
self.validate_received_bytes(received_bytes)?;
|
||||||
|
self.received_bytes = received_bytes;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn complete(&mut self, received_bytes: u64) -> Result<(), DomainError> {
|
||||||
|
self.require_state("complete", &[DownloadState::InProgress])?;
|
||||||
|
self.validate_received_bytes(received_bytes)?;
|
||||||
|
self.received_bytes = received_bytes;
|
||||||
|
self.state = DownloadState::Completed;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fail(&mut self) -> Result<(), DomainError> {
|
||||||
|
self.require_state("fail", &[DownloadState::InProgress, DownloadState::Paused])?;
|
||||||
|
self.state = DownloadState::Failed;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn id(&self) -> &DownloadId {
|
pub fn id(&self) -> &DownloadId {
|
||||||
&self.id
|
&self.id
|
||||||
@@ -88,4 +134,39 @@ impl DownloadEntry {
|
|||||||
pub fn started_at(&self) -> SystemTime {
|
pub fn started_at(&self) -> SystemTime {
|
||||||
self.started_at
|
self.started_at
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn require_state(
|
||||||
|
&self,
|
||||||
|
action: &'static str,
|
||||||
|
allowed_states: &[DownloadState],
|
||||||
|
) -> Result<(), DomainError> {
|
||||||
|
if allowed_states.iter().any(|state| state == &self.state) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(DomainError::InvalidDownloadTransition { action, state: self.state.as_str() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_received_bytes(&self, received_bytes: u64) -> Result<(), DomainError> {
|
||||||
|
if let Some(total_bytes) = self.total_bytes
|
||||||
|
&& received_bytes > total_bytes
|
||||||
|
{
|
||||||
|
return Err(DomainError::InvalidDownloadProgress { received_bytes, total_bytes });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DownloadState {
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::InProgress => "in_progress",
|
||||||
|
Self::Paused => "paused",
|
||||||
|
Self::Completed => "completed",
|
||||||
|
Self::Cancelled => "cancelled",
|
||||||
|
Self::Failed => "failed",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,10 @@ pub enum DomainError {
|
|||||||
|
|
||||||
#[error("invalid command query")]
|
#[error("invalid command query")]
|
||||||
InvalidCommand,
|
InvalidCommand,
|
||||||
|
|
||||||
|
#[error("cannot {action} download while state is {state}")]
|
||||||
|
InvalidDownloadTransition { action: &'static str, state: &'static str },
|
||||||
|
|
||||||
|
#[error("download progress {received_bytes} exceeds total {total_bytes}")]
|
||||||
|
InvalidDownloadProgress { received_bytes: u64, total_bytes: u64 },
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user