use std::path::{Path, PathBuf};
use tokio::sync::watch;
use tracing::{debug, instrument};
use crate::error::NetworkError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DownloadState {
InProgress,
Completed,
Canceled,
}
#[derive(Debug)]
pub struct Download {
guid: String,
url: String,
suggested_filename: String,
temp_path: Option<PathBuf>,
state: DownloadState,
failure: Option<String>,
state_rx: watch::Receiver<DownloadState>,
path_rx: watch::Receiver<Option<PathBuf>>,
}
impl Download {
pub(crate) fn new(
guid: String,
url: String,
suggested_filename: String,
state_rx: watch::Receiver<DownloadState>,
path_rx: watch::Receiver<Option<PathBuf>>,
) -> Self {
Self {
guid,
url,
suggested_filename,
temp_path: None,
state: DownloadState::InProgress,
failure: None,
state_rx,
path_rx,
}
}
pub fn url(&self) -> &str {
&self.url
}
pub fn suggested_filename(&self) -> &str {
&self.suggested_filename
}
pub fn guid(&self) -> &str {
&self.guid
}
#[instrument(level = "debug", skip(self), fields(guid = %self.guid))]
pub async fn path(&mut self) -> Result<PathBuf, NetworkError> {
let mut path_rx = self.path_rx.clone();
loop {
{
let path = path_rx.borrow();
if let Some(ref p) = *path {
self.temp_path = Some(p.clone());
return Ok(p.clone());
}
}
if self.failure.is_some() {
return Err(NetworkError::IoError(
self.failure
.clone()
.unwrap_or_else(|| "Unknown download error".to_string()),
));
}
if path_rx.changed().await.is_err() {
return Err(NetworkError::Aborted);
}
}
}
#[instrument(level = "debug", skip(self), fields(guid = %self.guid, dest = %dest.as_ref().display()))]
pub async fn save_as(&mut self, dest: impl AsRef<Path>) -> Result<(), NetworkError> {
let source = self.path().await?;
debug!("Copying download to destination");
tokio::fs::copy(&source, dest.as_ref())
.await
.map_err(|e| NetworkError::IoError(e.to_string()))?;
Ok(())
}
#[instrument(level = "debug", skip(self), fields(guid = %self.guid))]
pub async fn cancel(&mut self) -> Result<(), NetworkError> {
self.state = DownloadState::Canceled;
self.failure = Some("canceled".to_string());
Ok(())
}
pub fn failure(&self) -> Option<&str> {
self.failure.as_deref()
}
pub(crate) fn update_state(&mut self, state: DownloadState, failure: Option<String>) {
self.state = state;
if let Some(f) = failure {
self.failure = Some(f);
}
}
pub(crate) fn set_path(&mut self, path: PathBuf) {
self.temp_path = Some(path);
}
}
#[derive(Debug)]
pub(crate) struct DownloadManager {
download_dir: PathBuf,
}
impl DownloadManager {
pub fn new() -> Self {
let download_dir = std::env::temp_dir().join("viewpoint-downloads");
Self { download_dir }
}
pub fn download_dir(&self) -> &Path {
&self.download_dir
}
}
impl Default for DownloadManager {
fn default() -> Self {
Self::new()
}
}