Skip to content

Commit

Permalink
Make CLI game name handling more consistent
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Apr 13, 2024
1 parent f2ebbae commit df5a478
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 111 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Expand Up @@ -17,6 +17,8 @@
* GUI: After performing a cloud upload preview on the other screen,
the very next backup preview wouldn't do anything.
* CLI: The `wrap` command did not fail gracefully when the game launch commands were missing.
* CLI: Several commands did not resolve aliases.
* CLI: The `cloud` commands did not reject unknown game titles.
* If a game had more data that failed to back up than succeeded,
then the backup size would be reported incorrectly.
* Changed:
Expand All @@ -37,13 +39,18 @@
* If you try to restore Windows-style paths on Linux or vice versa,
it will now produce an error,
unless you've configured an applicable redirect.
* On Windows, the way Ludusavi hides its console in GUI mode has changed,
* GUI: On Windows, the way Ludusavi hides its console in GUI mode has changed,
in order to avoid a new false positive from Windows Defender.

Instead of relaunching itself, Ludusavi now detaches the console from the current instance.
This reverts a change from v0.18.1,
but care has been taken to address the problems that originally led to that change.
If you do notice any issues related to this, please report them.
* CLI: Previously, the `restore` and `backups` (not `backup`) commands would return an error
if you specified a game that did not have any backups available to restore.
This was inconsistent with the `backup` command,
which would simply return empty data if there was nothing to back up.
Now, `restore` and `backups` will also return empty data if there are no backups.
* When synchronizing to the cloud after a backup,
Ludusavi now instructs Rclone to only check paths for games with updated saves.
This improves the cloud sync performance.
Expand Down
219 changes: 110 additions & 109 deletions src/cli.rs
Expand Up @@ -2,12 +2,7 @@ mod parse;
mod report;
mod ui;

use std::{
collections::{BTreeSet, HashMap},
fmt::Debug,
process::Command,
time::Duration,
};
use std::{collections::BTreeSet, process::Command, time::Duration};

