dwipe 1.0.0__py3-none-any.whl → 1.0.2__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/main.py CHANGED
@@ -15,13 +15,13 @@ import re
15
15
  import json
16
16
  import subprocess
17
17
  import time
18
+ import datetime
18
19
  import threading
19
20
  import random
20
21
  import shutil
21
22
  import traceback
22
23
  import curses as cs
23
24
  from types import SimpleNamespace
24
- from typing import Tuple, List
25
25
  from dwipe.PowerWindow import Window, OptionSpinner
26
26
 
27
27
  def human(number):
@@ -30,9 +30,9 @@ def human(number):
30
30
  number = float(number)
31
31
  while suffixes:
32
32
  suffix = suffixes.pop(0)
33
- number /= 1024
34
- if number < 99.95 or not suffixes:
35
- return f'{number:.1f}{suffix}'
33
+ number /= 1000 # decimal
34
+ if number < 999.95 or not suffixes:
35
+ return f'{number:.1f}{suffix}B' # decimal
36
36
  return None
37
37
  ##############################################################################
38
38
  def ago_str(delta_secs, signed=False):
@@ -62,6 +62,7 @@ class WipeJob:
62
62
  # Generate a 1MB buffer of random data
63
63
  BUFFER_SIZE = 1 * 1024 * 1024 # 1MB
64
64
  WRITE_SIZE = 16 * 1024 # 16KB
65
+ STATE_OFFSET = 15 * 1024 # where json is written
65
66
  buffer = bytearray(os.urandom(BUFFER_SIZE))
66
67
  zero_buffer = bytes(WRITE_SIZE)
67
68
 
@@ -123,6 +124,62 @@ class WipeJob:
123
124
 
124
125
  return ago_str(int(round(elapsed_time))), pct_str, rate_str, when_str
125
126
 
127
+ def prep_marker_buffer(self, is_random):
128
+ """ Get the 1st 16KB to write:
129
+ - 15K zeros
130
+ - JSON status + zero fill to 1KB
131
+ """
132
+ data = { "unixtime": int(time.time()),
133
+ "scrubbed_bytes": self.total_written,
134
+ "size_bytes": self.total_size,
135
+ "mode": 'Rand' if is_random else 'Zero'
136
+ }
137
+ json_data = json.dumps(data).encode('utf-8')
138
+ buffer = bytearray(self.BUFFER_SIZE)
139
+ buffer[:self.STATE_OFFSET] = b'\x00' * self.STATE_OFFSET
140
+ buffer[self.STATE_OFFSET:self.STATE_OFFSET+len(json_data)] = json_data
141
+ remaining_size = self.BUFFER_SIZE - (self.STATE_OFFSET+len(json_data))
142
+ buffer[self.STATE_OFFSET+len(json_data):] = b'\x00' * remaining_size
143
+ return buffer
144
+
145
+ @staticmethod
146
+ def read_marker_buffer(device_name):
147
+ """ Open the device and read the first 16 KB """
148
+ try:
149
+ with open(f'/dev/{device_name}', 'rb') as device:
150
+ device.seek(0)
151
+ buffer = device.read(WipeJob.BUFFER_SIZE)
152
+ except Exception:
153
+ return None # cannot find info
154
+
155
+ if buffer[:WipeJob.STATE_OFFSET] != b'\x00' * (WipeJob.STATE_OFFSET):
156
+ return None # First 15 KB are not zeros
157
+
158
+ # Extract JSON data from the next 1 KB Strip trailing zeros
159
+ json_data_bytes = buffer[WipeJob.STATE_OFFSET:WipeJob.BUFFER_SIZE].rstrip(b'\x00')
160
+
161
+ if not json_data_bytes:
162
+ return None # No JSON data found
163
+
164
+ # Deserialize the JSON data
165
+ try:
166
+ data = json.loads(json_data_bytes.decode('utf-8'))
167
+ except (json.JSONDecodeError, Exception):
168
+ return None # Invalid JSON data!
169
+
170
+ rv = {}
171
+ for key, value in data.items():
172
+ if key in ('unixtime', 'scrubbed_bytes', 'size_bytes') and isinstance(value, int):
173
+ rv[key] = value
174
+ elif key in ('mode', ) and isinstance(value, str):
175
+ rv[key] = value
176
+ else:
177
+ return None # bogus data
178
+ if len(rv) != 4:
179
+ return None # bogus data
180
+ return SimpleNamespace(**rv)
181
+
182
+
126
183
  def write_partition(self):
