Skip to content

Commit

Permalink
Don't overwrite existing config files (#308)
Browse files Browse the repository at this point in the history
* Switch to `PathBuf`

* Unify extraction logic

* Append ".new" to existing config files

* Fix problem with github direct file download
  • Loading branch information
CosmicHorrorDev authored Mar 25, 2021
1 parent 50cc8d2 commit a651d2b
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 119 deletions.
2 changes: 1 addition & 1 deletion src/odin/commands/install_mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub fn invoke(args: &ArgMatches) {
let mut valheim_mod = ValheimMod::new(args.value_of("URL").unwrap());
info!("Installing {}", valheim_mod.url);
debug!("Mod URL: {}", valheim_mod.url);
debug!("Mod staging location: {}", valheim_mod.staging_location);
debug!("Mod staging location: {:?}", valheim_mod.staging_location);
match valheim_mod.download() {
Ok(_) => valheim_mod.install(),
Err(message) => {
Expand Down
276 changes: 158 additions & 118 deletions src/odin/mods/mod.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,79 @@
pub mod bepinex;

use crate::constants::SUPPORTED_FILE_TYPES;
use crate::utils::common_paths::{
bepinex_config_directory, bepinex_plugin_directory, game_directory,
use crate::{
constants::SUPPORTED_FILE_TYPES,
utils::{common_paths, get_md5_hash, parse_file_name, url_parse_file_type},
};
use crate::utils::{common_paths, get_md5_hash, parse_file_name, path_exists, url_parse_file_type};
use fs_extra::dir;
use fs_extra::dir::CopyOptions;
use log::{debug, error, info};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::fs::{create_dir_all, File};
use std::path::Path;
use std::fs::{self, create_dir_all, File};
use std::io;
use std::path::{Path, PathBuf};
use std::process::exit;
use zip::result::ZipError;
use zip::ZipArchive;
use zip::{
result::{ZipError, ZipResult},
ZipArchive,
};

trait ZipExt {
fn extract_sub_dir_custom<P: AsRef<Path>>(&mut self, dst_dir: P, sub_dir: &str) -> ZipResult<()>;
}

impl ZipExt for ZipArchive<File> {
fn extract_sub_dir_custom<P: AsRef<Path>>(&mut self, dst_dir: P, sub_dir: &str) -> ZipResult<()> {
for i in 0..self.len() {
let mut file = self.by_index(i)?;
let filepath = match file
.enclosed_name()
.ok_or(ZipError::InvalidArchive("Invalid file path"))?
.strip_prefix(sub_dir)
{
Ok(path) => path,
Err(_) => continue,
};

let mut outpath = dst_dir.as_ref().join(filepath);

if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(&p)?;
}
}

// Don't overwrite old cfg files
if outpath.extension().unwrap() == "cfg" && outpath.exists() {
outpath = outpath.with_extension("cfg.new");
}
let mut outfile = File::create(&outpath)?;

io::copy(&mut file, &mut outfile)?;
}

// Get and Set permissions
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
}
}
}

Ok(())
}
}

pub struct ValheimMod {
pub(crate) url: String,
pub(crate) file_type: String,
pub(crate) staging_location: String,
pub(crate) staging_location: PathBuf,
pub(crate) installed: bool,
pub(crate) downloaded: bool,
pub(crate) staged: bool,
}

#[derive(Serialize, Deserialize)]
Expand All @@ -36,160 +87,149 @@ impl ValheimMod {
ValheimMod {
url: String::from(url),
file_type,
staging_location: common_paths::mods_directory(),
staging_location: common_paths::mods_directory().into(),
installed: false,
downloaded: false,
staged: false,
}
}
// fn uninstall(&self) {}

fn try_parse_manifest(&self, archive: &mut ZipArchive<File>) -> Result<Manifest, ZipError> {
debug!("Parsing manifest...");
let manifest = archive.by_name("manifest.json")?;
Ok(serde_json::from_reader(manifest).expect("Failed deserializing manifest"))
}