use clap::CommandFactory;
use indicatif::{ParallelProgressIterator, ProgressBar};
Expand Down Expand Up @@ -37,52 +32,6 @@ use crate::{

const PROGRESS_BAR_REFRESH_INTERVAL: Duration = Duration::from_millis(50);

#[derive(Clone, Debug, Default)]
struct GameSubjects {
// TODO: Use BTreeSet
valid: Vec<String>,
invalid: Vec<String>,
}

impl GameSubjects {
pub fn new(known: Vec<String>, requested: Vec<String>, aliases: Option<&HashMap<String, String>>) -> Self {
let mut subjects = Self::default();

if requested.is_empty() {
subjects.valid = known;
} else {
for game in requested {
if known.contains(&game) {
subjects.valid.push(game);
} else {
subjects.invalid.push(game);
}
}
}

if let Some(aliases) = aliases.as_ref() {
subjects.resolve_aliases(aliases);
}

subjects.valid.sort();
subjects.invalid.sort();
subjects
}

fn resolve_aliases(&mut self, aliases: &HashMap<String, String>) {
let mut filtered = self.valid.iter().cloned().collect::<BTreeSet<_>>();

for (alias, target) in aliases {
if filtered.contains(alias) {
filtered.remove(alias);
filtered.insert(target.to_string());
}
}

self.valid = filtered.into_iter().collect();
}
}

fn warn_backup_deprecations(merge: bool, no_merge: bool, update: bool, try_update: bool) {
if merge {
eprintln!("WARNING: `--merge` is deprecated. Merging is now always enforced.");
Expand Down Expand Up @@ -115,15 +64,15 @@ fn load_manifest(
try_manifest_update: bool,
) -> Result<Manifest, Error> {
if no_manifest_update {
Ok(Manifest::load().unwrap_or_default())
Ok(Manifest::load().unwrap_or_default().with_extensions(config))
} else if try_manifest_update {
if let Err(e) = Manifest::update_mut(config, cache, false) {
eprintln!("{}", TRANSLATOR.handle_error(&e));
}
Ok(Manifest::load().unwrap_or_default())
Ok(Manifest::load().unwrap_or_default().with_extensions(config))
} else {
Manifest::update_mut(config, cache, false)?;
Manifest::load()
Manifest::load().map(|x| x.with_extensions(config))
}
}

Expand All @@ -144,6 +93,36 @@ fn parse_games(games: Vec<String>) -> Vec<String> {
}
}

pub fn evaluate_games(
default: BTreeSet<String>,
requested: Vec<String>,
title_finder: &TitleFinder,
) -> Result<Vec<String>, Vec<String>> {
if requested.is_empty() {
return Ok(default.into_iter().collect());
}

let mut valid = BTreeSet::new();
let mut invalid = BTreeSet::new();

for game in requested {
match title_finder.find_one_primary_or_alias(&game) {
Some(found) => {
valid.insert(found);
}
None => {
invalid.insert(game);
}
}
}

if !invalid.is_empty() {
return Err(invalid.into_iter().collect());
}

Ok(valid.into_iter().collect())
}

pub fn parse() -> Cli {
use clap::Parser;
Cli::parse()
Expand Down Expand Up @@ -188,7 +167,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)

let mut reporter = if api { Reporter::json() } else { Reporter::standard() };

let mut manifest = load_manifest(&config, &mut cache, no_manifest_update, try_manifest_update)?;
let manifest = load_manifest(&config, &mut cache, no_manifest_update, try_manifest_update)?;

let backup_dir = match path {
None => config.backup.path.clone(),
Expand All @@ -211,18 +190,6 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
prepare_backup_target(&backup_dir)?;
}

manifest.incorporate_extensions(&config);

let games_specified = !games.is_empty();
let subjects = GameSubjects::new(manifest.0.keys().cloned().collect(), games, Some(&manifest.aliases()));
if !subjects.invalid.is_empty() {
reporter.trip_unknown_games(subjects.invalid.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames {
games: subjects.invalid,
});
}

let mut retention = config.backup.retention.clone();
if let Some(full_limit) = full_limit {
retention.full = full_limit;
Expand All @@ -233,7 +200,18 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)

let layout = BackupLayout::new(backup_dir.clone(), retention);
let title_finder = TitleFinder::new(&manifest, &layout);
let launchers = Launchers::scan(&roots, &manifest, &subjects.valid, &title_finder, None);

let games_specified = !games.is_empty();
let games = match evaluate_games(manifest.primary_titles(), games, &title_finder) {
Ok(games) => games,
Err(games) => {
reporter.trip_unknown_games(games.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames { games });
}
};

let launchers = Launchers::scan(&roots, &manifest, &games, &title_finder, None);
let filter = config.backup.filter.clone();
let toggled_paths = config.backup.toggled_paths.clone();
let toggled_registry = config.backup.toggled_registry.clone();
Expand All @@ -254,7 +232,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
&config.cloud.path,
SyncDirection::Upload,
Finality::Preview,
if games_specified { &subjects.valid } else { &[] },
if games_specified { &games } else { &[] },
);
match changes {
Ok(changes) => {
Expand All @@ -270,15 +248,14 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
}
}

log::info!("beginning backup with {} steps", subjects.valid.len());
log::info!("beginning backup with {} steps", games.len());

let mut info: Vec<_> = subjects
.valid
let mut info: Vec<_> = games
.par_iter()
.enumerate()
.progress_with(scan_progress_bar(subjects.valid.len() as u64))
.progress_with(scan_progress_bar(games.len() as u64))
.filter_map(|(i, name)| {
log::trace!("step {i} / {}: {name}", subjects.valid.len());
log::trace!("step {i} / {}: {name}", games.len());
let game = &manifest.0[name];

let previous = layout.latest_backup(name, false, &config.redirects, &config.restore.toggled_paths);
Expand Down Expand Up @@ -420,22 +397,23 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)

let layout = BackupLayout::new(restore_dir.clone(), config.backup.retention.clone());

let restorable_names = layout.restorable_games();

if backup.is_some() && games.len() != 1 {
return Err(Error::CliBackupIdWithMultipleGames);
}
let backup_id = backup.as_ref().map(|x| BackupId::Named(x.clone()));

let manifest = load_manifest(&config, &mut cache, true, false).unwrap_or_default();
let title_finder = TitleFinder::new(&manifest, &layout);

let games_specified = !games.is_empty();
let subjects = GameSubjects::new(restorable_names, games, None);
if !subjects.invalid.is_empty() {
reporter.trip_unknown_games(subjects.invalid.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames {
games: subjects.invalid,
});
}
let games = match evaluate_games(layout.restorable_game_set(), games, &title_finder) {
Ok(games) => games,
Err(games) => {
reporter.trip_unknown_games(games.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames { games });
}
};

let cloud_sync = negatable_flag(
cloud_sync && !preview,
Expand All @@ -451,7 +429,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
&config.cloud.path,
SyncDirection::Upload,
Finality::Preview,
if games_specified { &subjects.valid } else { &[] },
if games_specified { &games } else { &[] },
);
match changes {
Ok(changes) => {
Expand All @@ -465,15 +443,14 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
}
}

log::info!("beginning restore with {} steps", subjects.valid.len());
log::info!("beginning restore with {} steps", games.len());

let mut info: Vec<_> = subjects
.valid
let mut info: Vec<_> = games
.par_iter()
.enumerate()
.progress_with(scan_progress_bar(subjects.valid.len() as u64))
.progress_with(scan_progress_bar(games.len() as u64))
.filter_map(|(i, name)| {
log::trace!("step {i} / {}: {name}", subjects.valid.len());
log::trace!("step {i} / {}: {name}", games.len());
let mut layout = layout.game_layout(name);
let scan_info = layout.scan_for_restoration(
name,
Expand Down Expand Up @@ -583,22 +560,21 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
};

let layout = BackupLayout::new(restore_dir.clone(), config.backup.retention.clone());
let manifest = load_manifest(&config, &mut cache, true, false).unwrap_or_default();
let title_finder = TitleFinder::new(&manifest, &layout);

let restorable_names = layout.restorable_games();

let subjects = GameSubjects::new(restorable_names, games, None);
if !subjects.invalid.is_empty() {
reporter.trip_unknown_games(subjects.invalid.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames {
games: subjects.invalid,
});
}
let games = match evaluate_games(layout.restorable_game_set(), games, &title_finder) {
Ok(games) => games,
Err(games) => {
reporter.trip_unknown_games(games.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames { games });
}
};

let info: Vec<_> = subjects
.valid
let info: Vec<_> = games
.par_iter()
.progress_count(subjects.valid.len() as u64)
.progress_count(games.len() as u64)
.map(|name| {
let mut layout = layout.game_layout(name);
let backups = layout.get_backups();
Expand Down Expand Up @@ -629,9 +605,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
let mut reporter = if api { Reporter::json() } else { Reporter::standard() };
reporter.suppress_overall();

let mut manifest = load_manifest(&config, &mut cache, no_manifest_update, try_manifest_update)?;

manifest.incorporate_extensions(&config);
let manifest = load_manifest(&config, &mut cache, no_manifest_update, try_manifest_update)?;

let restore_dir = match path {
None => config.restore.path.clone(),
Expand Down Expand Up @@ -662,8 +636,7 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
}
Subcommand::Manifest { sub: manifest_sub } => match manifest_sub {
ManifestSubcommand::Show { api } => {
let mut manifest = Manifest::load().unwrap_or_default();
manifest.incorporate_extensions(&config);
let manifest = load_manifest(&config, &mut cache, true, false).unwrap_or_default();

if api {
println!("{}", serde_json::to_string(&manifest).unwrap());
Expand Down Expand Up @@ -784,6 +757,20 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
let finality = if preview { Finality::Preview } else { Finality::Final };
let direction = SyncDirection::Upload;

let layout = BackupLayout::new(config.restore.path.clone(), config.backup.retention.clone());
let manifest = load_manifest(&config, &mut cache, true, false).unwrap_or_default();
let title_finder = TitleFinder::new(&manifest, &layout);

let games = match evaluate_games(layout.restorable_game_set(), games, &title_finder) {
Ok(games) => games,
Err(games) => {
let mut reporter = if api { Reporter::json() } else { Reporter::standard() };
reporter.trip_unknown_games(games.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames { games });
}
};

if !ask(
TRANSLATOR.confirm_cloud_upload(&local.render(), &cloud),
finality,
Expand Down Expand Up @@ -811,6 +798,20 @@ pub fn run(sub: Subcommand, no_manifest_update: bool, try_manifest_update: bool)
let finality = if preview { Finality::Preview } else { Finality::Final };
let direction = SyncDirection::Download;

let layout = BackupLayout::new(config.restore.path.clone(), config.backup.retention.clone());
let manifest = load_manifest(&config, &mut cache, true, false).unwrap_or_default();
let title_finder = TitleFinder::new(&manifest, &layout);

let games = match evaluate_games(layout.restorable_game_set(), games, &title_finder) {
Ok(games) => games,
Err(games) => {
let mut reporter = if api { Reporter::json() } else { Reporter::standard() };
reporter.trip_unknown_games(games.clone());
reporter.print_failure();
return Err(Error::CliUnrecognizedGames { games });
}
};

if !ask(
TRANSLATOR.confirm_cloud_download(&local.render(), &cloud),
finality,
Expand Down

0 comments on commit df5a478

Please sign in to comment.