Compare commits
2 Commits
d263d487b1
...
c81178567a
| Author | SHA1 | Date | |
|---|---|---|---|
|
c81178567a
|
|||
|
b6efa0a818
|
29
Cargo.lock
generated
29
Cargo.lock
generated
@@ -325,6 +325,7 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -725,6 +726,15 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "saphyr"
|
||||
version = "0.0.6"
|
||||
@@ -896,12 +906,31 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
|
||||
@@ -14,3 +14,4 @@ quick-xml = { version = "0.39.2", features = ["serde-types", "serialize"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
thiserror = "2.0.18"
|
||||
toml = "1.0.3"
|
||||
walkdir = "2.5.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::basic_types::{Game, Link, ModdedInstance, RootConfig};
|
||||
use crate::types::{Game, Link, ModdedInstance, RootConfig};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
@@ -14,7 +14,7 @@ pub fn activate_instance(
|
||||
) -> Result<()> {
|
||||
// TODO: Resolve game for instance config
|
||||
let game = root_config
|
||||
.games
|
||||
.games()
|
||||
.first()
|
||||
.ok_or(anyhow!("TODO: resolve game from config"))?;
|
||||
|
||||
@@ -40,7 +40,7 @@ fn resolve_links(
|
||||
let mut map: HashMap<PathBuf, PathBuf> = HashMap::new();
|
||||
|
||||
for link in game_links.into_iter().chain(mod_links).chain(overrides) {
|
||||
map.insert(link.dst, link.src);
|
||||
map.insert(link.dst().to_owned(), link.src().to_owned());
|
||||
}
|
||||
|
||||
let final_links: Vec<Link> = map
|
||||
@@ -57,13 +57,13 @@ fn resolve_link_for_instance(
|
||||
) -> anyhow::Result<Vec<Link>> {
|
||||
let mut links: Vec<Link> = Vec::new();
|
||||
|
||||
for installed_mod in &instance.mods {
|
||||
let mod_config = root_config.get_mod_by_id(&installed_mod.mod_id()).unwrap();
|
||||
let mod_source_root = root_config.get_mod_location(&mod_config);
|
||||
for installed_mod in instance.mods() {
|
||||
let mod_config = root_config.get_mod_by_id(installed_mod.mod_id()).unwrap();
|
||||
let mod_source_root = root_config.mod_location().join(mod_config.path());
|
||||
|
||||
for link in installed_mod.files() {
|
||||
let link_target = mod_source_root.join(&link.src);
|
||||
links.push(Link::new(link_target, &link.dst));
|
||||
let link_target = mod_source_root.join(link.src());
|
||||
links.push(Link::new(link_target, link.dst()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ fn resolve_link_for_instance(
|
||||
}
|
||||
|
||||
fn apply_link(link: &Link, target: impl AsRef<Path>) -> Result<(), io::Error> {
|
||||
let link_target = &link.src;
|
||||
let link_name = target.as_ref().join(&link.dst);
|
||||
let link_target = &link.src();
|
||||
let link_name = target.as_ref().join(link.dst());
|
||||
|
||||
link_file(link_target, &link_name)
|
||||
}
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
use log::trace;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::{self, read_to_string},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
fomod::{FileType, FileTypeEnum},
|
||||
utils::walk_files_recursive,
|
||||
};
|
||||
|
||||
/// A link between a file from a mod and a destination in a ModdedInstance
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
#[serde(from = "(PathBuf, PathBuf)", into = "(PathBuf,PathBuf)")]
|
||||
pub struct Link {
|
||||
pub src: PathBuf,
|
||||
pub dst: PathBuf,
|
||||
}
|
||||
|
||||
impl Link {
|
||||
pub fn new(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
src: src.as_ref().to_owned(),
|
||||
dst: dst.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_mod_file(file: &ModFile) -> Self {
|
||||
Self::new(&file.source, &file.dest)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(PathBuf, PathBuf)> for Link {
|
||||
fn from(value: (PathBuf, PathBuf)) -> Self {
|
||||
Self {
|
||||
src: value.0,
|
||||
dst: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Link> for (PathBuf, PathBuf) {
|
||||
fn from(value: Link) -> Self {
|
||||
(value.src, value.dst)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ModFile> for Link {
|
||||
fn from(value: ModFile) -> Self {
|
||||
Self::new(value.source, value.dest)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct RootConfig {
|
||||
/// Available games
|
||||
pub games: Vec<Game>,
|
||||
|
||||
/// Where all mods are stored
|
||||
pub mod_location: PathBuf,
|
||||
|
||||
#[serde(default)]
|
||||
pub instances: Vec<InstancePointer>,
|
||||
|
||||
/// All available mods
|
||||
#[serde(default)]
|
||||
pub mods: Vec<ModConfig>,
|
||||
}
|
||||
|
||||
impl RootConfig {
|
||||
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadWriteError> {
|
||||
trace!(
|
||||
"Loading RootConfig from file: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
);
|
||||
|
||||
let data = read_to_string(path)?;
|
||||
let config = toml::from_str(&data)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mod_location(&self, mod_config: &ModConfig) -> PathBuf {
|
||||
self.mod_location.join(mod_config.path.clone())
|
||||
}
|
||||
|
||||
pub fn get_mod_by_id(&self, id: &str) -> Option<ModConfig> {
|
||||
self.mods.iter().find(|e| e.id == id).cloned()
|
||||
}
|
||||
|
||||
pub fn load_instance_by_id(&self, id: &str) -> Result<ModdedInstance, ConfigReadWriteError> {
|
||||
let conf = self
|
||||
.get_instance_config(id)
|
||||
.ok_or(ConfigReadWriteError::IDNotFound)?;
|
||||
|
||||
ModdedInstance::load_from_file(&conf.path)
|
||||
}
|
||||
|
||||
pub fn get_instance_config(&self, id: &str) -> Option<&InstancePointer> {
|
||||
self.instances.iter().find(|e| e.id == id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Available game
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct Game {
|
||||
pub install_location: PathBuf,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn export_links(&self) -> Result<Vec<Link>, io::Error> {
|
||||
let links: Vec<Link> = walk_files_recursive(&self.install_location)
|
||||
.unwrap()
|
||||
.map(|file| file.path())
|
||||
.map(|path| Link::new(&path, path.strip_prefix(&self.install_location).unwrap()))
|
||||
.collect();
|
||||
Ok(links)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct InstancePointer {
|
||||
pub id: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// Config for an available mod
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct ModConfig {
|
||||
/// ID of the mod
|
||||
pub id: String,
|
||||
|
||||
/// Relative to the mod_location from root config
|
||||
pub path: PathBuf,
|
||||
|
||||
/// If the files should be included on the root
|
||||
#[serde(default)]
|
||||
root_mod: bool,
|
||||
|
||||
/// Globs of what files to ignore
|
||||
#[serde(default)]
|
||||
ignore: Vec<String>,
|
||||
}
|
||||
|
||||
impl ModConfig {
|
||||
pub fn new(id: &str, source: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
id: id.to_owned(),
|
||||
path: source.as_ref().to_owned(),
|
||||
root_mod: false,
|
||||
ignore: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_root_mod(&self) -> bool {
|
||||
self.root_mod
|
||||
}
|
||||
|
||||
pub fn ignore(&self) -> &[String] {
|
||||
&self.ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// An modded game with all plugins and files
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ModdedInstance {
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub mods: Vec<InstalledMod>,
|
||||
|
||||
#[serde(default)]
|
||||
pub load_order: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
game_file_overrides: Vec<Link>,
|
||||
}
|
||||
|
||||
impl ModdedInstance {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_owned(),
|
||||
mods: Vec::new(),
|
||||
load_order: Vec::new(),
|
||||
game_file_overrides: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadWriteError> {
|
||||
trace!(
|
||||
"Loading ModdedInstance from file: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
);
|
||||
|
||||
let data = read_to_string(path)?;
|
||||
let config = toml::from_str(&data)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<(), ConfigReadWriteError> {
|
||||
trace!(
|
||||
"Saving ModdedInstance to: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
);
|
||||
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
let mut file = fs::File::create(path)?;
|
||||
write!(file, "{}", content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_load_order(&mut self, order: Vec<String>) {
|
||||
self.load_order = order;
|
||||
}
|
||||
|
||||
pub fn load_order(&self) -> &[String] {
|
||||
&self.load_order
|
||||
}
|
||||
|
||||
pub fn game_file_overrides(&self) -> &[Link] {
|
||||
&self.game_file_overrides
|
||||
}
|
||||
|
||||
pub fn update_or_create_mod(&mut self, installed_mod: &InstalledMod) {
|
||||
match self.mods.iter_mut().find(|e| e.id == installed_mod.id) {
|
||||
Some(existing) => {
|
||||
*existing = installed_mod.to_owned();
|
||||
}
|
||||
None => {
|
||||
self.mods.push(installed_mod.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct InstalledMod {
|
||||
id: String,
|
||||
files: Vec<Link>,
|
||||
priority: isize,
|
||||
}
|
||||
|
||||
impl InstalledMod {
|
||||
pub fn new(root_mod_id: &str, priority: isize) -> Self {
|
||||
Self {
|
||||
id: root_mod_id.to_owned(),
|
||||
files: Vec::new(),
|
||||
priority,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, file: &ModFile) {
|
||||
self.files.push(Link::from_mod_file(file));
|
||||
}
|
||||
|
||||
/// Get the id of the mod
|
||||
pub fn mod_id(&self) -> String {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
/// The priority over other mods. Only used when 2 files conflict.
|
||||
pub fn priority(&self) -> isize {
|
||||
self.priority
|
||||
}
|
||||
|
||||
/// The selected files
|
||||
pub fn files(&self) -> &[Link] {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct ModFile {
|
||||
/// Relative path in the mod
|
||||
source: PathBuf,
|
||||
|
||||
/// Relative path on where to install file in game dir
|
||||
dest: PathBuf,
|
||||
|
||||
/// Internal priority inside the mod itself. In case the mod overwrites internal files.
|
||||
internal_priority: isize,
|
||||
}
|
||||
|
||||
impl ModFile {
|
||||
pub fn new(src: impl AsRef<Path>, dst: impl AsRef<Path>, prio: isize) -> Self {
|
||||
Self {
|
||||
source: src.as_ref().to_owned(),
|
||||
dest: dst.as_ref().to_owned(),
|
||||
internal_priority: prio,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_installer(file: FileType) -> Self {
|
||||
let dest: PathBuf = file.destination.unwrap_or_default().into();
|
||||
|
||||
ModFile {
|
||||
source: file.source.into(),
|
||||
dest: dest.to_owned(),
|
||||
internal_priority: file.priority.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_installer(
|
||||
entry: FileTypeEnum,
|
||||
source: impl AsRef<Path>,
|
||||
) -> Result<Vec<Self>, std::io::Error> {
|
||||
match entry {
|
||||
FileTypeEnum::File(file_type) => Ok(vec![Self::new_from_installer(file_type)]),
|
||||
FileTypeEnum::Folder(dir_type) => {
|
||||
let source_root = source.as_ref().join(&dir_type.source);
|
||||
let priority = dir_type.priority.unwrap_or(0);
|
||||
let dest_base: PathBuf =
|
||||
Path::new("Data").join(PathBuf::from(dir_type.destination.unwrap_or_default()));
|
||||
|
||||
Ok(walk_files_recursive(&source_root)?
|
||||
.map(|file| Self {
|
||||
internal_priority: priority,
|
||||
source: file.path().strip_prefix(&source).unwrap().to_owned(),
|
||||
dest: dest_base.join(file.path().strip_prefix(&source_root).unwrap()),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the realtive path this file should be installed
|
||||
#[inline]
|
||||
pub fn destination(&self) -> PathBuf {
|
||||
self.dest.clone()
|
||||
}
|
||||
|
||||
/// Get the iternal priority. Only used when 2 files conflict.
|
||||
#[inline]
|
||||
pub fn internal_priority(&self) -> isize {
|
||||
self.internal_priority
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigReadWriteError {
|
||||
#[error("IO failure")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("Failed to deserialize toml")]
|
||||
Deserialize(#[from] toml::de::Error),
|
||||
|
||||
#[error("Failed to serialize to toml")]
|
||||
Serialize(#[from] toml::ser::Error),
|
||||
|
||||
#[error("The provided ID could not be found")]
|
||||
IDNotFound,
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use log::{debug, trace};
|
||||
use log::debug;
|
||||
|
||||
use crate::basic_types::{InstalledMod, ModConfig, ModFile, ModdedInstance};
|
||||
use crate::types::{InstalledMod, ModFile};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Conflict<'a> {
|
||||
@@ -23,22 +23,18 @@ impl<'a> ConflictSolver<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// fn add_file_unchecked(&mut self, file: &'a ModFile, from_mod: &'a InstalledMod) {
|
||||
// self.files.insert(file.destination(), (file, from_mod));
|
||||
// }
|
||||
|
||||
pub fn add_file(
|
||||
&mut self,
|
||||
file: &'a ModFile,
|
||||
from_mod: &'a InstalledMod,
|
||||
) -> Option<Conflict<'a>> {
|
||||
let path = &file.destination();
|
||||
let path = &file.dst().to_owned();
|
||||
match self.files.get(path) {
|
||||
Some((current_file, current_file_mod)) => {
|
||||
debug!(
|
||||
"Trying to resolve file conflict between at {}",
|
||||
path.to_string_lossy()
|
||||
);
|
||||
// debug!(
|
||||
// "Trying to resolve file conflict between at {}",
|
||||
// path.to_string_lossy()
|
||||
// );
|
||||
|
||||
if from_mod == *current_file_mod {
|
||||
// File from the same mod
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
collections::HashMap,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
@@ -8,11 +8,11 @@ use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||
use log::warn;
|
||||
|
||||
use crate::{
|
||||
basic_types::{InstalledMod, Link, ModConfig, ModFile, ModdedInstance, RootConfig},
|
||||
file_conflict_solver::ConflictSolver,
|
||||
fomod, install_prompt,
|
||||
mod_config_installer::run_fomod_installer,
|
||||
utils::{resolve_case_insensitive, walk_files_recursive},
|
||||
types::{InstalledMod, ModConfig, ModFile, ModdedInstance, RootConfig},
|
||||
utils::{resolve_case_insensitive, walk_all_files},
|
||||
};
|
||||
|
||||
pub fn insert_mod_to_instance(
|
||||
@@ -24,9 +24,9 @@ pub fn insert_mod_to_instance(
|
||||
let mut solver = ConflictSolver::new();
|
||||
|
||||
let mut installed_files: Vec<(ModFile, &InstalledMod)> = Vec::new();
|
||||
for installed_mod in &instance.mods {
|
||||
for installed_mod in instance.mods() {
|
||||
for link in installed_mod.files() {
|
||||
let recreated_mod_file = ModFile::new(&link.src, &link.dst, 0);
|
||||
let recreated_mod_file = ModFile::new(link.src(), link.dst(), 0);
|
||||
installed_files.push((recreated_mod_file, installed_mod));
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ pub fn insert_mod_to_instance(
|
||||
}
|
||||
}
|
||||
|
||||
let new_mod = InstalledMod::new(&from_mod.id, priority);
|
||||
let new_mod = InstalledMod::new(from_mod.id(), priority);
|
||||
for file in files {
|
||||
if let Some(conflict) = solver.add_file(file, &new_mod) {
|
||||
// TODO: Return conflict
|
||||
@@ -49,14 +49,14 @@ pub fn insert_mod_to_instance(
|
||||
let mut map: HashMap<String, InstalledMod> = HashMap::new();
|
||||
|
||||
for (file, from_mod) in new_link_tree {
|
||||
match map.get_mut(&from_mod.mod_id()) {
|
||||
match map.get_mut(from_mod.mod_id()) {
|
||||
Some(existing) => {
|
||||
existing.add_file(file);
|
||||
}
|
||||
None => {
|
||||
let mut new_mod = InstalledMod::new(&from_mod.mod_id(), from_mod.priority());
|
||||
let mut new_mod = InstalledMod::new(from_mod.mod_id(), from_mod.priority());
|
||||
new_mod.add_file(file);
|
||||
map.insert(new_mod.mod_id(), new_mod);
|
||||
map.insert(new_mod.mod_id().to_owned(), new_mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ pub fn files_to_install_mod(
|
||||
instance: &ModdedInstance,
|
||||
mod_to_install: &ModConfig,
|
||||
) -> anyhow::Result<Vec<ModFile>> {
|
||||
let mod_location = root_config.get_mod_location(mod_to_install);
|
||||
let mod_location = root_config.mod_location().join(mod_to_install.path());
|
||||
|
||||
let files = match determain_mod_kind(mod_to_install, &mod_location)? {
|
||||
ModKind::Fomod(xml_path) => install_fomod(instance, xml_path, &mod_location)?,
|
||||
@@ -126,15 +126,16 @@ fn install_fomod(
|
||||
|
||||
fn install_from_dir(
|
||||
mod_config: &ModConfig,
|
||||
path: impl AsRef<Path>,
|
||||
mod_location: impl AsRef<Path>,
|
||||
) -> anyhow::Result<Vec<ModFile>> {
|
||||
let glob_filter = create_glob_filter(mod_config.ignore())?;
|
||||
let files: Vec<_> = walk_files_recursive(&path)?
|
||||
.map(|entry| entry.path())
|
||||
.map(|file_path| file_path.strip_prefix(&path).unwrap().to_owned())
|
||||
|
||||
let files: Vec<_> = walk_all_files(&mod_location)?
|
||||
.map(|entry| entry.path().strip_prefix(&mod_location).unwrap().to_owned())
|
||||
.filter(|rel_path| !glob_filter.is_match(rel_path))
|
||||
.map(|rel_path| ModFile::new(&rel_path, &rel_path, 0))
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
@@ -144,13 +145,13 @@ fn install_from_dir_to_data(
|
||||
) -> anyhow::Result<Vec<ModFile>> {
|
||||
let glob_filter = create_glob_filter(mod_config.ignore())?;
|
||||
let data = PathBuf::from("Data");
|
||||
let files: Vec<_> = walk_files_recursive(&path)?
|
||||
.map(|entry| entry.path())
|
||||
.map(|file_path| file_path.strip_prefix(&path).unwrap().to_owned())
|
||||
.filter(|rel_path| should_be_included(rel_path))
|
||||
let files: Vec<ModFile> = walk_all_files(&path)?
|
||||
.map(|entry| entry.path().strip_prefix(&path).unwrap().to_owned())
|
||||
.filter(|rel_path| !glob_filter.is_match(rel_path))
|
||||
.filter(|rel_path| should_be_included(rel_path))
|
||||
.map(|rel_path| ModFile::new(&rel_path, data.join(&rel_path), 0))
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,44 +3,61 @@ use libloot::{
|
||||
error::{GameHandleCreationError, LoadPluginsError, SortPluginsError},
|
||||
};
|
||||
use log::trace;
|
||||
use std::{io, path::Path};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
basic_types::{self, ModdedInstance, RootConfig},
|
||||
utils::walk_files_recursive,
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::types::{self, ModdedInstance, RootConfig};
|
||||
|
||||
pub fn create_loadorder(
|
||||
root_config: &RootConfig,
|
||||
game: &basic_types::Game,
|
||||
game: &types::Game,
|
||||
instance: &ModdedInstance,
|
||||
) -> Result<Vec<String>, LoadOrderError> {
|
||||
let mut loot_game = Game::new(GameType::SkyrimSE, &game.install_location)?;
|
||||
let mut loot_game = Game::with_local_path(
|
||||
GameType::SkyrimSE,
|
||||
game.install_location(),
|
||||
&game.install_location().join(PathBuf::from("appdata")),
|
||||
)?;
|
||||
|
||||
// Add plugins files from the game install
|
||||
let install_plugins: Vec<_> = walk_files_recursive(game.install_location.join("Data"))?
|
||||
.filter(|f| is_plugin_file(f.path()))
|
||||
.map(|f| f.path())
|
||||
.collect();
|
||||
let refs: Vec<_> = install_plugins.iter().map(|e| e.as_path()).collect();
|
||||
let install_plugins: Vec<PathBuf> = WalkDir::new(game.install_location().join("Data"))
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if is_plugin_file(path) {
|
||||
Ok(Some(path.to_path_buf()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.filter_map(|r| r.transpose())
|
||||
.collect::<Result<_, io::Error>>()?;
|
||||
|
||||
// The loaded_plugins function requires &[&Path]
|
||||
let refs: Vec<&Path> = install_plugins.iter().map(|e| e.as_path()).collect();
|
||||
|
||||
trace!("Loading {} plugins to game", refs.len());
|
||||
loot_game.load_plugins(&refs)?;
|
||||
|
||||
// Add plugins from the instance
|
||||
let instance_plugins: Vec<_> = instance
|
||||
.mods
|
||||
.mods()
|
||||
.iter()
|
||||
.flat_map(|installed_mod| {
|
||||
let mod_config = root_config.get_mod_by_id(&installed_mod.mod_id()).unwrap();
|
||||
let mod_source_root = root_config.get_mod_location(&mod_config);
|
||||
let mod_config = root_config.get_mod_by_id(installed_mod.mod_id()).unwrap();
|
||||
let mod_source_root = root_config.mod_location().join(mod_config.path());
|
||||
|
||||
installed_mod
|
||||
.files()
|
||||
.iter()
|
||||
.filter(|f| is_plugin_file(&f.dst))
|
||||
.map(move |link| mod_source_root.join(&link.src))
|
||||
.filter(|f| is_plugin_file(f.dst()))
|
||||
.map(move |link| mod_source_root.join(link.src()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -5,13 +5,12 @@ use std::{error::Error, path::Path};
|
||||
|
||||
use crate::{
|
||||
activator::activate_instance,
|
||||
basic_types::RootConfig,
|
||||
cli::Args,
|
||||
instance::{files_to_install_mod, insert_mod_to_instance},
|
||||
types::RootConfig,
|
||||
};
|
||||
|
||||
mod activator;
|
||||
mod basic_types;
|
||||
mod cli;
|
||||
mod file_conflict_solver;
|
||||
mod fomod;
|
||||
@@ -19,6 +18,7 @@ mod install_prompt;
|
||||
mod instance;
|
||||
mod load_order;
|
||||
mod mod_config_installer;
|
||||
mod types;
|
||||
mod utils;
|
||||
|
||||
fn command_activate(
|
||||
@@ -50,7 +50,7 @@ fn command_add(root_config: &RootConfig, instance_id: &str, mod_id: &str) -> any
|
||||
fn command_order(root_config: &RootConfig, instance_id: &str) -> anyhow::Result<()> {
|
||||
let mut instance = root_config.load_instance_by_id(instance_id)?;
|
||||
let game = root_config
|
||||
.games
|
||||
.games()
|
||||
.first()
|
||||
.ok_or(anyhow!("TODO: get game from instance"))?;
|
||||
|
||||
|
||||
33
src/types.rs
Normal file
33
src/types.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use thiserror::Error;
|
||||
|
||||
mod game;
|
||||
mod installed_mod;
|
||||
mod link;
|
||||
mod mod_config;
|
||||
mod mod_file;
|
||||
mod modded_instance;
|
||||
mod root_config;
|
||||
|
||||
|
||||
pub use game::*;
|
||||
pub use installed_mod::*;
|
||||
pub use link::*;
|
||||
pub use mod_config::*;
|
||||
pub use mod_file::*;
|
||||
pub use modded_instance::*;
|
||||
pub use root_config::*;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ConfigReadWriteError {
|
||||
#[error("IO failure")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Failed to deserialize toml")]
|
||||
Deserialize(#[from] toml::de::Error),
|
||||
|
||||
#[error("Failed to serialize to toml")]
|
||||
Serialize(#[from] toml::ser::Error),
|
||||
|
||||
#[error("The provided ID could not be found")]
|
||||
IDNotFound,
|
||||
}
|
||||
33
src/types/game.rs
Normal file
33
src/types/game.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{types::link::Link, utils::walk_all_files};
|
||||
|
||||
/// Available game
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct Game {
|
||||
install_location: PathBuf,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn export_links(&self) -> Result<Vec<Link>, io::Error> {
|
||||
let links: Vec<Link> = walk_all_files(&self.install_location)?
|
||||
.map(|entry| {
|
||||
Link::new(
|
||||
entry.path(),
|
||||
entry.path().strip_prefix(&self.install_location).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(links)
|
||||
}
|
||||
|
||||
pub fn install_location(&self) -> &Path {
|
||||
&self.install_location
|
||||
}
|
||||
}
|
||||
39
src/types/installed_mod.rs
Normal file
39
src/types/installed_mod.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::{link::Link, mod_file::ModFile};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct InstalledMod {
|
||||
id: String,
|
||||
files: Vec<Link>,
|
||||
priority: isize,
|
||||
}
|
||||
|
||||
impl InstalledMod {
|
||||
pub fn new(root_mod_id: &str, priority: isize) -> Self {
|
||||
Self {
|
||||
id: root_mod_id.to_owned(),
|
||||
files: Vec::new(),
|
||||
priority,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_file(&mut self, file: &ModFile) {
|
||||
self.files.push(Link::from_mod_file(file));
|
||||
}
|
||||
|
||||
/// Get the id of the mod
|
||||
pub fn mod_id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// The priority over other mods. Only used when 2 files conflict.
|
||||
pub fn priority(&self) -> isize {
|
||||
self.priority
|
||||
}
|
||||
|
||||
/// The selected files
|
||||
pub fn files(&self) -> &[Link] {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
55
src/types/link.rs
Normal file
55
src/types/link.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::mod_file::ModFile;
|
||||
|
||||
/// A link between a file from a mod and a destination in a ModdedInstance
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
#[serde(from = "(PathBuf, PathBuf)", into = "(PathBuf,PathBuf)")]
|
||||
pub struct Link {
|
||||
src: PathBuf,
|
||||
dst: PathBuf,
|
||||
}
|
||||
|
||||
impl Link {
|
||||
pub fn new(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
src: src.as_ref().to_owned(),
|
||||
dst: dst.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_mod_file(file: &ModFile) -> Self {
|
||||
Self::new(file.src(), file.dst())
|
||||
}
|
||||
|
||||
pub fn src(&self) -> &Path {
|
||||
&self.src
|
||||
}
|
||||
|
||||
pub fn dst(&self) -> &Path {
|
||||
&self.dst
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(PathBuf, PathBuf)> for Link {
|
||||
fn from(value: (PathBuf, PathBuf)) -> Self {
|
||||
Self {
|
||||
src: value.0,
|
||||
dst: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Link> for (PathBuf, PathBuf) {
|
||||
fn from(value: Link) -> Self {
|
||||
(value.src, value.dst)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ModFile> for Link {
|
||||
fn from(value: ModFile) -> Self {
|
||||
Self::new(value.src(), value.dst())
|
||||
}
|
||||
}
|
||||
49
src/types/mod_config.rs
Normal file
49
src/types/mod_config.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Config for an available mod
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct ModConfig {
|
||||
/// ID of the mod
|
||||
id: String,
|
||||
|
||||
/// Relative to the mod_location from root config
|
||||
path: PathBuf,
|
||||
|
||||
/// If the files should be included on the root
|
||||
#[serde(default)]
|
||||
root_mod: bool,
|
||||
|
||||
/// Globs of what files to ignore
|
||||
#[serde(default)]
|
||||
ignore: Vec<String>,
|
||||
}
|
||||
|
||||
impl ModConfig {
|
||||
pub fn new(id: &str, source: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
id: id.to_owned(),
|
||||
path: source.as_ref().to_owned(),
|
||||
root_mod: false,
|
||||
ignore: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Get the relative path to the mod from the mod directory
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn is_root_mod(&self) -> bool {
|
||||
self.root_mod
|
||||
}
|
||||
|
||||
pub fn ignore(&self) -> &[String] {
|
||||
&self.ignore
|
||||
}
|
||||
}
|
||||
80
src/types/mod_file.rs
Normal file
80
src/types/mod_file.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
fomod::{FileType, FileTypeEnum},
|
||||
utils::walk_all_files,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct ModFile {
|
||||
/// Relative path in the mod
|
||||
src: PathBuf,
|
||||
|
||||
/// Relative path on where to install file in game dir
|
||||
dst: PathBuf,
|
||||
|
||||
/// Internal priority inside the mod itself. In case the mod overwrites internal files.
|
||||
internal_priority: isize,
|
||||
}
|
||||
|
||||
impl ModFile {
|
||||
pub fn new(src: impl AsRef<Path>, dst: impl AsRef<Path>, prio: isize) -> Self {
|
||||
Self {
|
||||
src: src.as_ref().to_owned(),
|
||||
dst: dst.as_ref().to_owned(),
|
||||
internal_priority: prio,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_installer(file: FileType) -> Self {
|
||||
let dest: PathBuf = file.destination.unwrap_or_default().into();
|
||||
|
||||
ModFile {
|
||||
src: file.source.into(),
|
||||
dst: dest.to_owned(),
|
||||
internal_priority: file.priority.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_installer(
|
||||
entry: FileTypeEnum,
|
||||
source: impl AsRef<Path>,
|
||||
) -> Result<Vec<Self>, std::io::Error> {
|
||||
match entry {
|
||||
FileTypeEnum::File(file_type) => Ok(vec![Self::new_from_installer(file_type)]),
|
||||
FileTypeEnum::Folder(dir_type) => {
|
||||
let source_root = source.as_ref().join(&dir_type.source);
|
||||
let priority = dir_type.priority.unwrap_or(0);
|
||||
let dest_base: PathBuf =
|
||||
Path::new("Data").join(PathBuf::from(dir_type.destination.unwrap_or_default()));
|
||||
|
||||
let files = walk_all_files(&source_root)?
|
||||
.map(|entry| Self {
|
||||
src: entry.path().strip_prefix(&source).unwrap().to_owned(),
|
||||
dst: dest_base.join(entry.path().strip_prefix(&source_root).unwrap()),
|
||||
internal_priority: priority,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn src(&self) -> &Path {
|
||||
&self.src
|
||||
}
|
||||
|
||||
/// Get the realtive path this file should be installed
|
||||
pub fn dst(&self) -> &Path {
|
||||
&self.dst
|
||||
}
|
||||
|
||||
/// Get the iternal priority. Only used when 2 files conflict.
|
||||
#[inline]
|
||||
pub fn internal_priority(&self) -> isize {
|
||||
self.internal_priority
|
||||
}
|
||||
}
|
||||
91
src/types/modded_instance.rs
Normal file
91
src/types/modded_instance.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use std::{
|
||||
fs::{self, read_to_string},
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use log::trace;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::{ConfigReadWriteError, installed_mod::InstalledMod, link::Link};
|
||||
|
||||
/// An modded game with all plugins and files
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ModdedInstance {
|
||||
name: String,
|
||||
|
||||
#[serde(default)]
|
||||
mods: Vec<InstalledMod>,
|
||||
|
||||
#[serde(default)]
|
||||
load_order: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
game_file_overrides: Vec<Link>,
|
||||
}
|
||||
|
||||
impl ModdedInstance {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_owned(),
|
||||
mods: Vec::new(),
|
||||
load_order: Vec::new(),
|
||||
game_file_overrides: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadWriteError> {
|
||||
trace!(
|
||||
"Loading ModdedInstance from file: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
);
|
||||
|
||||
let data = read_to_string(path)?;
|
||||
let config = toml::from_str(&data)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn save_to_file(&self, path: impl AsRef<Path>) -> Result<(), ConfigReadWriteError> {
|
||||
trace!(
|
||||
"Saving ModdedInstance to: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
);
|
||||
|
||||
let content = toml::to_string_pretty(self)?;
|
||||
let mut file = fs::File::create(path)?;
|
||||
write!(file, "{}", content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_load_order(&mut self, order: Vec<String>) {
|
||||
self.load_order = order;
|
||||
}
|
||||
|
||||
pub fn load_order(&self) -> &[String] {
|
||||
&self.load_order
|
||||
}
|
||||
|
||||
pub fn game_file_overrides(&self) -> &[Link] {
|
||||
&self.game_file_overrides
|
||||
}
|
||||
|
||||
pub fn update_or_create_mod(&mut self, installed_mod: &InstalledMod) {
|
||||
match self
|
||||
.mods
|
||||
.iter_mut()
|
||||
.find(|e| e.mod_id() == installed_mod.mod_id())
|
||||
{
|
||||
Some(existing) => {
|
||||
*existing = installed_mod.to_owned();
|
||||
}
|
||||
None => {
|
||||
self.mods.push(installed_mod.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mods(&self) -> &[InstalledMod] {
|
||||
&self.mods
|
||||
}
|
||||
}
|
||||
69
src/types/root_config.rs
Normal file
69
src/types/root_config.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::{
|
||||
fs::read_to_string,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use log::trace;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::types::{ConfigReadWriteError, ModConfig, game::Game, modded_instance::ModdedInstance};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct RootConfig {
|
||||
/// Available games
|
||||
games: Vec<Game>,
|
||||
|
||||
/// Where all mods are stored
|
||||
mod_location: PathBuf,
|
||||
|
||||
#[serde(default)]
|
||||
instances: Vec<InstancePointer>,
|
||||
|
||||
/// All available mods
|
||||
#[serde(default)]
|
||||
mods: Vec<ModConfig>,
|
||||
}
|
||||
|
||||
impl RootConfig {
|
||||
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self, ConfigReadWriteError> {
|
||||
trace!(
|
||||
"Loading RootConfig from file: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
);
|
||||
|
||||
let data = read_to_string(path)?;
|
||||
let config = toml::from_str(&data)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn games(&self) -> &[Game] {
|
||||
&self.games
|
||||
}
|
||||
|
||||
pub fn get_mod_by_id(&self, id: &str) -> Option<&ModConfig> {
|
||||
self.mods.iter().find(|e| e.id() == id)
|
||||
}
|
||||
|
||||
pub fn load_instance_by_id(&self, id: &str) -> Result<ModdedInstance, ConfigReadWriteError> {
|
||||
let conf = self
|
||||
.get_instance_config(id)
|
||||
.ok_or(ConfigReadWriteError::IDNotFound)?;
|
||||
|
||||
ModdedInstance::load_from_file(&conf.path)
|
||||
}
|
||||
|
||||
pub fn get_instance_config(&self, id: &str) -> Option<&InstancePointer> {
|
||||
self.instances.iter().find(|e| e.id == id)
|
||||
}
|
||||
|
||||
pub fn mod_location(&self) -> &Path {
|
||||
&self.mod_location
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||
pub struct InstancePointer {
|
||||
pub id: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
39
src/utils.rs
39
src/utils.rs
@@ -1,31 +1,10 @@
|
||||
use std::{
|
||||
fs::{self, DirEntry},
|
||||
fs::{self},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn walk_files_recursive(
|
||||
root: impl AsRef<Path>,
|
||||
) -> std::io::Result<impl Iterator<Item = DirEntry>> {
|
||||
fn visit(dir: &Path, out: &mut Vec<DirEntry>) -> std::io::Result<()> {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_type = entry.file_type()?;
|
||||
|
||||
if file_type.is_dir() {
|
||||
visit(&path, out)?;
|
||||
} else if file_type.is_file() {
|
||||
out.push(entry);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
visit(root.as_ref(), &mut files)?;
|
||||
Ok(files.into_iter())
|
||||
}
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub fn path_to_lowercase(path: impl AsRef<Path>) -> PathBuf {
|
||||
PathBuf::from(path.as_ref().to_string_lossy().to_lowercase())
|
||||
@@ -63,3 +42,17 @@ pub fn resolve_case_insensitive(
|
||||
|
||||
Ok(Some(current))
|
||||
}
|
||||
|
||||
/// Use walkdir to walk all actual files in a dir
|
||||
/// Returns early id any error occurs
|
||||
pub fn walk_all_files(
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<impl Iterator<Item = walkdir::DirEntry>, walkdir::Error> {
|
||||
let a = WalkDir::new(path)
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>, walkdir::Error>>()?
|
||||
.into_iter()
|
||||
.filter(|entry| entry.file_type().is_file());
|
||||
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user