Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
a7bf152485 | |||
543a0e59f1 | |||
c7180d5863 | |||
87ee1f2064 | |||
edb10d9a54 | |||
45535d7ff5 | |||
b24dabf203 | |||
d7a2221941 | |||
21f6b4b3ac | |||
d8e953bf05 | |||
a336776904 | |||
3423502e68 | |||
5852ac6b62 | |||
8e7afcedce | |||
845b7e9c9a | |||
a1c5fbf7ea | |||
a8199debc9 | |||
2c00a11676 | |||
5d367fa1a8 | |||
1dfe37a970 |
8 changed files with 587 additions and 418 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
@ -1,6 +1,34 @@
|
|||
# Changelog Backuppy
|
||||
|
||||
## [1.02.000] - 2021-05-05
|
||||
## [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
|
||||
- GUI-mode: Installing necessary PySide2 package if not existing
|
||||
|
||||
## [0.7] - 2021-05-06
|
||||
### Added
|
||||
- Reworked "install.sh" to call "install.py"
|
||||
- Launches graphical installer when called with "--gui" option: "install.sh --gui"
|
||||
|
||||
## [0.6] - 2021-05-06
|
||||
### Added
|
||||
- Write alias to Backuppy.sh into .bashrc/.zshrc only if not already existing
|
||||
|
||||
## [0.5.2] - 2021-05-05
|
||||
### Added
|
||||
- Now fully functional as the shell version
|
||||
|
||||
|
@ -11,11 +39,11 @@
|
|||
### Fixed
|
||||
- Fixed problem with the Bash/ZSH config
|
||||
|
||||
## [1.01.001] - 2021-05-04
|
||||
## [0.5.1] - 2021-05-04
|
||||
### Changed
|
||||
- Use E-MAIL constant
|
||||
|
||||
## [1.01.000] - 2021-05-04
|
||||
## [0.5] - 2021-05-04
|
||||
### Added
|
||||
- Graphical installer based on Python3 with PySide2 GUI framework (Qt-bindings for Python)
|
||||
- Changelog.MD
|
||||
|
|
32
README.md
32
README.md
|
@ -6,9 +6,9 @@ Name: Backuppy
|
|||
|
||||
Description: Make daily backups with Backuppy to avoid losing your data.
|
||||
|
||||
Installation: execute `chmod +x install.sh && .\install.sh`
|
||||
Installation: execute `chmod +x install.sh && ./install.sh`
|
||||
|
||||
Install. GUI: execute `chmod +x install.sh && .\install.sh --gui`
|
||||
Install. GUI: execute `chmod +x install.sh && ./install.sh --gui`
|
||||
|
||||
Usage: execute `backuppy`
|
||||
|
||||
|
@ -16,32 +16,32 @@ Author: Joël Schurter
|
|||
|
||||
Licence: GPL3
|
||||
|
||||
More infos: see constants and README.md
|
||||
More infos: See README.md and CHANGELOG.md
|
||||
|
||||
# ToDo
|
||||
|
||||
- add a log-file for the rsync errors
|
||||
|
||||
- check user-input for validity
|
||||
|
||||
# Dependencies
|
||||
- rsync (because Backuppy makes its backups with rsync)
|
||||
|
||||
execute `sudo apt install rsync` on Debian/Ubuntu
|
||||
|
||||
execute `sudo pacman -S rsync` on Arch/Manjaro
|
||||
|
||||
execute `sudo apt install rsync` on Debian/Ubuntu
|
||||
|
||||
execute `sudo dnf install rsync` on Fedora
|
||||
|
||||
execute `sudo zypper install rsync` on openSUSE
|
||||
|
||||
# Dependencies for the graphical installer:
|
||||
- pip (the python package manager)
|
||||
|
||||
execute `sudo pacman -S pip` on Arch/Manjaro
|
||||
|
||||
execute `sudo apt install python3-pip` on Debian/Ubuntu
|
||||
|
||||
execute `sudo pacman -S pip` on Arch/Manjaro
|
||||
|
||||
- pip package 'PySide2'
|
||||
|
||||
execute `pip3 install pyside2` on Debian/Ubuntu
|
||||
|
||||
execute `pip install pyside2` on Arch/Manjaro
|
||||
|
||||
# IMPORTANT
|
||||
|
||||
You have to remove the "backuppy" alias in your .bashrc or .zshrc before reinstall Backuppy.
|
||||
execute `sudo dnf install pip` on Fedora
|
||||
|
||||
execute `sudo zypper install python3-pip` on openSUSE
|
||||
|
|
411
install.py
411
install.py
|
@ -1,336 +1,155 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
project: Backuppy
|
||||
version: 1.02.000
|
||||
version: 0.9
|
||||
file: install.py
|
||||
summary: main entry python file
|
||||
summary: python installer-script in CLI-mode
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Third party imports
|
||||
from PySide2 import QtCore
|
||||
from PySide2 import QtGui
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
|
||||
# local imports
|
||||
from languages import english
|
||||
from languages import german
|
||||
from utils import set_language, query, _print, trace
|
||||
|
||||
# local globals
|
||||
VERSION: str = "1.02.000"
|
||||
EMAIL: str = "fotocoder@joschu.ch"
|
||||
TARGET_SCRIPT: str = "Backuppy.sh"
|
||||
EXCLUDE_FILE: str = "exclude.txt"
|
||||
LANG_EN: str = "English"
|
||||
LANG_DE: str = "German"
|
||||
LANGUAGE: str = LANG_EN
|
||||
MYDIR: str = os.environ.get("mydir")
|
||||
if not MYDIR:
|
||||
MYDIR = os.getcwd()
|
||||
SOURCEDIR: str = None
|
||||
EXCLUDE: bool = True
|
||||
TARGETDIR: str = None
|
||||
RSYNC_CMD: str = None
|
||||
VERSION: str = "0.9"
|
||||
EMAIL = "fotocoder@joschu.ch"
|
||||
EXCLUDE_FILE = "exclude.txt"
|
||||
BACKUPPY_SCRIPT = "Backuppy.sh"
|
||||
|
||||
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 main_install_cli(mydir, exclude_file):
|
||||
language = query("welcome")
|
||||
if not language:
|
||||
return False, None, None
|
||||
set_language(language)
|
||||
|
||||
def trace(message_txt: str):
|
||||
""" Print a formatted message to std out. """
|
||||
print(f"[ OK ]", message_txt)
|
||||
_print("languagepack")
|
||||
|
||||
class BackuppyWizard(QtWidgets.QWizard):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
_print("intromsg1")
|
||||
|
||||
self.addPage(Page01(self))
|
||||
self.addPage(Page02(self))
|
||||
self.addPage(Page03(self))
|
||||
self.addPage(Page04(self))
|
||||
self.addPage(Page05(self))
|
||||
self.addPage(Page06(self))
|
||||
self.addPage(Page07(self))
|
||||
self.addPage(Page08(self))
|
||||
self.addPage(Page09(self))
|
||||
self.addPage(Page10(self))
|
||||
|
||||
self.setWindowTitle(english.intromsg1)
|
||||
self.resize(640, 480)
|
||||
_print("intromsg2")
|
||||
|
||||
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.comboBox = QIComboBox(self)
|
||||
self.comboBox = QtWidgets.QComboBox(self)
|
||||
self.comboBox.addItem(LANG_EN, LANG_EN)
|
||||
self.comboBox.addItem(LANG_DE, LANG_DE)
|
||||
# which Rsync options are available and which one you want to use
|
||||
_print("rsyncopt")
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.comboBox)
|
||||
self.setLayout(layout)
|
||||
# asks if you want to exclude files/directories from backup and creates an exclude file in case of Yes
|
||||
exclude = query("excludefile1")
|
||||
if not exclude:
|
||||
return False, None, None
|
||||
elif exclude.upper() in ("J", "Y"):
|
||||
_print("excludefile2")
|
||||
exclude = True
|
||||
else:
|
||||
_print("excludefile3")
|
||||
exclude = False
|
||||
|
||||
def validatePage(self):
|
||||
global LANGUAGE
|
||||
if self.comboBox.currentText() == LANG_DE:
|
||||
LANGUAGE = LANG_DE
|
||||
else:
|
||||
LANGUAGE = LANG_EN
|
||||
return True
|
||||
# Asks for the source directory which should be saved
|
||||
_print("srcdir1")
|
||||
sourcedir = query("srcdir2")
|
||||
if not sourcedir:
|
||||
return False, None, None
|
||||
|
||||
_print("srcdir3_1")
|
||||
print(sourcedir)
|
||||
_print("srcdir3_2")
|
||||
|
||||
class Page02(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
# asks for the destination directory in which the backup should be saved
|
||||
targetdir = query("targetdir1")
|
||||
if not targetdir:
|
||||
return False, None, None
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
self.setLayout(layout)
|
||||
_print("targetdir2_1")
|
||||
print(targetdir)
|
||||
_print("targetdir2_2")
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("languagepack"))
|
||||
self.label2.setText(get_lang_text("intromsg2"))
|
||||
# collects all the information needed to execute the rsync command and creates it.
|
||||
_print("collect")
|
||||
|
||||
rsync_cmd = f"rsync -aqp --exclude-from={os.path.join(mydir, exclude_file)} {sourcedir} {targetdir}"
|
||||
|
||||
class Page03(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label = QtWidgets.QLabel()
|
||||
self.groupbox = QtWidgets.QGroupBox()
|
||||
self.groupbox.setFlat(True)
|
||||
self.radio1 = QtWidgets.QRadioButton()
|
||||
self.radio2 = QtWidgets.QRadioButton()
|
||||
print(f"{rsync_cmd}")
|
||||
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
vbox.addWidget(self.radio1)
|
||||
vbox.addWidget(self.radio2)
|
||||
vbox.addStretch(1)
|
||||
self.groupbox.setLayout(vbox)
|
||||
# Outro
|
||||
_print("outro1")
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.groupbox)
|
||||
self.setLayout(layout)
|
||||
_print("outro2")
|
||||
print(EMAIL)
|
||||
|
||||
def initializePage(self):
|
||||
self.label.setText(get_lang_text("excludefile1"))
|
||||
self.radio1.setText(get_lang_text("Yes"))
|
||||
self.radio2.setText(get_lang_text("No"))
|
||||
self.radio1.setChecked(True)
|
||||
return True, exclude, rsync_cmd
|
||||
|
||||
def validatePage(self):
|
||||
global EXCLUDE
|
||||
if self.radio1.isChecked():
|
||||
EXCLUDE = True
|
||||
else:
|
||||
EXCLUDE = False
|
||||
return True
|
||||
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")
|
||||
|
||||
class Page04(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label = QtWidgets.QLabel()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
global EXCLUDE
|
||||
if EXCLUDE:
|
||||
self.label.setText(get_lang_text("excludefile2"))
|
||||
else:
|
||||
self.label.setText(get_lang_text("excludefile3"))
|
||||
|
||||
class Page05(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
self.edit = QtWidgets.QLineEdit()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
layout.addWidget(self.edit)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("srcdir1"))
|
||||
self.label2.setText(get_lang_text("srcdir2"))
|
||||
|
||||
def validatePage(self):
|
||||
global SOURCEDIR
|
||||
SOURCEDIR = self.edit.text()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Page06(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
self.label3 = QtWidgets.QLabel()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
layout.addWidget(self.label3)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("srcdir3_1"))
|
||||
bold_text = f"<p><strong>{SOURCEDIR}</strong></p>"
|
||||
self.label2.setText(bold_text)
|
||||
self.label3.setText(get_lang_text("srcdir3_2"))
|
||||
|
||||
|
||||
class Page07(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.edit = QtWidgets.QLineEdit()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.edit)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("targetdir1"))
|
||||
|
||||
def validatePage(self):
|
||||
global TARGETDIR
|
||||
TARGETDIR = self.edit.text()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Page08(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
self.label3 = QtWidgets.QLabel()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
layout.addWidget(self.label3)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("targetdir2_1"))
|
||||
bold_text = f"<p><strong>{TARGETDIR}</strong></p>"
|
||||
self.label2.setText(bold_text)
|
||||
self.label3.setText(get_lang_text("targetdir2_2"))
|
||||
|
||||
def validatePage(self):
|
||||
global RSYNC_CMD, MYDIR, SOURCEDIR, TARGETDIR
|
||||
RSYNC_CMD = f"rsync -aqp --exclude-from={MYDIR}/exclude.txt {SOURCEDIR} {TARGETDIR}"
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Page09(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("collect"))
|
||||
global RSYNC_CMD
|
||||
#self.label2.setText(f"{RSYNC_CMD}")
|
||||
bold_text = f"<p><strong>{RSYNC_CMD}</strong></p>"
|
||||
self.label2.setText(bold_text)
|
||||
|
||||
|
||||
class Page10(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
self.label3 = QtWidgets.QLabel()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
layout.addWidget(self.label3)
|
||||
self.setLayout(layout)
|
||||
self.setFinalPage(True)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("outro1"))
|
||||
self.label2.setText(get_lang_text("outro2"))
|
||||
urlLink = f"<a href='mailto: {EMAIL}'>{EMAIL}</a>"
|
||||
self.label3.setText(urlLink)
|
||||
self.label3.setOpenExternalLinks(True)
|
||||
|
||||
def validatePage(self):
|
||||
# create shell script with rsync command
|
||||
create_shell_script(RSYNC_CMD)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_shell_script(command_str: str):
|
||||
rc_filepath: str = None
|
||||
|
||||
global MYDIR, EXCLUDE, TARGET_SCRIPT
|
||||
|
||||
TARGET_SCRIPT = os.path.join(MYDIR, TARGET_SCRIPT)
|
||||
ALIAS_STR = f"alias backuppy='sudo {TARGET_SCRIPT}'"
|
||||
current_shell = os.environ.get("SHELL")
|
||||
user_home = os.environ.get("HOME")
|
||||
|
||||
## STEP 1: update shell config scripts with alias for backuppy
|
||||
# .zshrc case1 and case2
|
||||
if current_shell in ("/usr/bin/zsh", "/bin/zsh"):
|
||||
# Appending to bash config file
|
||||
rc_filepath = os.path.join(user_home, ".zshrc")
|
||||
|
||||
# .bashrc case1 and case2
|
||||
elif current_shell in ("/usr/bin/bash", "/bin/bash"):
|
||||
# Appending to zsh config file
|
||||
rc_filepath = os.path.join(user_home, ".bashrc")
|
||||
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(mydir, backuppy_script)
|
||||
alias_str = f"alias backuppy='sudo {backuppy_script}'"
|
||||
|
||||
# Check for installed ZSH
|
||||
if shell.upper().find("ZSH") > 0:
|
||||
rc_filepath = os.path.join(home_dir, ".zshrc")
|
||||
# Check for installed BASH
|
||||
if shell.upper().find("BASH") > 0:
|
||||
rc_filepath = os.path.join(home_dir, ".bashrc")
|
||||
# Append our alias if not already existing
|
||||
if os.path.isfile(rc_filepath):
|
||||
with open(rc_filepath, 'a') as file1:
|
||||
trace(f"Writing {ALIAS_STR} to config file '{rc_filepath}'.")
|
||||
file1.write("\n# Following line was created by Backuppy\n" + ALIAS_STR + "\n")
|
||||
fileRc = open(rc_filepath, "r") # open file in read mode
|
||||
backuppy_entry_exists = False
|
||||
for line in fileRc:
|
||||
if "alias backuppy=" in line:
|
||||
backuppy_entry_exists = True
|
||||
break
|
||||
if not backuppy_entry_exists:
|
||||
trace(f"Writing {alias_str} to config file '{rc_filepath}'.")
|
||||
fileRc = open(rc_filepath, "a") # open file in append mode
|
||||
fileRc.write("\n# Following line was created by Backuppy\n" + alias_str + "\n")
|
||||
fileRc.close()
|
||||
|
||||
## STEP 2: Create the exclude script if desired
|
||||
if EXCLUDE:
|
||||
global EXCLUDE_FILE
|
||||
EXCLUDE_FILE = os.path.join(MYDIR, EXCLUDE_FILE)
|
||||
with open(EXCLUDE_FILE, "w") as file2:
|
||||
trace(f"creating exclude-file '{EXCLUDE_FILE}'.")
|
||||
file2.writelines("\n")
|
||||
def create_backuppy_script(directory, backuppy_script, rsync_cmd):
|
||||
# creates the file 'Backuppy.sh'
|
||||
backuppy_file = os.path.join(directory, backuppy_script)
|
||||
with open(backuppy_file, "w") as fBackuppy:
|
||||
trace(f"creating backuppy-file '{backuppy_file}'.")
|
||||
fBackuppy.write("#!/bin/bash\n" + rsync_cmd + "\n")
|
||||
|
||||
os.chmod(backuppy_file, 0o777) # make file executable
|
||||
|
||||
## STEP 3: enter the rsync command in Backuppy.sh
|
||||
with open(TARGET_SCRIPT, "w") as file3:
|
||||
trace(f"Writing rsync-command to file '{TARGET_SCRIPT}':\n {command_str}")
|
||||
file3.writelines("#!/bin/bash" + "\n")
|
||||
file3.writelines(command_str + "\n")
|
||||
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)
|
||||
|
||||
if __name__ == '__main__':
|
||||
if rsync_cmd:
|
||||
create_backuppy_script(mydir, backuppy_script, rsync_cmd)
|
||||
create_alias(mydir, backuppy_script)
|
||||
|
||||
def main(argv):
|
||||
trace(f"Starting Backuppy install.py v{VERSION}")
|
||||
app = QtWidgets.QApplication()
|
||||
wizard = BackuppyWizard()
|
||||
wizard.show()
|
||||
is_finalized = False
|
||||
mydir = os.getcwd()
|
||||
|
||||
app.exec_()
|
||||
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(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(mydir, EXCLUDE_FILE) # collect user input via CLI and store in env. variables
|
||||
|
||||
if is_finalized:
|
||||
do_the_install(mydir, EXCLUDE_FILE, is_exclude, BACKUPPY_SCRIPT, rsync_cmd)
|
||||
|
||||
trace("Ending Backuppy install.py")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# sys.argv.append("--gui") # TODO: disable for production
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
|
|
124
install.sh
124
install.sh
|
@ -1,107 +1,29 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Variables
|
||||
mydir=$PWD
|
||||
langDE="./languages/german.py"
|
||||
langEN="./languages/english.py"
|
||||
# """
|
||||
# project: Backuppy
|
||||
# version: 0.8
|
||||
# file: install.sh
|
||||
# summary: main entry shell script
|
||||
# """
|
||||
|
||||
# Check if graphical installer should be executed
|
||||
if [ "$1" == "--gui" ]; then
|
||||
python3 -B install.py
|
||||
exit 0
|
||||
# Check if PIP ist installed
|
||||
if ! command -v pip3> /dev/null
|
||||
then
|
||||
echo "Please install PIP on your system for the graphical version of Backuppy!"
|
||||
exit
|
||||
fi
|
||||
# Check if PIP module "PySide2" is installed
|
||||
if ! pip list | grep PySide2> /dev/null
|
||||
then
|
||||
# Install PySide2
|
||||
echo -e "Installing necessary PySide2 package for GUI-mode."
|
||||
pip3 install PySide2
|
||||
fi
|
||||
# Launch python installer in GUI mode
|
||||
python3 -B install.py "$1"
|
||||
else
|
||||
# Intro
|
||||
# language
|
||||
echo -e "Hello, first of all, which language do you prefer: German [DE] or English [EN]?"
|
||||
read language
|
||||
if [ $language = "DE" ]; then
|
||||
echo -e "Perfekt, nun ist das deutsche Sprachpaket aktiviert. Willkommen! \n"
|
||||
. $langDE
|
||||
sleep 1
|
||||
fi
|
||||
if [ $language = "EN" ]; then
|
||||
echo -e "Perfect, the English language package is now activated. Welcome!. \n"
|
||||
. $langEN
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
echo -e "\n$intromsg1 \n"
|
||||
sleep 1
|
||||
echo -e " \n$intromsg2\n"
|
||||
sleep 1
|
||||
# Installer
|
||||
|
||||
# creates the file 'Backuppy.sh'
|
||||
touch Backuppy.sh
|
||||
echo "#!/bin/bash" >> Backuppy.sh
|
||||
chmod +x Backuppy.sh
|
||||
# which Rsync options are available and which one you want to use
|
||||
echo -e "$rsyncopt \n"
|
||||
sleep 1
|
||||
|
||||
# asks if you want to exclude files/directories from backup and creates an exclude file in case of Yes
|
||||
echo -e "$excludefile1"
|
||||
read exclude
|
||||
if [ $exclude = "J" ]; then
|
||||
echo -e "$excludefile2 \n"
|
||||
touch exclude.txt
|
||||
sleep 1
|
||||
elif [ $exclude = "Y" ]; then
|
||||
echo -e "$excludefile2 \n"
|
||||
touch exclude.txt
|
||||
sleep 1
|
||||
fi
|
||||
if [ $exclude = "N" ]; then
|
||||
echo -e "$excludefile3 \n"
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
# Asks for the source directory which should be saved
|
||||
echo -e "$srcdir1"
|
||||
sleep 1
|
||||
echo -e "$srcdir2"
|
||||
read sourcedir
|
||||
echo -e "$srcdir3_1 $sourcedir $srcdir3_2"
|
||||
sleep 1
|
||||
|
||||
# asks for the destination directory in which the backup should be saved
|
||||
echo -e "$targetdir1"
|
||||
read targetdir
|
||||
echo -e "$targetdir2_1 $targetdir $targetdir2_2"
|
||||
sleep 1
|
||||
|
||||
# alias entry in .bashrc or .zshrc
|
||||
# .zshrc case 1
|
||||
echo $SHELL
|
||||
if [ $SHELL = "/usr/bin/zsh" ]; then
|
||||
echo "alias backuppy='sudo $mydir/Backuppy.sh'" >> ~/.zshrc
|
||||
fi
|
||||
# zshrc case 2
|
||||
echo $SHELL
|
||||
if [ $SHELL = "/bin/zsh" ]; then
|
||||
echo "alias backuppy='sudo $mydir/Backuppy.sh'" >> ~/.zshrc
|
||||
fi
|
||||
#bashrc case 1
|
||||
if [ $SHELL = "/usr/bin/bash" ]; then
|
||||
echo "alias backuppy='sudo $mydir/Backuppy.sh'" >> ~/.bashrc
|
||||
fi
|
||||
# bashrc case 2
|
||||
if [ $SHELL = "/bin/bash" ]; then
|
||||
echo "alias backuppy='sudo $mydir/Backuppy.sh'" >> ~/.bashrc
|
||||
fi
|
||||
|
||||
# collects all the information needed to execute the rsync command and creates it.
|
||||
echo -e "$collect \n"
|
||||
sleep 1
|
||||
echo -e "rsync -aqp --exclude-from=$mydir/exclude.txt $sourcedir $targetdir \n"
|
||||
sleep 1
|
||||
|
||||
# enter the rsync command in Backuppy.sh
|
||||
echo "rsync -aqp --exclude-from=$mydir/exclude.txt $sourcedir $targetdir" >> Backuppy.sh
|
||||
|
||||
# Outro
|
||||
echo -e "$outro1"
|
||||
sleep 2
|
||||
echo "$outro2"
|
||||
exit 0
|
||||
# Launch python installer in CLI mode
|
||||
python3 -B install.py
|
||||
fi
|
||||
|
|
318
install_gui.py
Normal file
318
install_gui.py
Normal file
|
@ -0,0 +1,318 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
project: Backuppy
|
||||
version: 0.9
|
||||
file: install_gui.py
|
||||
summary: python installer-script in GUI-mode (needs PySide2)
|
||||
"""
|
||||
|
||||
# Standard library imports
|
||||
import os
|
||||
|
||||
# Third party imports
|
||||
from PySide2 import QtWidgets
|
||||
|
||||
# local imports
|
||||
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, 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))
|
||||
self.addPage(Page02(self))
|
||||
self.addPage(Page03(self))
|
||||
self.addPage(Page04(self))
|
||||
self.addPage(Page05(self))
|
||||
self.addPage(Page06(self))
|
||||
self.addPage(Page07(self))
|
||||
self.addPage(Page08(self))
|
||||
self.addPage(Page09(self))
|
||||
self.addPage(Page10(self))
|
||||
|
||||
self.resize(640, 480)
|
||||
|
||||
|
||||
class Page01(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label = QtWidgets.QLabel(get_lang_text("welcome"))
|
||||
|
||||
self.comboBox = QtWidgets.QComboBox(self)
|
||||
self.comboBox.addItem(LANG_EN)
|
||||
self.comboBox.addItem(LANG_DE)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.comboBox)
|
||||
self.setLayout(layout)
|
||||
|
||||
def validatePage(self):
|
||||
set_language(self.comboBox.currentText())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Page02(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.setWindowTitle(get_lang_text("intromsg1"))
|
||||
|
||||
self.label1.setText(get_lang_text("languagepack"))
|
||||
self.label2.setText(get_lang_text("intromsg2"))
|
||||
|
||||
|
||||
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)
|
||||
self.radio1 = QtWidgets.QRadioButton()
|
||||
self.radio2 = QtWidgets.QRadioButton()
|
||||
|
||||
vbox = QtWidgets.QVBoxLayout()
|
||||
vbox.addWidget(self.radio1)
|
||||
vbox.addWidget(self.radio2)
|
||||
vbox.addStretch(1)
|
||||
self.groupbox.setLayout(vbox)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label)
|
||||
layout.addWidget(self.groupbox)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label.setText(get_lang_text("excludefile1"))
|
||||
self.radio1.setText(get_lang_text("Yes"))
|
||||
self.radio2.setText(get_lang_text("No"))
|
||||
self.radio1.setChecked(True)
|
||||
|
||||
def validatePage(self):
|
||||
if self.radio1.isChecked():
|
||||
self.parent.exclude = True
|
||||
else:
|
||||
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):
|
||||
if self.parent.exclude:
|
||||
self.label.setText(get_lang_text("excludefile2"))
|
||||
else:
|
||||
self.label.setText(get_lang_text("excludefile3"))
|
||||
|
||||
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()
|
||||
self.pbBrowse = QtWidgets.QPushButton("Browse")
|
||||
self.pbBrowse.clicked.connect(self.on_pbBrowse_clicked)
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
|
||||
hLayout = QtWidgets.QHBoxLayout()
|
||||
hLayout.addWidget(self.efSourceDir)
|
||||
hLayout.addWidget(self.pbBrowse)
|
||||
|
||||
layout.addLayout(hLayout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("srcdir1"))
|
||||
self.label2.setText(get_lang_text("srcdir2"))
|
||||
|
||||
def on_pbBrowse_clicked(self):
|
||||
options = QtWidgets.QFileDialog.Options() | QtWidgets.QFileDialog.ShowDirsOnly
|
||||
dirName = QtWidgets.QFileDialog.getExistingDirectory(self, "Select Directory", None, options)
|
||||
self.efSourceDir.setText(dirName)
|
||||
|
||||
def validatePage(self):
|
||||
self.parent.sourcedir = self.efSourceDir.text()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
layout.addWidget(self.label3)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("srcdir3_1"))
|
||||
bold_text = f"<p><strong>{self.parent.sourcedir}</strong></p>"
|
||||
self.label2.setText(bold_text)
|
||||
self.label3.setText(get_lang_text("srcdir3_2"))
|
||||
|
||||
|
||||
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")
|
||||
self.pbBrowse.clicked.connect(self.on_pbBrowse_clicked)
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
|
||||
hLayout = QtWidgets.QHBoxLayout()
|
||||
hLayout.addWidget(self.efTargetDir)
|
||||
hLayout.addWidget(self.pbBrowse)
|
||||
|
||||
layout.addLayout(hLayout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("targetdir1"))
|
||||
|
||||
def on_pbBrowse_clicked(self):
|
||||
options = QtWidgets.QFileDialog.Options() | QtWidgets.QFileDialog.ShowDirsOnly
|
||||
dirName = QtWidgets.QFileDialog.getExistingDirectory(self, "Select Directory", None, options)
|
||||
self.efTargetDir.setText(dirName)
|
||||
|
||||
def validatePage(self):
|
||||
self.parent.targetdir = self.efTargetDir.text()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
layout.addWidget(self.label3)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("targetdir2_1"))
|
||||
bold_text = f"<p><strong>{self.parent.targetdir}</strong></p>"
|
||||
self.label2.setText(bold_text)
|
||||
self.label3.setText(get_lang_text("targetdir2_2"))
|
||||
|
||||
def validatePage(self):
|
||||
exclude_file = os.path.join(self.parent.mydir, self.parent.exclude_file)
|
||||
|
||||
self.parent.rsync_cmd = f"rsync -aqp --exclude-from={exclude_file} {self.parent.sourcedir} {self.parent.targetdir}"
|
||||
|
||||
return True
|
||||
|
||||
|
||||
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()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
self.setLayout(layout)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("collect"))
|
||||
bold_text = f"<p><strong>{self.parent.rsync_cmd}</strong></p>"
|
||||
self.label2.setText(bold_text)
|
||||
|
||||
|
||||
class Page10(QtWidgets.QWizardPage):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.label1 = QtWidgets.QLabel()
|
||||
self.label2 = QtWidgets.QLabel()
|
||||
self.label3 = QtWidgets.QLabel()
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.label1)
|
||||
layout.addWidget(self.label2)
|
||||
layout.addWidget(self.label3)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.setFinalPage(True)
|
||||
|
||||
def initializePage(self):
|
||||
self.label1.setText(get_lang_text("outro1"))
|
||||
self.label2.setText(get_lang_text("outro2"))
|
||||
global EMAIL
|
||||
urlLink = f"<a href='mailto: {EMAIL}'>{EMAIL}</a>"
|
||||
self.label3.setText(urlLink)
|
||||
self.label3.setOpenExternalLinks(True)
|
||||
|
||||
def validatePage(self):
|
||||
return True
|
||||
|
||||
|
||||
def main_install_gui(mydir, exclude_file):
|
||||
app = QtWidgets.QApplication()
|
||||
wizard = BackuppyWizard(parent=None, mydir=mydir, exclude_file=exclude_file)
|
||||
wizard.show()
|
||||
|
||||
app.exec_()
|
||||
|
||||
return True, wizard.exclude, wizard.rsync_cmd
|
||||
|
||||
if __name__ == '__main__':
|
||||
is_finalized, is_exclude, rsync_cmd = main_install_gui(mydir=os.getcwd(), exclude_file=EXCLUDE_FILE)
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
80
utils.py
Normal file
80
utils.py
Normal file
|
@ -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
|
Loading…
Reference in a new issue