127
184
  """Writes random chunks to a device and updates the progress status."""
128
185
  self.total_written = 0 # Track total bytes written
@@ -155,11 +212,11 @@ class WipeJob:
155
212
  if self.opts.dry_run and self.total_written >= self.total_size:
156
213
  break
157
214
  # clear the beginning of device whether aborted or not
158
- # if we have started writing
215
+ # if we have started writing + status in JSON
159
216
  if not self.opts.dry_run and self.total_written > 0 and is_random:
160
217
  device.seek(0)
161
- chunk = memoryview(WipeJob.zero_buffer)
162
- bytes_written = device.write(chunk)
218
+ # chunk = memoryview(WipeJob.zero_buffer)
219
+ bytes_written = device.write(self.prep_marker_buffer(is_random))
163
220
  self.done = True
164
221
 
165
222
  class DeviceInfo:
@@ -183,13 +240,37 @@ class DeviceInfo:
183
240
  dflt=dflt, # default run-time state
184
241
  label='', # blkid
185
242
  fstype='', # blkid
243
+ model='', # /sys/class/block/{name}/device/vendor|model
186
244
  # fsuse='-',
187
245
  size_bytes=size_bytes, # /sys/block/{name}/...
246
+ marker='', # persistent status
188
247
  mounts=[], # /proc/mounts
189
248
  minors=[],
190
249
  job=None, # if zap running
191
250
  )
192
251
 
252
+
253
+ @staticmethod
254
+ def get_device_vendor_model(device_name):
255
+ """ Gets the vendor and model for a given device from the /sys/class/block directory.
256
+ - Args: - device_name: The device name, such as 'sda', 'sdb', etc.
257
+ - - Returns: A string containing the vendor and model information.
258
+ """
259
+ def get_str(device_name, suffix):
260
+ try:
261
+ rv = ''
262
+ fullpath = f'/sys/class/block/{device_name}/device/{suffix}'
263
+ with open(fullpath, 'r', encoding='utf-8') as f: # Read information
264
+ rv = f.read().strip()
265
+ except (FileNotFoundError, Exception):
266
+ # print(f"Error reading {info} for {device_name} : {e}")
267
+ pass
268
+ return rv
269
+
270
+ # rv = f'{get_str(device_name, "vendor")}' #vendor seems useless/confusing
271
+ rv = f'{get_str(device_name, "model")}'
272
+ return rv.strip()
273
+
193
274
  def parse_lsblk(self, dflt):
194
275
  """ Parse ls_blk for all the goodies we need """
195
276
  def eat_one(device):
@@ -216,6 +297,17 @@ class DeviceInfo:
216
297
  while len(mounts) >= 1 and mounts[0] is None:
217
298
  del mounts[0]
218
299
  entry.mounts = mounts
300
+ if not mounts:
301
+ marker = WipeJob.read_marker_buffer(entry.name)
302
+ now = int(round(time.time()))
303
+ if (marker and marker.size_bytes == entry.size_bytes
304
+ and 0 <= marker.scrubbed_bytes <= entry.size_bytes
305
+ and marker.unixtime < now):
306
+ pct = int(round((marker.scrubbed_bytes/marker.size_bytes)*100))
307
+ state = 'W' if pct >= 100 else 's'
308
+ dt = datetime.datetime.fromtimestamp(marker.unixtime)
309
+ entry.marker = f'{state} {pct}% {marker.mode} {dt.strftime('%Y/%m/%d %H:%M')}'
310
+
219
311
  return entry
