Added safety checks for the destination
* Dev size / avail space checks * Permission checks * No mount option checks (yet?)
This commit is contained in:
parent
88c28a3f25
commit
f2c557f77c
1 changed files with 81 additions and 9 deletions
|
|
@ -5,6 +5,7 @@ import pathlib
|
||||||
import psutil
|
import psutil
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
import stat
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from functions.common import *
|
from functions.common import *
|
||||||
|
|
@ -12,6 +13,7 @@ from functions.data import *
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
# STATIC VARIABLES
|
# STATIC VARIABLES
|
||||||
|
AUTHORIZED_DEST_FSTYPES = ['ext3', 'ext4', 'xfs']
|
||||||
AUTO_NEXT_PASS_1_THRESHOLD = 85
|
AUTO_NEXT_PASS_1_THRESHOLD = 85
|
||||||
AUTO_NEXT_PASS_2_THRESHOLD = 98
|
AUTO_NEXT_PASS_2_THRESHOLD = 98
|
||||||
DDRESCUE_SETTINGS = {
|
DDRESCUE_SETTINGS = {
|
||||||
|
|
@ -33,7 +35,8 @@ USAGE = """ {script_name} clone [source [destination]]
|
||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
def abort_ddrescue_tui():
|
def abort_ddrescue_tui():
|
||||||
run_program(['losetup', '-D'])
|
# TODO uncomment line below
|
||||||
|
# run_program(['losetup', '-D'])
|
||||||
abort()
|
abort()
|
||||||
|
|
||||||
def build_outer_panes(source, dest):
|
def build_outer_panes(source, dest):
|
||||||
|
|
@ -66,6 +69,71 @@ def build_outer_panes(source, dest):
|
||||||
'watch', '--color', '--no-title', '--interval', '1',
|
'watch', '--color', '--no-title', '--interval', '1',
|
||||||
'cat', source['Progress Out'])
|
'cat', source['Progress Out'])
|
||||||
|
|
||||||
|
def dest_safety_check(source, dest):
|
||||||
|
"""Verify the destination is appropriate for the source."""
|
||||||
|
source_size = source['Details']['size']
|
||||||
|
if dest['Is Dir']:
|
||||||
|
cmd = ['findmnt', '-D', '-J',
|
||||||
|
'-T', dest['Path']]
|
||||||
|
result = run_program(cmd)
|
||||||
|
try:
|
||||||
|
json_data = json.loads(result.stdout.decode())
|
||||||
|
except Exception:
|
||||||
|
# Welp, let's abort
|
||||||
|
print_error('Failed to verify destination usability.')
|
||||||
|
abort_ddrescue_tui()
|
||||||
|
else:
|
||||||
|
dest_size = json_data['filesystems'][0]['avail']
|
||||||
|
dest['Free Space'] = dest_size
|
||||||
|
dest['Filesystem'] = json_data['filesystems'][0]['fstype']
|
||||||
|
else:
|
||||||
|
dest_size = dest['Details']['size']
|
||||||
|
|
||||||
|
# Fix strings before converting to bytes
|
||||||
|
source_size = re.sub(
|
||||||
|
r'(\d+\.?\d*)\s*([KMGTB])B?', r'\1 \2B', source_size.upper())
|
||||||
|
dest_size = re.sub(
|
||||||
|
r'(\d+\.?\d*)\s*([KMGTB])B?', r'\1 \2B', dest_size.upper())
|
||||||
|
|
||||||
|
# Convert to bytes and compare size
|
||||||
|
source_size = convert_to_bytes(source_size)
|
||||||
|
dest_size = convert_to_bytes(dest_size)
|
||||||
|
if source['Type'] == 'Image' and dest_size < (source_size * 1.2):
|
||||||
|
# Imaging: ensure 120% of source size is available
|
||||||
|
print_error(
|
||||||
|
'Not enough free space on destination, refusing to continue.')
|
||||||
|
print_standard(' Dest {d_size} < Required {s_size}'.format(
|
||||||
|
d_size = human_readable_size(dest_size),
|
||||||
|
s_size = human_readable_size(source_size * 1.2)))
|
||||||
|
abort_ddrescue_tui()
|
||||||
|
elif source['Type'] == 'Clone' and source_size > dest_size:
|
||||||
|
# Cloning: ensure dest >= size
|
||||||
|
print_error('Destination is too small, refusing to continue.')
|
||||||
|
print_standard(' Dest {d_size} < Source {s_size}'.format(
|
||||||
|
d_size = human_readable_size(dest_size),
|
||||||
|
s_size = human_readable_size(source_size)))
|
||||||
|
abort_ddrescue_tui()
|
||||||
|
|
||||||
|
# Filesystem checks
|
||||||
|
if source['Type'] == 'Image':
|
||||||
|
# Filesystem Type
|
||||||
|
if dest['Filesystem'] not in AUTHORIZED_DEST_FSTYPES:
|
||||||
|
print_error(
|
||||||
|
'Destination filesystem "{}" is not a recommended type.'.format(
|
||||||
|
dest['Filesystem']))
|
||||||
|
if not ask('Proceed anyways? (strongly discouraged by author)'):
|
||||||
|
abort_ddrescue_tui()
|
||||||
|
# Read-Write access
|
||||||
|
## Note: only checks path permissions, not mount options
|
||||||
|
## if the FS is RO then ddrescue will fail later
|
||||||
|
dest_ok = True
|
||||||
|
dest_st_mode = os.stat(dest['Path']).st_mode
|
||||||
|
dest_ok = dest_ok and dest_st_mode & stat.S_IRUSR
|
||||||
|
dest_ok = dest_ok and dest_st_mode & stat.S_IWUSR
|
||||||
|
dest_ok = dest_ok and dest_st_mode & stat.S_IXUSR
|
||||||
|
if not dest_ok:
|
||||||
|
print_error('Destination is not writable, refusing to continue.')
|
||||||
|
abort_ddrescue_tui()
|
||||||
|
|
||||||
def get_device_details(dev_path):
|
def get_device_details(dev_path):
|
||||||
"""Get device details via lsblk, returns JSON dict."""
|
"""Get device details via lsblk, returns JSON dict."""
|
||||||
|
|
@ -180,6 +248,7 @@ def menu_clone(source_path, dest_path):
|
||||||
source['Type'] = 'Clone'
|
source['Type'] = 'Clone'
|
||||||
dest = select_device('destination', dest_path,
|
dest = select_device('destination', dest_path,
|
||||||
skip_device = source['Details'], allow_image_file = False)
|
skip_device = source['Details'], allow_image_file = False)
|
||||||
|
dest_safety_check(source, dest)
|
||||||
|
|
||||||
# Show selection details
|
# Show selection details
|
||||||
show_selection_details(source, dest)
|
show_selection_details(source, dest)
|
||||||
|
|
@ -237,6 +306,7 @@ def menu_image(source_path, dest_path):
|
||||||
source['Pass 3'] = {'Status': 'Pending', 'Done': False}
|
source['Pass 3'] = {'Status': 'Pending', 'Done': False}
|
||||||
source['Type'] = 'Image'
|
source['Type'] = 'Image'
|
||||||
dest = select_dest_path(dest_path, skip_device=source['Details'])
|
dest = select_dest_path(dest_path, skip_device=source['Details'])
|
||||||
|
dest_safety_check(source, dest)
|
||||||
|
|
||||||
# Show selection details
|
# Show selection details
|
||||||
show_selection_details(source, dest)
|
show_selection_details(source, dest)
|
||||||
|
|
@ -352,10 +422,6 @@ def menu_main(source):
|
||||||
elif current_pass == 'Pass 2' and recovered > 98:
|
elif current_pass == 'Pass 2' and recovered > 98:
|
||||||
auto_run = True
|
auto_run = True
|
||||||
# Update current pass for next iteration
|
# Update current pass for next iteration
|
||||||
print_info('State:')
|
|
||||||
print_standard(' Pass #: {}\n Auto: {}\n Recovered: {}'.format(
|
|
||||||
current_pass, auto_run, recovered))
|
|
||||||
pause()
|
|
||||||
current_pass = source['Current Pass']
|
current_pass = source['Current Pass']
|
||||||
|
|
||||||
elif selection == 'C':
|
elif selection == 'C':
|
||||||
|
|
@ -652,7 +718,8 @@ def run_ddrescue(source, settings):
|
||||||
try:
|
try:
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print_info('Current dev: {}'.format(dev['Dev Path']))
|
print_info('Current dev: {}'.format(dev['Dev Path']))
|
||||||
ddrescue_proc = popen_program(['./__choose_exit', *settings])
|
#ddrescue_proc = popen_program(['./__choose_exit', *settings])
|
||||||
|
ddrescue_proc = popen_program(['./__exit_ok', *settings])
|
||||||
ddrescue_proc.wait()
|
ddrescue_proc.wait()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
# Catch user abort
|
# Catch user abort
|
||||||
|
|
@ -715,7 +782,7 @@ def run_ddrescue(source, settings):
|
||||||
#update_progress(source)
|
#update_progress(source)
|
||||||
|
|
||||||
def select_dest_path(provided_path=None, skip_device={}):
|
def select_dest_path(provided_path=None, skip_device={}):
|
||||||
dest = {}
|
dest = {'Is Dir': True, 'Is Image': False}
|
||||||
|
|
||||||
# Set path
|
# Set path
|
||||||
if provided_path:
|
if provided_path:
|
||||||
|
|
@ -754,7 +821,7 @@ def select_dest_path(provided_path=None, skip_device={}):
|
||||||
def select_device(description='device', provided_path=None,
|
def select_device(description='device', provided_path=None,
|
||||||
skip_device={}, allow_image_file=True):
|
skip_device={}, allow_image_file=True):
|
||||||
"""Select device via provided path or menu, return dev as dict."""
|
"""Select device via provided path or menu, return dev as dict."""
|
||||||
dev = {'Is Image': False}
|
dev = {'Is Dir': False, 'Is Image': False}
|
||||||
|
|
||||||
# Set path
|
# Set path
|
||||||
if provided_path:
|
if provided_path:
|
||||||
|
|
@ -777,6 +844,8 @@ def select_device(description='device', provided_path=None,
|
||||||
|
|
||||||
# Get device details
|
# Get device details
|
||||||
dev['Details'] = get_device_details(dev['Dev Path'])
|
dev['Details'] = get_device_details(dev['Dev Path'])
|
||||||
|
if 'Children' not in dev:
|
||||||
|
dev['Children'] = {}
|
||||||
|
|
||||||
# Check for parent device(s)
|
# Check for parent device(s)
|
||||||
while dev['Details']['pkname']:
|
while dev['Details']['pkname']:
|
||||||
|
|
@ -877,6 +946,9 @@ def show_selection_details(source, dest):
|
||||||
else:
|
else:
|
||||||
print_success('Destination path')
|
print_success('Destination path')
|
||||||
print_standard(dest['Path'])
|
print_standard(dest['Path'])
|
||||||
|
print_info('{:<8}{}'.format('FREE', 'FSTYPE'))
|
||||||
|
print_standard('{:<8}{}'.format(
|
||||||
|
dest['Free Space'], dest['Filesystem']))
|
||||||
print_standard(' ')
|
print_standard(' ')
|
||||||
|
|
||||||
def show_usage(script_name):
|
def show_usage(script_name):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue