atomicshop 2.10.8__py3-none-any.whl → 2.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

atomicshop/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """Atomic Basic functions and classes to make developer life easier"""
2
2
 
3
3
  __author__ = "Den Kras"
4
- __version__ = '2.10.8'
4
+ __version__ = '2.11.0'
@@ -0,0 +1,5 @@
1
+ from atomicshop.wrappers.ctyping.msi_windows_installer import extract_msi_main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ extract_msi_main.extract_files_from_msi_main()
@@ -0,0 +1,86 @@
1
+ import os
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ from ..print_api import print_api
6
+ from .. import process, filesystem
7
+
8
+
9
+ def is_path_contains_7z_executable(sevenz_path: str) -> bool:
10
+ """
11
+ Checks if the path contains 7z executable.
12
+ :param sevenz_path: string, The path to the 7z executable.
13
+ :return: bool, True if the path contains 7z executable, False otherwise.
14
+ """
15
+ executable_path_parts: tuple = Path(sevenz_path).parts
16
+
17
+ if '7z' not in executable_path_parts[-1]:
18
+ return False
19
+ else:
20
+ return True
21
+
22
+
23
+ def is_executable_a_7z(sevenz_path: str) -> bool:
24
+ """
25
+ Checks if the 7z executable is installed.
26
+ :param sevenz_path: string, The path to the 7z executable.
27
+ :return: bool, True if the 7z executable is installed, False otherwise.
28
+ """
29
+
30
+ # Check if the process itself is installed.
31
+ if process.is_command_exists(sevenz_path):
32
+ # Check that this is the 7z executable.
33
+ try:
34
+ # Run '7z' command and capture output
35
+ result = subprocess.run([sevenz_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
36
+
37
+ # Check if the output contains identifying information
38
+ if b"7-Zip" in result.stdout or b"7-Zip" in result.stderr:
39
+ return True
40
+ except Exception:
41
+ return False
42
+
43
+ return False
44
+
45
+
46
+ def extract_file(
47
+ file_path: str,
48
+ extract_to: str,
49
+ sevenz_path: str = None,
50
+ force_overwrite: bool = False,
51
+ print_kwargs: dict = None
52
+ ):
53
+ """
54
+ Extracts a file to a directory using 7z executable.
55
+ :param file_path: string, The path to the file to extract.
56
+ :param extract_to: string, The directory to extract the file to.
57
+ :param sevenz_path: string, The path to the 7z executable.
58
+ If None, the default path is used, assuming you added 7z to the PATH environment variable.
59
+ :param force_overwrite: bool, If True, the files will be overwritten if they already exist in the output folder.
60
+ :param print_kwargs: dict, The keyword arguments to pass to the print function.
61
+ :return:
62
+ """
63
+
64
+ # Check if the path contains 7z executable.
65
+ if not is_path_contains_7z_executable(sevenz_path):
66
+ raise ValueError("The path to 7z does not contain 7z executable")
67
+
68
+ if not sevenz_path:
69
+ sevenz_path = '7z'
70
+
71
+ # Check if the 7z executable is installed.
72
+ if not is_executable_a_7z(sevenz_path):
73
+ raise RuntimeError("'7z executable' is not a 7z")
74
+
75
+ if not os.path.exists(extract_to):
76
+ os.makedirs(extract_to)
77
+
78
+ command = [f'{sevenz_path}', 'x', file_path, f'-o{extract_to}']
79
+ if force_overwrite:
80
+ command.append('-y')
81
+
82
+ try:
83
+ subprocess.run(command, check=True)
84
+ print_api(f"Extracted {file_path} to {extract_to}", **(print_kwargs or {}))
85
+ except subprocess.CalledProcessError as e:
86
+ print_api(f"An error occurred: {e}", **(print_kwargs or {}))
atomicshop/process.py CHANGED
@@ -3,6 +3,7 @@ import functools
3
3
  from typing import Union
4
4
  import shlex
5
5
  import subprocess
6
+ import shutil
6
7
 
7
8
  from .print_api import print_api
8
9
  from .inspect_wrapper import get_target_function_default_args_and_combine_with_current
@@ -12,6 +13,23 @@ if os.name == 'nt':
12
13
  from .process_poller import GetProcessList
13
14
 
14
15
 
16
+ def is_command_exists(cmd: str) -> bool:
17
+ """
18
+ The function checks if the command exists in the system.
19
+ :param cmd: string, the command to check.
20
+ :return: bool, True if the command exists, False otherwise.
21
+
22
+ With this you can also check if the command is in the PATH environment variable.
23
+ Example:
24
+ print(is_command_exists('7z')) # C:\\Program Files\\7-Zip\\7z.exe
25
+ """
26
+
27
+ if shutil.which(cmd):
28
+ return True
29
+ else:
30
+ return False
31
+
32
+
15
33
  def process_execution_decorator(function_name):
16
34
  @functools.wraps(function_name)
17
35
  def wrapper_process_execution_decorator(*args, **kwargs):
File without changes
@@ -0,0 +1,134 @@
1
+ from typing import Literal
2
+ import ctypes
3
+ from ctypes import wintypes
4
+
5
+
6
+ # Load Windows Installer functions
7
+ msi = ctypes.windll.msi
8
+
9
+
10
+ # Constants
11
+ MSIDBOPEN_READONLY = 0
12
+ ERROR_NO_MORE_ITEMS = 259
13
+
14
+
15
+ def create_open_db_handle(msi_filepath):
16
+ """Create a database handle for the specified MSI file."""
17
+ db_handle = ctypes.c_void_p()
18
+
19
+ result = msi.MsiOpenDatabaseW(msi_filepath, MSIDBOPEN_READONLY, ctypes.byref(db_handle))
20
+ if result != 0:
21
+ print(f"Failed to open MSI database. Error code: {result}")
22
+ raise ctypes.WinError(result)
23
+ return db_handle
24
+
25
+
26
+ def create_open_execute_view_handle(db_handle, query):
27
+ """Create a view handle for the specified query."""
28
+ # Create view handle.
29
+ view_handle = ctypes.c_void_p()
30
+
31
+ # Open the view.
32
+ result = msi.MsiDatabaseOpenViewW(db_handle, query, ctypes.byref(view_handle))
33
+ if result != 0:
34
+ print(f"Failed to open view. Error code: {result}")
35
+ msi.MsiCloseHandle(db_handle)
36
+ raise ctypes.WinError(result)
37
+
38
+ # Execute the view.
39
+ result = msi.MsiViewExecute(view_handle, None)
40
+ if result != 0:
41
+ print(f"Failed to execute view. Error code: {result}")
42
+ msi.MsiCloseHandle(view_handle)
43
+ msi.MsiCloseHandle(db_handle)
44
+ raise ctypes.WinError(result)
45
+
46
+ return view_handle
47
+
48
+
49
+ def create_fetch_record_from_view_handle(view_handle):
50
+ """Fetch a record from the specified view handle."""
51
+ # Fetch the record handle.
52
+ record_handle = ctypes.c_void_p()
53
+
54
+ # Fetch the record.
55
+ fetch_record_result = msi.MsiViewFetch(view_handle, ctypes.byref(record_handle))
56
+ if fetch_record_result == ERROR_NO_MORE_ITEMS:
57
+ msi.MsiCloseHandle(view_handle)
58
+ return record_handle
59
+ elif fetch_record_result != 0:
60
+ print(f"Failed to fetch record. Error code: {fetch_record_result}")
61
+ msi.MsiCloseHandle(view_handle)
62
+ raise ctypes.WinError(fetch_record_result)
63
+
64
+ return record_handle
65
+
66
+
67
+ def get_table_field_data_from_record(
68
+ record_handle,
69
+ field_index,
70
+ data_type: Literal['stringw', 'integer', 'stream'],
71
+ buffer_size: int = 2048
72
+ ):
73
+ """
74
+ Read data from a specific field in a record.
75
+ :param record_handle: Record handle.
76
+ :param field_index: The field index. Example: 2 for the second field.
77
+ Name,Data
78
+ field_index = 1 for Name
79
+ field_index = 2 for Data
80
+ :param data_type: The type of data to read.
81
+ stringw: Read the data as a wide string.
82
+ integer: Read the data as an integer.
83
+ stream: Read the data as a binary stream (bytes).
84
+ :param buffer_size: The size of the buffer to use when reading the data.
85
+ :return:
86
+ """
87
+
88
+ if data_type == 'stringw':
89
+ buf_size = wintypes.DWORD(buffer_size)
90
+ buf = ctypes.create_unicode_buffer(buf_size.value)
91
+ result = msi.MsiRecordGetStringW(record_handle, field_index, buf, ctypes.byref(buf_size))
92
+ if result != 0:
93
+ if result == 234:
94
+ print("Buffer size too small. Try again with a larger buffer.")
95
+ print(f"Failed to get string from record. Error code: {result}")
96
+ msi.MsiCloseHandle(record_handle)
97
+ raise ctypes.WinError(result)
98
+ return str(buf.value)
99
+
100
+ elif data_type == 'integer':
101
+ int_value = ctypes.c_int()
102
+ result = msi.MsiRecordGetInteger(record_handle, field_index, ctypes.byref(int_value))
103
+ if result != 0:
104
+ print(f"Failed to get integer from record. Error code: {result}")
105
+ msi.MsiCloseHandle(record_handle)
106
+ raise ctypes.WinError(result)
107
+ return int_value.value
108
+
109
+ elif data_type == 'stream':
110
+ # stream_size = wintypes.DWORD()
111
+ # result = msi.MsiRecordReadStream(record_handle, field_index, None, ctypes.byref(stream_size))
112
+ # if result != 0:
113
+ # print(f"Failed to read stream data. Error code: {result}")
114
+ # msi.MsiCloseHandle(record_handle)
115
+ # raise ctypes.WinError(result)
116
+ #
117
+ # stream_data = ctypes.create_string_buffer(stream_size.value)
118
+ # msi.MsiRecordReadStream(record_handle, field_index, stream_data, ctypes.byref(stream_size))
119
+ # return stream_data.raw
120
+
121
+ buffer = ctypes.create_string_buffer(buffer_size)
122
+ read_size = wintypes.DWORD(buffer_size)
123
+ data = bytearray()
124
+
125
+ while True:
126
+ result = msi.MsiRecordReadStream(record_handle, field_index, buffer, ctypes.byref(read_size))
127
+ if result != 0 or read_size.value == 0:
128
+ break
129
+ data.extend(buffer.raw[:read_size.value])
130
+
131
+ return data
132
+
133
+ else:
134
+ raise ValueError(f"Invalid data type: {data_type}. Valid options are 'stringw', 'integer', 'stream'.")
@@ -0,0 +1,75 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from . import tables
5
+ from ....archiver import sevenz_app_w
6
+
7
+
8
+ def resolve_directory_path(directory_info, directory_key):
9
+ parts = []
10
+ current_key = directory_key
11
+ while current_key:
12
+ entry = directory_info.get(current_key)
13
+ if entry:
14
+ parts.append(entry['default_dir'])
15
+ current_key = entry['parent']
16
+ else:
17
+ break
18
+ if not parts:
19
+ return ""
20
+ return str(os.path.join(*reversed(parts)))
21
+
22
+
23
+ def rename_extracted_files_by_file_table_info(
24
+ extracted_files_dir, file_table_info, component_table_info, directory_table_info):
25
+
26
+ for file_key, info in file_table_info.items():
27
+ component = info['component']
28
+ directory_key = component_table_info[component]['directory']
29
+ resolved_directory_path: str = resolve_directory_path(directory_table_info, directory_key)
30
+
31
+ # Divide the path into parts and remove the first part if it is a 'SourceDir' and the last part if it is a dot.
32
+ resolved_directory_parts = Path(resolved_directory_path).parts
33
+ if resolved_directory_parts[-1] == '.':
34
+ resolved_directory_parts = resolved_directory_parts[:-1]
35
+ if resolved_directory_parts[0] == 'SourceDir':
36
+ resolved_directory_parts = resolved_directory_parts[1:]
37
+ resolved_directory_path = str(os.path.join(*resolved_directory_parts))
38
+
39
+ extracted_path = os.path.join(extracted_files_dir, file_key)
40
+ if os.path.exists(extracted_path):
41
+ new_file_name = f"{info['file_name']}"
42
+ new_file_path = os.path.join(extracted_files_dir, resolved_directory_path, new_file_name)
43
+ os.makedirs(os.path.dirname(new_file_path), exist_ok=True)
44
+ os.rename(extracted_path, new_file_path)
45
+ print(f"Renamed: [{file_key}] to [{new_file_name}]")
46
+ else:
47
+ print(f"File not found: [{file_key}] in {extracted_path}")
48
+
49
+
50
+ def extract_files_from_cab(db_handle, main_cab_path: str, output_directory: str, sevenz_path: str = None):
51
+ """
52
+ Extracts the files from the CAB file using 7z executable and the MSI database handle to get the real file
53
+ names and paths.
54
+ :param db_handle: Database handle to the MSI database.
55
+ :param main_cab_path: string, The path to the main CAB file.
56
+ :param output_directory: string, The directory to extract the files to.
57
+ :param sevenz_path: string, Full path to the 7z executable.
58
+ :return:
59
+ """
60
+
61
+ # Get all the tables entries from the MSI database to correlate them while building the file paths and renaming.
62
+ file_table_info = tables.get_file_table_info(db_handle)
63
+ component_table_info = tables.get_component_table_info(db_handle)
64
+ directory_table_info = tables.get_directory_table_info(db_handle)
65
+
66
+ # Extract the contents of the CAB file (if there are several CAB files, they will be extracted as well).
67
+ sevenz_app_w.extract_file(
68
+ file_path=main_cab_path,
69
+ extract_to=output_directory,
70
+ sevenz_path=sevenz_path,
71
+ force_overwrite=True
72
+ )
73
+
74
+ rename_extracted_files_by_file_table_info(
75
+ output_directory, file_table_info, component_table_info, directory_table_info)
@@ -0,0 +1,136 @@
1
+ import os
2
+ import sys
3
+ import argparse
4
+
5
+ from .base import msi
6
+ from . import base, tables, cabs
7
+ from ... import olefilew
8
+ from ....print_api import print_api
9
+ from ....archiver import sevenz_app_w
10
+
11
+
12
+ # Directory names.
13
+ EXTRACTED_FILES: str = "Extracted_MSI_Installation_Files"
14
+ EMBEDDED_FILES: str = "Embedded_MSI_Files"
15
+ CAB_FILES: str = "CAB_Files"
16
+ BINARY_FILES: str = "Binary_Files"
17
+ TABLE_CONTENTS: str = "Table_Contents"
18
+ ICON_FILES: str = "Icon_Files"
19
+ ISSETUPFILES: str = "ISSETUPFILES"
20
+ OLE_METADATA: str = "OLE_Metadata"
21
+ REGISTRY_CHANGES: str = "Registry_Changes"
22
+
23
+
24
+ def parse_args():
25
+ parser = argparse.ArgumentParser(description="Extract files from the MSI file.")
26
+ parser.add_argument("--msi_filepath", "-m", type=str, help="The path to the MSI file.")
27
+ parser.add_argument("--output_directory", "-o", type=str, help="The main output directory.")
28
+ parser.add_argument("--sevenz_path", "-s", type=str, help="The path to the 7z executable.")
29
+ return parser.parse_args()
30
+
31
+
32
+ def extract_files_from_msi_main(
33
+ msi_filepath: str = None,
34
+ main_out_directory: str = None,
35
+ sevenz_path: str = None
36
+ ):
37
+ """
38
+ Extracts files from the MSI file using the MSI database.
39
+ What can be extracted:
40
+ - MSI file Table contents
41
+ - MSI file Binary files
42
+ - MSI file Icon files
43
+ - MSI file ISSETUPFILES
44
+ - MSI file Registry changes
45
+ - MSI file CAB files
46
+ - Application Installation files.
47
+ These are extracted from the CAB files and renamed to their real names from the DB.
48
+ - MSI file OLE metadata
49
+
50
+ Usage:
51
+ # You can create a python file with the following content and run it:
52
+ from atomicshop.wrappers.ctyping.msi_windows_installer import extract_msi_main
53
+
54
+
55
+ if __name__ == "__main__":
56
+ extract_msi_main.extract_files_from_msi_main()
57
+
58
+ Or you can just use the function with the appropriate parameters directly:
59
+ extract_msi_main.extract_files_from_msi_main(
60
+ msi_filepath=r"C:\\Setup.msi",
61
+ main_out_directory=r"c:\\unpacked_msi",
62
+ sevenz_path=r"C:\\7z.exe")
63
+
64
+ :param msi_filepath: string, The path to the MSI file.
65
+ :param main_out_directory: string, The main output directory.
66
+ :param sevenz_path: string, The path to the 7z executable. If None, the default path is used, which assumes that 7z
67
+ is in the PATH environment variable.
68
+ :return:
69
+ """
70
+
71
+ args = parse_args()
72
+
73
+ # If arguments to the function were not provided, use the arguments from the command line.
74
+ if not msi_filepath:
75
+ msi_filepath = args.msi_filepath
76
+ if not main_out_directory:
77
+ main_out_directory = args.output_directory
78
+ if not sevenz_path:
79
+ sevenz_path = args.sevenz_path
80
+
81
+ # If not provided, raise an error.
82
+ if not msi_filepath:
83
+ print_api("The path to the MSI file is not provided.", color="red")
84
+ sys.exit()
85
+ if not main_out_directory:
86
+ print_api("The main output directory is not provided.", color="red")
87
+ if not sevenz_path:
88
+ print_api(
89
+ "The path to the 7z executable is not provided. Assuming 7z is in the PATH environment variable.",
90
+ color="yellow")
91
+
92
+ if not sevenz_app_w.is_path_contains_7z_executable(sevenz_path):
93
+ print_api("The path to 7z does not contain 7z executable", color="red")
94
+ sys.exit()
95
+
96
+ if not os.path.isfile(sevenz_path):
97
+ print_api("The path to 7z executable doesn't exist.", color="red")
98
+ sys.exit()
99
+
100
+ if not sevenz_app_w.is_executable_a_7z(sevenz_path):
101
+ print_api("Provided 7z executable is not 7z.", color="red")
102
+ sys.exit()
103
+
104
+ # Create the main output directory.
105
+ os.makedirs(main_out_directory, exist_ok=True)
106
+
107
+ # Open the MSI database file.
108
+ db_handle = base.create_open_db_handle(msi_filepath)
109
+
110
+ embedded_files_directory = os.path.join(main_out_directory, EMBEDDED_FILES)
111
+
112
+ # Extract files using the MSI database handle.
113
+ tables.extract_table_contents_to_csv(db_handle, os.path.join(embedded_files_directory, TABLE_CONTENTS))
114
+ tables.extract_binary_table_entries(db_handle, os.path.join(embedded_files_directory, BINARY_FILES))
115
+ tables.extract_icon_table_entries(db_handle, os.path.join(embedded_files_directory, ICON_FILES))
116
+ tables.extract_issetupfiles_table_entries(db_handle, os.path.join(embedded_files_directory, ISSETUPFILES))
117
+ tables.extract_registry_changes(db_handle, os.path.join(embedded_files_directory, REGISTRY_CHANGES))
118
+
119
+ # Extract CAB files from the MSI file.
120
+ cab_files = tables.list_media_table_entries(db_handle)
121
+ print(f"CAB files found: {cab_files}")
122
+ extracted_cab_file_paths: list = tables.extract_cab_files_from_media(
123
+ db_handle, os.path.join(embedded_files_directory, CAB_FILES))
124
+
125
+ # Extract installation files from CAB files and rename them to their real names from DB.
126
+ cabs.extract_files_from_cab(
127
+ db_handle,
128
+ extracted_cab_file_paths[0],
129
+ os.path.join(main_out_directory, EXTRACTED_FILES),
130
+ sevenz_path=sevenz_path)
131
+
132
+ # Close the MSI database handle.
133
+ msi.MsiCloseHandle(db_handle)
134
+
135
+ # Extract OLE metadata from the MSI file.
136
+ olefilew.extract_ole_metadata(msi_filepath, os.path.join(embedded_files_directory, OLE_METADATA))
@@ -0,0 +1,419 @@
1
+ import os
2
+ import csv
3
+ import ctypes
4
+ from ctypes import wintypes
5
+
6
+ from . import base
7
+ from .base import msi
8
+
9
+
10
+ def get_column_names(db_handle, table_name):
11
+ """Fetch column names for a specific table."""
12
+ column_names = []
13
+ query = f"SELECT `Name` FROM `_Columns` WHERE `Table`='{table_name}' ORDER BY `Number`"
14
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
15
+
16
+ while True:
17
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
18
+ if not record_handle:
19
+ break
20
+
21
+ column_name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
22
+ if column_name:
23
+ column_names.append(column_name)
24
+
25
+ msi.MsiCloseHandle(record_handle)
26
+ msi.MsiCloseHandle(view_handle)
27
+ return column_names
28
+
29
+
30
+ def list_all_table_names(db_handle) -> list:
31
+ """List all tables in the MSI database."""
32
+ query = "SELECT `Name` FROM `_Tables`"
33
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
34
+
35
+ tables: list = []
36
+ while True:
37
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
38
+
39
+ # If record handle is empty then there is nothing more in the buffer, and we can stop the loop.
40
+ if not record_handle:
41
+ break
42
+
43
+ tables.append(base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw'))
44
+
45
+ msi.MsiCloseHandle(record_handle)
46
+ msi.MsiViewClose(view_handle)
47
+
48
+ return tables
49
+
50
+
51
+ def get_table_contents(db_handle, table_name):
52
+ """Fetch all contents of a specific table using ctypes."""
53
+ view_handle = base.create_open_execute_view_handle(db_handle, f"SELECT * FROM `{table_name}`")
54
+ contents = []
55
+
56
+ # Fetch column names
57
+ column_names = get_column_names(db_handle, table_name)
58
+ contents.append(column_names)
59
+
60
+ while True:
61
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
62
+ if not record_handle:
63
+ break
64
+
65
+ row = []
66
+ field_count = msi.MsiRecordGetFieldCount(record_handle)
67
+
68
+ for i in range(1, field_count + 1):
69
+ # Try to fetch as a string
70
+ buf_size = wintypes.DWORD(1024)
71
+ buf = ctypes.create_unicode_buffer(buf_size.value)
72
+ result = msi.MsiRecordGetStringW(record_handle, i, buf, ctypes.byref(buf_size))
73
+
74
+ if result == 0:
75
+ row.append(buf.value)
76
+ elif result == 234: # ERROR_MORE_DATA
77
+ # Increase buffer size and try again
78
+ buf_size = wintypes.DWORD(buf_size.value + 1)
79
+ buf = ctypes.create_unicode_buffer(buf_size.value)
80
+ result = msi.MsiRecordGetStringW(record_handle, i, buf, ctypes.byref(buf_size))
81
+ if result == 0:
82
+ row.append(buf.value)
83
+ else:
84
+ row.append(None)
85
+ else:
86
+ # Try to fetch as an integer
87
+ int_value = ctypes.c_int()
88
+ result = msi.MsiRecordGetInteger(record_handle, i, ctypes.byref(int_value))
89
+ if result == 0:
90
+ row.append(int_value.value)
91
+ else:
92
+ # Try to fetch as a stream
93
+ stream_size = wintypes.DWORD()
94
+ result = msi.MsiRecordReadStream(record_handle, i, None, ctypes.byref(stream_size))
95
+ if result == 0:
96
+ stream_data = ctypes.create_string_buffer(stream_size.value)
97
+ msi.MsiRecordReadStream(record_handle, i, stream_data, ctypes.byref(stream_size))
98
+ row.append(stream_data.raw)
99
+ else:
100
+ row.append(None)
101
+
102
+ contents.append(row)
103
+ msi.MsiCloseHandle(record_handle)
104
+
105
+ msi.MsiCloseHandle(view_handle)
106
+ return contents
107
+
108
+
109
+ def extract_table_contents_to_csv(db_handle, output_directory):
110
+ """Extracts all table contents to separate CSV files."""
111
+
112
+ os.makedirs(output_directory, exist_ok=True)
113
+
114
+ # Get all the table names.
115
+ table_names: list = list_all_table_names(db_handle)
116
+ # Get all the table contents by fetching each table.
117
+ table_contents = {table: get_table_contents(db_handle, table) for table in table_names}
118
+ print(f"Tables and their contents have been fetched.")
119
+
120
+ # Save each table to a separate CSV file
121
+ for table, contents in table_contents.items():
122
+ csv_file_path = os.path.join(output_directory, f"{table}.csv")
123
+ with open(csv_file_path, "w", newline='') as csv_file:
124
+ writer = csv.writer(csv_file)
125
+ writer.writerows(contents)
126
+ print(f"Table {table} contents saved to {csv_file_path}")
127
+
128
+ print("All table contents saved to separate CSV files in the 'tables' directory.")
129
+
130
+
131
+ def extract_binary_table_entries(db_handle, output_directory: str):
132
+ os.makedirs(output_directory, exist_ok=True)
133
+
134
+ # Create and execute a view to query the Binary table
135
+ query = "SELECT Name, Data FROM Binary"
136
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
137
+
138
+ while True:
139
+ # Fetch a record from the view
140
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
141
+ if not record_handle:
142
+ break
143
+
144
+ # Get the binary name
145
+ name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
146
+
147
+ # Get the size of the binary data
148
+ data_size = msi.MsiRecordDataSize(record_handle, 2)
149
+ if data_size == 0:
150
+ continue
151
+
152
+ # Read the binary data
153
+ binary_data = base.get_table_field_data_from_record(
154
+ record_handle, field_index=2, data_type='stream', buffer_size=data_size)
155
+
156
+ # Save the binary data to a file
157
+ output_filepath = os.path.join(output_directory, name)
158
+ print(f"Extracting binary file [{name}] to {output_filepath}")
159
+ with open(output_filepath, 'wb') as f:
160
+ f.write(binary_data)
161
+
162
+ msi.MsiCloseHandle(record_handle)
163
+ msi.MsiCloseHandle(view_handle)
164
+
165
+
166
+ def extract_icon_table_entries(db_handle, output_directory: str):
167
+ """Extracts icons from the Icon table in the specified MSI file."""
168
+ os.makedirs(output_directory, exist_ok=True)
169
+
170
+ query = "SELECT Name, Data FROM Icon"
171
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
172
+
173
+ while True:
174
+ # Fetch a record from the view
175
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
176
+ if not record_handle:
177
+ break
178
+
179
+ # Read the Name (field 1) and Data (field 2) from the record
180
+ icon_filename = base.get_table_field_data_from_record(record_handle, 1, 'stringw')
181
+ icon_data = base.get_table_field_data_from_record(record_handle, 2, 'stream')
182
+
183
+ # Define the output file path
184
+ output_file_path = os.path.join(output_directory, f"{icon_filename}")
185
+
186
+ # Write the icon data to a file
187
+ with open(output_file_path, 'wb') as icon_file:
188
+ icon_file.write(icon_data)
189
+
190
+ print(f"Extracted icon: {output_file_path}")
191
+
192
+ # Close handles.
193
+ msi.MsiCloseHandle(record_handle)
194
+ msi.MsiCloseHandle(view_handle)
195
+
196
+
197
+ def extract_issetupfiles_table_entries(db_handle, output_directory: str):
198
+ """Extracts icons from the Icon table in the specified MSI file."""
199
+ os.makedirs(output_directory, exist_ok=True)
200
+
201
+ query = "SELECT FileName, Stream FROM ISSetupFile"
202
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
203
+
204
+ while True:
205
+ # Fetch a record from the view
206
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
207
+ if not record_handle:
208
+ break
209
+
210
+ # Read the Name (field 1) and Data (field 2) from the record
211
+ file_name = base.get_table_field_data_from_record(record_handle, 1, 'stringw')
212
+ file_data = base.get_table_field_data_from_record(record_handle, 2, 'stream')
213
+
214
+ # Define the output file path
215
+ output_file_path = os.path.join(output_directory, f"{file_name}")
216
+
217
+ # Write the icon data to a file
218
+ with open(output_file_path, 'wb') as icon_file:
219
+ icon_file.write(file_data)
220
+
221
+ print(f"Extracted IsSetupFile: {output_file_path}")
222
+
223
+ # Close handles.
224
+ msi.MsiCloseHandle(record_handle)
225
+ msi.MsiCloseHandle(view_handle)
226
+
227
+
228
+ def extract_registry_changes(db_handle, output_directory: str):
229
+ """Extracts registry changes from the MSI file and writes them to a .reg file."""
230
+
231
+ os.makedirs(output_directory, exist_ok=True)
232
+ registry_file_path: str = os.path.join(output_directory, "registry_changes.reg")
233
+
234
+ # Create and execute a view for the Registry table
235
+ query = "SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`"
236
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
237
+
238
+ with open(registry_file_path, 'w') as reg_file:
239
+ # Write the .reg file header
240
+ reg_file.write("Windows Registry Editor Version 5.00\n\n")
241
+
242
+ while True:
243
+ # Fetch a record from the view
244
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
245
+ if not record_handle:
246
+ break
247
+
248
+ # Read the Root (field 1), Key (field 2), Name (field 3), and Value (field 4) from the record
249
+ root = int(base.get_table_field_data_from_record(record_handle, 1, 'stringw'))
250
+ key = base.get_table_field_data_from_record(record_handle, 2, 'stringw')
251
+ name = base.get_table_field_data_from_record(record_handle, 3, 'stringw')
252
+ value = base.get_table_field_data_from_record(record_handle, 4, 'stringw')
253
+
254
+ # Determine the root key name
255
+ root_key = {
256
+ 0: "HKEY_CLASSES_ROOT",
257
+ 1: "HKEY_CURRENT_USER",
258
+ 2: "HKEY_LOCAL_MACHINE",
259
+ 3: "HKEY_USERS"
260
+ }.get(root, "UNKNOWN_ROOT")
261
+
262
+ # Format the registry entry
263
+ if name:
264
+ reg_entry = f'[{root_key}\\{key}]\n"{name}"="{value}"\n\n'
265
+ else:
266
+ reg_entry = f'[{root_key}\\{key}]\n@="{value}"\n\n'
267
+
268
+ # Write the registry entry to the .reg file
269
+ reg_file.write(reg_entry)
270
+
271
+ msi.MsiCloseHandle(record_handle)
272
+ msi.MsiCloseHandle(view_handle)
273
+
274
+
275
+ def list_media_table_entries(db_handle):
276
+ """List all CAB files from the Media table."""
277
+ query = "SELECT `Cabinet` FROM `Media`"
278
+ media_view = base.create_open_execute_view_handle(db_handle, query)
279
+
280
+ cab_files = []
281
+ while True:
282
+ record_handle = base.create_fetch_record_from_view_handle(media_view)
283
+ if not record_handle:
284
+ break
285
+
286
+ # The CAB file names are prefixed with #
287
+ cab_name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
288
+ cab_files.append(cab_name.strip("#"))
289
+
290
+ msi.MsiCloseHandle(record_handle)
291
+ msi.MsiCloseHandle(media_view)
292
+ return cab_files
293
+
294
+
295
+ def extract_cab_files_from_media(db_handle, output_directory: str):
296
+ """Extract all CAB files from the list."""
297
+ os.makedirs(output_directory, exist_ok=True)
298
+
299
+ query = "SELECT `Cabinet` FROM `Media` WHERE `Cabinet` IS NOT NULL"
300
+ # Query to fetch CAB files from the Media table
301
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
302
+
303
+ cabinet_name_list: list = []
304
+ while True:
305
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
306
+ if not record_handle:
307
+ break
308
+
309
+ cabinet_name = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
310
+ cabinet_name_list.append(cabinet_name)
311
+ msi.MsiCloseHandle(record_handle)
312
+ msi.MsiCloseHandle(view_handle)
313
+
314
+ cab_file_paths: list = []
315
+ for cabinet_name in cabinet_name_list:
316
+ if cabinet_name.startswith("#"):
317
+ cabinet_name = cabinet_name[1:] # Remove the leading #
318
+
319
+ cab_file_path = os.path.join(output_directory, cabinet_name)
320
+ with open(cab_file_path, 'wb') as f:
321
+ # Read the binary stream from the MSI package
322
+ stream_query = f"SELECT `Data` FROM `_Streams` WHERE `Name`='{cabinet_name}'"
323
+ stream_view = base.create_open_execute_view_handle(db_handle, stream_query)
324
+
325
+ while True:
326
+ stream_record = base.create_fetch_record_from_view_handle(stream_view)
327
+ if not stream_record:
328
+ break
329
+
330
+ data = base.get_table_field_data_from_record(stream_record, field_index=1, data_type='stream')
331
+
332
+ f.write(data)
333
+
334
+ msi.MsiCloseHandle(stream_record)
335
+ msi.MsiViewClose(stream_view)
336
+ msi.MsiCloseHandle(stream_view)
337
+
338
+ print(f"Extracted: {cabinet_name}")
339
+ cab_file_paths.append(cab_file_path)
340
+
341
+ print(f"CAB files extraction completed. Files are saved to {output_directory}")
342
+ return cab_file_paths
343
+
344
+
345
+ def get_file_table_info(db_handle):
346
+ query = "SELECT `File`, `FileName`, `Component_` FROM `File`"
347
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
348
+
349
+ file_info = {}
350
+
351
+ # Fetch the records
352
+ while True:
353
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
354
+ if not record_handle:
355
+ break
356
+
357
+ file_key = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
358
+ # Handle cases with multiple file names
359
+ file_name = base.get_table_field_data_from_record(record_handle, field_index=2, data_type='stringw')
360
+ file_name = file_name.split('|')[-1]
361
+ component = base.get_table_field_data_from_record(record_handle, field_index=3, data_type='stringw')
362
+
363
+ file_info[file_key] = {'file_name': file_name, 'component': component}
364
+
365
+ msi.MsiCloseHandle(record_handle)
366
+ msi.MsiCloseHandle(view_handle)
367
+
368
+ return file_info
369
+
370
+
371
+ def get_component_table_info(db_handle):
372
+ component_info = {}
373
+
374
+ query = "SELECT `Component`, `Directory_` FROM `Component`"
375
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
376
+
377
+ # Fetch the records
378
+ while True:
379
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
380
+ if not record_handle:
381
+ break
382
+
383
+ component_key = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
384
+ directory = base.get_table_field_data_from_record(record_handle, field_index=2, data_type='stringw')
385
+
386
+ component_info[component_key] = {'directory': directory}
387
+
388
+ msi.MsiCloseHandle(record_handle)
389
+ msi.MsiCloseHandle(view_handle)
390
+
391
+ return component_info
392
+
393
+
394
+ def get_directory_table_info(db_handle):
395
+ directory_info = {}
396
+
397
+ # Open a view to the Directory table
398
+ query = "SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`"
399
+ view_handle = base.create_open_execute_view_handle(db_handle, query)
400
+
401
+ # Fetch the records
402
+ while True:
403
+ record_handle = base.create_fetch_record_from_view_handle(view_handle)
404
+ if not record_handle:
405
+ break
406
+
407
+ directory_key = base.get_table_field_data_from_record(record_handle, field_index=1, data_type='stringw')
408
+ parent_key = base.get_table_field_data_from_record(record_handle, field_index=2, data_type='stringw')
409
+
410
+ # Handle cases with multiple directory names with '|' character.
411
+ default_dir_buffer = base.get_table_field_data_from_record(record_handle, field_index=3, data_type='stringw')
412
+ default_dir = default_dir_buffer.split('|')[-1]
413
+
414
+ directory_info[directory_key] = {'parent': parent_key, 'default_dir': default_dir}
415
+
416
+ msi.MsiCloseHandle(record_handle)
417
+ msi.MsiCloseHandle(view_handle)
418
+
419
+ return directory_info
@@ -1,8 +1,10 @@
1
+ import os
1
2
  import subprocess
2
3
  import getpass
3
4
 
4
5
  from ... import process, filesystem, permissions
5
6
  from ...print_api import print_api
7
+ from .. import ubuntu_terminal
6
8
 
7
9
 
8
10
  def is_docker_installed():
@@ -59,14 +61,21 @@ def add_current_user_to_docker_group(print_kwargs: dict = None):
59
61
  return False
60
62
 
61
63
 
62
- def install_docker_ubuntu(rootless: bool = True, add_current_user_to_docker_group_bool: bool = False):
64
+ def install_docker_ubuntu(
65
+ use_docker_installer: bool = True,
66
+ rootless: bool = False,
67
+ add_current_user_to_docker_group_bool: bool = False
68
+ ):
63
69
  """
64
70
  The function will install docker on ubuntu.
65
71
  :param rootless: bool, if True, the rootless installation will be performed.
66
72
  Meaning, you will be able to run the 'docker' command without sudo and you will not need to add the
67
73
  current user to the docker group.
74
+ :param use_docker_installer: bool, if True, the docker installer will be used.
75
+ If False, the docker will be installed using the apt package manager, custom repo and keyring.
68
76
  :param add_current_user_to_docker_group_bool: bool, if True, the current user will be added to the docker group.
69
- So the user will be able to run the 'docker' command without sudo.
77
+ So the user will be able to run the 'docker' command without sudo. If you install docker in rootless mode
78
+ this is not needed.
70
79
 
71
80
  Usage in main.py (run with sudo):
72
81
  from atomicshop.wrappers.dockerw import install_docker
@@ -80,41 +89,66 @@ def install_docker_ubuntu(rootless: bool = True, add_current_user_to_docker_grou
80
89
  main()
81
90
  """
82
91
 
83
- # Remove the existing keyrings, so we will not be asked to overwrite it if it exists.
84
- docker_keyring_file_path: str = "/etc/apt/keyrings/docker.gpg"
85
- filesystem.remove_file(docker_keyring_file_path)
86
-
87
- script = f"""
88
- # Step 1: Set up Docker's apt repository
89
- sudo apt-get update
90
- sudo apt-get install -y ca-certificates curl gnupg
91
- sudo install -m 0755 -d /etc/apt/keyrings
92
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
93
- sudo chmod a+r /etc/apt/keyrings/docker.gpg
94
-
95
- # Add the repository to Apt sources
96
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
97
- sudo apt-get update
98
-
99
- # Step 2: Install the Docker packages
100
- sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
101
-
102
- # Step 3: Verify the installation
103
- # sudo docker run hello-world
104
-
105
- # Add Privileges to run docker without sudo. Add current user to Docker superuser group.
106
- # sudo usermod -aG docker $USER
107
- """
108
-
109
- process.execute_script(script, shell=True)
92
+ if use_docker_installer:
93
+ # Use the docker installer script.
94
+ # The script will install docker and add the current user to the docker group.
95
+ # The script will also install docker-compose and docker-buildx.
96
+ process.execute_script('curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh', shell=True)
97
+ else:
98
+ # Remove the existing keyrings, so we will not be asked to overwrite it if it exists.
99
+ docker_keyring_file_path: str = "/etc/apt/keyrings/docker.gpg"
100
+ filesystem.remove_file(docker_keyring_file_path)
101
+
102
+ script = f"""
103
+ # Step 1: Set up Docker's apt repository
104
+ sudo apt-get update
105
+ sudo apt-get install -y ca-certificates curl gnupg
106
+ sudo install -m 0755 -d /etc/apt/keyrings
107
+ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
108
+ sudo chmod a+r /etc/apt/keyrings/docker.gpg
109
+
110
+ # Add the repository to Apt sources
111
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
112
+ sudo apt-get update
113
+
114
+ # Step 2: Install the Docker packages
115
+ sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
116
+
117
+ # Step 3: Verify the installation
118
+ # sudo docker run hello-world
119
+
120
+ # Add Privileges to run docker without sudo. Add current user to Docker superuser group.
121
+ # sudo usermod -aG docker $USER
122
+ """
123
+
124
+ process.execute_script(script, shell=True)
125
+
126
+ if rootless:
127
+ # Install uidmap package.
128
+ ubuntu_terminal.update_system_packages()
129
+ ubuntu_terminal.install_packages(['uidmap'])
130
+
131
+ # After 'get-docker.sh' execution, we will install docker in rootless mode.
132
+ subprocess.run(['dockerd-rootless-setuptool.sh', 'install'], shell=True)
133
+
134
+ # Start and enable the docker service in user mode.
135
+ ubuntu_terminal.start_enable_service_check_availability('docker.service', user_mode=True, sudo=False)
136
+
137
+ # Enable lingering so Docker runs when the user is not logged in
138
+ subprocess.run(['sudo', 'loginctl', 'enable-linger', os.getlogin()], shell=True)
139
+
140
+ # Add $HOME/bin to your PATH if it's not already there.
141
+ ubuntu_terminal.add_path_to_bashrc()
110
142
 
111
143
  if add_current_user_to_docker_group_bool:
112
144
  # Check if current user that executed the script is a sudo user. If not, use the current user.
113
145
  # Add the current user to the docker group.
114
146
  add_current_user_to_docker_group()
115
147
 
116
- # Verify the installation.
117
- result: list = process.execute_with_live_output('sudo docker run hello-world')
148
+ # Verify the installation.
149
+ result: list = process.execute_with_live_output('sudo docker run hello-world')
150
+ else:
151
+ result: list = process.execute_with_live_output('docker run hello-world')
118
152
 
119
153
  print_api('\n'.join(result))
120
154
 
@@ -0,0 +1,63 @@
1
+ import os
2
+ import datetime
3
+ import json
4
+
5
+ import olefile
6
+
7
+
8
+ def convert_object_to_string(obj):
9
+ if isinstance(obj, bytes):
10
+ # MSI Database uses latin-1 encoding for strings, could be that other ole files too.
11
+ return obj.decode('latin-1')
12
+ elif isinstance(obj, datetime.datetime):
13
+ return obj.strftime('%Y-%m-%d-%H:%M:%S')
14
+ return obj
15
+
16
+
17
+ def extract_ole_metadata(ole_file_path: str, output_directory: str):
18
+ """
19
+ Extract metadata from an OLE2 file.
20
+ :param ole_file_path:
21
+ :param output_directory:
22
+ :return:
23
+ """
24
+ os.makedirs(output_directory, exist_ok=True)
25
+ metadata_file_path = os.path.join(output_directory, "metadata.json")
26
+
27
+ # Check if the file is ole2 file.
28
+ if not olefile.isOleFile(ole_file_path):
29
+ message = f"The file {ole_file_path} is not an OLE2 file."
30
+ print(message)
31
+ with open(metadata_file_path, "w") as metadata_file:
32
+ metadata_file.write(message)
33
+ return
34
+
35
+ # Open the OLE2 file.
36
+ ole = olefile.OleFileIO(ole_file_path)
37
+
38
+ # Get the metadata of the OLE2 file.
39
+ metadata = ole.get_metadata()
40
+
41
+ meta_properties: dict = {
42
+ 'SummaryInformation': {},
43
+ 'DocumentSummaryInformation': {}
44
+ }
45
+ # Properties from SummaryInformation stream.
46
+ for prop in metadata.SUMMARY_ATTRIBS:
47
+ value = getattr(metadata, prop)
48
+ value = convert_object_to_string(value)
49
+ meta_properties['SummaryInformation'][prop] = value
50
+ # Properties from DocumentSummaryInformation stream.
51
+ for prop in metadata.DOCSUM_ATTRIBS:
52
+ value = getattr(metadata, prop)
53
+ value = convert_object_to_string(value)
54
+ meta_properties['DocumentSummaryInformation'][prop] = value
55
+
56
+ # Save the metadata to a file.
57
+ with open(metadata_file_path, "w") as metadata_file:
58
+ json.dump(meta_properties, metadata_file, indent=4)
59
+
60
+ print(f"Metadata of the OLE2 file saved to {metadata_file_path}")
61
+
62
+ # Close the OLE2 file.
63
+ ole.close()
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import sys
2
3
  import subprocess
3
4
  import shutil
@@ -48,10 +49,25 @@ def upgrade_system_packages():
48
49
  subprocess.check_call(['sudo', 'apt-get', 'upgrade', '-y'])
49
50
 
50
51
 
51
- def is_service_running(service_name: str, return_false_on_error: bool = False) -> bool:
52
+ def is_service_running(service_name: str, user_mode: bool = False, return_false_on_error: bool = False) -> bool:
53
+ """
54
+ Function checks if a service is running.
55
+ :param service_name: str, the service name.
56
+ :param user_mode: bool, if True, the service will be checked in user mode.
57
+ :param return_false_on_error: bool, if True, the function will return False if an error occurs.
58
+ :return:
59
+ """
60
+
61
+ command: list = ['systemctl']
62
+
63
+ if user_mode:
64
+ command.append('--user')
65
+
66
+ command.extend(['is-active', service_name])
67
+
52
68
  try:
53
69
  # Use subprocess to run 'systemctl is-active' and capture its output
54
- status = subprocess.check_output(['systemctl', 'is-active', service_name], text=True).strip()
70
+ status = subprocess.check_output(command, text=True).strip()
55
71
  except subprocess.CalledProcessError as e:
56
72
  if return_false_on_error:
57
73
  # Handle error if systemctl command fails
@@ -66,22 +82,52 @@ def is_service_running(service_name: str, return_false_on_error: bool = False) -
66
82
  return False
67
83
 
68
84
 
69
- def enable_service(service_name: str):
85
+ def enable_service(service_name: str, sudo: bool = True, user_mode: bool = False):
70
86
  """
71
87
  Function enables a service.
72
88
  :param service_name: str, the service name.
89
+ :param sudo: bool, if True, the command will be executed with sudo.
90
+ :param user_mode: bool, if True, the service will be enabled in user mode.
73
91
  :return:
74
92
  """
75
- subprocess.check_call(['sudo', 'systemctl', 'enable', service_name])
76
93
 
94
+ command: list = []
95
+
96
+ if sudo:
97
+ command.append('sudo')
98
+
99
+ command.append('systemctl')
77
100
 
78
- def start_service(service_name: str):
101
+ if user_mode:
102
+ command.append('--user')
103
+
104
+ command.extend(['enable', service_name])
105
+
106
+ subprocess.check_call(command)
107
+
108
+
109
+ def start_service(service_name: str, sudo: bool = True, user_mode: bool = False):
79
110
  """
80
111
  Function starts a service.
81
112
  :param service_name: str, the service name.
113
+ :param sudo: bool, if True, the command will be executed with sudo.
114
+ :param user_mode: bool, if True, the service will be started in user mode.
82
115
  :return:
83
116
  """
84
- subprocess.check_call(['sudo', 'systemctl', 'start', service_name])
117
+
118
+ command: list = []
119
+
120
+ if sudo:
121
+ command.append('sudo')
122
+
123
+ command.append('systemctl')
124
+
125
+ if user_mode:
126
+ command.append('--user')
127
+
128
+ command.extend(['start', service_name])
129
+
130
+ subprocess.check_call(command)
85
131
 
86
132
 
87
133
  def start_enable_service_check_availability(
@@ -91,6 +137,8 @@ def start_enable_service_check_availability(
91
137
  start_service_bool: bool = True,
92
138
  enable_service_bool: bool = True,
93
139
  check_service_running: bool = True,
140
+ user_mode: bool = False,
141
+ sudo: bool = True,
94
142
  print_kwargs: dict = None
95
143
  ):
96
144
  """
@@ -103,6 +151,8 @@ def start_enable_service_check_availability(
103
151
  :param start_service_bool: bool, if True, the service will be started.
104
152
  :param enable_service_bool: bool, if True, the service will be enabled.
105
153
  :param check_service_running: bool, if True, the function will check if the service is running.
154
+ :param user_mode: bool, if True, the service will be started and enabled in user mode.
155
+ :param sudo: bool, if True, the command will be executed with sudo.
106
156
  :param print_kwargs: dict, the print arguments.
107
157
  :return:
108
158
  """
@@ -112,9 +162,9 @@ def start_enable_service_check_availability(
112
162
 
113
163
  # Start and enable the service.
114
164
  if start_service_bool:
115
- start_service(service_name)
165
+ start_service(service_name, user_mode=user_mode, sudo=sudo)
116
166
  if enable_service_bool:
117
- enable_service(service_name)
167
+ enable_service(service_name, user_mode=user_mode,sudo=sudo)
118
168
 
119
169
  if check_service_running:
120
170
  print_api(
@@ -122,10 +172,23 @@ def start_enable_service_check_availability(
122
172
  **(print_kwargs or {}))
123
173
  time.sleep(wait_time_seconds)
124
174
 
125
- if not is_service_running(service_name):
175
+ if not is_service_running(service_name, user_mode=user_mode):
126
176
  print_api(
127
177
  f"[{service_name}] service failed to start.", color='red', error_type=True, **(print_kwargs or {}))
128
178
  if exit_on_error:
129
179
  sys.exit(1)
130
180
  else:
131
181
  print_api(f"[{service_name}] service is running.", color='green', **(print_kwargs or {}))
182
+
183
+
184
+ def add_path_to_bashrc():
185
+ """Add $HOME/bin to the PATH in .bashrc if it's not already present."""
186
+ bashrc_path = os.path.expanduser("~/.bashrc")
187
+ new_path = 'export PATH=$PATH:$HOME/bin\n'
188
+ with open(bashrc_path, 'r+') as bashrc:
189
+ content = bashrc.read()
190
+ if "$HOME/bin" not in content:
191
+ bashrc.write(new_path)
192
+ print("Added $HOME/bin to .bashrc")
193
+ else:
194
+ print("$HOME/bin already in .bashrc")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 2.10.8
3
+ Version: 2.11.0
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License: MIT License
@@ -39,6 +39,7 @@ Requires-Dist: dnspython
39
39
  Requires-Dist: docker
40
40
  Requires-Dist: elasticsearch
41
41
  Requires-Dist: numpy
42
+ Requires-Dist: olefile
42
43
  Requires-Dist: openpyxl
43
44
  Requires-Dist: pandas
44
45
  Requires-Dist: paramiko
@@ -1,4 +1,4 @@
1
- atomicshop/__init__.py,sha256=jLebawbp1INbR4X9mVO70IY6_95wF8u3eJf77Id2Cis,123
1
+ atomicshop/__init__.py,sha256=s2wz8eSBb6qWra33TBucyEy5wBD-UCjN1HUhZ99l0VE,123
2
2
  atomicshop/_basics_temp.py,sha256=6cu2dd6r2dLrd1BRNcVDKTHlsHs_26Gpw8QS6v32lQ0,3699
3
3
  atomicshop/_create_pdf_demo.py,sha256=Yi-PGZuMg0RKvQmLqVeLIZYadqEZwUm-4A9JxBl_vYA,3713
4
4
  atomicshop/_patch_import.py,sha256=ENp55sKVJ0e6-4lBvZnpz9PQCt3Otbur7F6aXDlyje4,6334
@@ -24,7 +24,7 @@ atomicshop/keyboard_press.py,sha256=1W5kRtOB75fulVx-uF2yarBhW0_IzdI1k73AnvXstk0,
24
24
  atomicshop/pbtkmultifile_argparse.py,sha256=aEk8nhvoQVu-xyfZosK3ma17CwIgOjzO1erXXdjwtS4,4574
25
25
  atomicshop/permissions.py,sha256=y63kcxU6GAmnVXqWhs_N29XorOD2xYWlK_CjZ1PpHgA,2897
26
26
  atomicshop/print_api.py,sha256=DhbCQd0MWZZ5GYEk4oTu1opRFC-b31g1VWZgTGewG2Y,11568
27
- atomicshop/process.py,sha256=SXaqG8pzNkHvppPhuhixEIXSaqbeDlTUFLaS5bYSp54,14176
27
+ atomicshop/process.py,sha256=iP9H9Tr51o98REVjcFJ8D0BJUcjRpOVsD9yaEWoJ_h4,14686
28
28
  atomicshop/process_name_cmd.py,sha256=TNAK6kQZm5JKWzEW6QLqVHEG98ZLNDQiSS4YwDk8V8c,3830
29
29
  atomicshop/process_poller.py,sha256=WfmwCLALfTYOq8ri0vkPeqq8ruEyA_43DaN--CU2_XY,10854
30
30
  atomicshop/python_file_patcher.py,sha256=kd3rBWvTcosLEk-7TycNdfKW9fZbe161iVwmH4niUo0,5515
@@ -51,6 +51,7 @@ atomicshop/addons/a_setup_scripts/install_psycopg2_ubuntu.sh,sha256=lM7LkXQ2AxfF
51
51
  atomicshop/addons/a_setup_scripts/install_pywintrace_0.3.cmd,sha256=lEP_o6rWcBFUyup6_c-LTL3Q2LRMqryLuG3mJw080Zc,115
52
52
  atomicshop/addons/mains/install_docker_ubuntu_main_sudo.py,sha256=3VDGDO41Vubzf64DaBapLlFYX52dEdyPBNfolSsbGcM,161
53
53
  atomicshop/addons/mains/install_wsl_ubuntu_lts_admin.py,sha256=PrvZ4hMuadzj2GYKRZSwyNayJUuaSnCF9nV6ORqoPdo,123
54
+ atomicshop/addons/mains/msi_unpacker.py,sha256=XAJdEqs-3s9JqIgHpGRL-HxLKpFMXdrlXmq2Is2Pyfk,164
54
55
  atomicshop/addons/mains/search_for_hyperlinks_in_docx.py,sha256=HkIdo_Sz9nPbbbJf1mwfwFkyI7vkvpH8qiIkuYopN4w,529
55
56
  atomicshop/addons/mains/FACT/factw_fact_extractor_docker_image_main_sudo.py,sha256=DDKX3Wp2SmzMCEtCIEOUbEKMob2ZQ7VEQGLEf9uYXrs,320
56
57
  atomicshop/addons/mains/FACT/update_extract.py,sha256=H3VsdhlA7xxK5lI_nyrWUdk8GNZXbEUVR_K9cJ4ECAw,506
@@ -68,6 +69,7 @@ atomicshop/archiver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
68
69
  atomicshop/archiver/_search_in_zip.py,sha256=dd8qFSvIhcKmtnPj_uYNJFPmMwZp4tZys0kKgTw_ACw,8385
69
70
  atomicshop/archiver/archiver.py,sha256=BomnK7zT-nQXA1z0i2R2aTv8eu88wPx7tf2HtOdbmEc,1280
70
71
  atomicshop/archiver/search_in_archive.py,sha256=kui33oobL2F3VLgAE8L037rHR8Ud3HpXz_E0tbuiDD4,10473
72
+ atomicshop/archiver/sevenz_app_w.py,sha256=TEOhlZPzKqQPStmyIq5E_yP98brj1t_GuoBgeLXA4dA,3019
71
73
  atomicshop/archiver/sevenzs.py,sha256=5i_C50-deC1Cz_GQdMGofV2jeMPbbGWAvih-QnA72cg,1370
72
74
  atomicshop/archiver/zips.py,sha256=k742K1bEDtc_4N44j_Waebi-uOkxxavqltvV6q-BLW4,14402
73
75
  atomicshop/basics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -145,17 +147,24 @@ atomicshop/wrappers/cryptographyw.py,sha256=H5NaHHDkr97QYhUrHFO9vY218u8k3N3Zgh6b
145
147
  atomicshop/wrappers/ffmpegw.py,sha256=wcq0ZnAe0yajBOuTKZCCaKI7CDBjkq7FAgdW5IsKcVE,6031
146
148
  atomicshop/wrappers/githubw.py,sha256=mQGtj6up1HIvjOD2t0bmOWjLooJLYvuIa7d7H-tknrw,9998
147
149
  atomicshop/wrappers/numpyw.py,sha256=sBV4gSKyr23kXTalqAb1oqttzE_2XxBooCui66jbAqc,1025
150
+ atomicshop/wrappers/olefilew.py,sha256=biD5m58rogifCYmYhJBrAFb9O_Bn_spLek_9HofLeYE,2051
148
151
  atomicshop/wrappers/pipw.py,sha256=iq4pDhmrHqKbcRi3Y6_v2PQSdb4fqfAx79rsX7JEmXo,801
149
152
  atomicshop/wrappers/process_wrapper_pbtk.py,sha256=ycPmBRnv627RWks6N8OhxJQe8Gu3h3Vwj-4HswPOw0k,599
150
153
  atomicshop/wrappers/pyopensslw.py,sha256=OBWxA6EJ2vU_Qlf4M8m6ilcG3hyYB4yB0EsXUf7NhEU,6804
151
- atomicshop/wrappers/ubuntu_terminal.py,sha256=1CcTXASzxdFFFJTf3G1OtHcs8abbn_jgkPsY0OzEs1w,4132
154
+ atomicshop/wrappers/ubuntu_terminal.py,sha256=Ykt3vSkvS2IddjRfZqwDbW98g4E1cServj6iUhg0n30,6121
152
155
  atomicshop/wrappers/wslw.py,sha256=AKphiHLSddL7ErevUowr3f9Y1AgGz_R3KZ3NssW07h8,6959
153
156
  atomicshop/wrappers/certauthw/certauth.py,sha256=hKedW0DOWlEigSNm8wu4SqHkCQsGJ1tJfH7s4nr3Bk0,12223
154
157
  atomicshop/wrappers/certauthw/certauthw.py,sha256=4WvhjANI7Kzqrr_nKmtA8Kf7B6rute_5wfP65gwQrjw,8082
158
+ atomicshop/wrappers/ctyping/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
155
159
  atomicshop/wrappers/ctyping/process_winapi.py,sha256=QcXL-ETtlSSkoT8F7pYle97ubGWsjYp8cx8HxkVMgAc,2762
160
+ atomicshop/wrappers/ctyping/msi_windows_installer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
161
+ atomicshop/wrappers/ctyping/msi_windows_installer/base.py,sha256=Uu9SlWLsQQ6mjE-ek-ptHcmgiI3Ruah9bdZus70EaVY,4884
162
+ atomicshop/wrappers/ctyping/msi_windows_installer/cabs.py,sha256=htzwb2ROYI8yyc82xApStckPS2yCcoyaw32yC15KROs,3285
163
+ atomicshop/wrappers/ctyping/msi_windows_installer/extract_msi_main.py,sha256=eyFAD1itG0UClPs92GFefIWTccRgRF55KVLKl1pugGw,5402
164
+ atomicshop/wrappers/ctyping/msi_windows_installer/tables.py,sha256=ajiFiLXl1prg2kOhatfT8E2RKX7F8JjUJpwkVuKu1GY,16263
156
165
  atomicshop/wrappers/dockerw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
166
  atomicshop/wrappers/dockerw/dockerw.py,sha256=w8zSJr5C7cbvbuG09ORCpAe0BOcibqqL_Z2EKEBHYK4,6266
158
- atomicshop/wrappers/dockerw/install_docker.py,sha256=glX5UKReRc8rUrBQUKqIQb7HnLiG2QlPXxC-qemNpAc,5133
167
+ atomicshop/wrappers/dockerw/install_docker.py,sha256=QNKLgm_UVpwN63D8v8k8FhOyEwA9XjHi0CT5f1FfEhA,6846
159
168
  atomicshop/wrappers/elasticsearchw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
169
  atomicshop/wrappers/elasticsearchw/config_basic.py,sha256=fDujtrjEjbWiYh_WQ3OcYp_8mXhXPYeKLy4wSPL5qM0,1177
161
170
  atomicshop/wrappers/elasticsearchw/elasticsearchw.py,sha256=7TqFdEFznO8NlligJhEKk1vm641ALpCYdaRl1uoXdzM,9768
@@ -233,8 +242,8 @@ atomicshop/wrappers/socketw/socket_server_tester.py,sha256=AhpurHJmP2kgzHaUbq5ey
233
242
  atomicshop/wrappers/socketw/socket_wrapper.py,sha256=aXBwlEIJhFT0-c4i8iNlFx2It9VpCEpsv--5Oqcpxao,11624
234
243
  atomicshop/wrappers/socketw/ssl_base.py,sha256=k4V3gwkbq10MvOH4btU4onLX2GNOsSfUAdcHmL1rpVE,2274
235
244
  atomicshop/wrappers/socketw/statistics_csv.py,sha256=t3dtDEfN47CfYVi0CW6Kc2QHTEeZVyYhc57IYYh5nmA,826
236
- atomicshop-2.10.8.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
237
- atomicshop-2.10.8.dist-info/METADATA,sha256=35z6a8qlF_O0hPLHYCDfdrqRN_5nx52xrhoaQ0BbCqk,10423
238
- atomicshop-2.10.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
239
- atomicshop-2.10.8.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
240
- atomicshop-2.10.8.dist-info/RECORD,,
245
+ atomicshop-2.11.0.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
246
+ atomicshop-2.11.0.dist-info/METADATA,sha256=4qiSE0G-IMCVID80QijXfSjtq7_eJvhsvkINwCxNGjA,10447
247
+ atomicshop-2.11.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
248
+ atomicshop-2.11.0.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
249
+ atomicshop-2.11.0.dist-info/RECORD,,