220
312
 
221
313
  # Run the `lsblk` command and get its output in JSON format with additional columns
@@ -228,6 +320,7 @@ class DeviceInfo:
228
320
  # Parse each block device and its properties
229
321
  for device in parsed_data['blockdevices']:
230
322
  parent = eat_one(device)
323
+ parent.fstype = self.get_device_vendor_model(parent.name)
231
324
  entries[parent.name] = parent
232
325
  for child in device.get('children', []):
233
326
  entry = eat_one(child)
@@ -240,6 +333,8 @@ class DeviceInfo:
240
333
  if entry.mounts:
241
334
  entry.state = 'Mnt'
242
335
  parent.state = 'Mnt'
336
+ elif entry.marker:
337
+ entry.state = entry.marker[0]
243
338
 
244
339
  if self.DB:
245
340
  print('\n\nDB: --->>> after parse_lsblk:')
@@ -359,7 +454,7 @@ class DeviceInfo:
359
454
  def compute_field_widths(self, nss):
360
455
  """ TBD """
361
456
 
362
- wids = self.wids = SimpleNamespace(state=5, name=4, human=6, fstype=4, label=5)
457
+ wids = self.wids = SimpleNamespace(state=5, name=4, human=7, fstype=4, label=5)
363
458
  for ns in nss.values():
364
459
  wids.state = max(wids.state, len(ns.state))
365
460
  # wids.fsuse = max(wids.fsuse, len(ns.fsuse))
@@ -383,18 +478,23 @@ class DeviceInfo:
383
478
  emit += f'{sep}{"SIZE":_^{wids.human}}'
384
479
  emit += f'{sep}{"TYPE":_^{wids.fstype}}'
385
480
  emit += f'{sep}{"LABEL":_^{wids.label}}'
386
- emit += f'{sep}MOUNTS'
481
+ emit += f'{sep}MOUNTS/STATUS'
387
482
  return emit
388
483
 
389
484
  def part_str(self, partition):
390
485
  """ Convert partition to human value. """
