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/DeviceInfo.py +593 -0
- dwipe/DiskWipe.py +890 -0
- dwipe/PersistentState.py +187 -0
- dwipe/ToolManager.py +637 -0
- dwipe/Utils.py +199 -0
- dwipe/WipeJob.py +1243 -0
- dwipe/WipeJobFuture.py +245 -0
- dwipe/main.py +19 -853
- dwipe-2.0.1.dist-info/METADATA +410 -0
- dwipe-2.0.1.dist-info/RECORD +14 -0
- dwipe-1.0.7.dist-info/METADATA +0 -72
- dwipe-1.0.7.dist-info/RECORD +0 -7
- {dwipe-1.0.7.dist-info → dwipe-2.0.1.dist-info}/WHEEL +0 -0
- {dwipe-1.0.7.dist-info → dwipe-2.0.1.dist-info}/entry_points.txt +0 -0
- {dwipe-1.0.7.dist-info → dwipe-2.0.1.dist-info}/licenses/LICENSE +0 -0
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
|