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 {
code: String,
name: String,
version: Option<String>,
executable: bool,
pub files: Vec<Asset>,
}
#[derive(Deserialize, Debug)]
pub struct Version {
pub version: String,
}
#[derive(Deserialize, Debug)]
pub struct Asset {
pub(crate) path: String,
url: String,
size: u32,
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
}
}
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()))
}
}