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 +125 -22
- {dwipe-1.0.0.dist-info → dwipe-1.0.2.dist-info}/METADATA +20 -13
- dwipe-1.0.2.dist-info/RECORD +9 -0
- dwipe-1.0.0.dist-info/RECORD +0 -9
- {dwipe-1.0.0.dist-info → dwipe-1.0.2.dist-info}/LICENSE +0 -0
- {dwipe-1.0.0.dist-info → dwipe-1.0.2.dist-info}/WHEEL +0 -0
- {dwipe-1.0.0.dist-info → dwipe-1.0.2.dist-info}/entry_points.txt +0 -0
- {dwipe-1.0.0.dist-info → dwipe-1.0.2.dist-info}/top_level.txt +0 -0
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 /=
|
|
34
|
-
if number <
|
|
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(
|
|
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=
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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 +
|
|
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 +
|
|
517
|
+
emit += sep + print_str_or_dash(ns.label, wids.label)
|
|
418
518
|
# emit += f' {ns.label:>{wids.label}}'
|
|
419
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|

|
|
39
42
|
|
|
40
|
-
The possible state values and
|
|
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
|

|
|
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,,
|
dwipe-1.0.0.dist-info/RECORD
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|