Skip to content

Commit

Permalink
Better paths support (config and logs paths). Extended logging featur…
Browse files Browse the repository at this point in the history
…es. Code refactor and improvements. Version updated.
  • Loading branch information
kamildzi committed Feb 28, 2023
1 parent 78bce77 commit bf005bf
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 37 deletions.
12 changes: 10 additions & 2 deletions Src/Config/ConfigManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ def __init__(self):
"""
self.config_list = []
self.config_match_regex = re.compile(".*[.]" + ConfigVersion.config_files_extension + "$")
self.config_directory = ConfigVersion.config_files_directory
self.config_directory = self.get_config_directory()

if not os.path.isdir(self.config_directory):
raise Exception("Not a directory! Wrong config directory: " + self.config_directory)
raise SystemExit("Wrong config directory! Not a directory: " + self.config_directory)

@classmethod
def get_config_directory(cls):
"""
Returns the config directory. Can be called before the object is created.
:return: str: config directory path.
"""
return ConfigVersion.config_files_directory

def search_config_entries(self):
"""
Expand Down
31 changes: 23 additions & 8 deletions Src/EESync.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,57 @@
from Src.Common.BackupAction import BackupAction
from Src.Config.ConfigEntry import ConfigEntry
from Src.Config.ConfigManager import ConfigManager
from Src.IO.FileSystem import FileSystem
from Src.IO.Logger import Logger
from Src.IO.UserInputConsole import UserInputConsole
from Src.Service.CryptProvider import CryptProvider
from Src.Service.SyncProvider import SyncProvider


class EESync:
__version = '0.9.1'
__version = '0.9.2'

sync_service = SyncProvider()
crypt_service = CryptProvider()
cm_object = None # postpone initialization

def start(self):
"""
Starts the main process.
"""
Logger.init()
self.init_file_system()
self.cm_object = ConfigManager()

Logger.log(f"EESync {self.__version} started.")

print(f"~~ EESync {self.__version} ~~")
self.sync_service.version_check()
self.crypt_service.version_check()
print()

self.interactive_user_menu()

Logger.log("EESync finished.")
print("EESync finished.")

@classmethod
def init_file_system(cls):
"""
Inits and creates (if needed) directories required by the application (logs dir, config dir, etc).
"""
FileSystem.require_created_directory(Logger.get_log_path_dir())
FileSystem.require_created_directory(ConfigManager.get_config_directory())

def interactive_user_menu(self):
"""
Handles the interactive user menu - main menu.
"""
cm_object = ConfigManager()
cm_object.search_config_entries()
self.cm_object.search_config_entries()

if len(cm_object.config_list):
if len(self.cm_object.config_list):
entry_list_string = ''
for config_entry in cm_object.config_list:
for config_entry in self.cm_object.config_list:
entry_list_string += str(
f" {config_entry['config_number']} - {config_entry['general_name']}\n"
)
Expand All @@ -54,13 +69,13 @@ def interactive_user_menu(self):
match user_input:
case number if user_input.isdigit():
print("Selected entry no. " + number + " ...")
selected_config = cm_object.config_list[int(number) - 1]
selected_config = self.cm_object.config_list[int(number) - 1]
self.process_entry(selected_config['config_object'])
pass
case 'n':
print("Creating new config.")
new_config_entry = cm_object.new_entry_from_user()
cm_object.save_config_entry(new_config_entry)
new_config_entry = self.cm_object.new_entry_from_user()
self.cm_object.save_config_entry(new_config_entry)
self.interactive_user_menu()
case _:
print("Wrong command!")
Expand Down
51 changes: 51 additions & 0 deletions Src/IO/FileSystem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os

from Src.IO.Logger import Logger
from Src.IO.UserInputConsole import UserInputConsole


class FileSystem:
"""
File System (OS) level methods.
"""

@classmethod
def require_created_directory(cls, directory_path: str, interactive: bool = True):
"""
Checks if the directory exists.\n
If it does - the method does not do anything.\n
Otherwise - create_dir_interactive() or create_dir() is triggered (depending on `interactive` param value).
:param interactive: Boolean, True by default. Controls if the directory should be created in interactive mode.
:param directory_path: The path that should be created.
"""
if not os.path.isdir(directory_path):
if interactive:
cls.create_dir_interactive(directory_path)
else:
cls.create_dir(directory_path)

@classmethod
def create_dir_interactive(cls, directory_path: str):
"""
Creates a new directory (only if the user confirms).
:param directory_path: The path that should be created.
"""
print("About to create a new directory: \n"
+ directory_path
+ "\nPlease confirm. [y/n] ")

create_dir_confirmed = UserInputConsole.read_true_or_false()
if create_dir_confirmed:
cls.create_dir(directory_path)

