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
|
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:
|
def get_fstype_macos(path) -> str:
|
||||||
"""Get fstype for path under macOS, returns str."""
|
"""Get fstype for path under macOS, returns str."""
|
||||||
fstype = 'UNKNOWN'
|
fstype = 'UNKNOWN'
|
||||||
|
|
@ -1802,12 +1808,14 @@ def main() -> None:
|
||||||
# Quit
|
# Quit
|
||||||
if 'Quit' in selection:
|
if 'Quit' in selection:
|
||||||
total_percent = state.get_percent_recovered()
|
total_percent = state.get_percent_recovered()
|
||||||
if total_percent == 100:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Recovey < 100%
|
# Confirm exit if recovery is less than 100%
|
||||||
|
if total_percent < 100:
|
||||||
cli.print_warning('Recovery is less than 100%')
|
cli.print_warning('Recovery is less than 100%')
|
||||||
if cli.ask('Are you sure you want to quit?'):
|
if not cli.ask('Are you sure you want to quit?'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
finalize_recovery(state, dry_run=args['--dry-run'])
|
||||||
break
|
break
|
||||||
|
|
||||||
# Save results to log
|
# Save results to log
|
||||||
|
|
@ -1889,6 +1897,39 @@ def mount_raw_image_macos(path) -> pathlib.Path:
|
||||||
return loopback_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:
|
def run_ddrescue(state, block_pair, pass_name, settings, dry_run=True) -> None:
|
||||||
"""Run ddrescue using passed settings."""
|
"""Run ddrescue using passed settings."""
|
||||||
cmd = build_ddrescue_cmd(block_pair, pass_name, 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)
|
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__':
|
if __name__ == '__main__':
|
||||||
print("This file is not meant to be called directly.")
|
print("This file is not meant to be called directly.")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue