uwupdater 0.1.3

A library for UWUpdater v3 protocol
Documentation
use std::fs;
use std::io::{Read, Write};
use std::path::Path;
use std::time::Duration;

use log::{error, info};
use reqwest;
use serde::Deserialize;
use sha1::{Digest, Sha1};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum UWUpdaterError {
    #[error(transparent)]
    DownloadError(#[from] reqwest::Error),
    #[error(transparent)]
    SaveError(#[from] std::io::Error),
    #[error("Asset with name {0} not found")]
    AssetError(String),
}

#[derive(Deserialize, Debug)]
pub struct Bundle {
    pub code: String,
    pub name: String,
    pub version: Option<String>,
    pub executable: bool,
    pub files: Vec<Asset>,
}

#[derive(Deserialize, Debug)]
pub struct Version {
    pub version: String,
}

#[derive(Deserialize, Debug)]
pub struct Asset {
    pub path: String,
    pub url: String,
    pub size: u32,
    pub sha1: String,
}

pub struct UWUpdater {
    repo_url: String,
    client: reqwest::blocking::Client,
    tags: Vec<String>,
}

impl UWUpdater {
    pub fn new(repo_url: &String) -> UWUpdater {
        let repo_url = match repo_url.ends_with("/") {
            true => repo_url.to_owned(),
            false => format!("{}/", repo_url.to_owned()),
        };
        UWUpdater {
            repo_url,
            client: reqwest::blocking::Client::builder()
                .timeout(Duration::from_secs(60 * 60))
                .build()
                .unwrap(),
            tags: Vec::new(),
        }
    }

    pub fn set_tags(&mut self, tags: Vec<String>) {
        self.tags = tags;
    }

    pub fn get_bundle(
        &self,
        bundle_name: &String,
        access_token: Option<&String>,
    ) -> Result<Bundle, UWUpdaterError> {
        let manifest_url: String;
        if self.tags.is_empty() {
            manifest_url = format!("{}{}/", &self.repo_url, &bundle_name);
        } else {
            manifest_url = format!(
                "{}{}/?tags={}",
                &self.repo_url,
                &bundle_name,
                self.tags.join(",")
            );
        }
        info!("Fetch bundle {} ({})", &bundle_name, manifest_url);
        let response = self
            .request(&manifest_url, access_token)
            .send()?
            .error_for_status()?;
        let bundle_manifest = response.json()?;
        Ok(bundle_manifest)
    }

    pub fn get_bundle_version(
        &self,
        bundle_name: &String,
        access_token: Option<&String>,
    ) -> Result<Version, UWUpdaterError> {
        let version_url = format!("{}{}/version/", &self.repo_url, &bundle_name);
        info!("Fetch version for {} ({})", &bundle_name, version_url);
        let response = self
            .request(&version_url, access_token)
            .send()?
            .error_for_status()?;
        let version = response.json()?;
        Ok(version)
    }

    pub fn get_file(&self, bundle: &Bundle, path: &String) -> Result<String, UWUpdaterError> {
        info!("Fetch {} from {}", path, bundle.code);
        let asset = bundle.get_asset(path)?;
        let response = self.request(&asset.url, None).send()?.error_for_status()?;
        Ok(response.text()?)
    }

    pub fn sync_to(&mut self, bundle: &Bundle, path: &Path) -> Result<(), UWUpdaterError> {
        info!("Sync bundle {} to {}", &bundle.code, path.display());

        for f in &bundle.files {
            self.sync_file_to(&f, &path)?;
        }

        Ok(())
    }

    pub fn sync_file_to(&self, asset: &Asset, path: &Path) -> Result<(), UWUpdaterError> {
        let mut hasher = Sha1::new();
        let path = path.clone().join(&asset.path);
        if path.exists() {
            let mut file = fs::File::open(&path)?;
            let mut buf = [0u8; 4096];
            loop {
                let n = file.read(&mut buf)?;
                hasher.update(&buf[..n]);
                if n == 0 || n < 4096 {
                    break;
                }
            }
            let hash = format!("{:x}", hasher.finalize());
            if hash == asset.sha1 {
                return Ok(());
            }
        }

        info!("Fetch {} as {}", &asset.url, &asset.path);
        let response = self.client.get(&asset.url).send()?.error_for_status()?;
        fs::create_dir_all(&path.parent().unwrap())?;
        let mut file = fs::File::create(&path)?;
        file.write_all(&*response.bytes()?)?;

        Ok(())
    }

    fn request(
        &self,
        url: &String,
        access_token: Option<&String>,
    ) -> reqwest::blocking::RequestBuilder {
        let mut rb = self.client.get(url);
        if access_token.is_some() {
            rb = rb.header(
                "Authorization",
                format!("Bearer {}", access_token.as_ref().unwrap()),
            );
        }
        rb
    }

    pub fn arch() -> String {
        let arch = if cfg!(target_arch = "x86") {
            "x86"
        } else if cfg!(target_arch = "x86_64") {
            "x64"
        } else if cfg!(target_arch = "aarch64") {
            "aarch64"
        } else {
            "unknown"
        };
        String::from(arch)
    }

    pub fn os() -> String {
        std::env::consts::OS.to_string()
    }
}

impl Bundle {
    pub fn file_exists(&mut self, file: &String) -> bool {
        for f in &self.files {
            if &f.path == file {
                return true;
            }
        }
        false
    }

    pub fn get_asset(&self, path: &String) -> Result<&Asset, UWUpdaterError> {
        for f in &self.files {
            if &f.path == path {
                return Ok(f);
            }
        }

        Err(UWUpdaterError::AssetError(path.clone()))
    }
}