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:
parent
a20fdf7bd3
commit
4467369811
1 changed files with 110 additions and 6 deletions
|
|
@ -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.")
|
||||
|
|
|
|||
Loading…
Reference in a new issue