@classmethod
def create_dir(cls, directory_path: str):
"""
Creates a new directory (without user confirmation).
:param directory_path: The path that should be created.
"""
if os.path.exists(directory_path):
raise Exception("Given path already exists! The path: \n" + directory_path)
else:
os.makedirs(directory_path)
Logger.log("Created a new directory at: \n" + directory_path)
6 changes: 3 additions & 3 deletions Src/IO/Logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def init(cls):
cls.__log_dir = default_logs_directory

@classmethod
def get_log_path(cls):
def get_log_path_dir(cls):
"""
Returns the application log directory.
"""
Expand Down Expand Up @@ -94,7 +94,7 @@ def __handle_file(cls, text):

# file validation
if not isdir(cls.__log_dir):
raise SystemExit(f"Error! Wrong logs directory (not a directory): f{cls.__log_dir}")
raise SystemExit(f"Error! Wrong logs directory (not a directory): {cls.__log_dir}")
if not isfile(save_path):
raise SystemExit(f"Error! Tried to log to a file, but the file is gone: {save_path}")

Expand All @@ -113,7 +113,7 @@ def __init_log_file(cls):

# file validation
if not isdir(cls.__log_dir):
raise SystemExit(f"Error! Wrong logs directory (not a directory): f{cls.__log_dir}")
raise SystemExit(f"Error! Wrong logs directory (not a directory): {cls.__log_dir}")
if isfile(save_path):
raise SystemExit(f"Error! Tried to log to a new file, but the file already exists: {save_path}")

Expand Down
3 changes: 3 additions & 0 deletions Src/IO/UserInputConsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re

from Src.Common.BackupAction import BackupAction
from Src.IO.Logger import Logger


class UserInputConsole:
Expand All @@ -22,6 +23,7 @@ def general_input_int(cls):
print("Wrong value! Only numbers are allowed.")
return UserInputConsole.general_input_int()
except (KeyboardInterrupt, EOFError):
Logger.log("Input interrupted. Terminating.")
raise SystemExit("\nInput interrupted. Terminating.")
return user_input

Expand All @@ -33,6 +35,7 @@ def general_input(cls) -> str:
try:
user_input = input(cls.input_caret)
except (KeyboardInterrupt, EOFError):
Logger.log("Input interrupted. Terminating.")
raise SystemExit("\nInput interrupted. Terminating.")
return user_input

Expand Down
36 changes: 27 additions & 9 deletions Src/Service/CommandRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import GeneralSettings
from Src.IO.UserInputConsole import UserInputConsole
from Src.IO.Logger import Logger


class CommandRunner:
Expand Down Expand Up @@ -32,14 +33,15 @@ def __init__(self):
atexit.register(self.cleanup)

