mod common;
mod fresh;
mod manifest;
mod mismatch;
mod resume;
use self::{
common::{Fallback, PlanCommon},
fresh::FreshPlan,
manifest::ManifestFreshPlan,
mismatch::{MismatchPlan, MismatchPolicy},
resume::ResumePlan,
};
pub use self::{fresh::plan, manifest::plan_from_fd, resume::peek_resume, resume::plan_resume};
use super::try_load_resume_state;
use crate::{
Config, DownloadState, Event, PartialConfig, StateError, TerminationReason, Tx, prefetch,
utils::gen_path,
};
use fast_down::{ProgressEntry, Total, UrlInfo, reqwest::ReqwestResponseError};
use inherit_config::ConfigLayer;
use reqwest::Response;
use std::path::{Path, PathBuf};
use tokio_util::sync::CancellationToken;
use url::Url;
#[derive(Debug, thiserror::Error)]
#[allow(clippy::large_enum_variant)]
pub enum PlanError {
#[error("failed to build the HTTP client: {0}")]
BuildClient(reqwest::Error),
#[error("failed to fetch the remote metadata: {0}")]
Prefetch(ReqwestResponseError),
#[error("failed to compute the output path: {0}")]
GenPath(std::io::Error),
#[error(transparent)]
Resume(StateError),
}
impl PlanError {
pub fn emit(self, tx: &Tx) {
let _ = match self {
Self::BuildClient(e) => tx.send(Event::BuildClientError(e)),
Self::Prefetch(e) => tx.send(Event::PrefetchError(e)),
Self::GenPath(e) => tx.send(Event::GenPathError(e)),
Self::Resume(e) => tx.send(Event::ResumeError(e)),
};
}
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ResumeOutcome {
Fresh,
Resumable {
config_path: PathBuf,
progress: Vec<ProgressEntry>,
downloaded: u64,
},
Mismatch(StateError),
}
enum PlanKind {
Fresh(FreshPlan),
Resume(ResumePlan),
ManifestFresh(ManifestFreshPlan),
Mismatch(MismatchPlan),
}
impl PlanKind {
const fn common(&self) -> &PlanCommon {
match self {
Self::Fresh(plan) => plan.common(),
Self::Resume(plan) => plan.common(),
Self::ManifestFresh(plan) => plan.common(),
Self::Mismatch(plan) => plan.common(),
}
}
const fn outcome(&self) -> &ResumeOutcome {
match self {
Self::Fresh(plan) => plan.outcome(),
Self::Resume(plan) => plan.outcome(),
Self::ManifestFresh(plan) => plan.outcome(),
Self::Mismatch(plan) => plan.outcome(),
}
}
fn into_common(self) -> PlanCommon {
match self {
Self::Fresh(plan) => plan.into_common(),
Self::Resume(plan) => plan.into_common(),
Self::ManifestFresh(plan) => plan.into_common(),
Self::Mismatch(plan) => plan.into_common(),
}
}
async fn run(self) -> TerminationReason {
match self {
Self::Fresh(plan) => Box::pin(plan.run()).await,
Self::Resume(plan) => Box::pin(plan.run()).await,
Self::ManifestFresh(plan) => Box::pin(plan.run()).await,
Self::Mismatch(plan) => Box::pin(plan.run()).await,
}
}
async fn run_forced(self) -> TerminationReason {
match self {
Self::Mismatch(plan) => Box::pin(plan.run_forced()).await,
other => Box::pin(other.run()).await,
}
}
async fn run_fresh(self) -> TerminationReason {
self.into_common().claim_and_run().await
}
}
#[must_use = "a DownloadPlan does nothing until one of the start methods is called; drop it to abandon the download"]
pub struct DownloadPlan {
inner: PlanKind,
}
impl DownloadPlan {
#[must_use]
pub const fn info(&self) -> &UrlInfo {
&self.inner.common().info
}
#[must_use]
pub const fn config(&self) -> &Config {
&self.inner.common().config
}
#[must_use]
pub fn final_path(&self) -> &Path {
&self.inner.common().final_path
}
#[must_use]
pub fn tmp_path(&self) -> &Path {
&self.inner.common().tmp_path
}
#[must_use]
pub fn config_path(&self) -> &Path {
&self.inner.common().cfg_path
}
#[must_use]
pub const fn resume_outcome(&self) -> &ResumeOutcome {
self.inner.outcome()
}
pub async fn start(self) {
let tx = self.inner.common().tx.clone();
let reason = self.inner.run().await;
let _ = tx.send(Event::Terminated(reason));
}
pub async fn start_fresh(self) {
let tx = self.inner.common().tx.clone();
let reason = self.inner.run_fresh().await;
let _ = tx.send(Event::Terminated(reason));
}
pub async fn start_forced_resume(self) {
let tx = self.inner.common().tx.clone();
let reason = self.inner.run_forced().await;
let _ = tx.send(Event::Terminated(reason));
}
}
fn resumable_outcome(state: &DownloadState, config_path: &Path) -> ResumeOutcome {
let progress = state.get_progress();
let downloaded = progress.total();
ResumeOutcome::Resumable {
config_path: config_path.to_path_buf(),
progress,
downloaded,
}
}
struct Probe {
config: Config,
info: UrlInfo,
resp: Response,
final_path: PathBuf,
}
#[allow(clippy::result_large_err)]
async fn probe(url: &Url, partial_config: &PartialConfig, tx: &Tx) -> Result<Probe, PlanError> {
let config = partial_config.clone().build();
let (info, resp) = prefetch(url, &config, tx).await?;
let final_path = gen_path(url, &info, &config)
.await
.map_err(PlanError::GenPath)?;
Ok(Probe {
config,
info,
resp,
final_path,
})
}
async fn probe_resume(
url: &Url,
cfg_path: &Path,
tmp_path: &Path,
info: &UrlInfo,
partial_config: &PartialConfig,
) -> (Option<DownloadState>, ResumeOutcome) {
match try_load_resume_state(url, cfg_path, tmp_path, info, partial_config).await {
Ok(Some(state)) => {
let outcome = resumable_outcome(&state, cfg_path);
(Some(state), outcome)
}
Ok(None) => (None, ResumeOutcome::Fresh),
Err(e) => (None, ResumeOutcome::Mismatch(e)),
}
}
fn state_url(url: Option<Url>, loaded: &DownloadState) -> Option<Url> {
url.or_else(|| {
let guard = loaded.lock_inner();
if let Some(url) = &guard.url
&& matches!(url.scheme(), "http" | "https")
{
Some(url.clone())
} else {
None
}
})
}
fn inherit_persisted_config(config: &mut PartialConfig, loaded: &DownloadState) {
let requested_progress = config.downloaded_chunk.take();
let saved = loaded.lock_inner().config.clone();
if let Some(saved) = saved {
config.inherit_from(&saved);
}
config.downloaded_chunk = requested_progress;
}
fn resolved_plan_config(probe_config: Config, state: Option<&DownloadState>) -> Config {
let Some(state) = state else {
return probe_config;
};
state
.lock_inner()
.config
.clone()
.map_or(probe_config, ConfigLayer::build)
}
#[allow(clippy::too_many_arguments)]
fn plan_common(
url: Url,
partial_config: PartialConfig,
probe: Probe,
cfg_path: PathBuf,
tmp_path: PathBuf,
tx: Tx,
token: CancellationToken,
state: Option<&DownloadState>,
) -> PlanCommon {
let Probe {
config: probe_config,
info,
resp,
final_path,
} = probe;
PlanCommon {
url,
partial_config,
config: resolved_plan_config(probe_config, state),
info,
resp,
final_path,
cfg_path,
tmp_path,
tx,
token,
}
}
#[cfg(test)]
mod tests;