Skip to content

Commit

Permalink
#14: Add option to merge into an existing backup directory
Browse files Browse the repository at this point in the history
  • Loading branch information
mtkennerly committed Jul 29, 2020
1 parent e28e56f commit 3761d1d
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Unreleased

* Added:
* Option to merge into an existing backup directory.
* `--api` flag in CLI mode.
* `--by-steam-id` flag in CLI mode.
* Fixed:
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ If you are on Mac:
* You can press `back up` to perform the backup for real.
* If the target folder already exists, it will be deleted first,
then recreated.
<!--
However, if you've enabled the merge option, then it will not be deleted first.
-->
* Within the target folder, for every game with data to back up,
a subfolder will be created with the game's name encoded as
[Base64](https://en.wikipedia.org/wiki/Base64).
Expand Down Expand Up @@ -260,6 +263,10 @@ Here are the available settings (all are required unless otherwise noted):
This can be overridden in the CLI with `--path`.
* `ignoredGames` (optional, array of strings): Names of games to skip when backing up.
This can be overridden in the CLI by passing a list of games.
<!--
* `merge` (optional, boolean): Whether to merge save data into the target
directory rather than deleting the directory first. Default: false.
-->
* `restore` (map):
* `path` (string): Full path to a directory from which to restore data.
This can be overridden in the CLI with `--path`.
Expand Down
54 changes: 52 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ pub enum Subcommand {
#[structopt(long)]
force: bool,

/// Merge into existing directory instead of deleting/recreating it.
/// Within the target directory, the subdirectories for individual
/// games will still be cleared out first, though.
/// When not specified, this defers to Ludusavi's config file.
#[structopt(long)]
merge: bool,

/// Don't merge; delete and recreate the target directory.
/// When not specified, this defers to Ludusavi's config file.
#[structopt(long, conflicts_with("merge"))]
no_merge: bool,

/// Download the latest copy of the manifest.
#[structopt(long)]
update: bool,
Expand Down Expand Up @@ -357,6 +369,8 @@ pub fn run_cli(sub: Subcommand) -> Result<(), Error> {
preview,
path,
force,
merge,
no_merge,
update,
by_steam_id,
api,
Expand All @@ -377,9 +391,18 @@ pub fn run_cli(sub: Subcommand) -> Result<(), Error> {
let roots = &config.roots;

if !preview {
if !force && backup_dir.exists() {
if !force && !merge && backup_dir.exists() {
return Err(crate::prelude::Error::CliBackupTargetExists { path: backup_dir });
} else if let Err(e) = prepare_backup_target(&backup_dir) {
} else if let Err(e) = prepare_backup_target(
&backup_dir,
if merge {
true
} else if no_merge {
false
} else {
config.backup.merge
},
) {
return Err(e);
}
}
Expand Down Expand Up @@ -633,6 +656,8 @@ mod tests {
preview: false,
path: None,
force: false,
merge: false,
no_merge: false,
update: false,
by_steam_id: false,
api: false,
Expand All @@ -652,6 +677,7 @@ mod tests {
"--path",
"tests/backup",
"--force",
"--merge",
"--update",
"--by-steam-id",
"--api",
Expand All @@ -663,6 +689,8 @@ mod tests {
preview: true,
path: Some(StrictPath::new(s("tests/backup"))),
force: true,
merge: true,
no_merge: false,
update: true,
by_steam_id: true,
api: true,
Expand All @@ -681,6 +709,28 @@ mod tests {
preview: false,
path: Some(StrictPath::new(s("tests/fake"))),
force: false,
merge: false,
no_merge: false,
update: false,
by_steam_id: false,
api: false,
games: vec![],
}),
},
);
}

#[test]
fn accepts_cli_backup_with_no_merge() {
check_args(
&["ludusavi", "backup", "--no-merge"],
Cli {
sub: Some(Subcommand::Backup {
preview: false,
path: None,
force: false,
merge: false,
no_merge: true,
update: false,
by_steam_id: false,
api: false,
Expand Down
19 changes: 16 additions & 3 deletions src/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ enum Message {
BackupComplete,
RestoreComplete,
EditedBackupTarget(String),
EditedBackupMerge(bool),
EditedRestoreSource(String),
EditedRoot(EditAction),
SelectedRootStore(usize, Store),
Expand Down Expand Up @@ -307,8 +308,11 @@ impl ModalComponent {
.align_items(Align::Center)
.push(Text::new(match theme {
ModalTheme::Error { variant } => translator.handle_error(variant),
ModalTheme::ConfirmBackup => translator
.modal_confirm_backup(&config.backup.path, config.backup.path.exists()),
ModalTheme::ConfirmBackup => translator.modal_confirm_backup(
&config.backup.path,
config.backup.path.exists(),
config.backup.merge,
),
ModalTheme::ConfirmRestore => {
translator.modal_confirm_restore(&config.restore.path)
}
Expand Down Expand Up @@ -1063,6 +1067,11 @@ impl BackupScreenComponent {
)
.padding(5),
)
.push(Checkbox::new(
config.backup.merge,
translator.backup_merge_label(),
Message::EditedBackupMerge,
))
.push(
Button::new(&mut self.backup_target_browse_button, Icon::FolderOpen.as_text())
.on_press(match operation {
Expand Down Expand Up @@ -1371,7 +1380,7 @@ impl Application for App {

let backup_path = &self.config.backup.path;
if !preview {
if let Err(e) = prepare_backup_target(&backup_path) {
if let Err(e) = prepare_backup_target(&backup_path, self.config.backup.merge) {
self.modal_theme = Some(ModalTheme::Error { variant: e });
return Command::none();
}
Expand Down Expand Up @@ -1613,6 +1622,10 @@ impl Application for App {
self.config.save();
Command::none()
}
Message::EditedBackupMerge(enabled) => {
self.config.backup.merge = enabled;
Command::none()
}
Message::EditedRestoreSource(text) => {
self.restore_screen.restore_source_history.push(&text);
self.config.restore.path.reset(text);
Expand Down
16 changes: 12 additions & 4 deletions src/lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,13 @@ impl Translator {
.into()
}

pub fn backup_merge_label(&self) -> String {
match self.language {
Language::English => "Merge",
}
.into()
}

pub fn restore_source_label(&self) -> String {
match self.language {
Language::English => "Restore from:",
Expand Down Expand Up @@ -425,10 +432,11 @@ impl Translator {
.into()
}

pub fn modal_confirm_backup(&self, target: &StrictPath, target_exists: bool) -> String {
match (self.language, target_exists) {
(Language::English, false) => format!("Are you sure you want to proceed with the backup? The target folder does not already exist, so it will be created: {}", target.render()),
(Language::English, true) => format!("Are you sure you want to proceed with the backup? The target folder already exists, so it will be deleted and recreated from scratch: {}", target.render()),
pub fn modal_confirm_backup(&self, target: &StrictPath, target_exists: bool, merge: bool) -> String {
match (self.language, target_exists, merge) {
(Language::English, false, _) => format!("Are you sure you want to proceed with the backup? The target folder will be created: {}", target.render()),
(Language::English, true, false) => format!("Are you sure you want to proceed with the backup? The target folder will be deleted and recreated from scratch: {}", target.render()),
(Language::English, true, true) => format!("Are you sure you want to proceed with the backup? New save data will be merged into the target folder: {}", target.render()),
}
}

Expand Down
36 changes: 30 additions & 6 deletions src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,10 +560,15 @@ fn scan_game_for_restoration(name: &str, source: &StrictPath) -> ScanInfo {
}
}

pub fn prepare_backup_target(target: &StrictPath) -> Result<(), Error> {
target
.remove()
.map_err(|_| Error::CannotPrepareBackupTarget { path: target.clone() })?;
pub fn prepare_backup_target(target: &StrictPath, merge: bool) -> Result<(), Error> {
if !merge {
target
.remove()
.map_err(|_| Error::CannotPrepareBackupTarget { path: target.clone() })?;
} else if target.exists() && !target.is_dir() {
return Err(Error::CannotPrepareBackupTarget { path: target.clone() });
}

let p = target.as_std_path_buf();
std::fs::create_dir_all(&p).map_err(|_| Error::CannotPrepareBackupTarget { path: target.clone() })?;

Expand All @@ -575,9 +580,23 @@ pub fn back_up_game(info: &ScanInfo, target: &StrictPath, name: &str) -> BackupI
#[allow(unused_mut)]
let mut failed_registry = std::collections::HashSet::new();

for file in &info.found_files {
let mut unable_to_prepare = false;
if !info.found_files.is_empty() || !info.found_registry_keys.is_empty() {
let target_game = game_backup_dir(&target, &name);
if !target_game.as_path().is_dir() && std::fs::create_dir(target_game).is_err() {
match StrictPath::from_std_path_buf(&target_game).remove() {
Ok(_) => {
if std::fs::create_dir(target_game).is_err() {
unable_to_prepare = true;
}
}
Err(_) => {
unable_to_prepare = true;
}
}
}

for file in &info.found_files {
if unable_to_prepare {
failed_files.insert(file.clone());
continue;
}
Expand All @@ -592,6 +611,11 @@ pub fn back_up_game(info: &ScanInfo, target: &StrictPath, name: &str) -> BackupI
#[cfg(target_os = "windows")]
{
for reg_path in &info.found_registry_keys {
if unable_to_prepare {
failed_registry.insert(reg_path.to_string());
continue;
}

let mut hives = crate::registry::Hives::default();
match hives.store_key_from_full_path(&reg_path) {
Err(_) => {
Expand Down

0 comments on commit 3761d1d

Please sign in to comment.