fermah-common 0.1.2

Fermah common utilities and types.
Documentation
use std::path::Path;

use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
use tracing::info;
use url::Url;

#[derive(thiserror::Error, Debug)]
pub enum FileDownloadError {
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("fs error: {0}")]
    Fs(#[from] crate::fs::error::Error),
    #[error("reqwest error: {0}")]
    Reqwest(#[from] reqwest::Error),
    #[error("failed to get content length")]
    ContentLength,
}

pub struct FileDownload {
    pub url: Url,
}

impl FileDownload {
    pub fn new(url: Url) -> Self {
        Self { url }
    }

    pub async fn download_to_file<F>(
        &self,
        filepath: &Path,
        progress_callback: F,
    ) -> Result<(), FileDownloadError>
    where
        F: Fn(u64, u64) + Send + 'static,
    {
        info!("downloading file: {}", self.url);

        let res = reqwest::get(self.url.clone()).await?;
        match res.error_for_status() {
            Ok(res) => {
                let total_size = res
                    .content_length()
                    .ok_or(FileDownloadError::ContentLength)?;

                let mut downloaded_size: u64 = 0;

                let mut file = tokio::fs::File::create(filepath).await?;
                let mut stream = res.bytes_stream();

                while let Some(chunk) = stream.next().await {
                    let chunk = chunk?;
                    file.write_all(&chunk).await?;
                    downloaded_size += chunk.len() as u64;
                    progress_callback(downloaded_size, total_size);
                }

                Ok(())
            }
            Err(e) => Err(e.into()),
        }
    }
}