Add finalization options to ddrescue-tui

Specifically to zero-fill any gaps from the clone,
to fill the destination with zeros to the end of the device,
and/or to relocate the GPT to the end of the device.
This commit is contained in:
2Shirt 2023-07-08 23:27:05 -07:00
parent a20fdf7bd3
commit 4467369811
Signed by: 2Shirt
GPG key ID: 152FAC923B0E132C

View file

@ -1531,6 +1531,12 @@ def get_etoc() -> str:
return etoc
def finalize_recovery(state: State, dry_run: bool = True) -> None:
"""Show recovery finalization options."""
zero_fill_destination(state, dry_run=dry_run)
if state.mode == 'Clone':
relocate_backup_gpt(state, dry_run=dry_run)
def get_fstype_macos(path) -> str:
"""Get fstype for path under macOS, returns str."""
fstype = 'UNKNOWN'
@ -1802,13 +1808,15 @@ def main() -> None:
# Quit
if 'Quit' in selection:
total_percent = state.get_percent_recovered()
if total_percent == 100:
break
# Recovey < 100%
cli.print_warning('Recovery is less than 100%')
if cli.ask('Are you sure you want to quit?'):
break
# Confirm exit if recovery is less than 100%
if total_percent < 100:
cli.print_warning('Recovery is less than 100%')
if not cli.ask('Are you sure you want to quit?'):
continue
finalize_recovery(state, dry_run=args['--dry-run'])
break
# Save results to log
LOG.info('')
@ -1889,6 +1897,39 @@ def mount_raw_image_macos(path) -> pathlib.Path:
return loopback_path
def relocate_backup_gpt(state: State, dry_run: bool = True) -> None:
"""Relocate backup GPT on the destination if applicable and approved."""
cmd = ['sudo', 'sfdisk', '--relocate', 'gpt-bak-std', state.destination.path]
state.destination.update_details(skip_children=False)
# Safety checks
## Breakdown of below tests:
## Only offer this option when cloning a whole, non-child device
## where the source is smaller than the destination
## and both the source and destination are using a GUID_Partition_Table
if not (
len(state.block_pairs) == 1
and str(state.destination.raw_details.get('pttype', 'Unknown')).lower() == 'gpt'
and state.source.size < state.destination.size
and not state.source.parent
and str(state.source.raw_details.get('pttype', 'Unknown')).lower() == 'gpt'
and cli.ask('Relocate backup GPT to the end of the device?')
):
LOG.warning('Refusing to attempt a backup GPT relocation.')
return
# Dry run
if dry_run:
cli.print_standard(f'Dry-run: Relocate GPT with command: {cmd}')
return
# Relocate GPT data
proc = exe.run_program(cmd, check=False)
if proc.returncode:
cli.print_error('ERROR: Failed to relocate backup GPT.')
LOG.error('sfdisk result: %s, %s', proc.stdout, proc.stderr)
def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True) -> None:
"""Run ddrescue using passed settings."""
cmd = build_ddrescue_cmd(block_pair, pass_name, settings)
@ -2163,5 +2204,68 @@ def unmount_loopback_device(path) -> None:
exe.run_program(cmd, check=False)
def zero_fill_destination(state: State, dry_run: bool = True) -> None:
"""Zero-fill any gaps and space on destination beyond the source size."""
full_disk_clone = False
larger_destination = False
percent_recovered = state.get_percent_recovered()
if state.mode == 'Clone' and len(state.block_pairs) == 1:
full_disk_clone = True
# Bail early
if not (
(percent_recovered < 100
or (full_disk_clone and state.source.size < state.destination.size))
and cli.ask('Fill gaps with zeros?')):
return
for block_pair in state.block_pairs:
destination_size = block_pair.size
if (full_disk_clone and state.source.size < state.destination.size):
destination_size = state.destination.size
larger_destination = True
# Prep zero-fill map file
zero_map_path = block_pair.map_path.with_stem(
f'{block_pair.map_path.name}_zero-fill',
)
io.copy_file(block_pair.map_path, zero_map_path, overwrite=True)
if larger_destination:
with open(zero_map_path, 'a') as f:
f.write(
f'{hex(block_pair.size)} '
f'{hex(destination_size - block_pair.size)} ?'
)
# Build cmd
cmd = [
'sudo',
'ddrescue',
'--force',
f'--size={destination_size}',
'--binary-prefixes',
'--complete-only',
'--data-preview=5',
'--odirect',
'--retry-passes=0',
f'--sector-size={block_pair.sector_size}',
'-vvvv',
'/dev/zero',
block_pair.destination,
zero_map_path,
]
# Dry run
if dry_run:
cli.print_standard(f'Zero-fill with command: {cmd}')
return
# Re-run ddrescue to zero-fill gaps
proc = exe.run_program(cmd, check=False, pipe=False)
if proc.returncode:
cli.print_error('ERROR: Failed to zero-fill: {block_pair.destination}')
LOG.error('zero-fill error: %s, %s', proc.stdout, proc.stderr)
if __name__ == '__main__':
print("This file is not meant to be called directly.")