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.
@@ -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()