diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e0b1d..e3bd4f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog Backuppy +## [0.9] - 2021-05-10 +### Added +- CLI-mode: It's now possible to end the program anytime with ":q" (or ESC-key) and pressing ENTER. +- CLI-mode: Colored output on traces +- New helper script "utils.py" + +### Changed +- Code refactoring (get rid of global variables and constants, instead pass as arguments) + +## [0.8.1] - 2021-05-10 +### Fixed +- CLI-mode: Show text "Programm interrupted by user." instead of error message when Ctrl+C was pressed. +- CLI-mode: Removed obsolete trace messages + ## [0.8] - 2021-05-07 ### Added - GUI-mode: Introduce "Browse"-button on directory-selection dialogs diff --git a/install.py b/install.py index d6468be..490916b 100644 --- a/install.py +++ b/install.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ project: Backuppy -version: 0.8 +version: 0.9 file: install.py summary: python installer-script in CLI-mode """ @@ -9,114 +9,87 @@ summary: python installer-script in CLI-mode # Standard library imports import sys import os -import time # local imports -from languages import english -from languages import german +from utils import set_language, query, _print, trace # local globals -# ---------------------- -VERSION: str = "0.8" +VERSION: str = "0.9" EMAIL = "fotocoder@joschu.ch" EXCLUDE_FILE = "exclude.txt" BACKUPPY_SCRIPT = "Backuppy.sh" -# ---------------------- -MYDIR = os.getcwd() -EXCLUDE: bool = False -SHELL = os.environ.get("SHELL") -HOME = os.environ.get("HOME") -LANG_EN = "English" -LANG_DE = "German" -LANGUAGE = LANG_EN -RSYNC_CMD: str = None -def set_language(language): - global LANGUAGE - LANGUAGE = language +def main_install_cli(mydir, exclude_file): + language = query("welcome") + if not language: + return False, None, None + set_language(language) -def trace(message_txt): - """ Print a formatted message to std out. """ - print("[ OK ] " + message_txt) + _print("languagepack") -def get_lang_text(search_str: str): - global LANGUAGE - """ Returns a string from the appropriate language file. """ - return_str: str = eval("english." + search_str) - if LANGUAGE == LANG_DE: - return_str = eval("german." + search_str) - return return_str + _print("intromsg1") -def main_install_cli(): - language = input("Hello, first of all, which language do you prefer: German [DE] or English [EN]?\n> ") - if language.upper() == "DE": - set_language(LANG_DE) - print("Perfekt, nun ist das deutsche Sprachpaket aktiviert. Willkommen!\n") - else: - print("Perfect, the English language package is now activated. Welcome!.\n") - - time.sleep(1) - - print("\n" + get_lang_text("intromsg1") + "\n") - time.sleep(1) - - print("\n" + get_lang_text("intromsg2") + "\n") - time.sleep(1) + _print("intromsg2") # which Rsync options are available and which one you want to use - print(get_lang_text("rsyncopt") + "\n") - time.sleep(1) + _print("rsyncopt") # asks if you want to exclude files/directories from backup and creates an exclude file in case of Yes - exclude = input(get_lang_text("excludefile1") + "\n> ") - global EXCLUDE - if exclude.upper() in ("J", "Y"): - EXCLUDE = True - print(get_lang_text("excludefile2") + "\n") + exclude = query("excludefile1") + if not exclude: + return False, None, None + elif exclude.upper() in ("J", "Y"): + _print("excludefile2") + exclude = True else: - EXCLUDE = False - print(get_lang_text("excludefile3") + "\n") - time.sleep(1) + _print("excludefile3") + exclude = False # Asks for the source directory which should be saved - print(get_lang_text("srcdir1")) - time.sleep(1) - sourcedir = input(get_lang_text("srcdir2") + "\n> ") + _print("srcdir1") + sourcedir = query("srcdir2") + if not sourcedir: + return False, None, None - print(f"{get_lang_text('srcdir3_1')} {sourcedir} {get_lang_text('srcdir3_2')}") - time.sleep(1) + _print("srcdir3_1") + print(sourcedir) + _print("srcdir3_2") # asks for the destination directory in which the backup should be saved - targetdir = input(get_lang_text("targetdir1") + "\n> ") - print(f"{get_lang_text('targetdir2_1')} {targetdir} {get_lang_text('targetdir2_2')}") - time.sleep(1) + targetdir = query("targetdir1") + if not targetdir: + return False, None, None + + _print("targetdir2_1") + print(targetdir) + _print("targetdir2_2") # collects all the information needed to execute the rsync command and creates it. - print(get_lang_text("collect") + "\n") - time.sleep(1) - exclude_file = os.path.join(MYDIR, EXCLUDE_FILE) - - RSYNC_CMD = f"rsync -aqp --exclude-from={exclude_file} {sourcedir} {targetdir}" + _print("collect") - print(f"{RSYNC_CMD}") - time.sleep(1) + rsync_cmd = f"rsync -aqp --exclude-from={os.path.join(mydir, exclude_file)} {sourcedir} {targetdir}" + + print(f"{rsync_cmd}") # Outro - print(get_lang_text("outro1")) - time.sleep(2) - print(get_lang_text("outro2") + " " + EMAIL) + _print("outro1") - return True, EXCLUDE, RSYNC_CMD + _print("outro2") + print(EMAIL) -def create_exclude_file(directory, exclude_file): - exclude_file = os.path.join(directory, exclude_file) + return True, exclude, rsync_cmd + +def create_exclude_file(mydir, exclude_file): + exclude_file = os.path.join(mydir, exclude_file) with open(exclude_file, "w") as fExclude: trace(f"creating exclude-file '{exclude_file}'.") fExclude.write("\n") -def create_alias(shell, home_dir, directory, backuppy_script): +def create_alias(mydir, backuppy_script): + shell = os.environ.get("SHELL") + home_dir = os.environ.get("HOME") # alias entry in .bashrc or .zshrc - backuppy_script = os.path.join(directory, backuppy_script) + backuppy_script = os.path.join(mydir, backuppy_script) alias_str = f"alias backuppy='sudo {backuppy_script}'" # Check for installed ZSH @@ -148,37 +121,32 @@ def create_backuppy_script(directory, backuppy_script, rsync_cmd): os.chmod(backuppy_file, 0o777) # make file executable -def do_the_install(is_exclude: bool, rsync_cmd: str): +def do_the_install(mydir: str, exclude_file, is_exclude: bool, backuppy_script: str, rsync_cmd: str): """ Creates scripts and entries based on environment variables. """ if is_exclude: - create_exclude_file(MYDIR, EXCLUDE_FILE) + create_exclude_file(mydir, exclude_file) if rsync_cmd: - create_backuppy_script(MYDIR, BACKUPPY_SCRIPT, rsync_cmd) - create_alias(SHELL, HOME, MYDIR, BACKUPPY_SCRIPT) + create_backuppy_script(mydir, backuppy_script, rsync_cmd) + create_alias(mydir, backuppy_script) def main(argv): trace(f"Starting Backuppy install.py v{VERSION}") is_finalized = False + mydir = os.getcwd() if argv and argv[0] == "--gui": from install_gui import main_install_gui trace("Starting GUI-version.") - is_finalized, is_exclude, rsync_cmd = main_install_gui() # collect user input via GUI and store in env. variables + is_finalized, is_exclude, rsync_cmd = main_install_gui(mydir, EXCLUDE_FILE) # collect user input via GUI and store in env. variables else: trace("Starting CLI-version.\n") - is_finalized, is_exclude, rsync_cmd = main_install_cli() # collect user input via CLI and store in env. variables - if is_finalized: - print("CLI finalized.") - if is_exclude: - print("exclude is true.") - if rsync_cmd: - print("rsync command returned: " + rsync_cmd) + is_finalized, is_exclude, rsync_cmd = main_install_cli(mydir, EXCLUDE_FILE) # collect user input via CLI and store in env. variables if is_finalized: - do_the_install(is_exclude, rsync_cmd) + do_the_install(mydir, EXCLUDE_FILE, is_exclude, BACKUPPY_SCRIPT, rsync_cmd) trace("Ending Backuppy install.py") diff --git a/install_gui.py b/install_gui.py index 6a3dfae..6d9e1fd 100644 --- a/install_gui.py +++ b/install_gui.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ project: Backuppy -version: 0.8 +version: 0.9 file: install_gui.py summary: python installer-script in GUI-mode (needs PySide2) """ @@ -13,12 +13,21 @@ import os from PySide2 import QtWidgets # local imports -from install import * +from install import EXCLUDE_FILE, EMAIL +from utils import set_language, get_lang_text, LANG_EN, LANG_DE class BackuppyWizard(QtWidgets.QWizard): - def __init__(self, parent=None): + def __init__(self, parent=None, mydir=os.getcwd(), exclude_file=EXCLUDE_FILE): super().__init__(parent) + # init instance variables + self.mydir = mydir + self.exclude_file = exclude_file + self.exclude = None + self.sourcedir = None + self.targetdir = None + self.rsync_cmd = None + self.setWindowTitle(get_lang_text("intromsg1")) self.addPage(Page01(self)) @@ -38,11 +47,11 @@ class BackuppyWizard(QtWidgets.QWizard): class Page01(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) - self.label = QtWidgets.QLabel("Hello, first of all, which language do you prefer: English or German?") + self.label = QtWidgets.QLabel(get_lang_text("welcome")) self.comboBox = QtWidgets.QComboBox(self) - self.comboBox.addItem(LANG_EN, LANG_EN) - self.comboBox.addItem(LANG_DE, LANG_DE) + self.comboBox.addItem(LANG_EN) + self.comboBox.addItem(LANG_DE) layout = QtWidgets.QVBoxLayout() layout.addWidget(self.label) @@ -50,8 +59,7 @@ class Page01(QtWidgets.QWizardPage): self.setLayout(layout) def validatePage(self): - if self.comboBox.currentText() == LANG_DE: - set_language(LANG_DE) + set_language(self.comboBox.currentText()) return True @@ -77,6 +85,9 @@ class Page02(QtWidgets.QWizardPage): class Page03(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) + + self.parent = parent + self.label = QtWidgets.QLabel() self.groupbox = QtWidgets.QGroupBox() self.groupbox.setFlat(True) @@ -101,25 +112,26 @@ class Page03(QtWidgets.QWizardPage): self.radio1.setChecked(True) def validatePage(self): - global EXCLUDE if self.radio1.isChecked(): - EXCLUDE = True + self.parent.exclude = True else: - EXCLUDE = False + self.parent.exclude = False return True class Page04(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) + + self.parent = parent + self.label = QtWidgets.QLabel() layout = QtWidgets.QVBoxLayout() layout.addWidget(self.label) self.setLayout(layout) def initializePage(self): - global EXCLUDE - if EXCLUDE: + if self.parent.exclude: self.label.setText(get_lang_text("excludefile2")) else: self.label.setText(get_lang_text("excludefile3")) @@ -127,6 +139,9 @@ class Page04(QtWidgets.QWizardPage): class Page05(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) + + self.parent = parent + self.label1 = QtWidgets.QLabel() self.label2 = QtWidgets.QLabel() self.efSourceDir = QtWidgets.QLineEdit() @@ -154,8 +169,7 @@ class Page05(QtWidgets.QWizardPage): self.efSourceDir.setText(dirName) def validatePage(self): - global SOURCEDIR - SOURCEDIR = self.efSourceDir.text() + self.parent.sourcedir = self.efSourceDir.text() return True @@ -163,6 +177,9 @@ class Page05(QtWidgets.QWizardPage): class Page06(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) + + self.parent = parent + self.label1 = QtWidgets.QLabel() self.label2 = QtWidgets.QLabel() self.label3 = QtWidgets.QLabel() @@ -174,7 +191,7 @@ class Page06(QtWidgets.QWizardPage): def initializePage(self): self.label1.setText(get_lang_text("srcdir3_1")) - bold_text = f"
{SOURCEDIR}
" + bold_text = f"{self.parent.sourcedir}
" self.label2.setText(bold_text) self.label3.setText(get_lang_text("srcdir3_2")) @@ -182,6 +199,9 @@ class Page06(QtWidgets.QWizardPage): class Page07(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) + + self.parent = parent + self.label1 = QtWidgets.QLabel() self.efTargetDir = QtWidgets.QLineEdit() self.pbBrowse = QtWidgets.QPushButton("Browse") @@ -206,8 +226,7 @@ class Page07(QtWidgets.QWizardPage): self.efTargetDir.setText(dirName) def validatePage(self): - global TARGETDIR - TARGETDIR = self.efTargetDir.text() + self.parent.targetdir = self.efTargetDir.text() return True @@ -215,6 +234,9 @@ class Page07(QtWidgets.QWizardPage): class Page08(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) + + self.parent = parent + self.label1 = QtWidgets.QLabel() self.label2 = QtWidgets.QLabel() self.label3 = QtWidgets.QLabel() @@ -226,15 +248,14 @@ class Page08(QtWidgets.QWizardPage): def initializePage(self): self.label1.setText(get_lang_text("targetdir2_1")) - bold_text = f"{TARGETDIR}
" + bold_text = f"{self.parent.targetdir}
" self.label2.setText(bold_text) self.label3.setText(get_lang_text("targetdir2_2")) def validatePage(self): - global RSYNC_CMD, MYDIR, SOURCEDIR, TARGETDIR - exclude_file = os.path.join(MYDIR, EXCLUDE_FILE) + exclude_file = os.path.join(self.parent.mydir, self.parent.exclude_file) - RSYNC_CMD = f"rsync -aqp --exclude-from={exclude_file} {SOURCEDIR} {TARGETDIR}" + self.parent.rsync_cmd = f"rsync -aqp --exclude-from={exclude_file} {self.parent.sourcedir} {self.parent.targetdir}" return True @@ -242,6 +263,9 @@ class Page08(QtWidgets.QWizardPage): class Page09(QtWidgets.QWizardPage): def __init__(self, parent=None): super().__init__(parent) + + self.parent = parent + self.label1 = QtWidgets.QLabel() self.label2 = QtWidgets.QLabel() layout = QtWidgets.QVBoxLayout() @@ -251,9 +275,7 @@ class Page09(QtWidgets.QWizardPage): def initializePage(self): self.label1.setText(get_lang_text("collect")) - global RSYNC_CMD - #self.label2.setText(f"{RSYNC_CMD}") - bold_text = f"{RSYNC_CMD}
" + bold_text = f"{self.parent.rsync_cmd}
" self.label2.setText(bold_text) @@ -274,6 +296,7 @@ class Page10(QtWidgets.QWizardPage): def initializePage(self): self.label1.setText(get_lang_text("outro1")) self.label2.setText(get_lang_text("outro2")) + global EMAIL urlLink = f"{EMAIL}" self.label3.setText(urlLink) self.label3.setOpenExternalLinks(True) @@ -282,14 +305,14 @@ class Page10(QtWidgets.QWizardPage): return True -def main_install_gui(): +def main_install_gui(mydir, exclude_file): app = QtWidgets.QApplication() - wizard = BackuppyWizard() + wizard = BackuppyWizard(parent=None, mydir=mydir, exclude_file=exclude_file) wizard.show() app.exec_() - return True, EXCLUDE, RSYNC_CMD + return True, wizard.exclude, wizard.rsync_cmd if __name__ == '__main__': - is_finalized, is_exclude, rsync_cmd = main_install_gui() + is_finalized, is_exclude, rsync_cmd = main_install_gui(mydir=os.getcwd(), exclude_file=EXCLUDE_FILE) diff --git a/languages/english.py b/languages/english.py index e04cfe2..367e77a 100644 --- a/languages/english.py +++ b/languages/english.py @@ -9,6 +9,8 @@ Yes="Yes" No="No" +welcome="Hello, first of all, which language do you prefer: German [DE] or English [EN]?" + languagepack="Perfect, the English language package is now activated. Welcome!" intromsg1="Thanks for using Backuppy to make your backups!" @@ -33,7 +35,7 @@ srcdir3_2="if this path is not correct, adjust it in the file 'Backuppy.sh'." targetdir1="Now I need to know where Backuppy should save your backups, i.e. the target directory. \n Please enter this carefully and in the same way as for the source directory." targetdir2_1="you have typed in the following destination path:" -targetdir2_2="if this path is not correct, adjust it in the file 'backuppy.sh'." +targetdir2_2="if this path is not correct, adjust it in the file 'Backuppy.sh'." collect="Now we have almost reached the end of the installer. I will now create the rsync command for you and show it to you.\n If you notice a mistake, just cancel the installer and start again from the beginning.\nNote: I recommend that you download Backuppy completely again in this case." diff --git a/languages/german.py b/languages/german.py index 07508c9..8836f19 100644 --- a/languages/german.py +++ b/languages/german.py @@ -33,7 +33,7 @@ srcdir3_2="wenn dieser Pfad nicht stimmen sollte, dann passe ihn in der Datei 'B targetdir1="Nun muss ich noch wissen, wo Backuppy dein Backup ablegen soll, das Zielverzeichnis also.\nBitte tippe dieses gewissenhaft und auf die Weise wie beim Quellverzeichnis ein." targetdir2_1="du hast folgenden Zielpfad eingetippt:" -targetdir2_2="wenn dieser Pfad nicht stimmen sollte, dann passe ihn in der Datei 'backuppy.sh' an." +targetdir2_2="wenn dieser Pfad nicht stimmen sollte, dann passe ihn in der Datei 'Backuppy.sh' an." collect="Nun sind wir fast am Ende des Installers angelangt. Ich erstelle nun den rsync-Befehl für dich und zeige ihn\ndir nachher nochmal.\nWenn dir da etwas auffallen sollte, brich den Installer einfach ab und fange nochmal von Vorne an.\nAchtung: ich empfehle dir, Backuppy in diesem Fall nochmal komplett neu zu installieren." diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..ce6df85 --- /dev/null +++ b/utils.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +""" +project: Backuppy +version: 0.9 +file: utils.py +summary: utilities script +""" + +# Standard library imports +import time + +# local imports +from languages import english +from languages import german + +# local globals +LANG_EN = "EN" +LANG_DE = "DE" +LANGUAGE = LANG_EN +class bcolors: + """ Define colors for the std. output to console. """ + + OK = '\033[92m' #GREEN + WARNING = '\033[93m' #YELLOW + ERROR = '\033[91m' #RED + RESET = '\033[0m' #RESET COLOR + + +def trace(message_txt, prefix_crlf=False, err=False): + """ Print a formatted message to std out. + + :param "message_txt" [in] The message text that should be displayed. + :param "prefix_crlf" [in] If True, a carriage return/line feed will be done prior to the message text. + + """ + + if prefix_crlf: + print("\n") + if err: + print("[ " + bcolors.ERROR + "ERR" + bcolors.RESET + " ] " + message_txt) + else: + print("[ " + bcolors.OK + "OK" + bcolors.RESET + " ] " + message_txt) + +def get_lang_text(search_str: str): + """ Returns a string from the appropriate language file. """ + return_str: str = eval("english." + search_str) + global LANGUAGE + if LANGUAGE == LANG_DE: + return_str = eval("german." + search_str) + return return_str + +def _print(message_txt: str): + print("\n" + get_lang_text(message_txt) + "\n") + time.sleep(1) + +def set_language(language): + """ Set global constant "LANGUAGE" to given language ("EN"/"DE"). """ + global LANGUAGE, LANG_DE, LANG_EN + + if str(language) and language.upper() == LANG_DE: + LANGUAGE = LANG_DE + else: + LANGUAGE = LANG_EN + +def query(question_txt: str): + try: + answer_txt = input(get_lang_text(question_txt) + "\n> ") + + if not answer_txt: + answer_txt = "''" + + if answer_txt.upper() == ":Q" or answer_txt == chr(27): + trace("Program exited by user-input.") + return None + + except KeyboardInterrupt: + trace("Program interrupted by user.", prefix_crlf=True, err=True) + return None + + return answer_txt