Skip to main content

fast_down_api/core/download/plan/
fresh.rs

1use super::{
2    DownloadPlan, Fallback, MismatchPlan, MismatchPolicy, PlanCommon, PlanError, PlanKind,
3    ResumeOutcome, ResumePlan, plan_common, probe, probe_resume,
4};
5use crate::{PartialConfig, StateError, TerminationReason, Tx};
6use path_helper::IterStemExt;
7use std::path::PathBuf;
8use tokio::fs;
9use tokio_util::sync::CancellationToken;
10use url::Url;
11
12pub(super) struct FreshPlan {
13    common: PlanCommon,
14    outcome: ResumeOutcome,
15}
16
17impl FreshPlan {
18    pub(super) const fn new(common: PlanCommon) -> Self {
19        Self {
20            common,
21            outcome: ResumeOutcome::Fresh,
22        }
23    }
24
25    pub(super) const fn common(&self) -> &PlanCommon {
26        &self.common
27    }
28
29    pub(super) const fn outcome(&self) -> &ResumeOutcome {
30        &self.outcome
31    }
32
33    pub(super) fn into_common(self) -> PlanCommon {
34        self.common
35    }
36
37    pub(super) async fn run(self) -> TerminationReason {
38        self.common.claim_and_run().await
39    }
40}
41
42/// Prepare a download without touching the filesystem.
43///
44/// Prefetches the remote metadata, resolves the output path, and inspects the
45/// `.fd`/`.part` pair that a previous run may have left behind. The returned
46/// [`DownloadPlan`] reports what starting it would do; nothing is created until
47/// it is started.
48///
49/// When `overwrite` is disabled the probe walks the `name`, `name (1)`,
50/// `name (2)` … sequence looking for a resumable pair, and settles on the first
51/// unclaimed name. A pair whose state exists but fails validation is reported as
52/// [`ResumeOutcome::Mismatch`] instead of being skipped silently, so the caller
53/// can offer to force the resume.
54///
55/// # Errors
56///
57/// Returns [`PlanError`] when the client cannot be built, the prefetch exhausts
58/// its retries, or the output path cannot be computed.
59#[allow(clippy::result_large_err)]
60pub async fn plan(
61    url: Url,
62    mut partial_config: PartialConfig,
63    tx: Tx,
64    token: CancellationToken,
65) -> Result<DownloadPlan, PlanError> {
66    partial_config.downloaded_chunk = None;
67    let probe = probe(&url, &partial_config, &tx).await?;
68    let can_resume = probe.config.resume && probe.info.fast_download;
69
70    if probe.config.overwrite {
71        plan_overwrite(url, partial_config, probe, tx, token, can_resume).await
72    } else {
73        plan_without_overwrite(url, partial_config, probe, tx, token, can_resume).await
74    }
75}
76
77#[allow(clippy::result_large_err)]
78async fn plan_overwrite(
79    url: Url,
80    partial_config: PartialConfig,
81    probe: super::Probe,
82    tx: Tx,
83    token: CancellationToken,
84    can_resume: bool,
85) -> Result<DownloadPlan, PlanError> {
86    let cfg_path = probe.final_path.with_added_extension("fd");
87    let tmp_path = probe.final_path.with_added_extension("part");
88    let resume_probe = if can_resume {
89        probe_resume(&url, &cfg_path, &tmp_path, &probe.info, &partial_config).await
90    } else {
91        (None, ResumeOutcome::Fresh)
92    };
93    let inner = match resume_probe {
94        (Some(state), _) => {
95            let common = plan_common(
96                url,
97                partial_config,
98                probe,
99                cfg_path,
100                tmp_path,
101                tx,
102                token,
103                Some(&state),
104            );
105            PlanKind::Resume(ResumePlan::new(common, &state, Fallback::Fresh))
106        }
107        (None, ResumeOutcome::Mismatch(error)) => {
108            let common = plan_common(
109                url,
110                partial_config,
111                probe,
112                cfg_path,
113                tmp_path,
114                tx,
115                token,
116                None,
117            );
118            PlanKind::Mismatch(MismatchPlan::new(common, error, MismatchPolicy::Fresh))
119        }
120        (None, _) => {
121            let common = plan_common(
122                url,
123                partial_config,
124                probe,
125                cfg_path,
126                tmp_path,
127                tx,
128                token,
129                None,
130            );
131            PlanKind::Fresh(FreshPlan::new(common))
132        }
133    };
134    Ok(DownloadPlan { inner })
135}
136
137#[allow(clippy::result_large_err)]
138async fn plan_without_overwrite(
139    url: Url,
140    partial_config: PartialConfig,
141    probe: super::Probe,
142    tx: Tx,
143    token: CancellationToken,
144    can_resume: bool,
145) -> Result<DownloadPlan, PlanError> {
146    let mut mismatch: Option<(StateError, PathBuf, PathBuf)> = None;
147    for base_path in probe.final_path.iter_stem() {
148        let tmp_path = base_path.with_added_extension("part");
149        let cfg_path = base_path.with_added_extension("fd");
150
151        let tmp_exists = fs::try_exists(&tmp_path).await.unwrap_or(false);
152        let cfg_exists = fs::try_exists(&cfg_path).await.unwrap_or(false);
153        if !tmp_exists && !cfg_exists {
154            // The first unclaimed name. If an earlier stem held a state that
155            // failed validation, report that instead: the caller may prefer to
156            // force it rather than download the file again under a new name.
157            let inner = if let Some((error, cfg_path, tmp_path)) = mismatch {
158                let common = plan_common(
159                    url,
160                    partial_config,
161                    probe,
162                    cfg_path,
163                    tmp_path,
164                    tx,
165                    token,
166                    None,
167                );
168                PlanKind::Mismatch(MismatchPlan::new(common, error, MismatchPolicy::Fresh))
169            } else {
170                let common = plan_common(
171                    url,
172                    partial_config,
173                    probe,
174                    cfg_path,
175                    tmp_path,
176                    tx,
177                    token,
178                    None,
179                );
180                PlanKind::Fresh(FreshPlan::new(common))
181            };
182            return Ok(DownloadPlan { inner });
183        }
184
185        if can_resume {
186            match super::super::try_load_resume_state(
187                &url,
188                &cfg_path,
189                &tmp_path,
190                &probe.info,
191                &partial_config,
192            )
193            .await
194            {
195                Ok(Some(state)) => {
196                    let common = plan_common(
197                        url,
198                        partial_config,
199                        probe,
200                        cfg_path,
201                        tmp_path,
202                        tx,
203                        token,
204                        Some(&state),
205                    );
206                    return Ok(DownloadPlan {
207                        inner: PlanKind::Resume(ResumePlan::new(common, &state, Fallback::Fresh)),
208                    });
209                }
210                Err(e) if mismatch.is_none() => mismatch = Some((e, cfg_path, tmp_path)),
211                Ok(None) | Err(_) => {}
212            }
213        }
214    }
215    unreachable!()
216}