본문 바로가기

프로그래밍/RUST

[RUST] https 를 통해 파일 다운로드 간단 샘플

Cargo.toml

사용할 모듈 정보를 입력한다.

tokio: 파일, async
reqwest : http

main을 async로 동작시키려면 tokio feature중 "rt" or "rt-multi-thread", "macros" 필요
https 사용을 위해 reqwest feature 중 "rustls-tls" or "native-tls" 필요

[package]
name = "sample"
version = "0.1.0"
edition = "2021"


[features]
default = []

[dependencies]
tokio = { version = "1", features = [ "fs", "rt-multi-thread", "macros"] }
reqwest = { version = "0.12", features = ["rustls-tls"] }

 

 

에러 열거형 : main.rs

여러가지 모듈의 에러를 하나로 관리하기 위해  다운로드 패키지에서 사용할 Error를 작성해 준다.
각 에러타입에 따라 From을 작성해 주면 해당 에러 발생시 이 패키지의 에러로 변경이 가능해진다.
보통 에러는 매번 핸들링하지 않고, '?'로 돌려보내게 되는데, 이때 From을 통해 변환이 일어나게 된다.

use std::error::Error as StdError;
use std::fmt;

#[derive(Debug)]
pub enum Error {
    Io(std::io::Error),
    Fetcher(String),
    Timeout,
    Http(reqwest::Error),
    NotFound,
}


impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Self {
        Error::Io(err)
    }
}


impl From<reqwest::Error> for Error {
    fn from(err: reqwest::Error) -> Self {
        Error::Http(err)
    }
}


impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        match self {
            Error::Io(err) => Some(err),
            Error::Fetcher(_) => None,
            Error::Timeout => None,
            Error::Http(err) => Some(err),
            Error::NotFound => None,
        }
    }
}


impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Io(err) => write!(f, "io error: {}", err),
            Error::Fetcher(msg) => write!(f, "fetcher error: {}", msg),
            Error::Timeout => write!(f, "timeout"),
            Error::Http(err) => write!(f, "http error: {}", err),
            Error::NotFound => write!(f, "not found"),
        }
    }
}

 

다운로드 객체 : downloader.rs

reqwest::Client 만 하나 포함하고 있는 다운로드를 수행할 객체를 작성한다.

use std::path::{Path, PathBuf};
use crate::Error;
use tokio::{
    fs,                 // fs::File
    io::AsyncWriteExt , // write_all
};

#[derive(Debug)]
struct HttpFileDownloader {
    client: reqwest::Client,
}

impl Default for HttpFileDownloader {
    fn default() -> Self {
        Self {
            client: Default::default(),
        }
    }
}

 

다운로드 함수 : downloader.rs

파일을 저장할 path 를 전달받아 각 폴더를 만들고, 파일을 생성한다.
http/https로 파일을 다운로드 받아 해당 파일에 추가한다.

impl HttpFileDownloader {
    pub fn new() -> Self {
        Self {
            client: Default::default(),
        }
    }

    // destination : 다운로드 받을 폴더 위치
    // url : 다운로드할 파일 url 문자열
    pub async fn download(&self, destination: impl AsRef<Path>, url: &str) -> Result<PathBuf, Error> {     
        // 파일 저장할 폴더 path 얻기
        let destination = destination.as_ref();
        
        // 파일 저장할 폴더 생성
        if !destination.exists() {
            fs::create_dir_all(destination).await?
        }

        // path를 파일명을 포함한 full file path로 변경
        let path = if destination.is_file() {
            destination.to_owned()
        } else {
            destination.join("1.79.0.zip")
        };

        // 파일 생성 및 열기
        let mut file = fs::File::create(&path).await?;
        
        // https 파일 요청
        let mut response = self
            .client
            .get(url)
            .send()
            .await?
            .error_for_status()?;

        // 청크 단위로 파일다운로드 및 파일에 저장
        while let Some(chunk) = response.chunk().await? {
            file.write_all(&chunk).await?;
        }

        Ok(path)
    }
}

 

 

메인함수 : main.rs

pub mod downloader;
pub use crate::downloader::HttpFileDownloader;

#[tokio::main]
async fn main() -> Result<(), Box<dyn StdError>> {
    let _ = HttpFileDownloader::default().download(
            ".", 
            "https://github.com/rust-lang/rust/archive/refs/tags/1.79.0.zip"
        ).await?;

    Ok(())
}