15
\$\begingroup\$

This module has a ensure_downloaded function which takes a slice of Urls. It downloads all of the urls to a local downloads folder (if they aren't already downloaded) and returns.

use curl::easy::Easy;
use failure::Error;
use pbr::{ProgressBar, Units};
use std::fs::{rename, DirBuilder, File};
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use url::Url;

/// Return the local path where the given url will be stored.
pub fn downloaded_path(url: &Url) -> PathBuf {
    PathBuf::from("downloads")
        .join(url.host_str().unwrap())
        .join(&url.path()[1..])
}

/// Ensure that all the urls are downloaded into a local downloads directory.
pub fn ensure_downloaded(urls: &[&Url]) -> Result<(), Error> {
    // There is a single progress bar, the individual urls all contribute to it
    let mut progress = ProgressBar::new(0);
    progress.set_units(Units::Bytes);
    progress.set_max_refresh_rate(Some(Duration::from_millis(100)));
    let progress = Arc::new(Mutex::new(progress));

    // The following explicitly typed so that the closures have the correct error return type
    // and ? will work.
    let handles: Result<Vec<thread::JoinHandle<Result<(), Error>>>, Error> = urls.into_iter()
        // skip files which already exist
        .map(|url| (url, downloaded_path(url)))
        .filter(|(_, path)| !path.exists())
        .map(|(url, path)| {
            // ensure directory exists to contain the file
            DirBuilder::new()
                .recursive(true)
                .create(path.parent().unwrap())?;

            let mut easy = Easy::new();
            easy.url(url.as_str())?;
            easy.progress(true)?;

            let progress = progress.clone();

            // Create the file with a .tmp suffix
            // so if the download gets interrupted, we won't think it finished
            let mut temp_path = path.as_os_str().to_owned();
            temp_path.push(".tmp");
            let mut file = File::create(temp_path.clone())?;

            Ok(thread::spawn(move || {
                let mut transfer = easy.transfer();

                // curl doesn't accept actual errors of any sort returned here
                // so we just panic if we can't write for some reason
                transfer.write_function(|data| Ok(file.write(data).unwrap()))?;

                // We keep track of the last know total/completed
                // so we can adjust the progress bar
                let mut last_total = 0;
                let mut last_completed = 0;

                transfer.progress_function(move |total_download, completed_download, _, _| {
                    if let Ok(mut progress) = progress.lock() {
                        let total_download = total_download as u64;
                        let completed_download = completed_download as u64;

                        progress.total += total_download - last_total;
                        progress.add(completed_download - last_completed);

                        last_total = total_download;
                        last_completed = completed_download;
                    }

                    true
                })?;

                transfer.perform()?;

                // Only after completion, we rename the file to the target

                rename(temp_path, path)?;

                Ok(())
            }))
        })
        .collect();

    for handle in handles? {
        match handle.join() {
            Ok(result) => result?,
            Err(error) => return Err(format_err!("Thread failed {:?}", error)),
        }
    }

    Ok(())
}
\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.