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/PersistentState.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PersistentState class for saving user preferences and device states
|
|
3
|
+
"""
|
|
4
|
+
# pylint: disable=invalid-name,broad-exception-caught,line-too-long
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from .Utils import Utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PersistentState:
|
|
12
|
+
"""Manages persistent state for dwipe preferences and device locks"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config_path=None):
|
|
15
|
+
"""Initialize persistent state
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
config_path: Path to config file (default: ~/.config/dwipe/state.json)
|
|
19
|
+
"""
|
|
20
|
+
if config_path is None:
|
|
21
|
+
config_dir = Utils.get_config_dir()
|
|
22
|
+
config_path = config_dir / 'state.json'
|
|
23
|
+
|
|
24
|
+
self.config_path = Path(config_path)
|
|
25
|
+
self.state = {
|
|
26
|
+
'theme': 'default',
|
|
27
|
+
'wipe_mode': 'Zero', # 'Rand' or 'Zero' or +V
|
|
28
|
+
'passes': 1, # 1, 2, or 4 wipe pass
|
|
29
|
+
'confirmation': 'YES', # 'Y', 'y', 'YES', 'yes', 'device'
|
|
30
|
+
'verify_pct': 2, # 0, 2, 5, 10, 25, 50, 100
|
|
31
|
+
'dense': False, # True = compact view, False = blank lines between disks
|
|
32
|
+
'slowdown_stop': 16,
|
|
33
|
+
'stall_timeout': 60,
|
|
34
|
+
'port_serial': False,
|
|
35
|
+
'devices': {} # device_id -> {locked, last_seen, last_name, size_bytes}
|
|
36
|
+
}
|
|
37
|
+
self.dirty = False
|
|
38
|
+
self.max_devices = 400
|
|
39
|
+
|
|
40
|
+
self.load()
|
|
41
|
+
|
|
42
|
+
def save_updated_opts(self, opts):
|
|
43
|
+
"""Save updated option variables from opts to state"""
|
|
44
|
+
for key in self.state:
|
|
45
|
+
if key == 'devices':
|
|
46
|
+
continue # Skip devices dict
|
|
47
|
+
if hasattr(opts, key):
|
|
48
|
+
value = getattr(opts, key)
|
|
49
|
+
if self.state[key] != value:
|
|
50
|
+
self.state[key] = value
|
|
51
|
+
self.dirty = True
|
|
52
|
+
|
|
53
|
+
def restore_updated_opts(self, opts):
|
|
54
|
+
"""Restore option variables from state to opts"""
|
|
55
|
+
for key in self.state:
|
|
56
|
+
if hasattr(opts, key):
|
|
57
|
+
setattr(opts, key, self.state[key])
|
|
58
|
+
|
|
59
|
+
def load(self):
|
|
60
|
+
"""Load state from disk"""
|
|
61
|
+
if self.config_path.exists():
|
|
62
|
+
try:
|
|
63
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
64
|
+
loaded = json.load(f)
|
|
65
|
+
self.state.update(loaded)
|
|
66
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
67
|
+
print(f'Warning: Could not load state from {self.config_path}: {e}')
|
|
68
|
+
|
|
69
|
+
def save(self):
|
|
70
|
+
"""Save state to disk if dirty"""
|
|
71
|
+
if not self.dirty:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Clean up old devices before saving
|
|
76
|
+
self._cleanup_old_devices()
|
|
77
|
+
|
|
78
|
+
with open(self.config_path, 'w', encoding='utf-8') as f:
|
|
79
|
+
json.dump(self.state, f, indent=2)
|
|
80
|
+
# Fix ownership if running with sudo
|
|
81
|
+
Utils.fix_file_ownership(self.config_path)
|
|
82
|
+
self.dirty = False
|
|
83
|
+
except IOError as e:
|
|
84
|
+
print(f'Warning: Could not save state to {self.config_path}: {e}')
|
|
85
|
+
|
|
86
|
+
def _cleanup_old_devices(self):
|
|
87
|
+
"""Keep only the most recent max_devices entries"""
|
|
88
|
+
devices = self.state['devices']
|
|
89
|
+
if len(devices) <= self.max_devices:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Sort by last_seen timestamp, keep most recent
|
|
93
|
+
sorted_devices = sorted(
|
|
94
|
+
devices.items(),
|
|
95
|
+
key=lambda x: x[1].get('last_seen', 0),
|
|
96
|
+
reverse=True
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Keep only max_devices most recent
|
|
100
|
+
self.state['devices'] = dict(sorted_devices[:self.max_devices])
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def make_device_id(partition):
|
|
104
|
+
"""Create a stable device identifier
|
|
105
|
+
|
|
106
|
+
Priority:
|
|
107
|
+
1. partition.uuid (PARTUUID for partitions, UUID for filesystems)
|
|
108
|
+
2. partition.serial (for whole disks)
|
|
109
|
+
3. Fallback: hash of name+size+label+fstype
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
partition: SimpleNamespace with device info
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
str: Stable device identifier
|
|
116
|
+
"""
|
|
117
|
+
# Try UUID first (PARTUUID or filesystem UUID)
|
|
118
|
+
if hasattr(partition, 'uuid') and partition.uuid:
|
|
119
|
+
return f'uuid:{partition.uuid}'
|
|
120
|
+
|
|
121
|
+
# Try serial number (for whole disks)
|
|
122
|
+
if hasattr(partition, 'serial') and partition.serial:
|
|
123
|
+
return f'serial:{partition.serial}'
|
|
124
|
+
|
|
125
|
+
# Fallback: create stable ID from device characteristics
|
|
126
|
+
# This will break if device is repartitioned/reformatted, but that's acceptable
|
|
127
|
+
parts = [
|
|
128
|
+
partition.name,
|
|
129
|
+
str(partition.size_bytes),
|
|
130
|
+
partition.label if hasattr(partition, 'label') else '',
|
|
131
|
+
partition.fstype if hasattr(partition, 'fstype') else ''
|
|
132
|
+
]
|
|
133
|
+
fallback_id = ':'.join(parts)
|
|
134
|
+
return f'fallback:{fallback_id}'
|
|
135
|
+
|
|
136
|
+
def get_device_locked(self, partition):
|
|
137
|
+
"""Check if a device is locked
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
partition: SimpleNamespace with device info
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
bool: True if device is locked
|
|
144
|
+
"""
|
|
145
|
+
device_id = self.make_device_id(partition)
|
|
146
|
+
device_state = self.state['devices'].get(device_id, {})
|
|
147
|
+
return device_state.get('locked', False)
|
|
148
|
+
|
|
149
|
+
def set_device_locked(self, partition, locked):
|
|
150
|
+
"""Set device lock state
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
partition: SimpleNamespace with device info
|
|
154
|
+
locked: bool, True to lock device
|
|
155
|
+
"""
|
|
156
|
+
device_id = self.make_device_id(partition)
|
|
157
|
+
now = int(time.time())
|
|
158
|
+
|
|
159
|
+
if device_id not in self.state['devices']:
|
|
160
|
+
self.state['devices'][device_id] = {}
|
|
161
|
+
|
|
162
|
+
device_state = self.state['devices'][device_id]
|
|
163
|
+
device_state['locked'] = locked
|
|
164
|
+
device_state['last_seen'] = now
|
|
165
|
+
device_state['last_name'] = partition.name
|
|
166
|
+
device_state['size_bytes'] = partition.size_bytes
|
|
167
|
+
|
|
168
|
+
self.dirty = True
|
|
169
|
+
|
|
170
|
+
def update_device_seen(self, partition):
|
|
171
|
+
"""Update last_seen timestamp for a device
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
partition: SimpleNamespace with device info
|
|
175
|
+
"""
|
|
176
|
+
device_id = self.make_device_id(partition)
|
|
177
|
+
now = int(time.time())
|
|
178
|
+
|
|
179
|
+
if device_id in self.state['devices']:
|
|
180
|
+
self.state['devices'][device_id]['last_seen'] = now
|
|
181
|
+
self.state['devices'][device_id]['last_name'] = partition.name
|
|
182
|
+
self.dirty = True
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def sync(self):
|
|
186
|
+
"""Save state if dirty (called each loop)"""
|
|
187
|
+
self.save()
|