@staticmethod
def os_exec(command: list, confirmation_required: bool = False, silent: bool = False, capture_output: bool = True
) -> subprocess.CompletedProcess:
def os_exec(command: list, confirmation_required: bool = False, silent: bool = False, capture_output: bool = True,
logging_enabled: bool = True) -> subprocess.CompletedProcess:
"""
A runner method. Throws exception when returncode is not 0.
:param command: Command and the parameters in the form of a list.
:param confirmation_required: bool value, False by default. Decide if we should ask user for the confirmation.
:param silent: bool value, False by default. Allows to suppress printing the binary name that gets executed.
:param capture_output: bool value, True by default. Allows to control whether the command output is captured.
:param logging_enabled: bool value, True by default. Allows to control whether the logging feature is enabled.
:return: CompletedProcess object.
"""
process_env = dict(environ)
Expand All @@ -54,6 +56,8 @@ def os_exec(command: list, confirmation_required: bool = False, silent: bool = F
user_confirmed = True

if user_confirmed:
if logging_enabled:
Logger.log("Executing command: \n" + ' '.join(command))
if not silent:
print(f"( Running: {command[0]} ... )")
command_result = subprocess.run(
Expand All @@ -63,26 +67,40 @@ def os_exec(command: list, confirmation_required: bool = False, silent: bool = F
env=process_env
)
else:
if logging_enabled:
Logger.log("Skipped command / execution aborted: \n" + ' '.join(command))
raise SystemExit("Aborted.")

if command_result.returncode != 0:
raise SystemExit(f"Error: Failed to run: {command} "
failed_msg = str(f"Error: Failed to run: {command} "
+ f"\nDetails: \nSTDOUT: {command_result.stdout}\nSTDERR: {command_result.stderr}\n")
if logging_enabled:
Logger.log(failed_msg)
raise SystemExit(failed_msg)

return command_result

@staticmethod
def gen_run_report(exec_command, stdout, stderr) -> str:
def gen_run_report(exec_command, stdout, stderr, logging_enabled: bool = True) -> str:
"""
Generates formatted string from command output.
Generates formatted string from command output. Useful only for the reports.
:param exec_command: Command that you executed.
:param stdout: Standard output from the command.
:param stderr: Standard error output from the command.
:param logging_enabled: Allows to pass the output (returned value) to the logger.
:return: formatted string.
"""
if stdout is None:
stdout = str(f"{stdout} - no output or output disabled.\n")
if stderr is None:
stderr = str(f"{stderr} - no output or output disabled.\n")
return str(f"--- STDOUT: {exec_command}: ---\n"
+ str(stdout)
+ f"--- STDERR: {exec_command}: ---\n"
+ str(stderr))
formatted_string = str(f"--- STDOUT: {exec_command}: ---\n"
+ str(stdout)
+ f"--- STDERR: {exec_command}: ---\n"
+ str(stderr))
if logging_enabled:
Logger.log(formatted_string)
return formatted_string

def version_check(self):
"""
Expand Down
15 changes: 5 additions & 10 deletions Src/Service/CryptProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from Src.Common.BackupAction import BackupAction
from Src.Config.ConfigEntry import ConfigEntry
from Src.IO.Logger import Logger
from Src.Service.CommandRunner import CommandRunner


Expand Down Expand Up @@ -41,11 +40,11 @@ class CryptProvider(CommandRunner):

def version_detect(self):
# detect binary path
which_result = self.os_exec(["which", self.binary_name], silent=True)
which_result = self.os_exec(["which", self.binary_name], silent=True, logging_enabled=False)
self.binary_path = str(which_result.stdout).strip()

# detect/parse the version
version_check_result = self.os_exec([self.binary_path, "--version"], silent=True)
version_check_result = self.os_exec([self.binary_path, "--version"], silent=True, logging_enabled=False)
result_string = str(version_check_result.stderr).strip()
matched_string = re.search(r"^encfs\s+version\s+(\d.*\.\d)\s*$", result_string)
if not matched_string:
Expand Down Expand Up @@ -73,15 +72,13 @@ def __unmount_encfs(self):
# TODO: check if target is busy, get rid of sleep()
time.sleep(10.0)

which_result = self.os_exec(["which", "umount"], silent=True)
which_result = self.os_exec(["which", "umount"], silent=True, logging_enabled=False)
umount_binary = str(which_result.stdout).strip()
exec_command: list = [umount_binary, self.config.encfs_decryption_dir]

umount_result = self.os_exec(exec_command, confirmation_required=True)
self.__resource_mounted = False

run_report = self.gen_run_report(exec_command, umount_result.stdout, umount_result.stderr)
Logger.log(run_report)
self.gen_run_report(exec_command, umount_result.stdout, umount_result.stderr)

@validate_config
def __mount_encfs(self):
Expand All @@ -96,9 +93,7 @@ def __mount_encfs(self):
exec_command: list = [self.binary_path, self.config.encfs_encryption_dir, self.config.encfs_decryption_dir]
mount_result = self.os_exec(exec_command, confirmation_required=True, capture_output=False)
self.__resource_mounted = True

run_report = self.gen_run_report(exec_command, mount_result.stdout, mount_result.stderr)
Logger.log(run_report)
self.gen_run_report(exec_command, mount_result.stdout, mount_result.stderr)

def set_config(self, config: ConfigEntry):
"""
Expand Down
9 changes: 4 additions & 5 deletions Src/Service/SyncProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ class SyncProvider(CommandRunner):

def version_detect(self):
# detect binary path
which_result = self.os_exec(["which", self.binary_name], silent=True)
which_result = self.os_exec(["which", self.binary_name], silent=True, logging_enabled=False)
self.binary_path = str(which_result.stdout).strip()

# detect/parse the version
version_check_result = self.os_exec([self.binary_path, "--version"], silent=True)
version_check_result = self.os_exec([self.binary_path, "--version"], silent=True, logging_enabled=False)
result_string = str(version_check_result.stdout).strip()
matched_string = re.search(r"^rsync\s+version\s+(\d.*\.\d)\s+", result_string)
if not matched_string:
Expand Down Expand Up @@ -93,7 +93,7 @@ def __exec_rsync(self, source_dir: str, target_dir: str, dry_run: bool):
if rsync_settings["rsync_logging_enabled"]:
# rsync logging - prepare the path
current_date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_dir = Logger.get_log_path()
log_dir = Logger.get_log_path_dir()
rsync_log_path = log_dir + f"/rsync_{current_date}.log"
if not os.path.isdir(log_dir) or os.path.isfile(rsync_log_path):
raise SystemExit('Error! Rsync logging paths misconfigured!')
Expand All @@ -110,7 +110,6 @@ def __exec_rsync(self, source_dir: str, target_dir: str, dry_run: bool):
rsync_result = self.os_exec(exec_command, confirmation_required=True, capture_output=False)

# save the report
run_report = self.gen_run_report(exec_command, rsync_result.stdout, rsync_result.stderr)
Logger.log(run_report)
self.gen_run_report(exec_command, rsync_result.stdout, rsync_result.stderr)

print("Sync done!")

0 comments on commit bf005bf

Please sign in to comment.