diff --git a/.github/workflows/test-package.yaml b/.github/workflows/test-package.yaml index 0cf63a9..2569838 100644 --- a/.github/workflows/test-package.yaml +++ b/.github/workflows/test-package.yaml @@ -7,7 +7,7 @@ on: push: branches: [ main, dev ] pull_request: - branches: [ main ] + branches: [ main, dev ] jobs: test: diff --git a/.gitignore b/.gitignore index 644d0c5..a8e3ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -123,7 +123,7 @@ celerybeat.pid # Environments .env -.venv +.venv* env/ venv/ ENV/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d1372ca..99deeef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.2.0-beta - 25-06-2022 +### Added +- Autoconfirm CLI flag (-y/--autoconfirm) +- Add option for all targets at cli (-a/--all) +- detect if platform is not windows and quit. Only functions on windows. +- option to Tar before 7z for all targets +- Warning added for short password length with interactive config +- Keys missing in config file are filled with default values +### Changed +- upgrade 7z from version 19 to version 22 (~20% speed increase) +- Plex dedicated functions removed, backed up as a standard folder +- check plex database size correctly and decide on splitting - don't always force splitting. +- Refactoring of backup functions. +- Removed split-force config option +### Fixed +- Fix performance bug if compression level = 0 (store) (remove md + m0 cli flags) + + +## 0.1.7-beta +### Fixed +- Fix for virtualbox config +### Added +- GH CI/CD + + ## 0.1.0-beta - 03-05-2022 ### Added --Initial Release +- Initial Release diff --git a/README.md b/README.md index 726fbb0..1f36302 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ A python package to back up user files on Windows to 7z archives. Useful for offsite or cloud backups and backups can be optionally encrypted with AES256. Archives can be Optionally split archives into smaller archives for easier management. As well as user files can backup Plex Media Server, Hyper-V Virtual Machines and VirtualBox Virtual Machines. -- embeds 7za to perform compression +- embeds 7z to perform compression +- saves lists of installed programs and drivers - optional AES256 encryption - Archives produced are full backups - no incremental backups at present - Archives saved in the format - host_user_yyyy-mm-dd_folder.7z @@ -33,6 +34,22 @@ or ```winbackup --create-configfile```. This config file must be modified before To generate a configuration file interactively run ```winbackup -i``` or ```winbackup --interactive-config```. This file can be run without modification for later use. +Full CLI options +---------------- + +Available CLI options. + +Flag | Option | Desc | +-----|-----------------------|-------------------------------------------------------------------| +`-a` | `--all` | Run backup with all possible backup targets selected | +`-c` | `--configfile` | Run backup from a supplied yaml config file | +`-C` | `--create-configfile` | Create a default configuration file template. Will need modified before being run. +`-h` | `--help` | Displays help information +`-i` | `--interactive-config`| Generate a configuration file interactively, can be run directly after generation +`-q` | `--quiet` | Minimal terminal output | +`-v` | `--verbose` | Sets logging to debug. Only affects log file not stdout. | +`-V` | `--version` | Print version info. | +`-y` | `--autoconfirm` | Autoconfirm prompts | Tests ----- To run unitests diff --git a/pyproject.toml b/pyproject.toml index 0ad39d0..cacfddb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,9 @@ requires = [ "setuptools>=42", "wheel" ] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 95 +target-version = ['py37'] +include = '\.pyi?$' \ No newline at end of file diff --git a/requirements.dev.txt b/requirements.dev.txt new file mode 100644 index 0000000..3a56de9 --- /dev/null +++ b/requirements.dev.txt @@ -0,0 +1,8 @@ +colorama>=0.4.4 +Send2Trash>=1.8.0 +tqdm>=4.63.0 +humanize>=4.0.0 +PyYAML>=6.0 +black>=22 +flake8>=4.0 +build>=0.8 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index a2ae539..ce56f8a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,3 +35,8 @@ winbackup = scripts/*, bin/7z/* console_scripts = winbackup = winbackup.__main__:cli +[flake8] +extend-ignore = E203, E266, W503, E501 +max-line-length = 95 +max-complexity = 25 +select = B,C,E,F,W,T4,B9,B950 \ No newline at end of file diff --git a/setup.py b/setup.py index a0928ec..83227e8 100644 --- a/setup.py +++ b/setup.py @@ -2,5 +2,5 @@ import setuptools -if __name__ == '__main__': +if __name__ == "__main__": setuptools.setup() # see setup.cfg diff --git a/tests/test_configagent.py b/tests/test_configagent.py index d3a664a..508342a 100644 --- a/tests/test_configagent.py +++ b/tests/test_configagent.py @@ -4,374 +4,492 @@ ## tests for configagent module ## -from itertools import tee -import re import unittest import os -import sys from tempfile import TemporaryDirectory import winbackup.configagent import winbackup.windowspaths -class TestValidOutput(unittest.TestCase): +class TestValidOutput(unittest.TestCase): def setUp(self) -> None: self.config_agent = winbackup.configagent.ConfigAgent() self.windows_paths = winbackup.windowspaths.WindowsPaths() - def test_update_config_paths(self): response = self.config_agent.update_config_paths(self.windows_paths.get_paths()) - self.assertTrue(type(response['10_documents']['path']) == str) - + self.assertTrue(type(response["10_documents"]["path"]) == str) def test_add_item_key_inserted(self): test_item = { - 'name': 'TESTITEM', - 'type': 'folder', - 'path': './testpath', - 'dict_size': '192m', + "name": "TESTITEM", + "type": "folder", + "path": "./testpath", + "dict_size": "192m", } - response = self.config_agent.add_item('99_testitem', test_item) - self.assertTrue(response['99_testitem']) - + response = self.config_agent.add_item("99_testitem", test_item) + self.assertTrue(response["99_testitem"]) def test_add_item_key_has_expected_path(self): test_item = { - 'name': 'TESTITEM', - 'type': 'folder', - 'path': './testpath', - 'dict_size': '192m', + "name": "TESTITEM", + "type": "folder", + "path": "./testpath", + "dict_size": "192m", } - response = self.config_agent.add_item('99_testitem', test_item) - self.assertTrue(response['99_testitem']['path'] == './testpath') - + response = self.config_agent.add_item("99_testitem", test_item) + self.assertTrue(response["99_testitem"]["path"] == "./testpath") def test_add_item_raises_typeerror_on_invalid_key_type(self): test_item = { - 'name': 'TESTITEM', - 'type': 'folder', - 'path': './testpath', - 'dict_size': '192m', + "name": "TESTITEM", + "type": "folder", + "path": "./testpath", + "dict_size": "192m", } with self.assertRaises(TypeError): - response = self.config_agent.add_item(99, test_item) - + self.config_agent.add_item(99, test_item) def test_add_item_raises_typeerror_on_invalid_value_type(self): - test_item = 'testinvalid' + test_item = "testinvalid" with self.assertRaises(TypeError): - response = self.config_agent.add_item('99_testitem', test_item) - + self.config_agent.add_item("99_testitem", test_item) def test_add_item_raises_valueerror_with_invalid_configitem_key(self): test_item = { - 'name': 'TESTITEM', - 'type': 'folder', - 'path': './testpath', - 'dict_size': '192m', - 'notallowed': 'notallowed', + "name": "TESTITEM", + "type": "folder", + "path": "./testpath", + "dict_size": "192m", + "notallowed": "notallowed", } with self.assertRaises(ValueError): - response = self.config_agent.add_item('99_testitem', test_item) - + self.config_agent.add_item("99_testitem", test_item) def test_add_item_raises_valueerror_with_invalid_configitemid(self): test_item = { - 'name': 'TESTITEM', - 'type': 'folder', - 'path': './testpath', - 'dict_size': '192m', + "name": "TESTITEM", + "type": "folder", + "path": "./testpath", + "dict_size": "192m", } with self.assertRaises(ValueError): - response = self.config_agent.add_item('invalidtestitemid', test_item) - + self.config_agent.add_item("invalidtestitemid", test_item) def test_add_item_raises_valueerror_with_no_item_name(self): test_item = { - 'name': None, - 'type': 'folder', - 'path': './testpath', - 'dict_size': '192m', + "name": None, + "type": "folder", + "path": "./testpath", + "dict_size": "192m", } with self.assertRaises(ValueError): - response = self.config_agent.add_item('99_testitem', test_item) - + self.config_agent.add_item("99_testitem", test_item) def test_add_item_raises_valueerror_with_no_item_path(self): test_item = { - 'name': 'TESTITEM', - 'type': 'folder', - 'path': None, - 'dict_size': '192m', + "name": "TESTITEM", + "type": "folder", + "path": None, + "dict_size": "192m", } with self.assertRaises(ValueError): - response = self.config_agent.add_item('99_testitem', test_item) - + self.config_agent.add_item("99_testitem", test_item) def test_get_target_config_returns_config(self): response = self.config_agent.get_target_config() - self.assertTrue(response['10_documents']['name'] == 'Documents') - + self.assertTrue(response["10_documents"]["name"] == "Documents") def test_get_global_config_returns_config(self): response = self.config_agent.get_global_config() - self.assertTrue(response['encryption_enabled'] == False) - + self.assertTrue(response["encryption_enabled"] is False) def test_get_output_root_path(self): response = self.config_agent.get_output_root_path() - self.assertTrue(response == self.config_agent._global_config['output_root_dir']) - + self.assertTrue(response == self.config_agent._global_config["output_root_dir"]) def test_get_encryption_password(self): response = self.config_agent.get_encryption_password() - self.assertTrue(response == self.config_agent._global_config['encryption_password']) - + self.assertTrue(response == self.config_agent._global_config["encryption_password"]) def test_set_encryption_password(self): - self.config_agent.set_encryption_password('Testpassword') + self.config_agent.set_encryption_password("Testpassword") response = self.config_agent.get_encryption_password() - self.assertTrue(response == 'Testpassword') - + self.assertTrue(response == "Testpassword") def test_validate_target_config(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } self.assertTrue(self.config_agent.validate_target_config(test_config)) - def test_validate_target_config_invalid_target_id(self): test_config = { - 'invalidconfigid': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "invalidconfigid": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } self.assertFalse(self.config_agent.validate_target_config(test_config)) - def test_validate_target_config_invalid_target_invalid_configitem_key(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'namesinvalid': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "namesinvalid": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } self.assertFalse(self.config_agent.validate_target_config(test_config)) - def test_validate_target_config_invalid_target_invalid_mxlevel(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 99, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 99, + "full_path": False, + }, } self.assertFalse(self.config_agent.validate_target_config(test_config)) - def test_validate_target_config_invalid_target_invalid_dictsize(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': 'xx192m', 'mx_level': 99, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "xx192m", + "mx_level": 99, + "full_path": False, + }, } self.assertFalse(self.config_agent.validate_target_config(test_config)) - def test_validate_target_config_invalid_target_path(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 99, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 99, + "full_path": False, + }, } self.assertFalse(self.config_agent.validate_target_config(test_config)) - def test_validate_target_config_blank_config(self): self.assertFalse(self.config_agent.validate_target_config()) - def test_validate_global_config(self): test_config = { - 'encryption_enabled': False, - 'encryption_password' : '', - 'output_root_dir': '.', + "encryption_enabled": False, + "encryption_password": "", + "output_root_dir": ".", } self.assertTrue(self.config_agent.validate_global_config(test_config)) - def test_validate_global_config_invalid_missing_required(self): test_config = { - 'encryption_enabled': False, - 'encryption_password' : '', + "encryption_enabled": False, + "encryption_password": "", } self.assertFalse(self.config_agent.validate_global_config(test_config)) - def test_validate_global_config_invalid_value(self): test_config = { - 'encryption_enabled': False, - 'encryption_password' : 99, - 'output_root_dir': '.', + "encryption_enabled": False, + "encryption_password": 99, + "output_root_dir": ".", } self.assertFalse(self.config_agent.validate_global_config(test_config)) - def test_validate_global_config_invalid_output_path(self): test_config = { - 'encryption_enabled': False, - 'encryption_password' : 'test', - 'output_root_dir': './notarealpath', + "encryption_enabled": False, + "encryption_password": "test", + "output_root_dir": "./fake", } self.assertFalse(self.config_agent.validate_global_config(test_config)) - def test_validate_global_config_blank_config(self): self.assertFalse(self.config_agent.validate_global_config()) - def test_save_YAML_config(self): with TemporaryDirectory() as temp_directory: - response = self.config_agent.save_YAML_config(os.path.join(temp_directory, 'test.yaml')) - + response = self.config_agent.save_YAML_config( + os.path.join(temp_directory, "test.yaml") + ) + with self.subTest(msg="config path is real path"): self.assertTrue(os.path.isfile(response)) with self.subTest(msg="test file has expected content"): - with open(response, 'r') as fin: + with open(response, "r") as fin: read_string = fin.read() - self.assertTrue('backup_targets' in read_string) - + self.assertTrue("backup_targets" in read_string) def test_save_YAML_config_invalid_given_directory(self): with TemporaryDirectory() as temp_directory: with self.assertRaises(IsADirectoryError): - response = self.config_agent.save_YAML_config(os.path.join(temp_directory)) - + self.config_agent.save_YAML_config(os.path.join(temp_directory)) def test_save_YAML_config_invalid_extension(self): with TemporaryDirectory() as temp_directory: with self.assertRaises(ValueError): - response = self.config_agent.save_YAML_config(os.path.join(temp_directory, 'test.invalid')) - + self.config_agent.save_YAML_config( + os.path.join(temp_directory, "test.invalid") + ) def test_parse_YAML_config(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } with TemporaryDirectory() as temp_directory: - save_path = self.config_agent.save_YAML_config(os.path.join(temp_directory, 'test.yaml'), test_config) + save_path = self.config_agent.save_YAML_config( + os.path.join(temp_directory, "test.yaml"), test_config + ) global_config, target_config = self.config_agent.parse_YAML_config_file(save_path) with self.subTest("test globalconfig"): - self.assertTrue(global_config['encryption_enabled'] == False) + self.assertTrue(global_config["encryption_enabled"] is False) with self.subTest("test targetconfig"): - self.assertTrue(target_config['10_documents']['type'] == 'folder') + self.assertTrue(target_config["10_documents"]["type"] == "folder") class TestReturnType(unittest.TestCase): - def setUp(self) -> None: self.config_agent = winbackup.configagent.ConfigAgent() self.windows_paths = winbackup.windowspaths.WindowsPaths() - def test_update_config_paths_returns_dict(self): response = self.config_agent.update_config_paths(self.windows_paths.get_paths()) self.assertTrue(type(response) == dict) - def test_update_config_paths_returns_nested_dict(self): response = self.config_agent.update_config_paths(self.windows_paths.get_paths()) - for k,v in response.items(): + for k, v in response.items(): with self.subTest(f"test {k} config_item is dict"): self.assertTrue(type(v) == dict) - def test_add_item_returns_dict(self): test_item = { - 'name': 'TESTITEM', - 'type': 'folder', - 'path': './testpath', - 'dict_size': '192m', + "name": "TESTITEM", + "type": "folder", + "path": "./testpath", + "dict_size": "192m", } - response = self.config_agent.add_item('99_test', test_item) + response = self.config_agent.add_item("99_test", test_item) self.assertTrue(type(response) == dict) - def test_get_target_config_returns_dict(self): response = self.config_agent.get_target_config() self.assertTrue(type(response) == dict) - def test_get_global_config_returns_dict(self): response = self.config_agent.get_global_config() self.assertTrue(type(response) == dict) - def test_get_output_root_path_returns_str(self): response = self.config_agent.get_output_root_path() self.assertTrue(type(response) == str) - def test_set_output_root_path_returns_dict(self): - response = self.config_agent.set_output_root_path('./test') + response = self.config_agent.set_output_root_path("./test") self.assertTrue(type(response) == dict) - def test_get_encryption_password_returns_str(self): response = self.config_agent.get_encryption_password() self.assertTrue(type(response) == str) - def test_set_encryption_password_path_returns_dict(self): - response = self.config_agent.set_encryption_password('test') + response = self.config_agent.set_encryption_password("test") self.assertTrue(type(response) == dict) - def test_validate_target_config_returns_bool(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } response = self.config_agent.validate_target_config(test_config) self.assertTrue(type(response) == bool) - def test_validate_global_config_returns_bool(self): test_config = { - 'encryption_enabled': False, - 'encryption_password' : '', - 'output_root_dir': '.', + "encryption_enabled": False, + "encryption_password": "", + "output_root_dir": ".", } response = self.config_agent.validate_global_config(test_config) self.assertTrue(type(response) == bool) - def test_save_YAML_config_returns_str(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } with TemporaryDirectory() as temp_directory: - response = self.config_agent.save_YAML_config(os.path.join(temp_directory, 'test.yaml'), test_config) + response = self.config_agent.save_YAML_config( + os.path.join(temp_directory, "test.yaml"), test_config + ) self.assertTrue(type(response) == str) - def test_parse_YAML_config_returns_tuple_of_dicts(self): test_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': '.', 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": ".", + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } with TemporaryDirectory() as temp_directory: - path = self.config_agent.save_YAML_config(os.path.join(temp_directory, 'test.yaml'), test_config) + path = self.config_agent.save_YAML_config( + os.path.join(temp_directory, "test.yaml"), test_config + ) response = self.config_agent.parse_YAML_config_file(path) with self.subTest("test is tuple"): self.assertTrue(type(response) == tuple) for item in response: with self.subTest("check items are dicts"): self.assertTrue(type(item) == dict) - - - - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main(verbosity=2) - diff --git a/tests/test_systemconfigsaver.py b/tests/test_systemconfigsaver.py index b0f6817..da2532f 100644 --- a/tests/test_systemconfigsaver.py +++ b/tests/test_systemconfigsaver.py @@ -10,155 +10,145 @@ from tempfile import TemporaryDirectory import winbackup.systemconfigsaver -class TestValidOutput(unittest.TestCase): +class TestValidOutput(unittest.TestCase): def setUp(self) -> None: self.config_saver = winbackup.systemconfigsaver.SystemConfigSaver() - def test_command_runner(self): - response = self.config_saver._command_runner(['powershell.exe', 'Write-Output', 'Test']).strip() - self.assertTrue(response == 'Test') - + response = self.config_saver._command_runner( + ["powershell.exe", "Write-Output", "Test"] + ).strip() + self.assertTrue(response == "Test") def test_set_videos_directory_path(self): - test_path = 'C:/Users' + test_path = "C:/Users" self.config_saver.set_videos_directory_path(test_path) self.assertTrue(self.config_saver.videos_path == test_path) - def test_save_config_files(self): """ Tests saves config files to disk and return value is a path """ with TemporaryDirectory() as temp_directory: - null_dev = open(os.devnull, 'w') + null_dev = open(os.devnull, "w") sys.stdout = null_dev response = self.config_saver.save_config_files(temp_directory) sys.stdout = sys.__stdout__ null_dev.close() files = [file for file in os.listdir(temp_directory)] num_files = len(files) - with self.subTest(msg='Number of files'): + with self.subTest(msg="Number of files"): self.assertTrue(num_files >= 9) - with self.subTest(msg='.ssh directory copied if exists'): - if os.path.exists(os.path.join(os.path.expanduser('~'), '.ssh')): - self.assertTrue(os.path.isdir(os.path.join(temp_directory, '.ssh'))) + with self.subTest(msg=".ssh directory copied if exists"): + if os.path.exists(os.path.join(os.path.expanduser("~"), ".ssh")): + self.assertTrue(os.path.isdir(os.path.join(temp_directory, ".ssh"))) else: - self.assertFalse(os.path.isdir(os.path.join(temp_directory, '.ssh'))) + self.assertFalse(os.path.isdir(os.path.join(temp_directory, ".ssh"))) with self.subTest(msg="config path is real path"): self.assertTrue(os.path.isdir(response)) - def test_save_winfetch(self): """ Will test a single line in the winfetch file that should always exist. """ with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_winfetch(temp_directory) - with open(os.path.join(temp_directory, 'winfetch_output.txt'), 'r') as fin: + self.config_saver.save_winfetch(temp_directory) + with open(os.path.join(temp_directory, "winfetch_output.txt"), "r") as fin: for line in fin.readlines(): - if line.startswith('OS:'): - self.assertTrue('Windows' in line) - + if line.startswith("OS:"): + self.assertTrue("Windows" in line) def test_save_installed_programs(self): """ - Will test a single line in the installedprograms file that should always exist. + Will test a single line in the installed programs file that should always exist. """ with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_installed_programs(temp_directory) - with open(os.path.join(temp_directory, 'installed_programs.csv'), 'r') as fin: + self.config_saver.save_installed_programs(temp_directory) + with open(os.path.join(temp_directory, "installed_programs.csv"), "r") as fin: for line in fin.readlines(): - if line.startswith('DisplayName'): - self.assertTrue('DisplayVersion' in line) - + if line.startswith("DisplayName"): + self.assertTrue("DisplayVersion" in line) def test_save_global_python_packages(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_global_python_packages(temp_directory) - with open(os.path.join(temp_directory, 'python_packages.txt'), 'r') as fin: + self.config_saver.save_global_python_packages(temp_directory) + with open(os.path.join(temp_directory, "python_packages.txt"), "r") as fin: line = fin.readlines()[0] - self.assertTrue('Python' in line) - + self.assertTrue("Python" in line) def test_save_choco_packages(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_choco_packages(temp_directory) - with open(os.path.join(temp_directory, 'choco_packages.txt'), 'r') as fin: + self.config_saver.save_choco_packages(temp_directory) + with open(os.path.join(temp_directory, "choco_packages.txt"), "r") as fin: line = fin.readlines()[0] - self.assertTrue('Chocolatey' in line) - + self.assertTrue("Chocolatey" in line) def test_save_vscode_extensions(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_vscode_extensions(temp_directory) - self.assertTrue(os.path.exists(os.path.join(temp_directory, 'vscode_extensions.txt'))) - + self.config_saver.save_vscode_extensions(temp_directory) + self.assertTrue( + os.path.exists(os.path.join(temp_directory, "vscode_extensions.txt")) + ) def test_save_path_env(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_path_env(temp_directory) - self.assertTrue(os.path.exists(os.path.join(temp_directory, 'path.txt'))) + self.config_saver.save_path_env(temp_directory) + self.assertTrue(os.path.exists(os.path.join(temp_directory, "path.txt"))) - @unittest.skipIf(not os.path.exists(os.path.join(os.path.expanduser('~'), '.ssh')), ".SSH Does not exist") + @unittest.skipIf(not os.path.exists(os.path.join(os.path.expanduser('~'), '.ssh')), ".SSH Does not exist") # fmt: skip def test_save_ssh_directory(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_ssh_directory(temp_directory) - self.assertTrue(os.path.isdir(os.path.join(temp_directory, '.ssh'))) - + self.config_saver.save_ssh_directory(temp_directory) + self.assertTrue(os.path.isdir(os.path.join(temp_directory, ".ssh"))) def test_videos_directory_filenames(self): with TemporaryDirectory() as temp_directory: videos_path = temp_directory - response = self.config_saver.save_videos_directory_filenames(temp_directory, videos_path) - with open(os.path.join(temp_directory, 'videos_tree.txt'), 'r') as fin: + self.config_saver.save_videos_directory_filenames(temp_directory, videos_path) + with open(os.path.join(temp_directory, "videos_tree.txt"), "r") as fin: line = fin.readlines()[0] - self.assertTrue('Folder' in line) - + self.assertTrue("Folder" in line) def test_save_file_associations(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_file_associations(temp_directory) - self.assertTrue(os.path.exists(os.path.join(temp_directory, 'file_associations.txt'))) - + self.config_saver.save_file_associations(temp_directory) + self.assertTrue( + os.path.exists(os.path.join(temp_directory, "file_associations.txt")) + ) def test_save_drivers(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_drivers(temp_directory) - with open(os.path.join(temp_directory, 'drivers.txt'), 'r') as fin: + self.config_saver.save_drivers(temp_directory) + with open(os.path.join(temp_directory, "drivers.txt"), "r") as fin: line = fin.readlines()[1] - self.assertTrue('Module Name' in line) - + self.assertTrue("Module Name" in line) def test_save_systeminfo(self): with TemporaryDirectory() as temp_directory: - response = self.config_saver.save_systeminfo(temp_directory) - with open(os.path.join(temp_directory, 'systeminfo.txt'), 'r') as fin: + self.config_saver.save_systeminfo(temp_directory) + with open(os.path.join(temp_directory, "systeminfo.txt"), "r") as fin: line = fin.readlines()[1] - self.assertTrue('Host Name:' in line) + self.assertTrue("Host Name:" in line) - - @unittest.skip('unfinished') + @unittest.skip("unfinished") def test_save_battery_report(self): pass - class TestReturnType(unittest.TestCase): - def setUp(self) -> None: self.config_saver = winbackup.systemconfigsaver.SystemConfigSaver() - def test_command_runner_returns_str(self): - response = self.config_saver._command_runner(['powershell.exe', 'Write-Output', 'Test']) + response = self.config_saver._command_runner( + ["powershell.exe", "Write-Output", "Test"] + ) self.assertTrue(type(response) == str) - def test_save_config_files_returns_str(self): with TemporaryDirectory() as temp_directory: - null_dev = open(os.devnull, 'w') + null_dev = open(os.devnull, "w") sys.stdout = null_dev response = self.config_saver.save_config_files(temp_directory) sys.stdout = sys.__stdout__ @@ -166,8 +156,5 @@ def test_save_config_files_returns_str(self): self.assertTrue(type(response) == str) - - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main(verbosity=2) - diff --git a/tests/test_windowspaths.py b/tests/test_windowspaths.py index 5cf3896..09db108 100644 --- a/tests/test_windowspaths.py +++ b/tests/test_windowspaths.py @@ -10,153 +10,121 @@ class TestValidPathsReturned(unittest.TestCase): - def setUp(self) -> None: self.windows_paths = winbackup.windowspaths.WindowsPaths() - def test_get_windows_path_documents(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_DOCUMENTS) self.assertTrue(os.path.isdir(path)) - def test_get_windows_path_pictures(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PICTURES) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_videos(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_VIDEOS) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_music(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_MUSIC) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_localappdata(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_LOCALAPPDATA) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_roamingappdata(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_ROAMINGAPPDATA) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_desktop(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_DESKTOP) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_downloads(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_DOWNLOADS) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_savedgames(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_SAVEDGAMES) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_publicdocuments(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PUBLICDOCUMENTS) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_programdata(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PROGRAMDATA) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_programfiles(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PROGRAMFILES) self.assertTrue(os.path.isdir(path)) - - + def test_get_windows_path_programfilesx86(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PROGRAMFILESX86) self.assertTrue(os.path.isdir(path)) - class TestReturnType(unittest.TestCase): - def setUp(self) -> None: self.windows_paths = winbackup.windowspaths.WindowsPaths() - def test_get_paths_returns_dict(self): self.assertTrue(type(self.windows_paths.get_paths() is dict)) - def test_get_windows_path_documents_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_DOCUMENTS) self.assertTrue(type(path) == str) - def test_get_windows_path_pictures_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PICTURES) self.assertTrue(type(path) == str) - - + def test_get_windows_path_videos_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_VIDEOS) self.assertTrue(type(path) == str) - - + def test_get_windows_path_music_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_MUSIC) self.assertTrue(type(path) == str) - - + def test_get_windows_path_localappdata_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_LOCALAPPDATA) self.assertTrue(type(path) == str) - - + def test_get_windows_path_roamingappdata_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_ROAMINGAPPDATA) self.assertTrue(type(path) == str) - - + def test_get_windows_path_desktop_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_DESKTOP) self.assertTrue(type(path) == str) - def test_get_windows_path_downloads_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_DOWNLOADS) self.assertTrue(type(path) == str) - - + def test_get_windows_path_savedgames_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_SAVEDGAMES) self.assertTrue(type(path) == str) - - + def test_get_windows_path_publicdocuments_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PUBLICDOCUMENTS) self.assertTrue(type(path) == str) - - + def test_get_windows_path_programdata_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PROGRAMDATA) self.assertTrue(type(path) == str) - - + def test_get_windows_path_programfiles_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PROGRAMFILES) self.assertTrue(type(path) == str) - - + def test_get_windows_path_programfilesx86_returns_string(self): path = self.windows_paths.get_windows_path(self.windows_paths.FOLDERID_PROGRAMFILESX86) self.assertTrue(type(path) == str) - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main(verbosity=2) - diff --git a/tests/test_zip7archiver.py b/tests/test_zip7archiver.py index 4871a88..005dcac 100644 --- a/tests/test_zip7archiver.py +++ b/tests/test_zip7archiver.py @@ -11,89 +11,76 @@ class TestValidArchive(unittest.TestCase): - def setUp(self) -> None: self.archiver = winbackup.zip7archiver.Zip7Archiver() self.temp_directory = tempfile.TemporaryDirectory() self.temp_path = os.path.join(self.temp_directory.name) self.testdir_path = self._fill_temp_dir_with_files(self.temp_path) - def tearDown(self) -> None: self.temp_directory.cleanup() - def _fill_temp_dir_with_files(self, path) -> None: - subfolder_path = os.path.join(path, 'test1') + subfolder_path = os.path.join(path, "test1") os.mkdir(subfolder_path) - for i in range(1,1000,1): - with open(os.path.join(subfolder_path, f'test_{i}.txt'), 'wb') as fout: + for i in range(1, 1000, 1): + with open(os.path.join(subfolder_path, f"test_{i}.txt"), "wb") as fout: fout.write(os.urandom(32768)) return subfolder_path - def test_backup_folder(self): - filename = 'test_backup.7z' - response = self.archiver.backup_folder(filename, - self.testdir_path, - self.temp_path, - password='', quiet=True) + filename = "test_backup.7z" + self.archiver.backup_folder( + filename, + self.testdir_path, + self.temp_path, + password="", + quiet=True, + ) self.assertTrue(os.path.isfile(os.path.join(self.temp_path, filename))) - - def test_backup_plex_folder(self): - filename = 'test_plex_backup.7z' - response = self.archiver.backup_plex_folder(filename, - self.testdir_path, - self.temp_path, - password='', quiet=True) - #split force is currently set for plex server so files will end with a number - self.assertTrue(os.path.isfile(os.path.join(self.temp_path, filename + '.001'))) + def test_backup_folder_tar_before(self): + filename = "test_plex_backup.7z" + self.archiver.backup_folder( + filename, + self.testdir_path, + self.temp_path, + password="", + quiet=True, + tar_before_7z=True, + ) + # split force is currently set for plex server so files will end with a number + self.assertTrue(os.path.isfile(os.path.join(self.temp_path, filename))) class TestReturnType(unittest.TestCase): - def setUp(self) -> None: self.archiver = winbackup.zip7archiver.Zip7Archiver() self.temp_directory = tempfile.TemporaryDirectory() self.temp_path = self.temp_directory.name self.testdir_path = self._fill_temp_dir_with_files(self.temp_path) - def tearDown(self) -> None: self.temp_directory.cleanup() - def _fill_temp_dir_with_files(self, path) -> None: - subfolder_path = os.path.join(path, 'test1') + subfolder_path = os.path.join(path, "test1") os.mkdir(subfolder_path) - for i in range(1,1000,1): - with open(os.path.join(subfolder_path, f'test_{i}.txt'), 'wb') as fout: + for i in range(1, 1000, 1): + with open(os.path.join(subfolder_path, f"test_{i}.txt"), "wb") as fout: fout.write(os.urandom(32768)) return subfolder_path - - def test_get_path_size_returns_int(self): - response = self.archiver._get_path_size(self.testdir_path) + def test_get_paths_size_returns_int(self): + response = self.archiver._get_paths_size(self.testdir_path) self.assertTrue(type(response) == int) - def test_backup_folder_returns_tuple(self): - response = self.archiver.backup_folder('test_backup_return.7z', - self.testdir_path, - self.temp_path, - password='', quiet=True) + response = self.archiver.backup_folder( + "test_backup_return.7z", self.testdir_path, self.temp_path, password="", quiet=True + ) self.assertTrue(type(response) == tuple) - def test_backup_plex_folder_returns_tuple(self): - response = self.archiver.backup_plex_folder('test_backup_plex_return.7z', - self.testdir_path, - self.temp_path, - password='', quiet=True) - self.assertTrue(type(response) == tuple) - - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main(verbosity=2) - diff --git a/winbackup/__init__.py b/winbackup/__init__.py index 13174e0..8f4b756 100644 --- a/winbackup/__init__.py +++ b/winbackup/__init__.py @@ -1,4 +1,5 @@ __copyright__ = "Copyright (C) 2021-2022 Joe Campbell" + # 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 @@ -13,5 +14,5 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see < https: // www.gnu.org/licenses/>. -__version__ = "0.1.7-beta" +__version__ = "0.2.0-beta" __license__ = "GPLv3" # GNU General Public License v3 diff --git a/winbackup/__main__.py b/winbackup/__main__.py index f702772..dd641b1 100644 --- a/winbackup/__main__.py +++ b/winbackup/__main__.py @@ -16,58 +16,79 @@ import sys import logging +import platform from argparse import ArgumentParser, RawTextHelpFormatter from . import __version__, __license__, __copyright__ from . import winbackup DEFAULT_LOG_LEVEL = logging.INFO -def get_commandline_arguments() -> dict: + +def get_cli_args() -> dict: """ Parse command line arguments. """ helpstring = """ - Backupscript for windows. + Backups for windows. Backs up user folders to 7z archives. Can also back up Plex Media Server, Hyper-V VMs and VirtualBox VMs - + winbackup version {} - This program comes with ABSOLUTELY NO WARRANTY. - This is free software, and you are welcome to redistribute it + This program comes with ABSOLUTELY NO WARRANTY. + This is free software, and you are welcome to redistribute it under certain conditions, see the GPLv3 Licence file attached. - {} - Licence: {} """.format(__version__, __copyright__, __license__) + {} - Licence: {} """.format( + __version__, __copyright__, __license__ + ) + # fmt: off parser = ArgumentParser(description=helpstring, formatter_class=RawTextHelpFormatter) parser.add_argument("path", type=str, help="The Path that should contain the backup", nargs="?") - parser.add_argument('-a', '--all', help="Backup all options selectable.", action="store_true") - parser.add_argument('-c', '--configfile', help="supply a configuration file.", action="store_true") - parser.add_argument('-C', '--create-configfile', help="Generate default configuration file. If no path given will save to CWD.", action="store_true") - parser.add_argument('-i', '--interactive-config', help="Generate a configuration file interactively", action="store_true") - parser.add_argument('-v', '--verbose', help="Enable verbose logging. Log will initially output to the CWD.", action="store_true") #?store_const - parser.add_argument('-V', '--version', action='version', version=__version__) + parser.add_argument("-a", "--all", help="Backup all options selectable.", action="store_true") + parser.add_argument("-c", "--configfile", help="supply a configuration file.", action="store_true") + parser.add_argument("-C", "--create-configfile", help="Generate default configuration file. If no path given will save to CWD.", action="store_true") + parser.add_argument("-i", "--interactive-config", help="Generate a configuration file interactively", action="store_true") + parser.add_argument("-q", "--quiet", help="Minimal terminal output.", action="store_true") + parser.add_argument("-v", "--verbose", help="Enable verbose logging. Log will initially output to the CWD.", action="store_true") + parser.add_argument("-V", "--version", action="version", version=__version__) + parser.add_argument("-y", "--autoconfirm", help="Run without confirmation. Defaults to no password if not run with config file.", action="store_true") args = vars(parser.parse_args()) return args + # fmt: on def cli(): - cli_args = get_commandline_arguments() - - path = cli_args['path'] - if cli_args['verbose']: + cli_args = get_cli_args() + + if not platform.system() == "Windows": + print(" Only Windows is supported by this program.") + sys.exit(1) + + path = cli_args["path"] + if cli_args["verbose"]: log_level = logging.DEBUG else: log_level = DEFAULT_LOG_LEVEL win_backup = winbackup.WinBackup(log_level) - if cli_args['configfile']: - win_backup.run_from_config_file(path) - elif cli_args['create_configfile']: + if cli_args["configfile"]: + win_backup.run_from_config_file( + path, + quiet=cli_args["quiet"], + auto_confirm=cli_args["autoconfirm"], + ) + elif cli_args["create_configfile"]: win_backup.generate_blank_configfile(path) - elif cli_args['interactive_config']: - win_backup.interactive_config_builder(path) + elif cli_args["interactive_config"]: + win_backup.interactive_config_builder(path, all_selected=cli_args["all"]) else: - win_backup.cli(path) - + win_backup.cli( + path, + all_selected=cli_args["all"], + quiet=cli_args["quiet"], + auto_confirm=cli_args["autoconfirm"], + ) + -if __name__ == '__main__': - sys.exit(cli()) \ No newline at end of file +if __name__ == "__main__": + sys.exit(cli()) diff --git a/winbackup/bin/7z/7z.dll b/winbackup/bin/7z/7z.dll index b32d7bf..2364211 100644 Binary files a/winbackup/bin/7z/7z.dll and b/winbackup/bin/7z/7z.dll differ diff --git a/winbackup/bin/7z/7z.exe b/winbackup/bin/7z/7z.exe index 37b8514..a5183de 100644 Binary files a/winbackup/bin/7z/7z.exe and b/winbackup/bin/7z/7z.exe differ diff --git a/winbackup/configagent.py b/winbackup/configagent.py index e19b929..25a22dc 100644 --- a/winbackup/configagent.py +++ b/winbackup/configagent.py @@ -24,6 +24,7 @@ from . import __version__ from datetime import datetime + class ConfigAgent: def __init__(self, paths=None) -> None: """ @@ -36,68 +37,134 @@ def __init__(self, paths=None) -> None: ### target_config is a dictionary with target_id as key config_item dictionary as value ### config_item dictionary keys: # name - target name, will also be used to derive the output folder name - # type - folder = single folder target, special = specific backup function (reqiring specific methods) + # type - folder = simple folders, special = special backup function # path - backup target path # enabled - if the target will be backed up, default false for all - # dict_size and mx_level - 7z dictionary size and compression level (See 7z cli docs for explanation) + # dict_size and mx_level - 7z dictionary size and compression level (ref 7z cli docs) # full path - store the full path to the compressed files. Defaults to relative paths. self._base_config_item = { - 'name': None, - 'type': 'folder', - 'path': None, - 'enabled': False, - 'dict_size': '192m', - 'mx_level': 9, - 'full_path': False + "name": None, + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + "tar_before_7z": False, + "extra_tar_flags": [], + "extra_7z_flags": [], } self._base_target_config = { - '01_config': {'name': 'System Config', 'type': 'special', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '10_documents': {'name': 'Documents', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '11_desktop': {'name': 'Desktop', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '12_pictures': {'name': 'Pictures', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '32m', 'mx_level': 5, 'full_path': False}, - '13_downloads': {'name': 'Downloads', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, - '14_videos': {'name': 'Videos', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '32m', 'mx_level': 4, 'full_path': False}, - '15_music': {'name': 'Music', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '32m', 'mx_level': 4, 'full_path': False}, - '16_saved_games': {'name': 'Saved Games', 'type': 'folder', 'path': None, 'enabled': False, 'dict_size': '192m', 'mx_level': 9, 'full_path': False}, + "01_config": { + "name": "System Config", + "type": "special", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "10_documents": { + "name": "Documents", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "11_desktop": { + "name": "Desktop", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "12_pictures": { + "name": "Pictures", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "32m", + "mx_level": 5, + "full_path": False, + }, + "13_downloads": { + "name": "Downloads", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, + "14_videos": { + "name": "Videos", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "32m", + "mx_level": 4, + "full_path": False, + }, + "15_music": { + "name": "Music", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "32m", + "mx_level": 4, + "full_path": False, + }, + "16_saved_games": { + "name": "Saved Games", + "type": "folder", + "path": None, + "enabled": False, + "dict_size": "192m", + "mx_level": 9, + "full_path": False, + }, } self._base_global_config = { - 'encryption_enabled': False, - 'encryption_password' : '', - 'output_root_dir': '.', + "encryption_enabled": False, + "encryption_password": "", + "output_root_dir": ".", } self._global_config = {} self._target_config = {} if paths: self._target_config = self.update_config_paths(paths) - - def update_config_paths(self, paths:dict, config:dict=None) -> dict: + def update_config_paths(self, paths: dict, config: dict = None) -> dict: """ - Base config does not have folder paths configured. Call with paths dictionary to update config. + Base config does not have folder paths configured. Call with paths dictionary to update. Parameters: - paths : dict of folder paths. Keys = text component of target id. - config : target_config to be updated. class variable used if not specified. Returns: - config : updated target_config. - """ - if not config: if not self._target_config: config = self._base_target_config else: config = self._target_config - + ## add the paths into config updated_config = config.copy() for id in config.keys(): try: - if updated_config[id]['type'].lower() == 'folder': - updated_config[id]['path'] = paths[id.split('_', 1)[1].lower()] - logging.debug(f"Path updated -> {id} is {paths[id.split('_', 1)[1].lower()]}") + if updated_config[id]["type"].lower() == "folder": + updated_config[id]["path"] = paths[id.split("_", 1)[1].lower()] + logging.debug( + f"Path updated -> {id} is {paths[id.split('_', 1)[1].lower()]}" + ) except KeyError: logging.error(f"Path not found for {id}") except Exception as e: @@ -109,29 +176,45 @@ def update_config_paths(self, paths:dict, config:dict=None) -> dict: self._target_config = updated_config return updated_config - - def add_item(self, config_item_id:str, config_item:dict, config:dict=None) -> dict: + def add_item(self, config_item_id: str, config_item: dict, config: dict = None) -> dict: """ Add a backup row to the config file. Missing config items in config_item_id will be added with defaults. Parameters: - config_item_id : key of the item, acts as an id format 00_name - - config_item : dictionary of the config items. - - config : target_config dictionary to be added to. If not specified uses class variable. + - config_item : dictionary of the config items. + - config : target_config dictionary to be added to. If not specified uses class variable. Returns: - config : new config with added item """ - #basic item validation before adding to config + # basic item validation before adding to config if not type(config_item_id) == str: - raise TypeError(f"config_item_id {config_item_id} not valid. Must be of type str, is of type {type(config_item_id)}") + raise TypeError( + f"config_item_id {config_item_id} not valid. Must be of type str, is of type {type(config_item_id)}" + ) if not type(config_item) == dict: - raise TypeError(f"config_item {config_item} not valid. Must be of type dict, is of type {type(config_item)}") + raise TypeError( + f"config_item {config_item} not valid. Must be of type dict, is of type {type(config_item)}" + ) for key in config_item: - if key not in {'name', 'type', 'path', 'enabled', 'dict_size', 'mx_level', 'full_path'}: + if key not in { + "name", + "type", + "path", + "enabled", + "dict_size", + "mx_level", + "full_path", + "tar_before_7z", + "extra_7z_flags", + "extra_tar_flags", + }: raise ValueError(f"Key {key} in config_item not permitted.") - if not re.match('(\d{2}_[a-z_]+)', config_item_id): - raise ValueError(f"Invalid config_item_id: {config_item_id}. must be in the format 00_name or 00_long_name") - + if not re.match(r"(\d{2}_[a-z_]+)", config_item_id): + raise ValueError( + f"Invalid config_item_id: {config_item_id}. must be in the format 00_name or 00_long_name" + ) + if not config: if not self._target_config: config = self._base_target_config @@ -141,11 +224,13 @@ def add_item(self, config_item_id:str, config_item:dict, config:dict=None) -> di new_config_item = self._base_config_item.copy() new_config_item.update(config_item) - if new_config_item['name'] == None: + if new_config_item["name"] is None: raise ValueError("Name is required for config_item to be added to target_config") - if new_config_item['type'] == 'folder': - if new_config_item['path'] == None: - raise ValueError("path is required for config_item with type folder to be added to target_config") + if new_config_item["type"] == "folder": + if new_config_item["path"] is None: + raise ValueError( + "path is required for config_item with type folder to be added to target_config" + ) logging.debug(f"config_item argument - {config_item}") logging.debug(f"combined config item - {new_config_item}") @@ -153,27 +238,28 @@ def add_item(self, config_item_id:str, config_item:dict, config:dict=None) -> di try: config[config_item_id] = new_config_item except Exception as e: - logging.error(f"Could not add config_item {config_item_id} to config, exception {e}") + logging.error( + f"Could not add config_item {config_item_id} to config, exception - {e}" + ) logging.debug(traceback.format_exc()) raise - # sort config again to maintain expected order. - sorted_config = {k:v for k,v in sorted(config.items())} + sorted_config = {k: v for k, v in sorted(config.items())} self._target_config = sorted_config return sorted_config - def get_target_config(self) -> dict: """ Returns a config dict to be used by the backup functions for targets. """ if not self._target_config: - logging.warning("Called before folder paths set. Base target_config used. Paths must be set before attempting backup.") + logging.warning( + "Called before folder paths set. Base target_config used. Paths must be set before attempting backup." + ) self._target_config = self._base_target_config return self._target_config - - def set_target_config(self, new_target_config:dict) -> None: + def set_target_config(self, new_target_config: dict) -> None: """ if the target_config is set - update if not - create from supplied config @@ -182,7 +268,6 @@ def set_target_config(self, new_target_config:dict) -> None: config.update(new_target_config) self._target_config = config - def get_global_config(self) -> dict: """ Returns the global config dictionary. @@ -193,8 +278,7 @@ def get_global_config(self) -> dict: return self._global_config - - def set_global_config(self, new_global_config:dict) -> None: + def set_global_config(self, new_global_config: dict) -> None: """ if the global_config is set - update if not - create from supplied config @@ -203,7 +287,6 @@ def set_global_config(self, new_global_config:dict) -> None: config.update(new_global_config) self._global_config = config - def get_output_root_path(self) -> str: """ Returns the output root path from the global config. @@ -211,10 +294,9 @@ def get_output_root_path(self) -> str: """ if not self._global_config: self._global_config = self._base_global_config - return self._global_config['output_root_dir'] - + return self._global_config["output_root_dir"] - def set_output_root_path(self, path:str) -> dict: + def set_output_root_path(self, path: str) -> dict: """ Set the output path in the global config. Returns the global config. @@ -223,12 +305,11 @@ def set_output_root_path(self, path:str) -> dict: raise TypeError("Path must be of type str") if not self._global_config: self._global_config = self._base_global_config - - self._global_config['output_root_dir'] = path + + self._global_config["output_root_dir"] = path return self._global_config - def get_encryption_password(self) -> str: """ Return the encryption password from the global config. @@ -236,10 +317,9 @@ def get_encryption_password(self) -> str: """ if not self._global_config: self._global_config = self._base_global_config - return self._global_config['encryption_password'] - + return self._global_config["encryption_password"] - def set_encryption_password(self, password:str) -> dict: + def set_encryption_password(self, password: str) -> dict: """ Sets the encryption key in the global config. Returns the global config. @@ -248,13 +328,12 @@ def set_encryption_password(self, password:str) -> dict: raise TypeError("Password must be of type str") if not self._global_config: self._global_config = self._base_global_config - - self._global_config['encryption_password'] = password - return self._global_config + self._global_config["encryption_password"] = password + return self._global_config - def validate_target_config(self, config:dict=None) -> bool: + def validate_target_config(self, config: dict = None) -> bool: """ Validate the supplied config dictionary. Returns true if the config is valid @@ -267,88 +346,140 @@ def validate_target_config(self, config:dict=None) -> bool: if not config: logging.error("Target config not set.") return False - + ## check id's match format 00_name 00_long_name for id in config: - if not re.match('(\d{2}_[a-z_]+)', id): - logging.warning(f"Invalid Target ID for {id}. must be in the format 00_name or 00_long_name") - print(f"Invalid Target ID for {id}. must be in the format 00_name or 00_long_name") + if not re.match(r"(\d{2}_[a-z_]+)", id): + logging.warning( + f"Invalid Target ID for {id}. must be in the format 00_name or 00_long_name" + ) + print( + f"Invalid Target ID for {id}. must be in the format 00_name or 00_long_name" + ) valid_config_flag = False - + for id, config_item in config.items(): ## check config_items have valid keys and that all keys are present. - valid_keys = {'name', 'type', 'path', 'enabled', 'dict_size', 'mx_level', 'full_path'} + valid_keys = { + "name", + "type", + "path", + "enabled", + "dict_size", + "mx_level", + "full_path", + "tar_before_7z", + "extra_7z_flags", + "extra_tar_flags", + } + required_keys = { + "name", + "type", + "path", + "enabled", + } for key, value in config_item.items(): if key not in valid_keys: - logging.warning(f"Unknown Key {key} in config_item for {id}. This will be ignored.") + logging.warning(f"Unknown Key {key} in config_item for {id}. This will be ignored.") # fmt: skip print(f"Unknown Key {key} in config_item for {id}. This will be ignored.") + if key in required_keys: + required_keys.remove(key) if key in valid_keys: valid_keys.remove(key) - # check valid key types + # check valid key types valid_type = True - if key in {'name', 'type', 'dict_size'}: + if key in {"name", "type", "dict_size"}: if type(value) != str: valid_type = False - if key in {'enabled', 'full_path'}: + if key in {"enabled", "full_path"}: if type(value) != bool: valid_type = False - if key in {'mx_level'}: + if key in {"mx_level"}: if type(value) != int: valid_type = False - if key in {'path'} and config_item['type'] == 'folder': + if key in {"path"} and config_item["type"] == "folder": if type(value) not in {str, list}: - valid_type = False + valid_type = False if not valid_type: - logging.error(f"Invalid type ({type(value)} for {key} in config_item for {id}.") - print(f"Invalid type ({type(value)} for {key} in config_item for {id}.") - valid_config_flag = False + logging.error(f"Invalid type ({type(value)} for {key} in config_item for {id}.") # fmt: skip + print(f"Invalid type ({type(value)} for {key} in config_item for {id}.") + valid_config_flag = False - if len(valid_keys) != 0: + if len(required_keys) != 0: + print(f"Required keys {valid_keys} not in config_item for {id}.") logging.error(f"Required keys {valid_keys} not in config_item for {id}.") - print(f"Required keys {valid_keys} not in config_item for {id}.") valid_config_flag = False + if len(valid_keys) != 0: + print(f"Keys {valid_keys} not in config_item for {id}.") + print("Missing keys will be filled with default values") + logging.warning(f"Valid keys {valid_keys} not in config_item for {id}.") + logging.warning("Missing keys will be filled with default values") + for key in valid_keys: + base_config = self._base_config_item.copy() + config_item[key] = base_config[key] + logging.info(f" {key} for {id} set to default of {base_config[key]}") + print(f" {key} for {id} set to default of {base_config[key]}") ## check mx_level within 0-9 - if 'mx_level' in config_item: - if not 0 <= config_item['mx_level'] <= 9: - logging.error(f"mx_level {config_item['mx_level']} for {id} not valid. Must be in range 0-9.") - print(f"mx_level {config_item['mx_level']} for {id} not valid. Must be in range 0-9.") + if "mx_level" in config_item: + if not 0 <= config_item["mx_level"] <= 9: + logging.error( + f"mx_level {config_item['mx_level']} for {id} not valid. Must be in range 0-9." + ) + print( + f"mx_level {config_item['mx_level']} for {id} not valid. Must be in range 0-9." + ) valid_config_flag = False ## check dict_size is valid - if 'dict_size' in config_item: - if not re.match('(^\d+[bkmg]?$)', config_item['dict_size']): - logging.error(f"dict_size {config_item['dict_size']} for {id} not valid. Must int followed by quantifier in bkmg") - print(f"dict_size {config_item['dict_size']} for {id} not valid. Must int followed by quantifier in bkmg") + if "dict_size" in config_item: + if not re.match(r"(^\d+[bkmg]?$)", config_item["dict_size"]): + logging.error( + f"dict_size {config_item['dict_size']} for {id} not valid. Must int followed by quantifier as b/k/m/g" + ) + print( + f"dict_size {config_item['dict_size']} for {id} not valid. Must int followed by quantifier as b/k/m/g" + ) valid_config_flag = False ## check paths in target_config exist and are readable - if 'path' in config_item and config_item['path'] != None: - path_value = config_item['path'] + if "path" in config_item and config_item["path"] is not None: + path_value = config_item["path"] test_paths = [] if type(path_value) == str: test_paths.append(path_value) else: test_paths = path_value - ## As long as the path is a directory the backup will run. + ## As long as the path is a directory the backup will run. # If the target is not readable (according to access flags) the backup will continue but will probably fail - warn user. for path in test_paths: if os.path.exists(path): if os.path.isdir(path): if not os.access(path, os.R_OK): - logging.warning(f"Target directory {path} for {id} exists but is not readable. Check access permissions.") - print(f"Target directory {path} for {id} exists but is not readable. Check access permissions.") + logging.warning( + f"Target directory {path} for {id} exists but is not readable. Check access permissions." + ) + print( + f"Target directory {path} for {id} exists but is not readable. Check access permissions." + ) else: - logging.error(f"Target path {path} for {id} exists but is not a directory. Backup target must be a directory.") - print(f"Target path {path} for {id} exists but is not a directory. Backup target must be a directory.") + logging.error( + f"Target path {path} for {id} exists but is not a directory. Backup target must be a directory." + ) + print( + f"Target path {path} for {id} exists but is not a directory. Backup target must be a directory." + ) valid_config_flag = False else: - logging.error(f"Target path {path} does not exist but was specified for {id}. Check configuration.") - print(f"Target path {path} does not exist but was specified for {id}. Check configuration.") + logging.error( + f"Target path {path} does not exist but was specified for {id}. Check configuration." + ) + print( + f"Target path {path} does not exist but was specified for {id}. Check configuration." + ) valid_config_flag = False return valid_config_flag - - def validate_global_config(self, global_config:dict=None) -> bool: + def validate_global_config(self, global_config: dict = None) -> bool: """ validate the supplied global config values """ @@ -360,8 +491,8 @@ def validate_global_config(self, global_config:dict=None) -> bool: return False for key in global_config: - valid_keys = {'encryption_password', 'output_root_dir', 'encryption_enabled'} - required_keys = {'output_root_dir'} + valid_keys = {"encryption_password", "output_root_dir", "encryption_enabled"} + required_keys = {"output_root_dir"} if key not in valid_keys: logging.warning(f"Unknown Key {key} in global config. This will be ignored.") print(f"Unknown Key {key} in global config. This will be ignored.") @@ -371,44 +502,60 @@ def validate_global_config(self, global_config:dict=None) -> bool: if len(required_keys) != 0: for key in valid_keys: logging.error(f"Required key {key} not in global config.") - print(f"Required key {key} not in global config.") + print(f"Required key {key} not in global config.") valid_config_flag = False for key, value in global_config.items(): valid_type = True - if key in {'encryption_password', 'output_root_dir'}: + if key in {"encryption_password", "output_root_dir"}: if type(value) != str: valid_type = False - if not valid_type: - logging.error(f"Invalid type ({type(value)} for {key} in config_item for {id}.") + if not valid_type: + logging.error( + f"Invalid type ({type(value)} for {key} in config_item for {id}." + ) print(f"Invalid type ({type(value)} for {key} in config_item for {id}.") valid_config_flag = False - - if 'output_root_dir' in global_config: - if os.path.isdir(global_config['output_root_dir']): - if not os.access(global_config['output_root_dir'], os.W_OK): - logging.warning(f"Output directory {global_config['output_root_dir']} is not writeable. Check access permissions.") - print(f"Output directory {global_config['output_root_dir']} is not writeable. Check access permissions.") + + if "output_root_dir" in global_config: + if os.path.isdir(global_config["output_root_dir"]): + if not os.access(global_config["output_root_dir"], os.W_OK): + logging.warning( + f"Output directory {global_config['output_root_dir']} is not writeable. Check access permissions." + ) + print( + f"Output directory {global_config['output_root_dir']} is not writeable. Check access permissions." + ) valid_config_flag = False else: - logging.error(f"Output Directory path {global_config['output_root_dir']} does not exist. Check configuration.") - print(f"Output Directory path {global_config['output_root_dir']} does not exist. Check configuration.") + logging.error( + f"Output Directory path {global_config['output_root_dir']} does not exist. Check configuration." + ) + print( + f"Output Directory path {global_config['output_root_dir']} does not exist. Check configuration." + ) valid_config_flag = False return valid_config_flag - - def save_YAML_config(self, path:str, target_config:dict=None, global_config:dict=None) -> str: + def save_YAML_config( + self, + path: str, + target_config: dict = None, + global_config: dict = None, + ) -> str: """ - Saves a YAML config to the chosen path for modification and use. + Saves a YAML config to the chosen path for modification and use. Returns the path where the config was saved """ if not target_config: target_config = self._target_config if not target_config: target_config = self._base_target_config - logging.warning(f"Saved config template does not have correct user paths. It is recommended to update config paths first.") + logging.warning( + "Saved config template does not have correct user paths. It is recommended to update config paths first." + ) if not global_config: global_config = self._global_config @@ -417,17 +564,14 @@ def save_YAML_config(self, path:str, target_config:dict=None, global_config:dict if os.path.isdir(path): raise IsADirectoryError("Path must be to a file, not a directory") - - if not path.endswith(('.yaml', '.yml')): + + if not path.endswith((".yaml", ".yml")): raise ValueError("File type saved must be YAML") - + ## combine dictionaries - combined_config = { - 'global': global_config, - 'backup_targets': target_config - } + combined_config = {"global": global_config, "backup_targets": target_config} - with open(os.path.join(path), 'w', newline='\n') as fout: + with open(os.path.join(path), "w", newline="\n") as fout: ## write comments to file with winbackup version and windows version fout.write(f"# WinBackup config file \n# WinBackup Version {__version__}\n") @@ -438,39 +582,38 @@ def save_YAML_config(self, path:str, target_config:dict=None, global_config:dict yaml.safe_dump(combined_config, fout, sort_keys=False, indent=2) except Exception as e: logging.error(f"could not dump configuration to yaml, exception {e}") - - return path + return path - def parse_YAML_config_file(self, path:str) -> tuple: + def parse_YAML_config_file(self, path: str) -> tuple: """ Parse the YAML config from file and return as a tuple of dictionaries (global, target) config file is also saved within class instance as self.config Validates config after loading - raises ValueError if config invalid. """ - ##load config - with open(path, 'r') as fin: + # load config + with open(path, "r") as fin: config = yaml.safe_load(fin) - - loaded_global_config = config['global'] - loaded_target_config = config['backup_targets'] + + loaded_global_config = config["global"] + loaded_target_config = config["backup_targets"] merged_global_config = self._base_global_config.copy() merged_global_config.update(loaded_global_config) - if len(merged_global_config['encryption_password']) != 0: - logging.debug(f'Encryption password from configfile is not empty, enable encryption') - merged_global_config['encryption_enabled'] = True + if len(merged_global_config["encryption_password"]) != 0: + logging.debug( + "Encryption password from configfile is not empty, enable encryption" + ) + merged_global_config["encryption_enabled"] = True if not self.validate_global_config(loaded_global_config): raise ValueError(f"Invalid global config loaded from {path}") if not self.validate_target_config(loaded_target_config): raise ValueError(f"Invalid target config loaded from {path}") - self._global_config = merged_global_config self._target_config = loaded_target_config - return merged_global_config, loaded_target_config - + return merged_global_config, loaded_target_config encryption_password = property(get_encryption_password, set_encryption_password) output_root_dir = property(get_output_root_path, set_output_root_path) @@ -479,19 +622,22 @@ def parse_YAML_config_file(self, path:str) -> tuple: if __name__ == "__main__": - logging.basicConfig(stream= sys.stdout, - format='%(asctime)s - %(levelname)s - %(message)s', - level = logging.DEBUG) - tmp_dir = os.path.join('.', 'tmp') + logging.basicConfig( + stream=sys.stdout, + format="%(asctime)s - %(levelname)s - %(message)s", + level=logging.DEBUG, + ) + tmp_dir = os.path.join(".", "tmp") if not os.path.exists(tmp_dir): os.mkdir(tmp_dir) import windowspaths + win_paths = windowspaths.WindowsPaths() config_agent = ConfigAgent() - config_agent.set_encryption_password('test') + config_agent.set_encryption_password("test") print(config_agent.output_root_dir) config_agent.update_config_paths(win_paths.get_paths()) - config_agent.save_YAML_config('./tmp/config.yaml') - config_agent.parse_YAML_config_file('./tmp/config.yaml') + config_agent.save_YAML_config("./tmp/config.yaml") + config_agent.parse_YAML_config_file("./tmp/config.yaml") - sys.exit() \ No newline at end of file + sys.exit() diff --git a/winbackup/systemconfigsaver.py b/winbackup/systemconfigsaver.py index 40538dc..608afe4 100644 --- a/winbackup/systemconfigsaver.py +++ b/winbackup/systemconfigsaver.py @@ -22,41 +22,42 @@ import traceback import shutil + class SystemConfigSaver: def __init__(self, config_save_path=None) -> None: """ Saves system configuration to files for backup. Config save path should be an empty folder to save all config files/folders to. - Within the winbackup script -> create 'config' folder, save config, 7z config, delete config folder. + winbackup script -> create 'config' folder, save config, 7z config, delete folder. """ + real_path = os.path.dirname(os.path.realpath(__file__)) self.config_save_path = config_save_path - self.winfetch_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'winfetch.ps1') - self.winfetch_config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'config.ps1') - self.installed_prog_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'installed_programs.ps1') - self.videos_path = os.path.join(os.path.expanduser('~'), 'Videos') - + self.winfetch_path = os.path.join(real_path, "scripts", "winfetch.ps1") + self.winfetch_config_path = os.path.join(real_path, "scripts", "config.ps1") + self.installed_prog_path = os.path.join(real_path, "scripts", "installed_programs.ps1") + self.videos_path = os.path.join(os.path.expanduser("~"), "Videos") @staticmethod - def _command_runner(shell_commands:list) -> str: - logging.debug(f'Command runner cmds: {shell_commands}') - return subprocess.run(shell_commands, stdout=subprocess.PIPE).stdout.decode('utf-8', errors='ignore') - - - def set_videos_directory_path(self, videos_path:str) -> None: - logging.debug(f'Video path set: {videos_path}') + def _command_runner(shell_commands: list) -> str: + logging.debug(f"Command runner cmds: {shell_commands}") + return subprocess.run(shell_commands, stdout=subprocess.PIPE).stdout.decode( + "utf-8", errors="ignore" + ) + + def set_videos_directory_path(self, videos_path: str) -> None: + logging.debug(f"Video path set: {videos_path}") self.videos_path = videos_path - - def save_config_files(self, out_path:str=None, quiet:bool=False) -> str: + def save_config_files(self, out_path: str = None, quiet: bool = False) -> str: """ Save all the configuration elements. Returns the path the files were saved to. """ - if out_path == None: + if out_path is None: out_path = self.config_save_path if quiet: - null_dev = open(os.devnull, 'w') + null_dev = open(os.devnull, "w") sys.stdout = null_dev self.save_winfetch(out_path) @@ -75,191 +76,215 @@ def save_config_files(self, out_path:str=None, quiet:bool=False) -> str: if quiet: sys.stdout = sys.__stdout__ null_dev.close() - - return out_path + return out_path - def save_winfetch(self, out_path:str) -> None: - winfetch_output = self._command_runner(['powershell.exe', self.winfetch_path, - '-noimage', - '-stripansi', - '-configpath', - self.winfetch_config_path]).replace('\r\n', '\n') - with open(os.path.join(out_path,'winfetch_output.txt'), 'w') as out_file: + def save_winfetch(self, out_path: str) -> None: + winfetch_output = self._command_runner( + [ + "powershell.exe", + self.winfetch_path, + "-noimage", + "-stripansi", + "-configpath", + self.winfetch_config_path, + ] + ).replace("\r\n", "\n") + with open(os.path.join(out_path, "winfetch_output.txt"), "w") as out_file: out_file.write(winfetch_output) - print(' > Winfetch saved.') - logging.info('Winfetch Saved.') - - - def save_installed_programs(self, out_path:str) -> None: - installed_output = self._command_runner(['powershell.exe', self.installed_prog_path]).replace('\r\n', '\n') - installed_output = installed_output.split('\n', 1)[1].strip() - with open(os.path.join(out_path,'installed_programs.csv'), 'w') as out_file: + print(" > Winfetch saved.") + logging.info("Winfetch Saved.") + + def save_installed_programs(self, out_path: str) -> None: + installed_output = self._command_runner( + ["powershell.exe", self.installed_prog_path] + ).replace("\r\n", "\n") + installed_output = installed_output.split("\n", 1)[1].strip() + with open(os.path.join(out_path, "installed_programs.csv"), "w") as out_file: out_file.write(installed_output) - print(' > Installed Programs saved.') - logging.info('Installed Programs Saved.') - + print(" > Installed Programs saved.") + logging.info("Installed Programs Saved.") - def save_global_python_packages(self, out_path:str) -> None: + def save_global_python_packages(self, out_path: str) -> None: try: - py_version = self._command_runner(['powershell.exe', 'python', '-V']).replace('\r\n', '\n') - pip_output = self._command_runner(['powershell.exe' ,'pip', 'freeze']).replace('\r\n', '\n') + py_version = self._command_runner(["powershell.exe", "python", "-V"]).replace( + "\r\n", "\n" + ) + pip_output = self._command_runner(["powershell.exe", "pip", "freeze"]).replace( + "\r\n", "\n" + ) pip_output = py_version + pip_output - with open(os.path.join(out_path, 'python_packages.txt'), 'w') as out_file: + with open(os.path.join(out_path, "python_packages.txt"), "w") as out_file: out_file.write(pip_output) - print(' > Global Python Packages saved.') - logging.info('Global Python Packages Saved.') + print(" > Global Python Packages saved.") + logging.info("Global Python Packages Saved.") except Exception as e: - print(Fore.RED + ' XX Unable to backup global python packages' + Style.RESET_ALL) - logging.warning(f'Unable to save global python packages. Exception: {e}') + print(Fore.RED + " XX Unable to backup global python packages" + Style.RESET_ALL) + logging.warning(f"Unable to save global python packages. Exception: {e}") - - def save_choco_packages(self, out_path:str) -> None: + def save_choco_packages(self, out_path: str) -> None: try: - choco_output = self._command_runner(['powershell.exe', - 'choco', 'list', '--local-only']).replace('\r\n', '\n') - with open(os.path.join(out_path, 'choco_packages.txt'), 'w') as out_file: + choco_output = self._command_runner( + ["powershell.exe", "choco", "list", "--local-only"] + ).replace("\r\n", "\n") + with open(os.path.join(out_path, "choco_packages.txt"), "w") as out_file: out_file.write(choco_output) - print(' > Choco Packages saved.') - logging.info('Choco Packages Saved.') + print(" > Choco Packages saved.") + logging.info("Choco Packages Saved.") except Exception as e: - print(Fore.RED + ' XX Unable to backup choco packages.' + Style.RESET_ALL) - logging.warning(f'Unable to save choco packages. Exception: {e}') - + print(Fore.RED + " XX Unable to backup choco packages." + Style.RESET_ALL) + logging.warning(f"Unable to save choco packages. Exception: {e}") - def save_vscode_extensions(self, out_path:str) -> None: + def save_vscode_extensions(self, out_path: str) -> None: try: - choco_output = self._command_runner(['powershell.exe', 'code', '--list-extensions']).replace('\r\n', '\n') - with open(os.path.join(out_path, 'vscode_extensions.txt'), 'w') as out_file: + choco_output = self._command_runner( + ["powershell.exe", "code", "--list-extensions"] + ).replace("\r\n", "\n") + with open(os.path.join(out_path, "vscode_extensions.txt"), "w") as out_file: out_file.write(choco_output) - print(' > VSCode Extensions saved.') - logging.info('VSCode extensions Saved.') + print(" > VSCode Extensions saved.") + logging.info("VSCode extensions Saved.") except Exception as e: - print(Fore.RED + ' XX Unable to backup VSCode extensions.' + Style.RESET_ALL) - logging.warning(f'Unable to save VSCode extensions packages. Exception: {e}') - + print(Fore.RED + " XX Unable to backup VSCode extensions." + Style.RESET_ALL) + logging.warning(f"Unable to save VSCode extensions packages. Exception: {e}") - def save_path_env(self, out_path:str) -> None: - path_list = os.environ['PATH'].split(';') - with open(os.path.join(out_path,'path.txt'), 'w') as out_file: + def save_path_env(self, out_path: str) -> None: + path_list = os.environ["PATH"].split(";") + with open(os.path.join(out_path, "path.txt"), "w") as out_file: for path in path_list: - fixed_path = path.replace('\\', '/') - out_file.write(fixed_path + '\n') - print(' > Path env saved.') - logging.info('Path env Saved.') + fixed_path = path.replace("\\", "/") + out_file.write(fixed_path + "\n") + print(" > Path env saved.") + logging.info("Path env Saved.") - - def save_ssh_directory(self, out_path:str) -> None: + def save_ssh_directory(self, out_path: str) -> None: try: - ssh_path = os.path.join(os.path.expanduser('~'), '.ssh') - logging.debug(f'ssh path {ssh_path}') + ssh_path = os.path.join(os.path.expanduser("~"), ".ssh") + logging.debug(f"ssh path {ssh_path}") if os.path.exists(ssh_path): - shutil.copytree(ssh_path, os.path.join(out_path, '.ssh')) - print(' > .ssh folder saved.') - logging.info('.ssh folder Saved.') + shutil.copytree(ssh_path, os.path.join(out_path, ".ssh")) + print(" > .ssh folder saved.") + logging.info(".ssh folder Saved.") else: - print(' > .ssh folder does not exist - skipping') - logging.info('.ssh folder does not exist, skipping.') + print(" > .ssh folder does not exist - skipping") + logging.info(".ssh folder does not exist, skipping.") except Exception as e: - print(Fore.RED + ' XX Unable to backup .ssh folder.' + Style.RESET_ALL) - logging.warning(f'Unable to backup .ssh folder. Exception: {e}') - - - def save_videos_directory_filenames(self, out_path:str, videos_path:str) -> None: - logging.debug(f'Videos file list directory: {videos_path}') - response = self._command_runner(['powershell.exe', 'tree', videos_path]).replace('\r\n', '\n') - with open(os.path.join(out_path, 'videos_tree.txt'), 'w') as out_file: + print(Fore.RED + " XX Unable to backup .ssh folder." + Style.RESET_ALL) + logging.warning(f"Unable to backup .ssh folder. Exception: {e}") + + def save_videos_directory_filenames(self, out_path: str, videos_path: str) -> None: + logging.debug(f"Videos file list directory: {videos_path}") + response = self._command_runner(["powershell.exe", "tree", videos_path]).replace( + "\r\n", "\n" + ) + with open(os.path.join(out_path, "videos_tree.txt"), "w") as out_file: out_file.write(response) - print(' > Videos file list saved.') - logging.info('Video Files list Saved.') - + print(" > Videos file list saved.") + logging.info("Video Files list Saved.") - def save_file_associations(self, out_path:str) -> None: + def save_file_associations(self, out_path: str) -> None: try: - file_assoc = self._command_runner(['powershell.exe', 'cmd', '/c', 'assoc']).replace('\r\n', '\n') - with open(os.path.join(out_path, 'file_associations.txt'), 'w') as out_file: + file_assoc = self._command_runner( + ["powershell.exe", "cmd", "/c", "assoc"] + ).replace("\r\n", "\n") + with open(os.path.join(out_path, "file_associations.txt"), "w") as out_file: out_file.write(file_assoc) - print(' > File associations saved.') - logging.info('File associations Saved.') + print(" > File associations saved.") + logging.info("File associations Saved.") except Exception as e: - print(Fore.RED + ' XX Unable to backup file associations.' + Style.RESET_ALL) - logging.warning(f'Unable to save file associations. Exception: {e}') - + print(Fore.RED + " XX Unable to backup file associations." + Style.RESET_ALL) + logging.warning(f"Unable to save file associations. Exception: {e}") - def save_drivers(self, out_path:str) -> None: + def save_drivers(self, out_path: str) -> None: try: - drivers = self._command_runner(['powershell.exe', 'driverquery']).replace('\r\n', '\n') - with open(os.path.join(out_path, 'drivers.txt'), 'w') as out_file: + drivers = self._command_runner(["powershell.exe", "driverquery"]).replace( + "\r\n", "\n" + ) + with open(os.path.join(out_path, "drivers.txt"), "w") as out_file: out_file.write(drivers) - print(' > Drivers saved.') - logging.info('Drivers Saved.') + print(" > Drivers saved.") + logging.info("Drivers Saved.") except Exception as e: - print(Fore.RED + ' XX Unable to backup drivers.' + Style.RESET_ALL) - logging.warning(f'Unable to save Drivers. Exception: {e}') + print(Fore.RED + " XX Unable to backup drivers." + Style.RESET_ALL) + logging.warning(f"Unable to save Drivers. Exception: {e}") - - def save_systeminfo(self, out_path:str) -> None: + def save_systeminfo(self, out_path: str) -> None: try: - sysinfo = self._command_runner(['powershell.exe', 'systeminfo']).replace('\r\n', '\n') - with open(os.path.join(out_path, 'systeminfo.txt'), 'w') as out_file: + sysinfo = self._command_runner(["powershell.exe", "systeminfo"]).replace( + "\r\n", "\n" + ) + with open(os.path.join(out_path, "systeminfo.txt"), "w") as out_file: out_file.write(sysinfo) - print(' > Systeminfo saved.') - logging.info('Systeminfo Saved.') + print(" > Systeminfo saved.") + logging.info("Systeminfo Saved.") except Exception as e: - print(Fore.RED + ' XX Unable to backup systeminfo.' + Style.RESET_ALL) - logging.warning(f'Unable to save systeminfo. Exception: {e}') - + print(Fore.RED + " XX Unable to backup systeminfo." + Style.RESET_ALL) + logging.warning(f"Unable to save systeminfo. Exception: {e}") - def save_battery_report(self, out_path:str) -> None: + def save_battery_report(self, out_path: str) -> None: try: - shell_commands = ['powershell.exe', 'powercfg', - '/batteryreport', '/output', - os.path.join(out_path, 'batteryreport.html')] - batreport = subprocess.run( - shell_commands, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE).stderr.decode('utf-8', errors='ignore').replace('\r\n', '\n') - - if not 'Unable to perform operation.' in batreport: + shell_commands = [ + "powershell.exe", + "powercfg", + "/batteryreport", + "/output", + os.path.join(out_path, "batteryreport.html"), + ] + batreport = ( + subprocess.run(shell_commands, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + .stderr.decode("utf-8", errors="ignore") + .replace("\r\n", "\n") + ) + + if "Unable to perform operation." not in batreport: print(" > Battery report saved.") logging.info("Battery report Saved.") - elif 'media pool is empty' in batreport: + elif "media pool is empty" in batreport: print(" - skipping battery report - this is not a battery powered device.") logging.info("skipping battery report - this is not a battery powered device.") else: - print(Fore.RED + " XX unable to save battery report with unknown error (Is this a battery powered device?)" + Style.RESET_ALL) - logging.error("Unable to save battery report with unknown error (Is this a battery powered device?)") + print( + Fore.RED + + " X unable to save battery report - unknown error (Is this a battery powered device?)" + + Style.RESET_ALL + ) + logging.error( + "Unable to save battery report - unknown error (Is this a battery powered device?)" + ) logging.debug(f"battery report response unknown: {batreport}") raise ValueError("unknown response from batreport") except Exception as e: - print(Fore.RED + ' XX unable to backup battery report.' + Style.RESET_ALL) - logging.warning(f'Unable to save battery report. Exception: {e}') + print(Fore.RED + " XX unable to backup battery report." + Style.RESET_ALL) + logging.warning(f"Unable to save battery report. Exception: {e}") logging.debug(traceback.format_exc()) -if __name__ == '__main__': +if __name__ == "__main__": from send2trash import send2trash - logging.basicConfig(stream= sys.stdout, - format='%(asctime)s - %(levelname)s - %(message)s', - level = logging.DEBUG) + + logging.basicConfig( + stream=sys.stdout, + format="%(asctime)s - %(levelname)s - %(message)s", + level=logging.DEBUG, + ) # ?use pathlib to recursively create path without if statement - if not os.path.isdir(os.path.join('.','tmp_test', 'config')): - os.makedirs(os.path.join('.', 'tmp_test', 'config')) + if not os.path.isdir(os.path.join(".", "tmp_test", "config")): + os.makedirs(os.path.join(".", "tmp_test", "config")) - config_saver = SystemConfigSaver(os.path.join('tmp_test', 'config')) - print(f'winfetch path {config_saver.winfetch_path}') - print(f'Videos path: {config_saver.videos_path}') - config_saver.set_videos_directory_path(os.path.join('D:/', 'Videos')) + config_saver = SystemConfigSaver(os.path.join("tmp_test", "config")) + print(f"winfetch path {config_saver.winfetch_path}") + print(f"Videos path: {config_saver.videos_path}") + config_saver.set_videos_directory_path(os.path.join("D:/", "Videos")) config_saver.save_config_files() - print('Complete.') + print("Complete.") - print(Fore.GREEN + ' > ' + 'Cleanup?' +' (y/n): ' + Style.RESET_ALL, end='') + print(Fore.GREEN + " > " + "Cleanup?" + " (y/n): " + Style.RESET_ALL, end="") reply = str(input()).lower().strip() - if reply.lower() in {'yes', 'y'}: - send2trash(os.path.join('.', 'tmp_test')) - print('Cleaned up temp dir.') + if reply.lower() in {"yes", "y"}: + send2trash(os.path.join(".", "tmp_test")) + print("Cleaned up temp dir.") else: - print('not cleaning up - remove temp directory manually') - sys.exit() \ No newline at end of file + print("not cleaning up - remove temp directory manually") + sys.exit() diff --git a/winbackup/winbackup.py b/winbackup/winbackup.py index faef592..d53a30f 100644 --- a/winbackup/winbackup.py +++ b/winbackup/winbackup.py @@ -39,6 +39,7 @@ init(autoreset=False) + class WinBackup: def __init__(self, log_level) -> None: """ @@ -51,64 +52,72 @@ def __init__(self, log_level) -> None: self.log_buffer = StringIO() self.log_level = log_level - self.logger_tempfile = self._start_logger(log_level) - + self.logger_tempfile = self._start_logger(log_level) + self.paths = self.windows_paths.get_paths() self.config_agent.update_config_paths(self.paths) - self.config_agent.encryption_password = '' + self.config_agent.encryption_password = "" self.start_time = datetime.now() - self.hyperv_paths_script = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scripts', 'hyperv_paths.ps1') + real_path = os.path.dirname(os.path.realpath(__file__)) + self.hyperv_paths_script = os.path.join(real_path, "scripts", "hyperv_paths.ps1") - self.config_saver.set_videos_directory_path(self.config_agent._target_config['14_videos']['path']) + self.config_saver.set_videos_directory_path( + self.config_agent._target_config["14_videos"]["path"] + ) ## ** Plex Specific setup plex_config_item = { - 'name': 'Plex Server', - 'type': 'special', - 'path': os.path.join(self.paths['local_appdata'], 'Plex Media Server'), + "name": "Plex Server", + "type": "folder", + "path": os.path.join(self.paths["local_appdata"], "Plex Media Server"), + "dict_size": "128m", + "mx_level": 5, + "tar_before_7z": True, + "extra_tar_flags": ["-xr!Cache*", "-xr!Updates", "-xr!Crash*"], } - if os.path.exists(os.path.join(self.paths['local_appdata'], 'Plex Media Server')): - logging.debug('Plex item added to config') - self.config_agent.add_item('30_plexserver', plex_config_item) + if os.path.exists(os.path.join(self.paths["local_appdata"], "Plex Media Server")): + logging.debug("Plex item added to config") + self.config_agent.add_item("30_plexserver", plex_config_item) ## ** virtualbox specific setup virtualbox_config_item = { - 'name': 'VirtualBox VMs', - 'path': os.path.join(os.path.expanduser('~'), 'VirtualBox VMs'), - 'dict_size': '128m', + "name": "VirtualBox VMs", + "path": os.path.join(os.path.expanduser("~"), "VirtualBox VMs"), + "dict_size": "128m", } - if os.path.exists(os.path.join(os.path.expanduser('~'), 'VirtualBox VMs')): - logging.debug('VirtualboxVMs item added to config') - self.config_agent.add_item('31_virtualboxvms', virtualbox_config_item) + if os.path.exists(os.path.join(os.path.expanduser("~"), "VirtualBox VMs")): + logging.debug("VirtualboxVMs item added to config") + self.config_agent.add_item("31_virtualboxvms", virtualbox_config_item) ## ** HyperV specific setup hyperv_config_item = { - 'name': 'HyperV VMs', - 'type': 'folder', - 'path': None, - 'dict_size': '128m', - 'full_path': True + "name": "HyperV VMs", + "type": "folder", + "path": None, + "dict_size": "128m", + "full_path": True, } if self.check_if_admin() and self._hyperv_possible(): - logging.debug('HyperV item added to config') - hyperv_config_item['path'] = self._get_hyperv_paths() - self.config_agent.add_item('32_hypervvms', hyperv_config_item) - + logging.debug("HyperV item added to config") + hyperv_config_item["path"] = self._get_hyperv_paths() + self.config_agent.add_item("32_hypervvms", hyperv_config_item) + ## ** onenote specific setup ## 33_onenote - ## default compression settings, to be implemented + ## default compression settings, to be implemented @staticmethod - def _command_runner(shell_commands:list) -> str: - logging.debug(f'Command runner cmds: {shell_commands}') - return subprocess.run(shell_commands, stdout=subprocess.PIPE).stdout.decode('utf-8', errors='ignore') - + def _command_runner(shell_commands: list) -> str: + logging.debug(f"Command runner cmds: {shell_commands}") + return subprocess.run( + shell_commands, + stdout=subprocess.PIPE, + ).stdout.decode("utf-8", errors="ignore") def _ctrl_c_handler(self, signum, frame): print() print(Fore.RED + " Ctrl-C received - Exiting." + Style.RESET_ALL) - sys.exit(1) - + sys.exit(1) def cli_get_output_root_path(self) -> str: """ @@ -116,42 +125,46 @@ def cli_get_output_root_path(self) -> str: if is a path - return path """ loop_limit = 5 - while (1): - print(Fore.GREEN + - " Backup output directory: " + Style.RESET_ALL, end='') + while 1: + print(Fore.GREEN + " Backup output directory: " + Style.RESET_ALL, end="") reply = str(input()).strip() logging.debug(f"Path reply {reply}") if os.path.isdir(reply): path = os.path.abspath(reply) break - else: - print(Fore.RED + " Output directory must be a real path. Ctrl+C to exit." + Style.RESET_ALL) + else: + print( + Fore.RED + + " Output directory must be a real path. Ctrl+C to exit." + + Style.RESET_ALL + ) loop_limit -= 1 logging.debug(f"loop limit = {loop_limit}") if loop_limit <= 0: - print(Fore.RED + " ERROR - Too many incorrect attempts. Exiting" + Style.RESET_ALL) + print( + Fore.RED + + " ERROR - Too many incorrect attempts. Exiting." + + Style.RESET_ALL + ) sys.exit(1) logging.debug(f"CLI path given: {path}") return path - - def _start_logger(self, log_level) -> None: """ - if log level is debug, write logger to file in pwd unless a backup is run, then move file to backup output dir + if log level is debug, write logger to file in pwd unless a backup is run, then move file to backup output dir if log level is info or above, write logger to buffer and flush to disk in backup output dir when backup is run. """ - #get the root logger + # get the root logger logger = logging.getLogger() logger.setLevel(log_level) - if log_level == logging.DEBUG: log_format = "%(asctime)s - %(levelname)s [%(module)s:%(funcName)s:%(lineno)d] -> %(message)s" - log_handler = logging.FileHandler(os.path.join(os.getcwd(), 'winbackup.log'), - mode='w', - encoding='utf-8') + log_handler = logging.FileHandler( + os.path.join(os.getcwd(), "winbackup.log"), mode="w", encoding="utf-8" + ) else: log_format = "%(asctime)s - %(levelname)s -> %(message)s" log_handler = logging.StreamHandler(self.log_buffer) @@ -161,125 +174,133 @@ def _start_logger(self, log_level) -> None: logger.addHandler(log_handler) logging.debug("Log Handler Started") - def _redirect_logger(self, out_path, log_level) -> None: logging.debug("Log Handler Redirect Started") logger = logging.getLogger() - + # remove root log handlers for handler in logger.handlers: logger.removeHandler(handler) handler.close() - + logger.setLevel(log_level) if log_level == logging.DEBUG: # move temp log in cwd to output path - shutil.move(os.path.join(os.getcwd(), 'winbackup.log'), - os.path.join(out_path, 'winbackup.log')) + shutil.move( + os.path.join(os.getcwd(), "winbackup.log"), + os.path.join(out_path, "winbackup.log"), + ) # create new handler to append to the new log location log_format = "%(asctime)s - %(levelname)s [%(module)s:%(funcName)s:%(lineno)d] -> %(message)s" else: # write the log buffer to disk and close the buffer - with open(os.path.join(out_path, 'winbackup.log'), 'w', encoding='utf-8') as fout: + with open(os.path.join(out_path, "winbackup.log"), "w", encoding="utf-8") as fout: self.log_buffer.seek(0) shutil.copyfileobj(self.log_buffer, fout) self.log_buffer.close() log_format = "%(asctime)s - %(levelname)s -> %(message)s" - + # create new handler to append the rest of the log in the output dir - log_handler = logging.FileHandler(os.path.join(out_path, 'winbackup.log'), - mode='a', - encoding='utf-8') + log_handler = logging.FileHandler( + os.path.join(out_path, "winbackup.log"), mode="a", encoding="utf-8" + ) formatter = logging.Formatter(log_format) log_handler.setFormatter(formatter) logger.addHandler(log_handler) logging.debug(f"Log Handler Redirected to: {os.path.join(out_path, 'winbackup.log')}") - def _hyperv_possible(self) -> bool: """ the vmcompute service is required by hyper-v, so check if it exists. """ - response = self._command_runner(['powershell.exe', 'Get-Service', 'vmcompute']).split('\r\n')[0] + response = self._command_runner( + [ + "powershell.exe", + "Get-Service", + "vmcompute", + ], + ).split("\r\n")[0] if "Cannot find any service with service name" in response: return False else: return True - def _onenote_possible(self) -> bool: - #placeholder + # placeholder stub return False - def _plex_server_running(self) -> bool: - response = self._command_runner(['powershell.exe', 'Get-Process', '"Plex Media Server"']).split('\r\n')[0] + response = self._command_runner( + ["powershell.exe", "Get-Process", '"Plex Media Server"'] + ).split("\r\n")[0] if "Cannot find a process with the name" in response: return False else: return True - def _get_hyperv_paths(self) -> list: """ Needs to be run as admin """ - response = self._command_runner(['powershell.exe', self.hyperv_paths_script]).split('\r\n') + response = self._command_runner( + [ + "powershell.exe", + self.hyperv_paths_script, + ] + ).split("\r\n") paths = list(filter(None, response)) - if 'is not recognized as a name of a cmdlet' in paths[0]: - logging.error('HyperV not installed') - elif 'You do not have the required permission' in paths[0]: - logging.error('HyperV tools need to be run as admin.') + if "is not recognized as a name of a cmdlet" in paths[0]: + logging.error("HyperV not installed") + elif "You do not have the required permission" in paths[0]: + logging.error("HyperV tools need to be run as admin.") raise PermissionError("HyperV needs to be run as admin") else: logging.debug(f"HyperV Paths: {paths}") return paths - @staticmethod def check_if_admin() -> bool: """ - Returns if the program is executed with admin privileges. + Returns if the program is executed with admin privileges. """ try: is_admin = bool(ctypes.windll.shell32.IsUserAnAdmin() != 0) - except: + except Exception: is_admin = False return is_admin - @staticmethod - def _yes_no_prompt(question:str) -> bool: - while (1): - print(Fore.GREEN + " > " + question + " (y/n): " + Style.RESET_ALL, end='') + def _yes_no_prompt(question: str) -> bool: + while 1: + print(Fore.GREEN + " > " + question + " (y/n): " + Style.RESET_ALL, end="") reply = str(input()).lower().strip() - if reply in {'yes', 'ye', 'y'}: + if reply in {"yes", "ye", "y"}: return True - elif reply in {'no', 'n'}: + elif reply in {"no", "n"}: return False else: - print(Fore.RED + " Only yes or no permitted. Ctrl+C to exit." + Style.RESET_ALL) - + print( + Fore.RED + " Only yes or no permitted. Ctrl+C to exit." + Style.RESET_ALL + ) @staticmethod - def _create_filename(output_dir_name:str) -> str: - pcname = os.environ['COMPUTERNAME'] + def _create_filename(output_dir_name: str) -> str: + pcname = os.environ["COMPUTERNAME"] uname = getpass.getuser() date_str = datetime.now().strftime("%Y-%m-%d") - return f'{pcname}_{uname}_{date_str}_{output_dir_name}.7z' - + return f"{pcname}_{uname}_{date_str}_{output_dir_name}.7z" @staticmethod - def _create_output_directory(output_dir:str) -> tuple: + def _create_output_directory(output_dir: str) -> tuple: """ takes the tgt output dir and creates an output path returns the path and folder_name and a flag indicating if the path needed created. """ path_created = False - pcname = os.environ['COMPUTERNAME'] + pcname = os.environ["COMPUTERNAME"] uname = getpass.getuser() date_str = datetime.now().strftime("%Y-%m-%d") - output_folder_name = f'{pcname}_{uname}_{date_str}' + output_folder_name = f"{pcname}_{uname}_{date_str}" output_path = os.path.join(output_dir, output_folder_name) if not os.path.exists(output_path): @@ -288,118 +309,194 @@ def _create_output_directory(output_dir:str) -> tuple: return output_path, output_folder_name, path_created - @staticmethod - def _save_file_hashes(out_path:str) -> None: + def _save_file_hashes(out_path: str) -> None: hashes_list = [] for file in os.listdir(out_path): - if not file.endswith('.log') and not file.endswith('.txt'): + if not file.endswith(".log") and not file.endswith(".txt"): h = hashlib.sha256() - b = bytearray(128*1024) + b = bytearray(128 * 1024) mv = memoryview(b) - with open(os.path.join(out_path, file), 'rb', buffering=0) as f: + with open(os.path.join(out_path, file), "rb", buffering=0) as f: for n in iter(lambda: f.readinto(mv), 0): h.update(mv[:n]) hashes_list.append((file, h.hexdigest())) logging.debug(f" Hash {file} {h.hexdigest()}") - with open(os.path.join(out_path, 'sha256.txt'), 'w') as hash_file: + with open(os.path.join(out_path, "sha256.txt"), "w") as hash_file: for item in hashes_list: - hash_file.write(f'{item[0]} {item[1]}\n') + hash_file.write(f"{item[0]} {item[1]}\n") - - def _recursive_loop_check(self, target_path:str, config:dict) -> bool: + def _recursive_loop_check(self, target_path: str, config: dict) -> bool: """ Returns True if the output directory is a child of a backup directory. """ backup_dirs = [] for key, target in config.items(): - if target['enabled']: - if type(target['path']) == str: - backup_dirs.append(target['path']) - elif type(target['path']) == list: - backup_dirs += target['path'] + if target["enabled"]: + if type(target["path"]) == str: + backup_dirs.append(target["path"]) + elif type(target["path"]) == list: + backup_dirs += target["path"] for path in backup_dirs: if Path(path) in Path(target_path).parents: return True return False - - def cli_header(self, output_path:str, output_folder_name:str, start_time:datetime) -> None: - print(Fore.BLACK + Back.WHITE + " WINDOWS BACKUP - v" + __version__ + " " + Style.RESET_ALL) - print(Fore.GREEN + f" Backup started at : " + Style.RESET_ALL + f" {start_time.strftime('%Y-%m-%d %H:%M')}") - print(Fore.GREEN + f" Output filename style : " + Style.RESET_ALL + f" {self._create_filename('example')}") - print(Fore.GREEN + f" Output folder : " + Style.RESET_ALL + f" {output_path}") - print(Fore.GREEN + f" Archives folder name : " + Style.RESET_ALL + f" {output_folder_name}") + def cli_header( + self, output_path: str, output_folder_name: str, start_time: datetime + ) -> None: + print( + Fore.BLACK + + Back.WHITE + + " WINDOWS BACKUP - v" + + __version__ + + " " + + Style.RESET_ALL + ) + print( + Fore.GREEN + + " Backup started at : " + + Style.RESET_ALL + + f" {start_time.strftime('%Y-%m-%d %H:%M')}" + ) + print( + Fore.GREEN + + " Output filename style : " + + Style.RESET_ALL + + f" {self._create_filename('example')}" + ) + print(Fore.GREEN + " Output folder : " + Style.RESET_ALL + f" {output_path}") + print( + Fore.GREEN + + " Archives folder name : " + + Style.RESET_ALL + + f" {output_folder_name}" + ) print() - print(" A new folder will be created in the specified output folder with the above archive folder name pattern.") + print( + " A new folder will be created in the specified output folder with the above archive folder name pattern." + ) print(" All produced archives and files will be placed in this new folder.") print() - - def cli_config(self, config:dict) -> dict: + def cli_config(self, config: dict, all_selected: bool = False) -> dict: new_config = config.copy() for key, target in sorted(config.items()): - new_config[key]['enabled'] = self._yes_no_prompt(f"Backup {target['name']}?") + if all_selected: + new_config[key]["enabled"] = True + else: + new_config[key]["enabled"] = self._yes_no_prompt(f"Backup {target['name']}?") print() return new_config - def cli_get_password(self) -> str: - passwd = '' - while(1): - print(Fore.GREEN + f" > Password for encryption (leave blank to disable) : " + Style.RESET_ALL, end='') - passwd = getpass.getpass(prompt='') + passwd = "" + while 1: + print( + Fore.GREEN + + " > Password for encryption (leave blank to disable) : " + + Style.RESET_ALL, + end="", + ) + passwd = getpass.getpass(prompt="") if len(passwd) == 0: print(" No password provided. 7z files produced will not be encrypted.") break else: - print(Fore.GREEN + f" > Confirm encryption password: " + Style.RESET_ALL, end='') - passwd_confirm = getpass.getpass(prompt='') + print( + Fore.GREEN + " > Confirm encryption password: " + Style.RESET_ALL, end="" + ) + passwd_confirm = getpass.getpass(prompt="") if passwd == passwd_confirm: logging.info("Archives will be encrypted.") - print(f" Passwords match. 7z archives will be AES256 encrypted with the given password.") - print(f" The archive headers will be encrypted (Filenames will be encrypted).") + print( + " Passwords match. 7z archives will be AES256 encrypted with the given password." + ) + print( + " The archive headers will be encrypted (Filenames will be encrypted)." + ) break else: - print(Fore.RED + f" > passwords don't match - try again. " + Style.RESET_ALL) + print( + Fore.RED + " > passwords don't match - try again. " + Style.RESET_ALL + ) print() return passwd - - def cli_config_summary(self, config:dict, passwd:str, path_created:bool) -> None: - print(Fore.BLACK + Back.WHITE + " ** CONFIG SUMMARY ** " + Style.RESET_ALL + Fore.GREEN) + def cli_config_summary(self, config: dict, passwd: str, path_created: bool) -> None: + print( + Fore.BLACK + Back.WHITE + " ** CONFIG SUMMARY ** " + Style.RESET_ALL + Fore.GREEN + ) for key, target in sorted(config.items()): - print(f" Backup {target['name']:<14} - {'Yes' if target['enabled']==True else 'No'}") + print( + f" Backup {target['name']:<14} - {'Yes' if target['enabled']==True else 'No'}" + ) logging.info(f"Config > {target['name']} - {target['enabled']}") print(f" Encryption - {'No' if len(passwd)==0 else 'Yes'}") print(Style.RESET_ALL) - - if len(passwd) <= 12 and len(passwd)!= 0: - print(Fore.YELLOW + f" !! CAUTION - The given password is short. Consider a longer password." + Style.RESET_ALL) - logging.debug(f"The given password is short ({len(passwd)} chars). Consider a longer password.") + + if len(passwd) <= 12 and len(passwd) != 0: + print( + Fore.YELLOW + + " !! CAUTION - The given password is short. Consider a longer password." + + Style.RESET_ALL + ) + logging.debug( + f"The given password is short ({len(passwd)} chars). Consider a longer password." + ) if not path_created: - print(Fore.YELLOW + " !! CAUTION - Output directory already exists - Contents may be destroyed if you proceed." + Style.RESET_ALL) - logging.info(f"Output directory already exists - Contents may be destroyed if you proceed.") - if '30_plexserver' in config: - if config['30_plexserver']['enabled'] and self._plex_server_running(): - print(Fore.YELLOW + " !! CAUTION - Plex Media Server is running - Recommend stopping Plex Server before backing up." + Style.RESET_ALL) - logging.info("Plex Media Server is running - Recommend stopping Plex Server before backing up.") - if '32_hypervvms' in config: - if config['32_hypervvms']['enabled']: - print(Fore.YELLOW + " !! CAUTION - HyperV has been enabled - Please check VMs are stopped before running." + Style.RESET_ALL) - logging.info("HyperV has been enabled - Please check VMs are stopped before running.") + print( + Fore.YELLOW + + " !! CAUTION - Output directory already exists - Contents may be destroyed if you proceed." + + Style.RESET_ALL + ) + logging.info( + "Output directory already exists - Contents may be destroyed if you proceed." + ) + if "30_plexserver" in config: + if config["30_plexserver"]["enabled"] and self._plex_server_running(): + print( + Fore.YELLOW + + " !! CAUTION - Plex Media Server is running - Recommend stopping Plex Server before backing up." + + Style.RESET_ALL + ) + logging.info( + "Plex Media Server is running - Recommend stopping Plex Server before backing up." + ) + if "32_hypervvms" in config: + if config["32_hypervvms"]["enabled"]: + print( + Fore.YELLOW + + " !! CAUTION - HyperV has been enabled - Please check VMs are stopped before running." + + Style.RESET_ALL + ) + logging.info( + "HyperV has been enabled - Please check VMs are stopped before running." + ) if self._hyperv_possible() and not self.check_if_admin(): - print(Fore.CYAN + " -- INFO - HyperV detected on system. To backup HyperV run winbackup as admin." + Style.RESET_ALL) + print( + Fore.CYAN + + " -- INFO - HyperV detected on system. To backup HyperV run winbackup as admin." + + Style.RESET_ALL + ) logging.info("HyperV detected on system. To backup HyperV run winbackup as admin.") - print(Fore.CYAN + " -- INFO - Archives produced are split into 4092Mb volumes (FAT32 limitation)." + Style.RESET_ALL) - print() + print( + Fore.CYAN + + " -- INFO - Archives produced are split into 4092Mb volumes (FAT32 limitation)." + + Style.RESET_ALL + ) + print() @staticmethod def remove_existing_archive(filename, path): - paths_to_remove = [os.path.join(path, file) for file in os.listdir(path) if file.startswith(filename)] - logging.debug(f"existing archives to be removed before backing up: {len(paths_to_remove)}") + paths_to_remove = [ + os.path.join(path, file) for file in os.listdir(path) if file.startswith(filename) + ] + logging.debug( + f"existing archives to be removed before backing up: {len(paths_to_remove)}" + ) for path in paths_to_remove: try: send2trash(path) @@ -408,204 +505,302 @@ def remove_existing_archive(filename, path): logging.error(f"could not delete file: {path}, exception {e}") logging.debug(traceback.format_exc()) - - def backup_run(self, config:dict, out_path:str, passwd:str, quiet:bool=False) -> None: + def backup_run( + self, + config: dict, + out_path: str, + passwd: str, + quiet: bool = False, + ) -> None: for key, target in sorted(config.items()): - if target['enabled']: - if not quiet: - print(Fore.GREEN + f" >>> Backing up {target['name']} ... " + Style.RESET_ALL) - logging.info(f"Backup starting - {target['name']}") - filename = self._create_filename(target['name'].replace(' ', '')) - - if target['type'] == 'folder': - try: - self.remove_existing_archive(filename, out_path) - self.archiver.backup_folder(filename, - target['path'], out_path, passwd, dict_size=target['dict_size'], - mx_level=target['mx_level'], full_path=target['full_path'], quiet=quiet) - except Exception as e: - logging.error(f"backup {filename} failed. Exception: {e}") - print(Fore.RED + f" XX - Backup {filename} failed. See logs." + Style.RESET_ALL) - else: - if key == '01_config': - config_path = os.path.join(out_path, 'config') - os.mkdir(config_path) - self.remove_existing_archive(filename, out_path) - self.config_saver.save_config_files(config_path, quiet=quiet) - try: - self.archiver.backup_folder(filename, - config_path, out_path, passwd, dict_size=target['dict_size'], - mx_level=target['mx_level'], full_path=target['full_path'], quiet=quiet) - except Exception as e: - logging.error(f"backup {filename} failed. Exception: {e}") - print(Fore.RED + f" XX - Backup {filename} failed. See logs." + Style.RESET_ALL) - - send2trash(config_path) - - elif key == '30_plexserver': - try: - self.archiver.backup_plex_folder(self._create_filename(target['name'].replace(' ','')), - target['path'], out_path, passwd, dict_size=target['dict_size'], - mx_level=target['mx_level'], quiet=quiet) - except Exception as e: - logging.error(f"backup {filename} failed. Exception: {e}") - print(Fore.RED + f" XX - Backup {filename} failed. See logs." + Style.RESET_ALL) - - # elif key == '33_onenote': - # pass - if not quiet: - print(f" >> {target['name']} saved to 7z - {filename}") - logging.debug(f"Backup finished for - {target['name']} - filename: {filename}") - if not quiet: + if not target["enabled"]: + continue + + if not quiet: + print(Fore.GREEN + f" >>> Backing up {target['name']} ... " + Style.RESET_ALL) + logging.info(f"Backup starting - {target['name']}") + filename = self._create_filename(target["name"].replace(" ", "")) + if target["type"] == "special" and key == "01_config": + try: + config_path = os.path.join(out_path, "config") + os.mkdir(config_path) + self.config_saver.save_config_files(config_path, quiet=quiet) + in_target_path = str(config_path) + except Exception as e: + logging.debug(f"could not backup config - Exception {e}") + else: + config_path = None + in_target_path = target["path"] + try: + self.remove_existing_archive(filename, out_path) + self.archiver.backup_folder( + filename, + in_target_path, + out_path, + passwd, + dict_size=target["dict_size"], + mx_level=target["mx_level"], + full_path=target["full_path"], + quiet=quiet, + tar_before_7z=target.get("tar_before_7z", False), + extra_tar_flags=target.get("extra_tar_flags", []), + extra_7z_flags=target.get("extra_7z_flags", []), + ) + except Exception as e: + logging.error(f"backup {filename} failed. Exception: {e}") + logging.debug(traceback.format_exc()) + print( + Fore.RED + f" XX - Backup {filename} failed. See logs." + Style.RESET_ALL + ) + if config_path is not None: + send2trash(config_path) + + if not quiet: + print(f" >> {target['name']} saved to 7z - {filename}") + logging.debug(f"Backup finished for - {target['name']} - filename: {filename}") + if not quiet: print() print(Fore.GREEN + " >>> Saving File hashes ... " + Style.RESET_ALL) self._save_file_hashes(out_path) - if not quiet: + if not quiet: print(" >> SHA-256 hashes of all archive files saved to sha256.txt") logging.info("SHA-256 hashes of all archive files saved to sha256.txt") if not len(passwd) == 0: - with open(os.path.join(out_path, "Archives_are_encrypted.txt"), 'w') as file: + with open(os.path.join(out_path, "Archives_are_encrypted.txt"), "w") as file: file.write("7z Archives in this folder are encrypted.") - - def cli_exit(self, out_path:str, start_time:datetime) -> None: + def cli_exit(self, out_path: str, start_time: datetime) -> None: duration = datetime.now() - start_time - backup_size = self.archiver._get_path_size(out_path) + backup_size = self.archiver._get_paths_size(out_path) print() logging.debug(f"humanize completion time {humanize.naturaldelta(duration)}") logging.info(f"Backup completed in {duration}") - logging.info(f"Total backup size {backup_size} ({humanize.naturalsize(backup_size, True)})") - print(Fore.WHITE + f" Completed in {humanize.naturaldelta(duration)}" + Style.RESET_ALL) + logging.info( + f"Total backup size {backup_size} ({humanize.naturalsize(backup_size, True)})" + ) + print( + Fore.WHITE + f" Completed in {humanize.naturaldelta(duration)}" + Style.RESET_ALL + ) print(Fore.WHITE + f" Total backup size {humanize.naturalsize(backup_size, True)}") - print(Fore.GREEN + f" Backups done! " + Style.RESET_ALL) - + print(Fore.GREEN + " Backups done! " + Style.RESET_ALL) def generate_blank_configfile(self, path=None): if not path: path = os.getcwd() if os.path.isdir(path): - path = os.path.join(os.path.abspath(path), 'winbackup_config.yaml') + path = os.path.join(os.path.abspath(path), "winbackup_config.yaml") if self._yes_no_prompt(f"Save default configuration to {path}?"): save_path = self.config_agent.save_YAML_config(path) print(f"Default config winbackup_config.yaml saved to {save_path}") - - def interactive_config_builder(self, target_path:str) -> None: + def interactive_config_builder(self, target_path: str, all_selected: bool = False) -> None: """ Build a custom config from the interactive script but save as config file to path """ - print(Fore.BLACK + Back.WHITE + " WINDOWS BACKUP - v" + __version__ + " " + Style.RESET_ALL) - print(Fore.GREEN + f" Interactive configuration builder" + Style.RESET_ALL) + print( + Fore.BLACK + + Back.WHITE + + " WINDOWS BACKUP - v" + + __version__ + + " " + + Style.RESET_ALL + ) + print(Fore.GREEN + " Interactive configuration builder" + Style.RESET_ALL) if not target_path: target_path = os.getcwd() if os.path.isdir(target_path): - target_path = os.path.join(os.path.abspath(target_path), 'winbackup_config.yaml') - print(Fore.GREEN + f" Generated configuration file will be saved to: " + Style.RESET_ALL + f"{target_path}") + target_path = os.path.join(os.path.abspath(target_path), "winbackup_config.yaml") + print( + Fore.GREEN + + " Generated configuration file will be saved to: " + + Style.RESET_ALL + + f"{target_path}" + ) print() - - self.config_agent.target_config = self.cli_config(self.config_agent.target_config) - password = self.cli_get_password() + self.config_agent.target_config = self.cli_config( + self.config_agent.target_config, + all_selected=all_selected, + ) + if all_selected: + logging.info("all flag at CLI - Defaulting to no password") + print("All options flag set at CLI - Defaulting to no password") + password = "" + else: + password = self.cli_get_password() if not len(password) == 0: self.config_agent.encryption_password = password - self.config_agent.global_config['encryption_enabled'] = True + self.config_agent.global_config["encryption_enabled"] = True + if len(password) <= 12: + print( + Fore.YELLOW + + " !! CAUTION - The given password is short. Consider a longer password." + + Style.RESET_ALL + ) try: save_path = self.config_agent.save_YAML_config(target_path) logging.debug(f"Interactive Configuration successfully saved to: {save_path}") - print(Fore.GREEN + f" Configuration successfully saved to: " + Style.RESET_ALL + f"{save_path}") + print( + Fore.GREEN + + " Configuration successfully saved to: " + + Style.RESET_ALL + + f"{save_path}" + ) except Exception as e: logging.critical(f"could not save the configuration generated - exception {e}") logging.critical(traceback.format_exc()) - print(Fore.RED + " XX - Could not save configuration file. See logs. Exiting." + Style.RESET_ALL) + print( + Fore.RED + + " XX - Could not save configuration file. See logs. Exiting." + + Style.RESET_ALL + ) sys.exit(1) - - def run_from_config_file(self, path:str) -> None: + def run_from_config_file( + self, + path: str, + quiet: bool = False, + auto_confirm: bool = False, + ) -> None: if path: - if os.path.exists(path) and path.lower().endswith(('.yaml', '.yml')): + if os.path.exists(path) and path.lower().endswith((".yaml", ".yml")): logging.debug("Valid config path given") path = os.path.abspath(path) else: - logging.critical(f"Config file does not exist or is not a YAML file at {path}. Exiting.") - print(Fore.RED + f" XX - Config file does not exist or is not a YAML file at {path}. Exiting." + Style.RESET_ALL) + logging.critical( + f"Config file does not exist or is not a YAML file at {path}. Exiting." + ) + print( + Fore.RED + + f" XX - Config file does not exist or is not a YAML file at {path}. Exiting." + + Style.RESET_ALL + ) sys.exit(1) else: - logging.critical(f"Must give path to configuration file, none given. Exiting.") - print(Fore.RED + f" XX - Must give path to configuration file. Exiting." + Style.RESET_ALL) + logging.critical("Must give path to configuration file, none given. Exiting.") + print( + Fore.RED + + " XX - Must give path to configuration file. Exiting." + + Style.RESET_ALL + ) sys.exit(1) try: global_config, target_config = self.config_agent.parse_YAML_config_file(path) - logging.debug(f'config successfully loaded from file at {path}') + logging.debug(f"config successfully loaded from file at {path}") except Exception as e: logging.critical(f"could not load config from file {path}. Exiting. Exception {e}") logging.critical(traceback.format_exc()) - print(Fore.RED + " XX - Could not load config from file. See logs. Exiting." + Style.RESET_ALL) + print( + Fore.RED + + " XX - Could not load config from file. See logs. Exiting." + + Style.RESET_ALL + ) sys.exit(1) - self.cli(self.config_agent.output_root_dir, config_set=True) - - - def cli(self, root_path=None, config_set:bool=False) -> None: + self.cli( + self.config_agent.output_root_dir, + config_set=True, + quiet=quiet, + auto_confirm=auto_confirm, + ) + + def cli( + self, + root_path=None, + config_set: bool = False, + all_selected: bool = False, + quiet: bool = False, + auto_confirm: bool = False, + ) -> None: signal.signal(signal.SIGINT, self._ctrl_c_handler) - logging.debug(f"sigint connected to ctrl_c_handler") + logging.debug("sigint connected to ctrl_c_handler") if not root_path: self.config_agent.output_root_dir = self.cli_get_output_root_path() else: - if not os.path.isdir(root_path): - print(Fore.RED + " Output directory must be a real path. Exiting." + Style.RESET_ALL) + if not os.path.isdir(root_path): + print( + Fore.RED + + " Output directory must be a real path. Exiting." + + Style.RESET_ALL + ) logging.critical(f"given path {root_path} is not a real path. Exiting") sys.exit(1) self.config_agent.output_root_dir = os.path.abspath(root_path) - self.output_path, self.output_folder_name, self.path_created = self._create_output_directory(self.config_agent.output_root_dir) + ( + self.output_path, + self.output_folder_name, + self.path_created, + ) = self._create_output_directory(self.config_agent.output_root_dir) self._redirect_logger(self.output_path, self.log_level) logging.info("WINDOWS BACKUP - v" + __version__) - logging.info(f"Output Folder: {self.output_path}") + logging.info(f"Output Folder: {self.output_path}") logging.debug(f"Output Root Dir: {self.config_agent.output_root_dir}") logging.debug(f"Output path: {self.output_path}") logging.debug(f"Output folder name: {self.output_folder_name}") - logging.debug(f"path created? {self.path_created}") + logging.debug(f"Path created? {self.path_created}") self.cli_header(self.output_path, self.output_folder_name, self.start_time) if not config_set: - logging.debug(f"Config not set - getting config interactively") - self.config_agent.target_config = self.cli_config(self.config_agent.target_config) - self.config_agent.encryption_password = self.cli_get_password() + logging.debug("Config not set - getting config interactively") + self.config_agent.target_config = self.cli_config( + self.config_agent.target_config, + all_selected=all_selected, + ) + if all_selected: + logging.info("all flag at CLI - Defaulting to no password") + print("All options flag set at CLI - Defaulting to no password") + self.config_agent.encryption_password = "" + else: + self.config_agent.encryption_password = self.cli_get_password() if len(self.config_agent.encryption_password) != 0: - self.config_agent.global_config['encryption_enabled'] = True + self.config_agent.global_config["encryption_enabled"] = True else: - logging.debug(f"Config already set - using config from config_agent") - if self.config_agent.global_config['encryption_enabled'] and len(self.config_agent.encryption_password) == 0: - logging.debug(f"encryption enabled but key length 0 - get password from cli") - self.config_agent.encryption_password = self.cli_get_password() + logging.debug("Config already set - using config from config_agent") + if self.config_agent.global_config["encryption_enabled"]: + if len(self.config_agent.encryption_password) == 0: + logging.debug("encryption enabled but key len 0 - get password from cli") + self.config_agent.encryption_password = self.cli_get_password() + else: + logging.debug("encryption enabled + pw set. skip getting from cli") else: - logging.debug(f"encryption disabled or password set - skip getting from cli") - + logging.debug("encryption disabled - skip getting from cli") + if self._recursive_loop_check(self.output_path, self.config_agent.target_config): - print(Fore.RED + " XX - Output path is a child of a path that will be backed up. This will case an infinite loop. Choose a different path." + Style.RESET_ALL) - logging.critical("Output path is a child of path that will be backed up. This will case an infinite loop. Choose a different path.") + print( + Fore.RED + + " XX - Output path is a child of a path that will be backed up. This will case an infinite loop. Choose a different path." + + Style.RESET_ALL + ) + logging.critical( + "Output path is a child of path that will be backed up. This will case an infinite loop. Choose a different path." + ) print(" Aborted. Exiting.") sys.exit(0) - - self.cli_config_summary(self.config_agent.target_config, - self.config_agent.encryption_password, - self.path_created) - - if not self._yes_no_prompt("Do you want to continue?"): - logging.info("Backup cancelled after summary. Exiting.") - print(" Aborted. Exiting.") - sys.exit(0) - - print('-' * 40) + + self.cli_config_summary( + self.config_agent.target_config, + self.config_agent.encryption_password, + self.path_created, + ) + if not auto_confirm: + if not self._yes_no_prompt("Do you want to continue?"): + logging.info("Backup cancelled after summary. Exiting.") + print(" Aborted. Exiting.") + sys.exit(0) + + print("-" * 40) print() - self.backup_run(self.config_agent.target_config, - self.output_path, - self.config_agent.encryption_password, - False) + self.backup_run( + self.config_agent.target_config, + self.output_path, + self.config_agent.encryption_password, + False, + ) self.cli_exit(self.output_path, self.start_time) diff --git a/winbackup/windowspaths.py b/winbackup/windowspaths.py index 5e183fe..b446fa5 100644 --- a/winbackup/windowspaths.py +++ b/winbackup/windowspaths.py @@ -20,22 +20,25 @@ import logging from uuid import UUID -## from https://gist.github.com/mkropat/7550097 + +## from https://gist.github.com/mkropat/7550097 ## below GUID class is under MIT licence copyright Michael Kropat (https://github.com/mkropat) -class GUID(ctypes.Structure): # [1] +class GUID(ctypes.Structure): # [1] _fields_ = [ ("Data1", ctypes.wintypes.DWORD), ("Data2", ctypes.wintypes.WORD), ("Data3", ctypes.wintypes.WORD), - ("Data4", ctypes.wintypes.BYTE * 8) - ] + ("Data4", ctypes.wintypes.BYTE * 8), + ] def __init__(self, uuid_): ctypes.Structure.__init__(self) self.Data1, self.Data2, self.Data3, self.Data4[0], self.Data4[1], rest = uuid_.fields for i in range(2, 8): - self.Data4[i] = rest>>(8 - i - 1)*8 & 0xff -## end MIT licenced code + self.Data4[i] = rest >> (8 - i - 1) * 8 & 0xFF + + ## end MIT licenced code + class WindowsPaths: def __init__(self) -> None: @@ -45,7 +48,7 @@ def __init__(self) -> None: i.e. if the user default videos folder has been changed, it will return the new path. """ # https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid - # guids from windows API (KnownFolderID) + # GUIDs from windows API (KnownFolderID) self.FOLDERID_DOCUMENTS = "{FDD39AD0-238F-46AF-ADB4-6C85480369C7}" self.FOLDERID_PICTURES = "{33E28130-4E1E-4676-835A-98395C3BC3BB}" self.FOLDERID_VIDEOS = "{18989B1D-99B5-455B-841C-AB7C74E4DDFC}" @@ -62,7 +65,6 @@ def __init__(self) -> None: self.paths = {} - def get_paths(self) -> dict: """ uses the KnownFolderID API @@ -86,45 +88,47 @@ def get_paths(self) -> dict: } self.paths = paths return paths - - def get_windows_path(self, known_folder_id:str) -> str: + def get_windows_path(self, known_folder_id: str) -> str: """ Takes a known_folder_id GUID as per win32 API and returns the folder path. SHGetKnownFolderPath is the current maintained version supported by windows not compatible with windows before vista. """ - #KF_FLAG_DEFAULT_PATH by default is not set, so will return the current, not default path + # KF_FLAG_DEFAULT_PATH not set by default, so will return the current path # https://docs.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid - path = '' + path = "" # below function adapted from https://gist.github.com/mkropat/7550097 - MIT licence buf = ctypes.c_wchar_p(ctypes.wintypes.MAX_PATH) try: return_type = ctypes.windll.shell32.SHGetKnownFolderPath( - ctypes.byref(GUID(UUID(known_folder_id))), - ctypes.wintypes.DWORD(0), - ctypes.wintypes.HANDLE(0), - ctypes.byref(buf)) + ctypes.byref(GUID(UUID(known_folder_id))), + ctypes.wintypes.DWORD(0), + ctypes.wintypes.HANDLE(0), + ctypes.byref(buf), + ) if return_type != 0: raise ValueError path = buf.value ctypes.windll.ole32.CoTaskMemFree(buf) - logging.debug(f'KnownFolderID {known_folder_id} Path {path}') - + logging.debug(f"KnownFolderID {known_folder_id} Path {path}") + except Exception as e: - logging.error(f'{e}') + logging.error(f"Error getting path - {e}") return path if __name__ == "__main__": - logging.basicConfig(stream= sys.stdout, - format='%(asctime)s - %(levelname)s - %(message)s', - level = logging.DEBUG) + logging.basicConfig( + stream=sys.stdout, + format="%(asctime)s - %(levelname)s - %(message)s", + level=logging.DEBUG, + ) windows_paths = WindowsPaths() paths = windows_paths.get_paths() for k, v in paths.items(): - print(f'{k:<17} : {v}') - print('\nComplete.') - sys.exit() \ No newline at end of file + print(f"{k:<17} : {v}") + print("\nComplete.") + sys.exit() diff --git a/winbackup/zip7archiver.py b/winbackup/zip7archiver.py index c911bc8..082031e 100644 --- a/winbackup/zip7archiver.py +++ b/winbackup/zip7archiver.py @@ -30,13 +30,17 @@ def __init__(self): """ Class exposing 7z compression methods for creating the 7z and tar archives. """ - self.zip7_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'bin', '7z', '7z.exe') - self.onenotemdexporter_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'OneNoteMdExporter', 'OneNoteMdExporter.exe') - self.onenotemdexporter_files_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'OneNoteMdExporter') - + real_path = os.path.dirname(os.path.realpath(__file__)) + self.zip7_path = os.path.join(real_path, "bin", "7z", "7z.exe") + self.onenote_ex_path = os.path.join( + real_path, + "OneNoteMdExporter", + "OneNoteMdExporter.exe", + ) + self.onenote_ex_files_path = os.path.join(real_path, "OneNoteMdExporter") @staticmethod - def _get_path_size(path:str) -> int: + def _get_size(path: str) -> int: """ Calculate the size of a directory tree and return size in bytes. """ @@ -47,178 +51,247 @@ def _get_path_size(path:str) -> int: total_bytes += os.path.getsize(os.path.join(path, file)) except Exception as e: logging.error(f"Get pathsize exception - Path: {path} Exception: {e}") - logging.debug(f"Pathsize: {path} Size: {total_bytes}") + logging.debug(f"Size: {total_bytes} bytes for path: {path} ") return total_bytes + def _get_paths_size(self, paths: Union[str, list]) -> int: + total_bytes = 0 + if type(paths) == str: + total_bytes = self._get_size(paths) + elif type(paths) == list: + for path in paths: + total_bytes += self._get_size(path) + else: + raise TypeError("path must be str or list of str") + logging.debug( + f"Total size of paths - {total_bytes} bytes " + + f"({total_bytes/1048576:0.0f} MiB)", + ) + return total_bytes - def backup_folder(self, filename:str, in_folder_path:Union[str,list], out_folder:str, - password:str='', dict_size:str='192m', mx_level:int=9, full_path:bool=False, - split:bool=True, split_force:bool=False, quiet:bool=False) -> tuple: + @staticmethod + def _archiver(filename: str, cmd_args: list, quiet: bool = False) -> tuple: + b_size_line = "" + a_size_line = "" + # run the backup task with a tqdm progress bar. + if filename.endswith(".tar"): + desc_stub = "Tarball" + else: + desc_stub = "Compress" + try: + logging.debug(f"cli args - {' '.join(cmd_args)}") + with subprocess.Popen( + cmd_args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=False, + bufsize=1, + universal_newlines=True, + errors="ignore", + ) as p: + if not quiet: + with tqdm( + total=100, + colour="Cyan", + leave=False, + desc=f" {desc_stub}ing ", + unit="%", + ) as pbar: + logging.debug("progress bar started") + for ( + line + ) in p.stdout: # cp1252 decoded string, ignore invalid chars like 0x81 + if len(line.strip()) != 0: + logging.debug("archive line output: " + line.strip()) + if "Add new data to archive: " in line: + b_size_line = line.split("Add new data to archive: ")[1].strip() # fmt: skip + tqdm.write( + Fore.CYAN + + f" >> Data to {desc_stub}: {b_size_line}" + + Style.RESET_ALL + ) + logging.debug(f"{filename} Data to {desc_stub}: {b_size_line}") + if "Archive size: " in line: + a_size_line = line.split("Archive size: ")[1].strip() + tqdm.write( + Fore.CYAN + + f" >> {desc_stub}ed Size : {a_size_line}" + + Style.RESET_ALL + ) + logging.debug(f"{filename} {desc_stub}ed Size: {a_size_line}") + if "%" in line: + pbar.update(int(line.split("%")[0].strip()) - pbar.n) + else: + for line in p.stdout: + if "Add new data to archive: " in line: + b_size_line = line.split("Add new data to archive: ")[1].strip() + if "Archive size: " in line: + a_size_line = line.split("Archive size: ")[1].strip() + if len(line.strip()) != 0: + logging.debug("archive line output: " + line.strip()) + except Exception as e: + logging.debug(f"Exception: {e}", exc_info=True, stack_info=True) + if not quiet: + print( + Fore.RED + + f" XX - Failed to archive {filename}. Set log level to debug for info." + + Style.RESET_ALL + ) + logging.error(f"Failed to archive {filename}. Set log level to debug for info.") + raise e + before_bytes = int(b_size_line.split("bytes")[0].split()[-1].strip()) + after_bytes = int(a_size_line.split("bytes")[0].split()[-1].strip()) + return before_bytes, after_bytes + + def backup_folder( + self, + zip_filename: str, + input_paths: Union[str, list], + out_folder: str, + password: str = "", + dict_size: str = "192m", + mx_level: int = 9, + full_path: bool = False, + split: bool = True, + quiet: bool = False, + tar_before_7z: bool = False, + extra_tar_flags: list = [], + extra_7z_flags: list = [], + ) -> tuple: """ Main function for creating 7z archives. Uses TQDM to display the progress to the console. Parameters: - filename : filename of the created archive - - in_folder_path : string or list - path(s) of the folder to be added to the archive + - input_paths : string or list - path(s) of the folder to be added to the archive - out_folder : where the 7z files are output - password : optional password for AES encryption - if empty string encryption is disabled - dict_size : string representing the LZMA2 dictionary size - mx_level : LZMA2 compression level 0-9 (9 is max) - full_path : archive will store the full path to the archived files, useful if multiple directories from several volumes - split : if the archives should be split into separate files if larger than FAT32 limit. - - split_force : don't check if archive will be larger than FAT32 limit, just force splitting. - + - quiet : dont print progress + - tar_before_7z : tarball input files before compressing + - extra_tar_flags: extra flags to pass with the tar function (if used) + - extra_7z_flags : extra flags to pass with the 7z function + Returns: - - before_size, after_size : tuple of before compresssion size and after compression size as int + - before_size, after_size : tuple of before/after as int in bytes """ - # base args for all types - # 7z normally disables progress reporting when output redirected, bsp1 fixes this. - cmd_args = [self.zip7_path, 'a', '-t7z', '-m0=lzma2', f'-md={dict_size}', f'-mx={str(mx_level)}', '-bsp1'] - # add additional arguments depending on function arguments - - if not type(filename) == str: + # validate inputs + if not type(zip_filename) == str: raise TypeError("Filename must be a string") - - if type(in_folder_path) == str: - if not os.path.exists(in_folder_path): - raise FileNotFoundError() - elif type(in_folder_path) == list: - for path in in_folder_path: + if type(input_paths) == str: + if not os.path.exists(input_paths): + raise FileNotFoundError() + elif type(input_paths) == list: + for path in input_paths: if not os.path.exists(path): raise FileNotFoundError() else: - raise TypeError("in_folder_path must be string or list") - + raise TypeError("input_paths must be string or list") if type(out_folder) == str: if not os.path.exists(out_folder): raise FileNotFoundError() else: raise TypeError("output path must be a string") + # 7z normally disables progress reporting when output redirected, bsp1 fixes this. + base_args = [ + self.zip7_path, + "a", + ] + if mx_level == 0: + base_7z_args = [ + "-t7z", + "-mx=0", + "-bsp1", + ] + else: + base_7z_args = [ + "-t7z", + "-m0=lzma2", + f"-md={dict_size}", + f"-mx={str(mx_level)}", + "-bsp1", + ] + base_tar_args = [ + "-ttar", + "-bsp1", + ] + zip_args = base_args + base_7z_args + extra_7z_flags + tar_args = base_args + base_tar_args + extra_tar_flags + tar_filename = zip_filename[:-3] + ".tar" + out_zip_path = os.path.join(out_folder, zip_filename) + out_tar_path = os.path.join(out_folder, tar_filename) + logging.debug(f"Base zip_args -> {' '.join(zip_args)}") + logging.debug(f"Base tar_args -> {' '.join(tar_args)}") + logging.debug(f"7z path -> {out_zip_path}") + logging.debug(f"tar path -> {out_tar_path}") + + # get input filesize + path_size = self._get_paths_size(input_paths) + + # parse split limit + split_size_bytes = 4290772992 + logging.debug(f"Archive Split size -> {split_size_bytes:,} bytes") + # add additional flags - if split or split_force: - if split_force: - logging.debug(f"split_force specified - {filename} will be split.") - cmd_args.append('-v4092m') + if split: + # only split if input files are bigger than the split size. + if path_size >= split_size_bytes: + logging.debug(f"Path size > split limit - Splitting {zip_filename}") + zip_args.append("-v4092m") else: - # only split if the output directory will be larger than the volume size. - # Always split if a list of paths are given. - if type(in_folder_path) is str: - path_size = self._get_path_size(in_folder_path) - if path_size >= 4290772992: - logging.debug(f"Splitting - {filename} is > split limit of 4092m (path size: {path_size/1048576:0.0f} MiB).") - cmd_args.append('-v4092m') - else: - logging.debug(f"Not Splitting - {filename} is < split limit of 4092m (path size: {path_size/1048576:0.0f} MiB).") - else: - logging.debug("List of paths given - Splitting.") - cmd_args.append('-v4092m') + logging.debug(f"Path size < split limit - Not Splitting {zip_filename}") if len(password) != 0: - cmd_args.append('-mhe=on') - cmd_args.append(f'-p{password}') + zip_args.append("-mhe=on") + zip_args.append(f"-p{password}") if full_path: - cmd_args.append('-spf2') + zip_args.append("-spf2") # add output archive path - cmd_args.append(os.path.join(out_folder, filename)) - - # add input paths - if type(in_folder_path) is str: - cmd_args.append(in_folder_path) - elif type(in_folder_path) is list: - cmd_args += in_folder_path + tar_args.append(out_tar_path) + zip_args.append(out_zip_path) + + # convert input paths to list + if type(input_paths) is str: + input_cmd_args = [input_paths] + elif type(input_paths) is list: + input_cmd_args = input_paths else: raise ValueError - before_size_line = '' - after_size_line = '' - # run the backup task with a tqdm progress bar. try: - with subprocess.Popen(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - shell=False, bufsize=1, universal_newlines=True, errors='ignore') as p: - if not quiet: - with tqdm(total=100, colour='Cyan', leave=False, desc=f' Compressing ', unit='%') as pbar: - logging.debug("progress bar started") - for line in p.stdout: #cp1252 decoded string, ignore invalid chars like 0x81 - if len(line.strip()) != 0: - logging.debug("backup_folder line output: " + line.strip()) - if "Add new data to archive: " in line: - before_size_line = line.split('Add new data to archive: ')[1].strip() - tqdm.write(Fore.CYAN + f" >> Data to compress: {before_size_line}" + Style.RESET_ALL) - logging.debug(f"{filename} Data to compress: {before_size_line}") - if "Archive size: " in line: - after_size_line = line.split('Archive size: ')[1].strip() - tqdm.write(Fore.CYAN + f" >> Compressed Size : {after_size_line}" + Style.RESET_ALL) - logging.debug(f"{filename} Compressed Size: {after_size_line}") - if "%" in line: - pbar.update(int(line.split('%')[0].strip()) - pbar.n) - else: - for line in p.stdout: - if "Add new data to archive: " in line: - before_size_line = line.split('Add new data to archive: ')[1].strip() - if "Archive size: " in line: - after_size_line = line.split('Archive size: ')[1].strip() - if len(line.strip()) != 0: - logging.debug("Backup line output: " + line.strip()) - - except Exception as e: - logging.debug(f"Exception: {e}", exc_info=True, stack_info=True) - if not quiet: - print(Fore.RED + f" XX - Failed to backup {filename}. Set log level to debug for info." + Style.RESET_ALL) - logging.error(f'Failed to backup {filename}. Set log level to debug for info.') - - before_size_bytes = int(before_size_line.split('bytes')[0].split()[-1].strip()) - after_size_bytes = int(after_size_line.split('bytes')[0].split()[-1].strip()) - logging.debug(f"Backup size in bytes. Before: {before_size_bytes} after: {after_size_bytes}") - logging.info(f"Backup {filename} complete. Size: {humanize.naturalsize(before_size_bytes, True)}" + - f" >> {humanize.naturalsize(after_size_bytes, True)}" + - f" (Compressed to {(after_size_bytes/before_size_bytes)*100:0.1f}% of input size)") - - return before_size_bytes, after_size_bytes - - - def backup_plex_folder(self, filename:str, in_folder_path:str, out_folder:str, - password:str='', dict_size:str='128m', mx_level:int=5, quiet:bool=False) -> tuple: - # From testing - backing up plex database mx9 md128m takes 10gb of ram, mx9 md192m fails memory allocation on 16gb pc. - #plex server files should be tarballed before compression as compressing disk files causes issues when restoring. - tar_filename = filename[:-3] + '.tar' - - # create tar with tqdm progress bar - cmd_args = [self.zip7_path, 'a', '-ttar', '-bsp1', '-xr!Cache*', '-xr!Updates', '-xr!Crash*', - os.path.join(out_folder, tar_filename), in_folder_path] - - with subprocess.Popen(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, - bufsize=1, universal_newlines=True, errors='ignore') as p: - if not quiet: - with tqdm(total=100, colour='Cyan', leave=False, desc=f' Tarballing PMS ', unit='%') as pbar: - for line in p.stdout: - if len(line.strip()) != 0: - logging.debug(line.strip()) - if "Add new data to archive: " in line: - tqdm.write(Fore.CYAN + f" >> Data to tarball : {line.split('Add new data to archive: ')[1].strip()}" + Style.RESET_ALL) - logging.info(f"{filename} data to tarball: {line.split('Add new data to archive: ')[1].strip()}") - if "Archive size: " in line: - tqdm.write(Fore.CYAN + f" >> Tarball Size : {line.split('Archive size: ')[1].strip()}" + Style.RESET_ALL) - logging.info(f"{filename} Tarball Size : {line.split('Archive size: ')[1].strip()}") - if "%" in line: - pbar.update(int(line.split('%')[0].strip()) - pbar.n) + if tar_before_7z: + full_tar_args = tar_args + input_cmd_args + full_7z_args = zip_args + [out_tar_path] + before_tar_bytes, after_tar_bytes = self._archiver(tar_filename, full_tar_args, quiet) # fmt: skip + before_7z_bytes, after_7z_bytes = self._archiver(zip_filename, full_7z_args, quiet) # fmt: skip + logging.debug(f"tar size: {before_tar_bytes} --> {after_tar_bytes} bytes") + logging.debug(f"7z size : {before_7z_bytes} --> {after_7z_bytes} bytes") + try: + logging.debug(f"Deleting tar from -> {out_tar_path}") + send2trash(out_tar_path) + except Exception as e: + logging.error(f"Could not delete {out_tar_path} - Exception {e}") + before_bytes = before_tar_bytes + after_bytes = after_7z_bytes else: - for line in p.stdout: - if len(line.strip()) != 0: - logging.debug("Backup line output: " + line.strip()) - - # compress the tar - before_size, after_size = self.backup_folder(filename, os.path.join(out_folder, tar_filename), out_folder, - password, dict_size=dict_size, mx_level=mx_level, full_path=False, - split=True, split_force=True, quiet=quiet) - - # delete the tar file - send2trash(os.path.join(out_folder,tar_filename)) - - return before_size, after_size + full_7z_args = zip_args + input_cmd_args + before_bytes, after_bytes = self._archiver(zip_filename, full_7z_args, quiet) + logging.debug(f"7z size : {before_bytes} -> {after_bytes} bytes") + except Exception as e: + raise e + logging.info( + f"Backup {zip_filename} complete. Size: {humanize.naturalsize(before_bytes, True)}" + + f" >> {humanize.naturalsize(after_bytes, True)}" + + f" (Compressed to {(after_bytes/before_bytes)*100:0.1f}% of input size)" + ) + return before_bytes, after_bytes - def backup_onenote_files(self, out_folder:str, password:str='') -> None: + def backup_onenote_files(self, out_folder: str, password: str = "") -> None: """ CURRENTLY NOT WORKING CORRECTLY. Needs OneNote 2016 (not Microsoft Store OneNote), Word 2016 and OneNoteMdExporter. @@ -226,22 +299,35 @@ def backup_onenote_files(self, out_folder:str, password:str='') -> None: working_dir = os.getcwd() - if not os.path.isdir(os.path.join(out_folder, 'onenote')): - os.mkdir(os.path.join(out_folder, 'onenote')) - os.mkdir(os.path.join(out_folder, 'onenote', 'md')) - os.mkdir(os.path.join(out_folder, 'onenote', 'joplin')) + if not os.path.isdir(os.path.join(out_folder, "onenote")): + os.mkdir(os.path.join(out_folder, "onenote")) + os.mkdir(os.path.join(out_folder, "onenote", "md")) + os.mkdir(os.path.join(out_folder, "onenote", "joplin")) # backup MD mode try: print(Fore.CYAN) - os.chdir(self.onenotemdexporter_files_path) - subprocess.run(['powershell.exe', self.onenotemdexporter_path, '--all-notebooks', '-f 1', '--no-input']) - subprocess.run(['powershell.exe', 'move', - os.path.join(self.onenotemdexporter_files_path,'Output','*'), - os.path.join(out_folder, 'onenote', 'md')]) - send2trash(os.path.join(self.onenotemdexporter_files_path, 'Output')) + os.chdir(self.onenote_ex_files_path) + subprocess.run( + [ + "powershell.exe", + self.onenote_ex_path, + "--all-notebooks", + "-f 1", + "--no-input", + ] + ) + subprocess.run( + [ + "powershell.exe", + "move", + os.path.join(self.onenote_ex_files_path, "Output", "*"), + os.path.join(out_folder, "onenote", "md"), + ] + ) + send2trash(os.path.join(self.onenote_ex_files_path, "Output")) except Exception as e: - print(Fore.RED + f' xx unable to export as md. exception {e}' + Style.RESET_ALL) + print(Fore.RED + f" xx unable to export as md. exception {e}" + Style.RESET_ALL) finally: os.chdir(working_dir) print(Style.RESET_ALL) @@ -249,46 +335,68 @@ def backup_onenote_files(self, out_folder:str, password:str='') -> None: # backup joplin style try: print(Fore.CYAN) - os.chdir(self.onenotemdexporter_files_path) - subprocess.run(['powershell.exe', self.onenotemdexporter_path, '--all-notebooks', '-f 2', '--no-input']) - subprocess.run(['powershell.exe', 'move', - os.path.join(self.onenotemdexporter_files_path,'Output','*'), - os.path.join(out_folder, 'onenote', 'joplin')]) - send2trash(os.path.join(self.onenotemdexporter_files_path, 'Output')) + os.chdir(self.onenote_ex_files_path) + subprocess.run( + [ + "powershell.exe", + self.onenote_ex_path, + "--all-notebooks", + "-f 2", + "--no-input", + ] + ) + subprocess.run( + [ + "powershell.exe", + "move", + os.path.join(self.onenote_ex_files_path, "Output", "*"), + os.path.join(out_folder, "onenote", "joplin"), + ] + ) + send2trash(os.path.join(self.onenote_ex_files_path, "Output")) except Exception as e: - print(Fore.RED + f' xx unable to export as joplin. exception {e}' + Style.RESET_ALL) + print( + Fore.RED + f" xx unable to export as joplin. exception {e}" + Style.RESET_ALL + ) finally: os.chdir(working_dir) print(Style.RESET_ALL) - #backup to 7z - self.backup_folder('onenote', os.path.join(out_folder, 'onenote'), out_folder, password, dict_size='128m') - send2trash(os.path.join(out_folder, 'OneNote')) - + # backup to 7z + self.backup_folder( + "onenote", + os.path.join(out_folder, "onenote"), + out_folder, + password, + dict_size="128m", + ) + send2trash(os.path.join(out_folder, "OneNote")) def backup_hyperv(self) -> None: pass -if __name__ == '__main__': - print('backup folder') - os.mkdir(os.path.join('.', 'temp')) +if __name__ == "__main__": + print("backup folder") + os.mkdir(os.path.join(".", "temp")) + + logging.basicConfig( + filename=os.path.join(".", "temp", "test.log"), + encoding="utf-8", + filemode="a", + format="%(asctime)s - %(levelname)s - %(message)s", + level=logging.DEBUG, + ) - logging.basicConfig(filename=os.path.join('.', 'temp', 'test.log'), - encoding='utf-8', - filemode='a', - format='%(asctime)s - %(levelname)s - %(message)s', - level = logging.DEBUG) - archiver = Zip7Archiver() - archiver.backup_folder('test_archive', '.', './temp', password='') - print('Complete.') + archiver.backup_folder("test_archive", ".", "./temp", password="") + print("Complete.") - print(Fore.GREEN + ' > ' + 'Cleanup?' +' (y/n): ' + Style.RESET_ALL, end='') + print(Fore.GREEN + " > " + "Cleanup?" + " (y/n): " + Style.RESET_ALL, end="") reply = str(input()).lower().strip() - if reply.lower() in ('yes', 'y'): - send2trash(os.path.join('.', 'temp')) - print('Cleaned up temp dir.') + if reply.lower() in ("yes", "y"): + send2trash(os.path.join(".", "temp")) + print("Cleaned up temp dir.") else: - print('not cleaning up - remove temp directory manually') - sys.exit() \ No newline at end of file + print("not cleaning up - remove temp directory manually") + sys.exit()