Skip to content

Commit

Permalink
Rewrites Rescuezilla in Python3 from Perl
Browse files Browse the repository at this point in the history
Rewrites the Rescuezilla frontend in the Python3 programming language (#111),
adding the ability to backup and restore hard drives in Clonezilla image
format, among many other major including the ability to restore partitions
individually. (#46)

For time efficiency and maximum flexibility around refactoring, this rewrite
was completed as a single monolithic commit.

A brief summary of the key change:

* Added Clonezilla support (#4, #44)

* Added ability to restore individual partitions, and optional ability to not
  overwrite partition table (#46)

* Refactored the frontend to ensure information moves through the Rescuezilla
  wizard in a more modular fashion. (#6, #48, #49, #95, #96)

* Ports Glade GTKBuilder XML file from GTK2 to GTK3

* Updates French/German/Spanish translations (#67)

* Improves exit code handling and error messages (#29)

Some images have been imported and modified from external sources [1] [2] [3]

[1] Clonezilla icon: https://clonezilla.org/screenshots/?op=show&filepath=./album//01_DRBL-Clonezilla-logo/clonezilla_logo1.jpg

[2] Cropped and scaled old 1.0.4:src/livecd/image/isolinux/bg_redo.png

[3] warning icon taken from: https://publicdomainvectors.org/en/free-clipart/Warning-vector-symbol/78747.html
  • Loading branch information
shasheene committed Oct 14, 2020
1 parent 5e0d3cd commit 45161a3
Show file tree
Hide file tree
Showing 66 changed files with 11,978 additions and 5,233 deletions.
5 changes: 3 additions & 2 deletions chroot.steps.part.1.sh
Expand Up @@ -139,6 +139,7 @@ common_pkgs=("discover"
"baobab"
"gsettings-desktop-schemas"
"gparted"
"mdadm"
"lshw-gtk"
"testdisk"
"gddrescue"
Expand Down Expand Up @@ -214,8 +215,8 @@ cat << EOF > /tmp/gparted.rescuezilla.check.sh
#
# Cannot launch GParted if Rescuezilla is running.
#
if test "z\`ps -e | grep rescuezillapl\`" != "z"; then
MESSAGE="Cannot launch GParted becuase the process rescuezillapl is running.\n\nClose Rescuezilla then try again."
if test "z\`ps -e | grep rescuezillapy\`" != "z"; then
MESSAGE="Cannot launch GParted because the process rescuezillapy is running.\n\nClose Rescuezilla then try again."
printf "\$MESSAGE"
yad --center --width 300 --title="\$TITLE." --button="OK:0" --text "\$MESSAGE"
exit 1
Expand Down
2 changes: 1 addition & 1 deletion src/apps/rescuezilla/debian/control
Expand Up @@ -10,7 +10,7 @@ Vcs-Git: https://github.com/rescuezilla/rescuezilla.git

Package: rescuezilla
Architecture: all
Depends: ${shlibs:Depends}, ${misc:Depends}, smbclient, fsarchiver, partclone, libcapture-tiny-perl, libfile-tee-perl, libglib-perl, libgtk2-perl, libxml-simple-perl, libsys-cpu-perl, liblocale-maketext-lexicon-perl, libmethod-signatures-simple-perl, libstring-shellquote-perl, pigz, nmap, os-prober, gvfs, cifs-utils, smbclient, fsarchiver, file, libnotify-bin, yad
Depends: ${shlibs:Depends}, ${misc:Depends}, smbclient, fsarchiver, partclone, partimage, pv, pigz, nmap, os-prober, gvfs, cifs-utils, smbclient, fsarchiver, file, libnotify-bin, lshw, pciutils, yad, ecryptfs-utils, hdparm, pbzip2, lzop, pixz, plzip, lrzip, zstd, python3-hurry.filesize, python3-whichcraft, python3-babel
Description: Graphical hard drive backup and restore
Rescuezilla is an extremely easy-to-use graphical environment for hard drive
backup and restore. For many people, the alternative open-source tools
Expand Down
Empty file.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

@@ -0,0 +1,168 @@
# ----------------------------------------------------------------------
# Copyright (C) 2012 RedoBackup.org
# Copyright (C) 2003-2020 Steven Shiau <steven _at_ clonezilla org>
# Copyright (C) 2019-2020 Rescuezilla.com <rescuezilla@gmail.com>
# ----------------------------------------------------------------------
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------
import os
import threading
import traceback
from os.path import join, isfile, isdir


import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GdkPixbuf, GLib

from parser.clonezilla_image import ClonezillaImage
from parser.redobackup_legacy_image import RedoBackupLegacyImage
from utility import PleaseWaitModalPopup, ErrorMessageModalPopup, _
from wizard_state import MOUNT_DIR


class ImageFolderQuery:
def __init__(self, builder, image_list_store):
self.image_dict = {}
# Relying on CPython GIL to communicate between threads.
self.failed_to_read_image_dict = {}
self.builder = builder
self.image_list_store = image_list_store
self.win = self.builder.get_object("main_window")
self.icon_pixbufs = {
"RESCUEZILLA_1.5_FORMAT": self.builder.get_object("rescuezilla_icon").get_pixbuf().scale_simple(32, 32,
GdkPixbuf.InterpType.BILINEAR),
"REDOBACKUP_0.9.8_1.0.4_FORMAT": self.builder.get_object("redobackup_icon").get_pixbuf().scale_simple(32,
32,
GdkPixbuf.InterpType.BILINEAR),
"CLONEZILLA_FORMAT": self.builder.get_object("clonezilla_icon").get_pixbuf().scale_simple(32, 32,
GdkPixbuf.InterpType.BILINEAR),
"warning": self.builder.get_object("warning_icon").get_pixbuf().scale_simple(32, 32,
GdkPixbuf.InterpType.BILINEAR)
}
self.backup_label = self.builder.get_object("backup_folder_label")
self.restore_label = self.builder.get_object("restore_folder_label")
self.query_path = MOUNT_DIR

def query_folder(self, path):
self.query_path = path
self.image_list_store.clear()
print("Starting scan of provided path " + self.query_path)
self.backup_label.set_text(self.query_path)
self.restore_label.set_text(self.query_path)
self.image_list_store.clear()
self.failed_to_read_image_dict.clear()
self.win.set_sensitive(False)
self.please_wait_popup = PleaseWaitModalPopup(self.builder, title=_("Please wait..."), message=_("Scanning folder for backup images..."))
self.please_wait_popup.show()
thread = threading.Thread(target=self.scan_image_directory)
thread.daemon = True
thread.start()

def _populate_image_list_table(self):
print("Populating image list table. Image dict is length: " + str(len(self.image_dict)))
self.image_list_store.clear()
traceback_messages = ""
for key in self.image_dict.keys():
try:
image = self.image_dict[key]
format = image.image_format
if len(image.warning_dict.keys()) > 0:
warning_icon = self.icon_pixbufs['warning']
else:
warning_icon = None

self.image_list_store.append([key,
format,
warning_icon,
self.icon_pixbufs[format],
image.enduser_filename,
image.enduser_readable_size,
str(image.last_modified_timestamp),
image.get_enduser_friendly_partition_description()
])
except Exception as e:
tb = traceback.format_exc()
traceback_messages += tb + "\n\n"

# Highlight first image if there is only 1 image.
if len(self.image_dict.keys()) == 1:
self.builder.get_object("restore_partition_selection_treeselection").select_path(0)

if len(self.failed_to_read_image_dict.keys()) > 0:
for key in self.failed_to_read_image_dict.keys():
traceback_messages += key + ": " + self.failed_to_read_image_dict[key] + "\n\n"

if len(traceback_messages) > 0:
ErrorMessageModalPopup(self.builder,
_("Error processing the following images:") + "\n\n" + str(traceback_messages))
self.please_wait_popup.destroy()

def scan_file(self, absolute_path, filename, enduser_filename):
print("Scan file " + absolute_path)
try:
image = None
if isfile(absolute_path):
# Identify Clonezilla images by presence of a file named "parts". Cannot use "clonezilla-img" or
# "dev-fs.list" because these files were not created by in earlier Clonezilla versions. Cannot use
# "disk" as Clonezilla's 'saveparts' function does not create it. But both 'savedisk' and 'saveparts'
# always creates a file named 'parts' across every version of Clonezilla tested.
if absolute_path.endswith("parts"):
print("Found Clonezilla image " + filename)
image = ClonezillaImage(absolute_path, enduser_filename)
image_warning_message = ""
for short_partition_key in image.warning_dict.keys():
image_warning_message += " " + short_partition_key + ": " + image.warning_dict[short_partition_key] + "\n"
if len(image_warning_message) > 0:
self.failed_to_read_image_dict[
enduser_filename] = _("Unable to fully process the image associated with the following partitions:") + "\n" + image_warning_message + _("This can happen when loading images which Clonezilla was unable to completely backup. Any other filesystems within the image should be restorable as normal.")
elif absolute_path.endswith(".backup"):
print("Found a Rescuezilla image " + filename)
# It is a Rescuezilla v1.0.5 or Redo Backup and Recovery
image = RedoBackupLegacyImage(absolute_path, enduser_filename, filename)
if len(image.warning_dict.keys()) > 0:
self.failed_to_read_image_dict[absolute_path] = _("Unable to fully process the following image:") + "\n" + image.warning_dict[image.absolute_path]
if image is not None:
self.image_dict[image.absolute_path] = image
except Exception as e:
print("Failed to read: " + absolute_path)
tb = traceback.format_exc()
self.failed_to_read_image_dict[enduser_filename] = tb
traceback.print_exc()

def scan_image_directory(self):
self.image_dict.clear()
self.failed_to_read_image_dict.clear()
try:
# list files and directories
for filename in os.listdir(self.query_path):
abs_base_scan_path = os.path.abspath(join(self.query_path, filename))
print("Scanning " + abs_base_scan_path)
if isfile(abs_base_scan_path):
print("Scanning file " + abs_base_scan_path)
self.scan_file(abs_base_scan_path, filename, filename)
elif isdir(abs_base_scan_path):
# List the subdirectory (1 level deep)
for subdir_filename in os.listdir(abs_base_scan_path):
absolute_path = join(abs_base_scan_path, subdir_filename)
enduser_filename = os.path.join(filename, subdir_filename)
if isfile(absolute_path):
print("Scanning subdir file " + absolute_path)
self.scan_file(absolute_path, subdir_filename, enduser_filename)
except Exception as e:
tb = traceback.format_exc()
GLib.idle_add(ErrorMessageModalPopup.display_nonfatal_warning_message, self.builder,
"Failed to scan for images: " + tb)
# Relying on CPython GIL to access the self.image_list
GLib.idle_add(self._populate_image_list_table)
@@ -0,0 +1,64 @@
# ----------------------------------------------------------------------
# Copyright (C) 2019-2020 Rescuezilla.com <rescuezilla@gmail.com>
# ----------------------------------------------------------------------
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------
import os
import sys
import threading

class Logger:
# Logger to help debugging.
#
# Class based on answer: https://stackoverflow.com/a/24583265/4745097
def __init__(self, output_filepath):
# Threading note: This class is expected to be called on both GTK event thread
# and other threads. Whilst I believe stdout and file write operations have threading
# guarantees internally, a threading.lock is used just in case this is not true.
#
# TODO: Evaluate whether this threading.Lock() is required.
self.logger_lock = threading.Lock()
self.stdout = sys.stdout
self.file = open(output_filepath, "a", 10)
sys.stdout = self

def __del__(self):
self.close()

def __exit__(self, *args):
self.close()

def write(self, message):
with self.logger_lock:
self.stdout.write(message)
try:
self.file.write(message)
except:
print("Could not write log message to file: " + message)

def close(self):
with self.logger_lock:
if self.stdout is not None:
sys.stdout = self.stdout
self.stdout = None

if self.file is not None:
self.file.close()
self.file = None

def flush(self):
with self.logger_lock:
self.stdout.flush()
self.file.flush()
os.fsync(self.file.fileno())
@@ -0,0 +1,74 @@
# ----------------------------------------------------------------------
# Copyright (C) 2012 RedoBackup.org
# Copyright (C) 2019-2020 Rescuezilla.com <rescuezilla@gmail.com>
# ----------------------------------------------------------------------
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------
import os
import threading
import traceback

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import GLib

from utility import PleaseWaitModalPopup, Utility, _


class MountLocalPath:
def __init__(self, builder, callback, source_path, destination_path):
self.destination_path = destination_path

self.source_path = source_path
self.destination_path = destination_path

self.callback = callback
self.please_wait_popup = PleaseWaitModalPopup(builder, title=_("Please wait..."), message=_("Mounting..."))
self.please_wait_popup.show()
thread = threading.Thread(target=self._do_mount_command, args=(source_path, destination_path, ))
thread.daemon = True
thread.start()

def _do_mount_command(self, source_path, destination_path):
try:
if not os.path.exists(destination_path) and not os.path.isdir(destination_path):
os.mkdir(destination_path, 0o755)

is_unmounted, message = Utility.umount_warn_on_busy(destination_path)
if not is_unmounted:
GLib.idle_add(self.callback, False, message)
GLib.idle_add(self.please_wait_popup.destroy)
return

is_unmounted, message = Utility.umount_warn_on_busy(source_path)
if not is_unmounted:
GLib.idle_add(self.please_wait_popup.destroy)
GLib.idle_add(self.please_wait_popup.destroy)
return

mount_cmd_list = ['mount', source_path, destination_path]
process, flat_command_string, failed_message = Utility.run("Mounting selected partition: ", mount_cmd_list, use_c_locale=False)
if process.returncode != 0:
GLib.idle_add(self.callback, False, failed_message)
GLib.idle_add(self.please_wait_popup.destroy)
return
else:
GLib.idle_add(self.callback, True, "", destination_path)
GLib.idle_add(self.please_wait_popup.destroy)
except Exception as e:
tb = traceback.format_exc()
print(tb)
GLib.idle_add(self.callback, False, "Error mounting folder: " + tb)
GLib.idle_add(self.please_wait_popup.destroy)

0 comments on commit 45161a3

Please sign in to comment.