Skip to main content

fast_down_api/core/download/plan/
manifest.rs

1use super::super::open_create;
2use super::{
3    DownloadPlan, Fallback, PlanCommon, PlanError, PlanKind, ResumeOutcome, ResumePlan,
4    inherit_persisted_config, plan_common, probe, state_url,
5};
6use crate::{DownloadState, Event, PartialConfig, StateError, TerminationReason, Tx, tx_err};
7use std::ffi::OsStr;
8use std::io;
9use std::path::Path;
10use std::time::Duration;
11use tokio_util::sync::CancellationToken;
12use url::Url;
13
14pub(super) struct ManifestFreshPlan {
15    common: PlanCommon,
16    outcome: ResumeOutcome,
17}
18
19impl ManifestFreshPlan {
20    pub(super) const fn new(common: PlanCommon) -> Self {
21        Self {
22            common,
23            outcome: ResumeOutcome::Fresh,
24        }
25    }
26
27    pub(super) const fn common(&self) -> &PlanCommon {
28        &self.common
29    }
30
31    pub(super) const fn outcome(&self) -> &ResumeOutcome {
32        &self.outcome
33    }
34
35    pub(super) fn into_common(self) -> PlanCommon {
36        self.common
37    }
38
39    pub(super) async fn run(self) -> TerminationReason {
40        let state = match DownloadState::load(&self.common.cfg_path).await {
41            Ok(state) => state,
42            Err(e) => {
43                let _ = self.common.tx.send(Event::ResumeError(e));
44                return TerminationReason::Failed;
45            }
46        };
47        let state = fresh_from_loaded(
48            state,
49            &self.common.partial_config,
50            &self.common.url,
51            &self.common.info,
52        );
53        if !self.common.ensure_parent().await {
54            return TerminationReason::Failed;
55        }
56        tx_err!(
57            open_create().open(&self.common.tmp_path).await,
58            self.common.tx,
59            BuildPusherError,
60            TerminationReason::Failed
61        );
62        self.common.run_existing(state, false).await
63    }
64}
65
66fn fresh_from_loaded(
67    state: DownloadState,
68    partial_config: &crate::PartialConfig,
69    url: &url::Url,
70    info: &fast_down::UrlInfo,
71) -> DownloadState {
72    state.update(|inner| {
73        if let Some(c) = &mut inner.config {
74            c.downloaded_chunk = None;
75        }
76        inner.elapsed = Some(Duration::ZERO);
77    });
78    let mut fresh_config = partial_config.clone();
79    fresh_config.downloaded_chunk = None;
80    state.merge_config(&fresh_config);
81    state.refresh_identity(url, info);
82    state
83}
84
85/// Prepare a download driven by a `.fd` state file used as a download manifest.
86///
87/// Unlike [`super::plan_resume`], which is given the `.part` and looks for its
88/// companion `.fd`, this entry takes the `.fd` itself. The sibling `.part`
89/// decides whether the run resumes recorded progress or reuses the manifest
90/// configuration for a fresh download.
91///
92/// `url` is optional: when omitted, the durable initial URL recorded in the
93/// `.fd` is used. Resume requires a range-capable server; without one the
94/// manifest is still reused for a fresh, single-stream download.
95///
96/// # Errors
97///
98/// Returns [`PlanError::Resume`] when `fd_path` is not a `.fd` file, the `.fd`
99/// cannot be read or decoded, no URL can be resolved, or the prefetch fails.
100#[allow(clippy::result_large_err)]
101pub async fn plan_from_fd(
102    fd_path: impl AsRef<Path>,
103    url: Option<Url>,
104    mut partial_config: PartialConfig,
105    tx: Tx,
106    token: CancellationToken,
107) -> Result<DownloadPlan, PlanError> {
108    let fd_path = fd_path.as_ref();
109    if fd_path.extension() != Some(OsStr::new("fd")) {
110        return Err(PlanError::Resume(StateError::Open(io::Error::new(
111            io::ErrorKind::InvalidInput,
112            "fd_path must end with .fd extension",
113        ))));
114    }
115    partial_config.overwrite = Some(false);
116
117    let loaded = DownloadState::load(fd_path)
118        .await
119        .map_err(PlanError::Resume)?;
120    let Some(url) = state_url(url, &loaded) else {
121        return Err(PlanError::Resume(StateError::NoUrl(fd_path.to_path_buf())));
122    };
123
124    inherit_persisted_config(&mut partial_config, &loaded);
125    partial_config.overwrite = Some(false);
126    partial_config.resume = Some(true);
127    let probe = probe(&url, &partial_config, &tx).await?;
128
129    let tmp_path = fd_path.with_extension("part");
130    let state = if probe.info.fast_download {
131        match super::super::try_load_resume_state(
132            &url,
133            fd_path,
134            &tmp_path,
135            &probe.info,
136            &partial_config,
137        )
138        .await
139        {
140            Ok(Some(state)) => Some(state),
141            _ => None,
142        }
143    } else {
144        None
145    };
146    let common = plan_common(
147        url,
148        partial_config,
149        probe,
150        fd_path.to_path_buf(),
151        tmp_path,
152        tx,
153        token,
154        state.as_ref(),
155    );
156    let inner = match state {
157        Some(state) => PlanKind::Resume(ResumePlan::new(common, &state, Fallback::ManifestFresh)),
158        None => PlanKind::ManifestFresh(ManifestFreshPlan::new(common)),
159    };
160    Ok(DownloadPlan { inner })
161}