391
- def print_str_or_dashes(name, width, chrs=' -'):
392
- if not name.strip(): # Create a string of '─' characters of the specified width
393
- result = f'{chrs}' * (width//2)
394
- result += ' ' * (width%2)
395
- else: # Format the name to be right-aligned within the specified width
396
- result = f'{name:>{width}}'
397
- return result
486
+ # def print_str_or_dashes(name, width, chrs=' -'):
487
+ # if not name.strip(): # Create a string of '─' characters of the specified width
488
+ # result = f'{chrs}' * (width//2)
489
+ # result += ' ' * (width%2)
490
+ # else: # Format the name to be right-aligned within the specified width
491
+ # result = f'{name:>{width}}'
492
+ # return result
493
+
494
+ def print_str_or_dash(name, width, empty='-'):
495
+ if not name.strip(): # return
496
+ name = empty
497
+ return f'{name:^{width}}'
398
498
 
399
499
  sep = ' '
400
500
  ns = partition # shorthand
@@ -409,14 +509,17 @@ class DeviceInfo:
409
509
  emit += f'{sep}{name_str:<{wids.name}}'
410
510
  # emit += f'{sep}{ns.fsuse:^{wids.fsuse}}'
411
511
  emit += f'{sep}{human(ns.size_bytes):>{wids.human}}'
412
- emit += sep + print_str_or_dashes(ns.fstype, wids.fstype)
512
+ emit += sep + print_str_or_dash(ns.fstype, wids.fstype)
413
513
  # emit += f'{sep}{ns.fstype:>{wids.fstype}}'
414
514
  if ns.parent is None:
415
- emit += sep + '■' + '─'*(wids.label-2) + '■'
515
+ emit += sep + '■' + '─'*(wids.label-2) + '■'
416
516
  else:
417
- emit += sep + print_str_or_dashes(ns.label, wids.label, chrs=' -')
517
+ emit += sep + print_str_or_dash(ns.label, wids.label)
418
518
  # emit += f' {ns.label:>{wids.label}}'
419
- emit += f'{sep}{",".join(ns.mounts)}'
519
+ if ns.mounts:
520
+ emit += f'{sep}{",".join(ns.mounts)}'
521
+ else:
522
+ emit += f'{sep}{ns.marker}'
420
523
  return emit
421
524
 
422
525
  def merge_dev_infos(self, nss, prev_nss=None):
@@ -605,7 +708,7 @@ class DiskWipe:
605
708
  for part in self.partitions.values():
606
709
  stop_if_idle(part)
607
710
  return None
608
-
711
+
609
712
  if key == ord('l'):
610
713
  part = self.partitions[self.pick_name]
611
714
  self.set_state(part, 'Unlk' if part.state == 'Lock' else 'Lock')
@@ -739,7 +842,7 @@ class DiskWipe:
739
842
 
740
843
  if partition.parent and self.partitions[partition.parent].state == 'Lock':
741
844
  continue
742
-
845
+
743
846
  if wanted(name) or partition.job:
744
847
  partition.line = self.dev_info.part_str(partition)
745
848
  self.win.add_body(partition.line)
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dwipe
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: A tool to wipe disks and partitions for Linux
5
5
  Author-email: Joe Defen <joedef@google.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/joedefen/dwipe
8
8
  Project-URL: Bug Tracker, https://github.com/joedefen/dwipe/issues
9
- Keywords: disk,partition,wipe,clean
9
+ Keywords: disk,partition,wipe,clean,scrub
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: POSIX :: Linux
@@ -17,27 +17,30 @@ Requires-Dist: psutil>=5.9
17
17
  Requires-Dist: importlib-metadata; python_version < "3.8"
18
18
 
19
19
  # dwipe
20
- `dwipe` to wipe disks and partitions for Linux for your security.
20
+ `dwipe` is tool to wipe disks and partitions for Linux helps secure you data. `dwipes` aims to reduce mistakes by providing ample information about your devices during selection.
21
21
 
22
- * Install `dwipe` using `pipx install dwipe`, or however you python scripts from PyPi.org.
22
+ > **Quick Start:**
23
+ > * Install `dwipe` using `pipx install dwipe`, or however you install python scripts from PyPi.org.
24
+ > * Run `dwipe` from a terminal and observe the context sensitive help on the 1st line.
23
25
 
26
+ To help with your disk scrubbing, `dwipe`:
27
+ * shows disks and partitions that can be wiped along with selected information to help choose them (i.e.,; labels, sizes, and types); disallowed are mounted devices and overlapping wipes and manually "locked" disks.
28
+ * updates the device list when it changes; newly added devices are marked differently to make it easier to see them.
29
+ * supports starting multiple wipes, shows their progress, and shows completion states.
30
+ * supports either zeroing devices or filling with random data.
31
+ * supports filtering for devices by name/pattern in case of too many for one screen, etc.
32
+ * supports stopping wipes in progress.
24
33
 
25
- `dwipe` features include:
26
- * shows the disks and partitions that could be wiped along with useful information to help choose them (i.e., labels, sizes, and types)
27
- * allows starting multiple wipes and shows their progress.
28
- * allows filtering for devices by name in case of too many for one screen.
29
- * allows stopping wipes in progress.
30
- * not offering to wipe disks that are mounted or would have conflicting/overlapping wipes in progress.
31
- * allowing to "lock" a disk to prevent mistaken wipes on that disk.
34
+ `dwipe` shows file system labels, and if not the partition label. It is best practice to label partitions and file systems well to make selection easier.
32
35
 
33
36
  ## Usage
34
- * Run `dwipe` from the command line.
37
+ > Simply run `dwipe` from the command line w/o arguments normally. Its command line arguments mostly for debugging including "--dry-run" which lets you test/practice the interface w/o risk.
35
38
 
36
39
  Here is a typical screen:
37
40
 
38
41
  ![dwipe-help](https://raw.githubusercontent.com/joedefen/dwipe/master/resources/dwipe-main-screen.png?raw=true)
39
42
 
40
- The possible state values and meaning are:
43
+ The possible state values and meanings are:
41
44
  * **-** : indicates the device is ready for wiping if desired.
42
45
  * **^** : similar to **-**, but also indicates the device was added after `dwipe` started
43
46
  * **Mnt** : the partition is mounted or the disk has partitions that are mounted. You cannot wipe the device in this state.
@@ -60,8 +63,12 @@ The top line shows the "Mode" which is Random or Zeros. For some disks, zeroing
60
63
 
61
64
  The write rate and estimating remaining times are shown when wiping a device. Due to write queueing, the initial rates may be inflated, final rates are deflated, and the times are optimistic.
62
65
 
66
+ The 'W' (Wiped) and 's' (partly wiped) states are disk persistent. For those states, more information is provided about the wipe including when and percent complete.
67
+
63
68
 
64
69
  ### The Help Screen
65
70
  When **?** is typed, the help screen looks like:
66
71
 
67
72
  ![dwipe-help](https://raw.githubusercontent.com/joedefen/dwipe/master/resources/dwipe-help-screen.png?raw=true)
73
+
74
+ You can navigate the list of devices with arrow keys and vi-like keys.
@@ -0,0 +1,9 @@
1
+ dwipe/PowerWindow.py,sha256=pQGXsAMeuiHn-vtEiVG0rgW1eCslh3ukC-VrPhH_j3k,28587
2
+ dwipe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ dwipe/main.py,sha256=SyM9S69LkyLvhop3JbnlylGvCHzfv-Vgx8Qh2vhh2Ts,34200
4
+ dwipe-1.0.2.dist-info/LICENSE,sha256=qB9OdnyyF6WYHiEIXVm0rOSdcf8e2ctorrtWs6CC5lU,1062
5
+ dwipe-1.0.2.dist-info/METADATA,sha256=1vDa6BrAtgnmLjMO0qxx-K9mbtWKC4rjgvaHG8hcVbc,4538
6
+ dwipe-1.0.2.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
7
+ dwipe-1.0.2.dist-info/entry_points.txt,sha256=s-gAs_OhS9lr-oVMKii2ZjsLfCSO4-oHV7Wa9oJe-2g,42
8
+ dwipe-1.0.2.dist-info/top_level.txt,sha256=nJT1SUDcOmULgmF9JmYwIIQLmXAIn6qAWW8EdWuxsAg,6
9
+ dwipe-1.0.2.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- dwipe/PowerWindow.py,sha256=pQGXsAMeuiHn-vtEiVG0rgW1eCslh3ukC-VrPhH_j3k,28587
2
- dwipe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- dwipe/main.py,sha256=lXu-xsx1oQnDLBroKUyMnaYmnAujBP7IfXFpyX2T8Gc,29837
4
- dwipe-1.0.0.dist-info/LICENSE,sha256=qB9OdnyyF6WYHiEIXVm0rOSdcf8e2ctorrtWs6CC5lU,1062
5
- dwipe-1.0.0.dist-info/METADATA,sha256=rWG2JeZ-TIF6NZnt-9Slt0IyYKh8S93Bxso21tSp5u4,3601
6
- dwipe-1.0.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
7
- dwipe-1.0.0.dist-info/entry_points.txt,sha256=s-gAs_OhS9lr-oVMKii2ZjsLfCSO4-oHV7Wa9oJe-2g,42
8
- dwipe-1.0.0.dist-info/top_level.txt,sha256=nJT1SUDcOmULgmF9JmYwIIQLmXAIn6qAWW8EdWuxsAg,6
9
- dwipe-1.0.0.dist-info/RECORD,,
File without changes
File without changes