use super::progress_reporter::ProgressReporter;
use crate::{
DownloadState, Event, PartialConfig, TerminationReason, Tx,
core::download::pipeline::build_pipeline, tx_err,
};
use fast_down::{UrlInfo, invert, multi::download_multi, single::download_single};
use inherit_config::ConfigLayer;
use path_helper::tokio::gen_unique_path;
use reqwest::Response;
use std::{
path::PathBuf,
time::{Duration, Instant},
};
use tokio::fs;
use tokio_util::sync::CancellationToken;
pub struct OverwriteOption {
pub state: DownloadState,
pub final_path: PathBuf,
pub info: UrlInfo,
pub resp: Response,
pub tx: Tx,
pub token: CancellationToken,
}
#[allow(clippy::too_many_lines)]
pub async fn overwrite(option: OverwriteOption) -> TerminationReason {
let OverwriteOption {
state,
final_path,
info,
resp,
tx,
token,
} = option;
tx_err!(
state.store().await,
tx,
StateSaveError,
TerminationReason::Failed
);
let _ = state.take_dirty();
let inner_state = state.lock_inner().clone();
let parsed_config = inner_state.config.clone().unwrap_or_default();
let inner_state = inner_state.build();
let tmp_path = state.tmp_path();
let config = &inner_state.config;
let pipeline = build_pipeline(config, &info, resp, &tmp_path, &tx, &token).await;
let Some((puller, pusher)) = pipeline else {
return TerminationReason::Failed;
};
let _ = tx.send(Event::Start {
tmp_path: tmp_path.clone(),
config_path: state.config_path.clone(),
parsed_config,
});
let res = if info.fast_download {
download_multi(
puller,
pusher,
fast_down::multi::DownloadOptions {
download_chunks: invert(
config.downloaded_chunk.iter().cloned(),
info.size,
config.chunk_window,
),
concurrent: config.threads,
retry_gap: config.retry_gap,
pull_timeout: config.pull_timeout,
push_queue_cap: config.write_queue_cap,
min_chunk_size: config.min_chunk_size,
max_speculative: config.max_speculative,
},
)
} else {
download_single(
puller,
pusher,
fast_down::single::DownloadOptions {
retry_gap: config.retry_gap,
push_queue_cap: config.write_queue_cap,
},
)
};
let abort_handle = {
let token = token.clone();
let res = res.clone();
tokio::spawn(async move {
token.cancelled().await;
res.abort();
})
};
let persist_token = token.child_token();
let persist_task = {
let st = state.clone();
let tx = tx.clone();
let stop = persist_token.clone();
tokio::spawn(async move {
loop {
tokio::select! {
() = tokio::time::sleep(Duration::from_secs(1)) => {
if st.take_dirty() && let Err(e) = st.store().await {
let _ = tx.send(Event::StateSaveError(e));
st.mark_dirty(); }
}
() = stop.cancelled() => break,
}
}
})
};
let reporter = ProgressReporter::new(inner_state.elapsed, info.size, state.share_inner());
let progress_task = reporter.clone().spawn(&tx, config.progress_emit_gap);
while let Ok(e) = res.event_chain().recv().await {
if let fast_down::Event::PushProgress(range) = &e {
state.merge_progress(range.clone());
}
let _ = match e {
fast_down::Event::Pulling(id) => tx.send(Event::Pulling(id)),
fast_down::Event::PullError(id, e) => tx.send(Event::PullError(id, anyhow::anyhow!(e))),
fast_down::Event::PullTimeout(id) => tx.send(Event::PullTimeout(id)),
fast_down::Event::PullProgress(id, range) => tx.send(Event::PullProgress(id, range)),
fast_down::Event::Pushing(id, range) => tx.send(Event::Pushing(id, range)),
fast_down::Event::PushError(id, range, e) => {
tx.send(Event::PushError(id, range, anyhow::anyhow!(e)))
}
fast_down::Event::PushProgress(range) => tx.send(Event::PushProgress(range)),
fast_down::Event::Flushing => tx.send(Event::Flushing),
fast_down::Event::FlushError(e) => tx.send(Event::FlushError(anyhow::anyhow!(e))),
fast_down::Event::Finished(id) => tx.send(Event::Finished(id)),
};
}
progress_task.abort();
let _ = progress_task.await;
let sample = reporter.compute(Instant::now(), None);
let elapsed = sample.elapsed;
let _ = tx.send(Event::Progress(sample));
persist_token.cancel();
let _ = persist_task.await;
abort_handle.abort();
let download_complete = info.size == 0
|| matches!(&state.lock_inner().config, Some(PartialConfig { downloaded_chunk: Some(x), .. }) if x.len() == 1 && x[0] == (0..info.size));
if token.is_cancelled() || !download_complete {
state.set_elapsed(elapsed);
if let Err(e) = state.store().await {
let _ = tx.send(Event::StateSaveError(e));
}
return if token.is_cancelled() {
TerminationReason::Cancelled
} else {
TerminationReason::Incomplete
};
}
let final_path = if config.overwrite {
final_path
} else {
tx_err!(
gen_unique_path(final_path).await,
tx,
GenPathError,
TerminationReason::Failed
)
};
if let Err(e) = fs::rename(tmp_path, &final_path).await {
if !config.overwrite {
let _ = fs::remove_file(&final_path).await;
}
let _ = tx.send(Event::RenameFailed(e));
return TerminationReason::Failed;
}
let _ = fs::remove_file(&state.config_path).await;
let _ = tx.send(Event::Renamed(final_path));
TerminationReason::Completed
}