#![cfg_attr(feature = "cargo-clippy", deny(clippy::all))]
pub use tempdir::TempDir;
use either::Either;
use indicatif::{ProgressBar, ProgressStyle};
use std::cmp::min;
use std::fs;
use std::io;
use std::path;
#[macro_use]
mod macros;
pub mod backends;
pub mod errors;
pub mod version;
use errors::*;
pub fn get_target() -> &'static str {
env!("TARGET")
}
#[deprecated(
since = "0.4.2",
note = "`should_update` functionality has been moved to `version::bump_is_greater`.\
`version::bump_is_compatible` should be used instead."
)]
pub fn should_update(current: &str, latest: &str) -> Result<bool> {
use semver::Version;
Ok(Version::parse(latest)? > Version::parse(current)?)
}
fn confirm(msg: &str) -> Result<()> {
print_flush!("{}", msg);
let mut s = String::new();
io::stdin().read_line(&mut s)?;
let s = s.trim().to_lowercase();
if !s.is_empty() && s != "y" {
bail!(Error::Update, "Update aborted");
}
Ok(())
}
#[derive(Debug, Clone)]
pub enum Status {
UpToDate(String),
Updated(String),
}
impl Status {
pub fn version(&self) -> &str {
use Status::*;
match *self {
UpToDate(ref s) => s,
Updated(ref s) => s,
}
}
pub fn uptodate(&self) -> bool {
match *self {
Status::UpToDate(_) => true,
_ => false,
}
}
pub fn updated(&self) -> bool {
match *self {
Status::Updated(_) => true,
_ => false,
}
}
}
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use Status::*;
match *self {
UpToDate(ref s) => write!(f, "UpToDate({})", s),
Updated(ref s) => write!(f, "Updated({})", s),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArchiveKind {
Tar(Option<Compression>),
Plain(Option<Compression>),
Zip,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Compression {
Gz,
}
fn detect_archive(path: &path::Path) -> ArchiveKind {
match path.extension() {
Some(extension) if extension == std::ffi::OsStr::new("zip") => ArchiveKind::Zip,
Some(extension) if extension == std::ffi::OsStr::new("tar") => ArchiveKind::Tar(None),
Some(extension) if extension == std::ffi::OsStr::new("gz") => match path
.file_stem()
.map(|e| path::Path::new(e))
.and_then(|f| f.extension())
{
Some(extension) if extension == std::ffi::OsStr::new("tar") => {
ArchiveKind::Tar(Some(Compression::Gz))
}
_ => ArchiveKind::Plain(Some(Compression::Gz)),
},
_ => ArchiveKind::Plain(None),
}
}
#[derive(Debug)]
pub struct Extract<'a> {
source: &'a path::Path,
archive: Option<ArchiveKind>,
}
impl<'a> Extract<'a> {
pub fn from_source(source: &'a path::Path) -> Extract<'a> {
Self {
source,
archive: None,
}
}
pub fn archive(&mut self, kind: ArchiveKind) -> &mut Self {
self.archive = Some(kind);
self
}
fn get_archive_reader(
source: fs::File,
compression: Option<Compression>,
) -> Either<fs::File, flate2::read::GzDecoder<fs::File>> {
match compression {
Some(Compression::Gz) => Either::Right(flate2::read::GzDecoder::new(source)),
None => Either::Left(source),
}
}
pub fn extract_into(&self, into_dir: &path::Path) -> Result<()> {
let source = fs::File::open(self.source)?;
let archive = self.archive.unwrap_or_else(|| detect_archive(&self.source));
match archive {
ArchiveKind::Plain(compression) | ArchiveKind::Tar(compression) => {
let mut reader = Self::get_archive_reader(source, compression);
match archive {
ArchiveKind::Plain(_) => {
match fs::create_dir_all(into_dir) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(Error::Io(e));
}
}
}
let file_name = self.source.file_name().ok_or_else(|| {
Error::Update("Extractor source has no file-name".into())
})?;
let mut out_path = into_dir.join(file_name);
out_path.set_extension("");
let mut out_file = fs::File::create(&out_path)?;
io::copy(&mut reader, &mut out_file)?;
}
ArchiveKind::Tar(_) => {
let mut archive = tar::Archive::new(reader);
archive.unpack(into_dir)?;
}
_ => unreachable!(),
};
}
ArchiveKind::Zip => {
let mut archive = zip::ZipArchive::new(source)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let path = into_dir.join(file.name());
let mut output = fs::File::create(path)?;
io::copy(&mut file, &mut output)?;
}
}
};
Ok(())
}
pub fn extract_file<T: AsRef<path::Path>>(
&self,
into_dir: &path::Path,
file_to_extract: T,
) -> Result<()> {
let file_to_extract = file_to_extract.as_ref();
let source = fs::File::open(self.source)?;
let archive = self.archive.unwrap_or_else(|| detect_archive(&self.source));
match archive {
ArchiveKind::Plain(compression) | ArchiveKind::Tar(compression) => {
let mut reader = Self::get_archive_reader(source, compression);
match archive {
ArchiveKind::Plain(_) => {
match fs::create_dir_all(into_dir) {
Ok(_) => (),
Err(e) => {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(Error::Io(e));
}
}
}
let file_name = file_to_extract.file_name().ok_or_else(|| {
Error::Update("Extractor source has no file-name".into())
})?;
let out_path = into_dir.join(file_name);
let mut out_file = fs::File::create(&out_path)?;
io::copy(&mut reader, &mut out_file)?;
}
ArchiveKind::Tar(_) => {
let mut archive = tar::Archive::new(reader);
let mut entry = archive
.entries()?
.filter_map(|e| e.ok())
.find(|e| e.path().ok().filter(|p| p == file_to_extract).is_some())
.ok_or_else(|| {
Error::Update(format!(
"Could not find the required path in the archive: {:?}",
file_to_extract
))
})?;
entry.unpack_in(into_dir)?;
}
_ => {
panic!("Unreasonable code");
}
};
}
ArchiveKind::Zip => {
let mut archive = zip::ZipArchive::new(source)?;
let mut file = archive.by_name(file_to_extract.to_str().unwrap())?;
let mut output = fs::File::create(into_dir.join(file.name()))?;
io::copy(&mut file, &mut output)?;
}
};
Ok(())
}
}
#[derive(Debug)]
pub struct Move<'a> {
source: &'a path::Path,
temp: Option<&'a path::Path>,
}
impl<'a> Move<'a> {
pub fn from_source(source: &'a path::Path) -> Move<'a> {
Self { source, temp: None }
}
pub fn replace_using_temp(&mut self, temp: &'a path::Path) -> &mut Self {
self.temp = Some(temp);
self
}
pub fn to_dest(&self, dest: &path::Path) -> Result<()> {
match self.temp {
None => {
fs::rename(self.source, dest)?;
}
Some(temp) => {
if dest.exists() {
fs::rename(dest, temp)?;
if let Err(e) = fs::rename(self.source, dest) {
fs::rename(temp, dest)?;
return Err(Error::from(e));
}
} else {
fs::rename(self.source, dest)?;
}
}
};
Ok(())
}
}
#[derive(Debug)]
pub struct Download {
show_progress: bool,
url: String,
headers: reqwest::header::HeaderMap,
progress_style: ProgressStyle,
}
impl Download {
pub fn from_url(url: &str) -> Self {
Self {
show_progress: false,
url: url.to_owned(),
headers: reqwest::header::HeaderMap::new(),
progress_style: ProgressStyle::default_bar()
.template("[{elapsed_precise}] [{bar:40}] {bytes}/{total_bytes} ({eta}) {msg}")
.progress_chars("=>-"),
}
}
pub fn show_progress(&mut self, b: bool) -> &mut Self {
self.show_progress = b;
self
}
pub fn set_progress_style(&mut self, progress_style: ProgressStyle) -> &mut Self {
self.progress_style = progress_style;
self
}
pub fn set_headers(&mut self, headers: reqwest::header::HeaderMap) -> &mut Self {
self.headers = headers;
self
}
pub fn download_to<T: io::Write>(&self, mut dest: T) -> Result<()> {
use io::BufRead;
set_ssl_vars!();
let resp = reqwest::Client::new()
.get(&self.url)
.headers(self.headers.clone())
.send()?;
let size = resp
.headers()
.get(reqwest::header::CONTENT_LENGTH)
.map(|val| {
val.to_str()
.map(|s| s.parse::<u64>().unwrap_or(0))
.unwrap_or(0)
})
.unwrap_or(0);
if !resp.status().is_success() {
bail!(
Error::Update,
"Download request failed with status: {:?}",
resp.status()
)
}
let show_progress = if size == 0 { false } else { self.show_progress };
let mut src = io::BufReader::new(resp);
let mut downloaded = 0;
let mut bar = if show_progress {
let pb = ProgressBar::new(size);
pb.set_style(self.progress_style.clone());
Some(pb)
} else {
None
};
loop {
let n = {
let buf = src.fill_buf()?;
dest.write_all(&buf)?;
buf.len()
};
if n == 0 {
break;
}
src.consume(n);
downloaded = min(downloaded + n as u64, size);
if let Some(ref mut bar) = bar {
bar.set_position(downloaded);
}
}
if let Some(ref mut bar) = bar {
bar.finish_with_message("Done");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use flate2;
use flate2::write::GzEncoder;
use std::fs::{self, File};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use tar;
use tempdir::TempDir;
use zip;
#[test]
fn detect_plain() {
assert_eq!(
ArchiveKind::Plain(None),
detect_archive(&PathBuf::from("Something.exe"))
);
}
#[test]
fn detect_plain_gz() {
assert_eq!(
ArchiveKind::Plain(Some(Compression::Gz)),
detect_archive(&PathBuf::from("Something.exe.gz"))
);
}
#[test]
fn detect_tar_gz() {
assert_eq!(
ArchiveKind::Tar(Some(Compression::Gz)),
detect_archive(&PathBuf::from("Something.tar.gz"))
);
}
#[test]
fn detect_plain_tar() {
assert_eq!(
ArchiveKind::Tar(None),
detect_archive(&PathBuf::from("Something.tar"))
);
}
#[test]
fn detect_zip() {
assert_eq!(
ArchiveKind::Zip,
detect_archive(&PathBuf::from("Something.zip"))
);
}
fn cmp_content<T: AsRef<Path>>(path: T, s: &str) {
let mut content = String::new();
let mut f = File::open(&path).unwrap();
f.read_to_string(&mut content).unwrap();
assert!(s == content);
}
#[test]
fn unpack_plain_gzip() {
let tmp_dir = TempDir::new("self_update_unpack_plain_gzip_src").expect("tempdir fail");
let fp = tmp_dir.path().with_file_name("temp.gz");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
let mut e = GzEncoder::new(&mut tmp_file, flate2::Compression::default());
e.write_all(b"This is a test!").expect("gz encode fail");
e.finish().expect("gz finish fail");
let out_tmp = TempDir::new("self_update_unpack_plain_gzip_outdir").expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&fp)
.extract_into(&out_path)
.expect("extract fail");
let out_file = out_path.join("temp");
assert!(out_file.exists());
cmp_content(out_file, "This is a test!");
}
#[test]
fn unpack_plain_gzip_double_ext() {
let tmp_dir =
TempDir::new("self_update_unpack_plain_gzip_double_ext_src").expect("tempdir fail");
let fp = tmp_dir.path().with_file_name("temp.txt.gz");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
let mut e = GzEncoder::new(&mut tmp_file, flate2::Compression::default());
e.write_all(b"This is a test!").expect("gz encode fail");
e.finish().expect("gz finish fail");
let out_tmp =
TempDir::new("self_update_unpack_plain_gzip_double_ext_outdir").expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&fp)
.extract_into(&out_path)
.expect("extract fail");
let out_file = out_path.join("temp.txt");
assert!(out_file.exists());
cmp_content(out_file, "This is a test!");
}
#[test]
fn unpack_tar_gzip() {
let tmp_dir = TempDir::new("self_update_unpack_tar_gzip_src").expect("tempdir fail");
let tmp_path = tmp_dir.path();
let archive_src = tmp_path.join("src_archive");
fs::create_dir_all(&archive_src).expect("tmp archive-dir create fail");
let fp = archive_src.join("temp.txt");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
tmp_file.write_all(b"This is a test!").unwrap();
let fp2 = archive_src.join("temp2.txt");
let mut tmp_file = File::create(&fp2).expect("temp file 2 create fail");
tmp_file.write_all(b"This is a second test!").unwrap();
let mut ar = tar::Builder::new(vec![]);
ar.append_dir_all("inner_archive", &archive_src)
.expect("tar append dir all fail");
let tar_writer = ar.into_inner().expect("failed getting tar writer");
let archive_fp = tmp_path.with_file_name("archive_file.tar.gz");
let mut archive_file = File::create(&archive_fp).expect("failed creating archive file");
let mut e = GzEncoder::new(&mut archive_file, flate2::Compression::default());
io::copy(&mut tar_writer.as_slice(), &mut e)
.expect("failed writing from tar archive to gz encoder");
e.finish().expect("gz finish fail");
let out_tmp = TempDir::new("self_update_unpack_tar_gzip_outdir").expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_fp)
.extract_into(&out_path)
.expect("extract fail");
let out_file = out_path.join("inner_archive/temp.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a test!");
let out_file = out_path.join("inner_archive/temp2.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a second test!");
}
#[test]
fn unpack_file_plain_gzip() {
let tmp_dir = TempDir::new("self_update_unpack_file_plain_gzip_src").expect("tempdir fail");
let fp = tmp_dir.path().with_file_name("temp.gz");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
let mut e = GzEncoder::new(&mut tmp_file, flate2::Compression::default());
e.write_all(b"This is a test!").expect("gz encode fail");
e.finish().expect("gz finish fail");
let out_tmp =
TempDir::new("self_update_unpack_file_plain_gzip_outdir").expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&fp)
.extract_file(&out_path, "renamed_file")
.expect("extract fail");
let out_file = out_path.join("renamed_file");
assert!(out_file.exists());
cmp_content(out_file, "This is a test!");
}
#[test]
fn unpack_file_tar_gzip() {
let tmp_dir = TempDir::new("self_update_unpack_file_tar_gzip_src").expect("tempdir fail");
let tmp_path = tmp_dir.path();
let archive_src = tmp_path.join("src_archive");
fs::create_dir_all(&archive_src).expect("tmp archive-dir create fail");
let fp = archive_src.join("temp.txt");
let mut tmp_file = File::create(&fp).expect("temp file create fail");
tmp_file.write_all(b"This is a test!").unwrap();
let mut ar = tar::Builder::new(vec![]);
ar.append_dir_all("inner_archive", &archive_src)
.expect("tar append dir all fail");
let tar_writer = ar.into_inner().expect("failed getting tar writer");
let archive_fp = tmp_path.with_file_name("archive_file.tar.gz");
let mut archive_file = File::create(&archive_fp).expect("failed creating archive file");
let mut e = GzEncoder::new(&mut archive_file, flate2::Compression::default());
io::copy(&mut tar_writer.as_slice(), &mut e)
.expect("failed writing from tar archive to gz encoder");
e.finish().expect("gz finish fail");
let out_tmp =
TempDir::new("self_update_unpack_file_tar_gzip_outdir").expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_fp)
.extract_file(&out_path, "inner_archive/temp.txt")
.expect("extract fail");
let out_file = out_path.join("inner_archive/temp.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a test!");
}
#[test]
fn unpack_zip() {
let tmp_dir = TempDir::new("self_update_unpack_zip_src").expect("tempdir fail");
let tmp_path = tmp_dir.path();
let archive_path = tmp_path.join("archive.zip");
let archive_file = File::create(&archive_path).expect("create file fail");
let mut zip = zip::ZipWriter::new(archive_file);
let options =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("zipped.txt", options)
.expect("failed starting zip file");
zip.write_all(b"This is a test!")
.expect("failed writing to zip");
zip.start_file("zipped2.txt", options)
.expect("failed starting second zip file");
zip.write_all(b"This is a second test!")
.expect("failed writing to second zip");
zip.finish().expect("failed finishing zip");
let out_tmp = TempDir::new("self_update_unpack_zip_outdir").expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_path)
.extract_into(&out_path)
.expect("extract fail");
let out_file = out_path.join("zipped.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a test!");
let out_file2 = out_path.join("zipped2.txt");
assert!(out_file2.exists());
cmp_content(&out_file2, "This is a second test!");
}
#[test]
fn unpack_zip_file() {
let tmp_dir = TempDir::new("self_update_unpack_zip_src").expect("tempdir fail");
let tmp_path = tmp_dir.path();
let archive_path = tmp_path.join("archive.zip");
let archive_file = File::create(&archive_path).expect("create file fail");
let mut zip = zip::ZipWriter::new(archive_file);
let options =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("zipped.txt", options)
.expect("failed starting zip file");
zip.write_all(b"This is a test!")
.expect("failed writing to zip");
zip.start_file("zipped2.txt", options)
.expect("failed starting second zip file");
zip.write_all(b"This is a second test!")
.expect("failed writing to second zip");
zip.finish().expect("failed finishing zip");
let out_tmp = TempDir::new("self_update_unpack_zip_outdir").expect("tempdir fail");
let out_path = out_tmp.path();
Extract::from_source(&archive_path)
.extract_file(&out_path, "zipped2.txt")
.expect("extract fail");
let out_file = out_path.join("zipped2.txt");
assert!(out_file.exists());
cmp_content(&out_file, "This is a second test!");
}
}