use crate::{
errors::*,
get_target,
update::{Release, ReleaseAsset, ReleaseUpdate},
version::bump_is_greater,
DEFAULT_PROGRESS_CHARS, DEFAULT_PROGRESS_TEMPLATE,
};
use quick_xml::events::Event;
use quick_xml::Reader;
use regex::Regex;
use std::cmp::Ordering;
use std::env::{self, consts::EXE_SUFFIX};
use std::path::{Path, PathBuf};
const MAX_KEYS: u8 = 100;
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy, Debug)]
pub enum EndPoint {
S3,
S3DualStack,
GCS,
DigitalOceanSpaces,
}
impl Default for EndPoint {
fn default() -> Self {
EndPoint::S3
}
}
#[derive(Clone, Debug)]
pub struct ReleaseListBuilder {
end_point: EndPoint,
bucket_name: Option<String>,
asset_prefix: Option<String>,
target: Option<String>,
region: Option<String>,
}
impl ReleaseListBuilder {
pub fn bucket_name(&mut self, name: &str) -> &mut Self {
self.bucket_name = Some(name.to_owned());
self
}
pub fn asset_prefix(&mut self, prefix: &str) -> &mut Self {
self.asset_prefix = Some(prefix.to_owned());
self
}
pub fn region(&mut self, region: &str) -> &mut Self {
self.region = Some(region.to_owned());
self
}
pub fn end_point(&mut self, end_point: EndPoint) -> &mut Self {
self.end_point = end_point;
self
}
pub fn with_target(&mut self, target: &str) -> &mut Self {
self.target = Some(target.to_owned());
self
}
pub fn build(&self) -> Result<ReleaseList> {
Ok(ReleaseList {
end_point: self.end_point,
bucket_name: if let Some(ref name) = self.bucket_name {
name.to_owned()
} else {
bail!(Error::Config, "`bucket_name` required")
},
region: self.region.clone(),
asset_prefix: self.asset_prefix.clone(),
target: self.target.clone(),
})
}
}
#[derive(Clone, Debug)]
pub struct ReleaseList {
end_point: EndPoint,
bucket_name: String,
asset_prefix: Option<String>,
target: Option<String>,
region: Option<String>,
}
impl ReleaseList {
pub fn configure() -> ReleaseListBuilder {
ReleaseListBuilder {
end_point: EndPoint::default(),
bucket_name: None,
asset_prefix: None,
target: None,
region: None,
}
}
pub fn fetch(&self) -> Result<Vec<Release>> {
let releases = fetch_releases_from_s3(
self.end_point,
&self.bucket_name,
&self.region,
&self.asset_prefix,
)?;
let releases = match self.target {
None => releases,
Some(ref target) => releases
.into_iter()
.filter(|r| r.has_target_asset(target))
.collect::<Vec<_>>(),
};
Ok(releases)
}
}
#[derive(Debug)]
pub struct UpdateBuilder {
end_point: EndPoint,
bucket_name: Option<String>,
asset_prefix: Option<String>,
target: Option<String>,
region: Option<String>,
bin_name: Option<String>,
bin_install_path: Option<PathBuf>,
bin_path_in_archive: Option<PathBuf>,
show_download_progress: bool,
show_output: bool,
no_confirm: bool,
current_version: Option<String>,
target_version: Option<String>,
progress_template: String,
progress_chars: String,
auth_token: Option<String>,
}
impl Default for UpdateBuilder {
fn default() -> Self {
Self {
end_point: EndPoint::default(),
bucket_name: None,
asset_prefix: None,
target: None,
region: None,
bin_name: None,
bin_install_path: None,
bin_path_in_archive: None,
show_download_progress: false,
show_output: true,
no_confirm: false,
current_version: None,
target_version: None,
progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(),
progress_chars: DEFAULT_PROGRESS_CHARS.to_string(),
auth_token: None,
}
}
}
impl UpdateBuilder {
pub fn new() -> Self {
Default::default()
}
pub fn end_point(&mut self, end_point: EndPoint) -> &mut Self {
self.end_point = end_point;
self
}
pub fn bucket_name(&mut self, name: &str) -> &mut Self {
self.bucket_name = Some(name.to_owned());
self
}
pub fn asset_prefix(&mut self, prefix: &str) -> &mut Self {
self.asset_prefix = Some(prefix.to_owned());
self
}
pub fn region(&mut self, region: &str) -> &mut Self {
self.region = Some(region.to_owned());
self
}
pub fn current_version(&mut self, ver: &str) -> &mut Self {
self.current_version = Some(ver.to_owned());
self
}
pub fn target_version_tag(&mut self, ver: &str) -> &mut Self {
self.target_version = Some(ver.to_owned());
self
}
pub fn target(&mut self, target: &str) -> &mut Self {
self.target = Some(target.to_owned());
self
}
pub fn bin_name(&mut self, name: &str) -> &mut Self {
let raw_bin_name = format!("{}{}", name.trim_end_matches(EXE_SUFFIX), EXE_SUFFIX);
self.bin_name = Some(raw_bin_name.clone());
if self.bin_path_in_archive.is_none() {
self.bin_path_in_archive = Some(PathBuf::from(raw_bin_name));
}
self
}
pub fn bin_install_path<A: AsRef<Path>>(&mut self, bin_install_path: A) -> &mut Self {
self.bin_install_path = Some(PathBuf::from(bin_install_path.as_ref()));
self
}
pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self {
self.bin_path_in_archive = Some(PathBuf::from(bin_path));
self
}
pub fn show_download_progress(&mut self, show: bool) -> &mut Self {
self.show_download_progress = show;
self
}
pub fn set_progress_style(
&mut self,
progress_template: String,
progress_chars: String,
) -> &mut Self {
self.progress_template = progress_template;
self.progress_chars = progress_chars;
self
}
pub fn show_output(&mut self, show: bool) -> &mut Self {
self.show_output = show;
self
}
pub fn no_confirm(&mut self, no_confirm: bool) -> &mut Self {
self.no_confirm = no_confirm;
self
}
pub fn auth_token(&mut self, auth_token: &str) -> &mut Self {
self.auth_token = Some(auth_token.to_owned());
self
}
pub fn build(&self) -> Result<Box<dyn ReleaseUpdate>> {
let bin_install_path = if let Some(v) = &self.bin_install_path {
v.clone()
} else {
env::current_exe()?
};
Ok(Box::new(Update {
end_point: self.end_point,
bucket_name: if let Some(ref name) = self.bucket_name {
name.to_owned()
} else {
bail!(Error::Config, "`bucket_name` required")
},
region: self.region.clone(),
asset_prefix: self.asset_prefix.clone(),
target: self
.target
.as_ref()
.map(|t| t.to_owned())
.unwrap_or_else(|| get_target().to_owned()),
bin_name: if let Some(ref name) = self.bin_name {
name.to_owned()
} else {
bail!(Error::Config, "`bin_name` required")
},
bin_install_path,
bin_path_in_archive: if let Some(ref path) = self.bin_path_in_archive {
path.to_owned()
} else {
bail!(Error::Config, "`bin_path_in_archive` required")
},
current_version: if let Some(ref ver) = self.current_version {
ver.to_owned()
} else {
bail!(Error::Config, "`current_version` required")
},
target_version: self.target_version.as_ref().map(|v| v.to_owned()),
show_download_progress: self.show_download_progress,
progress_template: self.progress_template.clone(),
progress_chars: self.progress_chars.clone(),
show_output: self.show_output,
no_confirm: self.no_confirm,
auth_token: self.auth_token.clone(),
}))
}
}
#[derive(Debug)]
pub struct Update {
end_point: EndPoint,
bucket_name: String,
asset_prefix: Option<String>,
target: String,
region: Option<String>,
current_version: String,
target_version: Option<String>,
bin_name: String,
bin_install_path: PathBuf,
bin_path_in_archive: PathBuf,
show_download_progress: bool,
show_output: bool,
no_confirm: bool,
progress_template: String,
progress_chars: String,
auth_token: Option<String>,
}
impl Update {
pub fn configure() -> UpdateBuilder {
UpdateBuilder::new()
}
}
impl ReleaseUpdate for Update {
fn get_latest_release(&self) -> Result<Release> {
let releases = fetch_releases_from_s3(
self.end_point,
&self.bucket_name,
&self.region,
&self.asset_prefix,
)?;
let rel = releases
.iter()
.max_by(|x, y| match bump_is_greater(&y.version, &x.version) {
Ok(is_greater) => {
if is_greater {
Ordering::Greater
} else {
Ordering::Less
}
}
Err(_) => {
Ordering::Less
}
});
match rel {
Some(r) => Ok(r.clone()),
None => bail!(Error::Release, "No release was found"),
}
}
fn get_release_version(&self, ver: &str) -> Result<Release> {
let releases = fetch_releases_from_s3(
self.end_point,
&self.bucket_name,
&self.region,
&self.asset_prefix,
)?;
let rel = releases.iter().find(|x| x.version == ver);
match rel {
Some(r) => Ok(r.clone()),
None => bail!(
Error::Release,
"No release with version '{}' was found",
ver
),
}
}
fn current_version(&self) -> String {
self.current_version.to_owned()
}
fn target(&self) -> String {
self.target.clone()
}
fn target_version(&self) -> Option<String> {
self.target_version.clone()
}
fn bin_name(&self) -> String {
self.bin_name.clone()
}
fn bin_install_path(&self) -> PathBuf {
self.bin_install_path.clone()
}
fn bin_path_in_archive(&self) -> PathBuf {
self.bin_path_in_archive.clone()
}
fn show_download_progress(&self) -> bool {
self.show_download_progress
}
fn show_output(&self) -> bool {
self.show_output
}
fn no_confirm(&self) -> bool {
self.no_confirm
}
fn progress_template(&self) -> String {
self.progress_template.to_owned()
}
fn progress_chars(&self) -> String {
self.progress_chars.to_owned()
}
fn auth_token(&self) -> Option<String> {
self.auth_token.clone()
}
}
fn fetch_releases_from_s3(
end_point: EndPoint,
bucket_name: &str,
region: &Option<String>,
asset_prefix: &Option<String>,
) -> Result<Vec<Release>> {
let prefix = match asset_prefix {
Some(prefix) => format!("&prefix={}", prefix),
None => "".to_string(),
};
let region = region
.as_ref()
.ok_or_else(|| Error::Config("`region` required".to_string()));
let download_base_url = match end_point {
EndPoint::S3 => format!("https://{}.s3.{}.amazonaws.com/", bucket_name, region?),
EndPoint::S3DualStack => format!(
"https://{}.s3.dualstack.{}.amazonaws.com/",
bucket_name, region?
),
EndPoint::DigitalOceanSpaces => format!(
"https://{}.{}.digitaloceanspaces.com/",
bucket_name, region?
),
EndPoint::GCS => format!("https://storage.googleapis.com/{}/", bucket_name),
};
let api_url = match end_point {
EndPoint::S3 | EndPoint::S3DualStack | EndPoint::DigitalOceanSpaces => format!(
"{}?list-type=2&max-keys={}{}",
download_base_url, MAX_KEYS, prefix
),
EndPoint::GCS => format!("{}?max-keys={}{}", download_base_url, MAX_KEYS, prefix),
};
debug!("using api url: {:?}", api_url);
let resp = reqwest::blocking::Client::new().get(&api_url).send()?;
if !resp.status().is_success() {
bail!(
Error::Network,
"S3 API request failed with status: {:?} - for: {:?}",
resp.status(),
api_url
)
}
let body = resp.text()?;
let mut reader = Reader::from_str(&body);
reader.trim_text(true);
enum Tag {
Contents,
Key,
LastModified,
Other,
}
let mut current_tag = Tag::Other;
let mut current_release: Option<Release> = None;
let regex =
Regex::new(r"(?i)(?P<prefix>.*/)*(?P<name>.+)-[v]{0,1}(?P<version>\d+\.\d+\.\d+)-.+")
.map_err(|err| {
Error::Release(format!(
"Failed constructing regex to parse S3 filenames: {}",
err
))
})?;
let mut buf = Vec::new();
let mut releases: Vec<Release> = vec![];
loop {
match reader.read_event(&mut buf) {
Ok(Event::Start(ref e)) => match e.name() {
b"Contents" => {
current_tag = Tag::Contents;
if let Some(release) = current_release {
add_to_releases_list(&mut releases, release);
}
current_release = None;
}
b"Key" => current_tag = Tag::Key,
b"LastModified" => current_tag = Tag::LastModified,
_ => current_tag = Tag::Other,
},
Ok(Event::Text(e)) => {
if let Ok(txt) = e.unescape_and_decode(&reader) {
match current_tag {
Tag::Key => {
let p = PathBuf::from(&txt);
let exe_name = match p.file_name().map(|v| v.to_str()) {
Some(Some(v)) => v,
_ => &txt,
};
if let Some(captures) = regex.captures(&txt) {
let release = current_release.get_or_insert(Release::default());
release.name = captures["name"].to_string();
release.version =
captures["version"].trim_start_matches('v').to_string();
release.assets = vec![ReleaseAsset {
name: exe_name.to_string(),
download_url: format!("{}{}", download_base_url, txt),
}];
debug!("Matched release: {:?}", release);
} else {
debug!("Regex mismatch: {:?}", &txt);
}
}
Tag::LastModified => {
let release = current_release.get_or_insert(Release::default());
release.date = txt;
}
_ => (),
}
}
}
Ok(Event::Eof) => {
if let Some(release) = current_release {
add_to_releases_list(&mut releases, release);
}
break; }
Err(e) => bail!(
Error::Release,
"Failed when parsing S3 XML response at position {}: {:?}",
reader.buffer_position(),
e
),
_ => (), }
buf.clear();
}
Ok(releases)
}
fn add_to_releases_list(releases: &mut Vec<Release>, mut rel: Release) {
if !rel.version.is_empty() && !rel.name.is_empty() {
match releases
.iter()
.position(|curr| curr.name == rel.name && curr.version == rel.version)
{
Some(index) => {
rel.assets.append(&mut releases[index].assets);
releases.push(rel);
releases.swap_remove(index);
}
None => releases.push(rel),
}
}
}