dwipe 1.0.7__py3-none-any.whl → 2.0.1__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.
dwipe/Utils.py ADDED
@@ -0,0 +1,199 @@
1
+ """
2
+ Utils class - Utility functions for dwipe
3
+ """
4
+ import os
5
+ import sys
6
+ import datetime
7
+ from pathlib import Path
8
+
9
+
10
+ class Utils:
11
+ """Utility functions encapsulated as a family"""
12
+
13
+ @staticmethod
14
+ def human(number):
15
+ """Return a concise number description."""
16
+ suffixes = ['K', 'M', 'G', 'T']
17
+ number = float(number)
18
+ while suffixes:
19
+ suffix = suffixes.pop(0)
20
+ number /= 1000 # decimal
21
+ if number < 999.95 or not suffixes:
22
+ return f'{number:.1f}{suffix}B' # decimal
23
+ return None
24
+
25
+ @staticmethod
26
+ def ago_str(delta_secs, signed=False):
27
+ """Turn time differences in seconds to a compact representation;
28
+ e.g., '18h·39m' or '450ms' for sub-second times
29
+ """
30
+ abs_delta = delta_secs if delta_secs >= 0 else -delta_secs
31
+
32
+ # For sub-second times, show milliseconds
33
+ if 0.00051 <= abs_delta < 0.99949:
34
+ ms = int(abs_delta * 1000)
35
+ rv = '-' if signed and delta_secs < 0 else ''
36
+ return rv + f'{ms}ms'
37
+
38
+ ago = int(max(0, round(abs_delta)))
39
+ divs = (60, 60, 24, 7, 52, 9999999)
40
+ units = ('s', 'm', 'h', 'd', 'w', 'y')
41
+ vals = (ago % 60, int(ago / 60)) # seed with secs, mins (step til 2nd fits)
42
+ uidx = 1 # best units
43
+ for div in divs[1:]:
44
+ if vals[1] < div:
45
+ break
46
+ vals = (vals[1] % div, int(vals[1] / div))
47
+ uidx += 1
48
+ rv = '-' if signed and delta_secs < 0 else ''
49
+ rv += f'{vals[1]}{units[uidx]}' if vals[1] else ''
50
+ rv += f'{vals[0]:d}{units[uidx - 1]}'
51
+ return rv
52
+
53
+ @staticmethod
54
+ def rerun_module_as_root(module_name):
55
+ """Rerun using the module name"""
56
+ if os.geteuid() != 0: # Re-run the script with sudo
57
+ os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
58
+ vp = ['sudo', sys.executable, '-m', module_name] + sys.argv[1:]
59
+ os.execvp('sudo', vp)
60
+
61
+ @staticmethod
62
+ def get_config_dir():
63
+ """Get the dwipe config directory, handling sudo correctly
64
+
65
+ Returns the real user's ~/.config/dwipe directory, even when running with sudo
66
+ """
67
+ real_user = os.environ.get('SUDO_USER')
68
+ if real_user:
69
+ # Running with sudo - get the real user's home directory
70
+ import pwd
71
+ real_home = pwd.getpwnam(real_user).pw_dir
72
+ config_dir = Path(real_home) / '.config' / 'dwipe'
73
+ else:
74
+ # Not running with sudo - use normal home
75
+ config_dir = Path.home() / '.config' / 'dwipe'
76
+
77
+ config_dir.mkdir(parents=True, exist_ok=True)
78
+
79
+ # Fix ownership if running with sudo
80
+ if real_user:
81
+ try:
82
+ import pwd
83
+ pw_record = pwd.getpwnam(real_user)
84
+ uid, gid = pw_record.pw_uid, pw_record.pw_gid
85
+ os.chown(config_dir, uid, gid)
86
+ # Also fix parent .config directory if we created it
87
+ parent = config_dir.parent
88
+ if parent.exists():
89
+ os.chown(parent, uid, gid)
90
+ except (OSError, KeyError):
91
+ pass # Ignore permission errors and missing users
92
+
93
+ return config_dir
94
+
95
+ @staticmethod
96
+ def fix_file_ownership(file_path):
97
+ """Fix file ownership to the real user when running with sudo"""
98
+ real_user = os.environ.get('SUDO_USER')
99
+ if real_user:
100
+ try:
101
+ import pwd
102
+ pw_record = pwd.getpwnam(real_user)
103
+ uid, gid = pw_record.pw_uid, pw_record.pw_gid
104
+ os.chown(file_path, uid, gid)
105
+ except (OSError, KeyError):
106
+ pass # Ignore permission errors and missing users
107
+
108
+ @staticmethod
109
+ def get_log_path():
110
+ """Get the path to the log file, creating directory if needed"""
111
+ log_dir = Utils.get_config_dir()
112
+ return log_dir / 'log.txt'
113
+
114
+ @staticmethod
115
+ def trim_log_if_needed(log_path, max_lines=1000):
116
+ """Trim log file by removing oldest 1/3 if it exceeds max_lines"""
117
+ try:
118
+ if not log_path.exists():
119
+ return
120
+
121
+ with open(log_path, 'r', encoding='utf-8') as f:
122
+ lines = f.readlines()
123
+
124
+ if len(lines) > max_lines:
125
+ # Keep the newest 2/3 of the log
126
+ keep_count = len(lines) * 2 // 3
127
+ with open(log_path, 'w', encoding='utf-8') as f:
128
+ f.writelines(lines[-keep_count:])
129
+ except Exception:
130
+ pass # Don't fail if log trimming fails
131
+
132
+ @staticmethod
133
+ def log_wipe(device_name, size_bytes, mode, result, elapsed_time=None, uuid=None, label=None, fstype=None, pct=None, verify_result=None):
134
+ """Log a wipe or verify operation to ~/.config/dwipe/log.txt
135
+
136
+ Args:
137
+ device_name: Device name (e.g., 'sdb1')
138
+ size_bytes: Size of device in bytes
139
+ mode: 'Rand', 'Zero', or 'Vrfy' (for verify operations)
140
+ result: 'completed' or 'stopped'
141
+ elapsed_time: Optional elapsed time in seconds
142
+ uuid: Optional UUID of the partition
143
+ label: Optional label of the partition (only for non-wiped)
144
+ fstype: Optional filesystem type (only for non-wiped)
145
+ pct: Optional percentage (for stopped wipes)
146
+ verify_result: Optional verify result (chi-squared value for Rand verifies)
147
+ """
148
+ log_path = Utils.get_log_path()
149
+
150
+ # Trim log if needed before appending
151
+ Utils.trim_log_if_needed(log_path)
152
+
153
+ timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M')
154
+ size_str = Utils.human(size_bytes)
155
+ time_str = f' in {Utils.ago_str(int(elapsed_time))}' if elapsed_time else ''
156
+
157
+ # Show percentage if available, otherwise 100% for completed or result status
158
+ if result == 'completed':
159
+ status_str = '100%'
160
+ elif result in ('OK', 'FAIL', 'skip'):
161
+ # For verify operations: show 100% and the result
162
+ status_str = f'100%'
163
+ elif pct is not None:
164
+ status_str = f'{pct:3d}%'
165
+ else:
166
+ status_str = f'{result:>4s}'
167
+
168
+ # Build UUID field (last 8 chars or full if shorter)
169
+ uuid_str = uuid[-8:] if uuid and len(uuid) >= 8 else (uuid if uuid else '-')
170
+
171
+ # For wiped/verified disks, don't show label/fstype (they shouldn't have any)
172
+ # Only show for non-completed operations
173
+ if (result in ('completed', 'OK', 'FAIL', 'skip')) and mode in ('Rand', 'Zero', 'Vrfy'):
174
+ info_str = ''
175
+ else:
176
+ # Show fstype and label for non-wiped disks
177
+ fstype_str = fstype if fstype and fstype.strip() else '-'
178
+ label_str = f"'{label}'" if label and label.strip() else "'-'"
179
+ info_str = f' | {fstype_str} {label_str}'
180
+
181
+ # Add result status for verify operations (OK/FAIL)
182
+ result_str = ''
183
+ if result in ('OK', 'FAIL', 'skip'):
184
+ result_str = f' | {result}'
185
+
186
+ # Add verify result details if available (failure reason or stats)
187
+ stats_str = ''
188
+ if verify_result:
189
+ stats_str = f' | {verify_result}'
190
+
191
+ log_line = f'{timestamp} | {mode:4s} | {status_str} {size_str:>8s}{time_str:>12s} | {device_name:8s} | {uuid_str:8s}{info_str}{result_str}{stats_str}\n'
192
+
193
+ try:
194
+ with open(log_path, 'a', encoding='utf-8') as f:
195
+ f.write(log_line)
196
+ # Fix ownership if running with sudo
197
+ Utils.fix_file_ownership(log_path)
198
+ except Exception:
199
+ pass # Don't fail if logging fails