fn copy_staged_plugin(&mut self, manifest: &Manifest) {
if !&self.staged {
error!("Zip file not extracted to staging location!!");
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1);
}
let working_directory = game_directory();
let mut staging_output = String::from(&self.staging_location);
let sub_dir = format!("{}/{}", &staging_output, &manifest.name);
debug!("Manifest suggests sub directory: {}", sub_dir);
let mut dir_copy_options = dir::CopyOptions::new();
dir_copy_options.overwrite = true;
let mut copy_destination = bepinex_plugin_directory();
if path_exists(&sub_dir)
&& (manifest.name.eq("BepInExPack_Valheim") || manifest.name.eq("BepInEx_Valheim_Full"))
{
staging_output = format!("{}/{}", &staging_output, &manifest.name);
copy_destination = String::from(&working_directory);
} else {
copy_destination = format!("{}/{}", &copy_destination, &manifest.name);
debug!("Creating mod directory: {}", &copy_destination);
match create_dir_all(&copy_destination) {
Ok(_) => info!("Created mod directory: {}", &copy_destination),
Err(_) => {
error!("Failed to create mod directory! {}", &copy_destination);
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1);
}
};
}
debug!(
"Copying contents from: \n{}\nInto Directory:\n{}",
&staging_output, &working_directory
);
let source_contents: Vec<_> = std::fs::read_dir(&staging_output)
.unwrap()
.map(|entry| entry.unwrap().path())
.collect();
match fs_extra::copy_items(&source_contents, &copy_destination, &dir_copy_options) {
Ok(_) => info!("Successfully installed {}", &self.url),
Err(_) => {
error!("Failed to install {}", &self.url);
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1);
}
}
}
fn copy_single_file(&self, from: &str, to: String) {
fn copy_single_file<P1, P2>(&self, from: P1, to: P2)
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let to = to.as_ref();
let from = from.as_ref();

let mut dir_options = CopyOptions::new();
dir_options.overwrite = true;
match fs_extra::copy_items(&[&from], &to, &dir_options) {
Ok(_) => debug!("Successfully copied {} to {}", &from, &to),
Ok(_) => debug!("Successfully copied {:?} to {:?}", from, to),
Err(_) => {
error!("Failed to install {}", self.url);
error!(
"File failed to copy from: \n{}To Destination:{}",
&from, &to
"File failed to copy from: \n{:?}To Destination:{:?}",
from, to
);
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1);
}
};
}
fn stage_plugin(&mut self, archive: &mut ZipArchive<File>) {
let mut staging_output = String::from(
Path::new(&self.staging_location)
.file_stem()
.unwrap()
.to_str()
.unwrap(),
);
staging_output = format!("{}/{}", common_paths::mods_directory(), &staging_output);
debug!(
"Extracting contents to staging directory: {}",
staging_output
);
archive.extract(&staging_output).unwrap();
self.staging_location = staging_output;
self.staged = true;

fn is_mod_framework(&self, archive: &mut ZipArchive<File>) -> bool {
let maybe_manifest = self.try_parse_manifest(archive).ok();
match maybe_manifest {
Some(Manifest { name }) => {
let mod_dir = format!("{}/", name);
let mod_dir_exists = archive.file_names().any(|file_name| file_name == mod_dir);

// It's a mod framework based on a specific name and if it has a matching directory in the
// archive
mod_dir_exists && (name == "BepInExPack_Valheim" || name == "BepInEx_Valheim_Full")
}
None => archive
// If there is no manifest, fall back to checking for winhttp.dll as a heuristic
.file_names()
.any(|file_name| file_name.eq_ignore_ascii_case("winhttp.dll")),
}
}

fn extract_plugin(&self, archive: &mut ZipArchive<File>) {
let output_path = if archive
.file_names()
.any(|file_name| file_name.eq_ignore_ascii_case("winhttp.dll"))
{
info!("Installing BepInEx...");
common_paths::game_directory()
// The output location to extract into and the directory to extract from the archive depends on
// if we're installing just a mod or a full framework, and if it is being downloaded from
// thunderstore where a manifest is provided, or not.
let (output_dir, archive_dir) = if self.is_mod_framework(archive) {
info!("Installing Framework...");
let output_dir = PathBuf::from(&common_paths::game_directory());

// All frameworks from thunderstore just need the directory matching the name extracted
let sub_dir = if let Ok(Manifest { name }) = self.try_parse_manifest(archive) {
format!("{}/", name)
} else {
String::new()
};

(output_dir, sub_dir)
} else {
info!("Installing Mod...");
common_paths::bepinex_plugin_directory()
// thunderstore mods are extracted into a subfolder in the plugin directory
let mut output_dir = PathBuf::from(&common_paths::bepinex_plugin_directory());
if let Ok(Manifest { name }) = self.try_parse_manifest(archive) {
output_dir.push(name);
}
create_dir_all(&output_dir).unwrap_or_else(|_| {
error!("Failed to create mod directory! {:?}", output_dir);
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1);
});

(output_dir, "".to_string())
};
match archive.extract(output_path) {

match archive.extract_sub_dir_custom(output_dir, &archive_dir) {
Ok(_) => info!("Successfully installed {}", &self.url),
Err(msg) => {
error!(
"Failed to install: {}\nDownloaded Archive: {}\n{}",
&self.url,
&self.staging_location,
"Failed to install: {}\nDownloaded Archive: {:?}\n{}",
self.url,
self.staging_location,
msg.to_string()
);
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1);
}
};
}

pub fn install(&mut self) {
if Path::new(&self.staging_location).is_dir() {
error!(
"Failed to install mod! Staging location is a directory! {}",
&self.staging_location
"Failed to install mod! Staging location is a directory! {:?}",
self.staging_location
);
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1)
}

if self.file_type.eq("dll") {
self.copy_single_file(&self.staging_location, bepinex_plugin_directory());
self.copy_single_file(
&self.staging_location,
&common_paths::bepinex_plugin_directory(),
);
} else if self.file_type.eq("cfg") {
self.copy_single_file(&self.staging_location, bepinex_config_directory());
info!("Copying single cfg into config directory");
let src_file_path = &self.staging_location;
let cfg_file_name = self.staging_location.file_name().unwrap();

// If the cfg already exists in the output directory then append a ".new"
let mut dst_file_path =
Path::new(&common_paths::bepinex_config_directory()).join(cfg_file_name);
if dst_file_path.exists() {
dst_file_path = dst_file_path.with_extension("cfg.new");
}

fs::rename(src_file_path, dst_file_path).unwrap();
} else {
let zip_file = std::fs::File::open(&self.staging_location).unwrap();
let mut archive = match zip::ZipArchive::new(zip_file) {
let zip_file = File::open(&self.staging_location).unwrap();
let mut archive = match ZipArchive::new(zip_file) {
Ok(file_archive) => {
debug!("Successfully parsed zip file {}", &self.staging_location);
debug!("Successfully parsed zip file {:?}", self.staging_location);
file_archive
}
Err(_) => {
error!("Failed to parse zip file {}", &self.staging_location);
error!("Failed to parse zip file {:?}", self.staging_location);
// TODO: Remove Exit Code and provide an Ok or Err.
exit(1);
}
};
match self.try_parse_manifest(&mut archive) {
Ok(manifest) => {
debug!("Manifest has name: {}", manifest.name);
self.stage_plugin(&mut archive);
self.copy_staged_plugin(&manifest);
}
Err(_) => {
self.extract_plugin(&mut archive);
}
}
self.extract_plugin(&mut archive);
}
self.installed = true
}
Expand All @@ -200,30 +240,30 @@ impl ValheimMod {
if !Path::new(&self.staging_location).exists() {
error!("Failed to download file to staging location!");
return Err(format!(
"Directory does not exist! {}",
&self.staging_location
"Directory does not exist! {:?}",
self.staging_location
));
}
if let Ok(parsed_url) = Url::parse(&download_url) {
match reqwest::blocking::get(parsed_url) {
Ok(mut response) => {
let file_type = url_parse_file_type(&self.url);
if !SUPPORTED_FILE_TYPES.contains(&file_type.as_str()) {
if !SUPPORTED_FILE_TYPES.contains(&self.file_type.as_str()) {
debug!("Using url (in case of redirect): {}", &self.url);
self.url = response.url().to_string();
self.file_type = url_parse_file_type(&response.url().to_string())
self.file_type = url_parse_file_type(&response.url().to_string());
}

let file_name = parse_file_name(
&Url::parse(&self.url).unwrap(),
format!("{}.{}", get_md5_hash(&download_url), &self.file_type).as_str(),
);
self.staging_location = format!("{}/{}", &self.staging_location, file_name);
debug!("Downloading to: {}", &self.staging_location);
self.staging_location = self.staging_location.join(file_name);
debug!("Downloading to: {:?}", self.staging_location);
let mut file = File::create(&self.staging_location).unwrap();
response.copy_to(&mut file).expect("Failed saving mod file");
self.downloaded = true;
debug!("Download Complete!: {}", &self.url);
debug!("Download Output: {}", &self.staging_location);
debug!("Download Output: {:?}", self.staging_location);
Ok(String::from("Successful"))
}
Err(err) => {
Expand Down

0 comments on commit a651d2b

Please sign in to comment.