statblk 1.1__py3-none-any.whl → 1.21__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statblk
3
- Version: 1.1
3
+ Version: 1.21
4
4
  Summary: Gather essential disk and partition info for block devices and print it in a nice table
5
5
  Home-page: https://github.com/yufei-pan/statblk
6
6
  Author: Yufei Pan
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Operating System :: POSIX :: Linux
11
11
  Requires-Python: >=3.6
12
12
  Description-Content-Type: text/plain
13
+ Requires-Dist: multiCMD>=1.35
13
14
  Dynamic: author
14
15
  Dynamic: author-email
15
16
  Dynamic: classifier
@@ -17,6 +18,7 @@ Dynamic: description
17
18
  Dynamic: description-content-type
18
19
  Dynamic: home-page
19
20
  Dynamic: license
21
+ Dynamic: requires-dist
20
22
  Dynamic: requires-python
21
23
  Dynamic: summary
22
24
 
@@ -0,0 +1,6 @@
1
+ statblk.py,sha256=KzueRFpQZTcTJzrLPuC2kIRX0DilrnyyJJcGTfN2pEw,26804
2
+ statblk-1.21.dist-info/METADATA,sha256=Zxv0Jkuz1hsATIkOG1cnRhOiFx-VF5SID3rWVEPGWMw,1465
3
+ statblk-1.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
+ statblk-1.21.dist-info/entry_points.txt,sha256=JDz-sa6FIdaOckmlz9NbnhZXQaB5Yle-cTKgUQmAV40,41
5
+ statblk-1.21.dist-info/top_level.txt,sha256=dBdU6_PD4tG_7uquWEs6YremqudiePASv3u3G59scf4,8
6
+ statblk-1.21.dist-info/RECORD,,
statblk.py CHANGED
@@ -7,16 +7,279 @@
7
7
  import os
8
8
  import re
9
9
  import stat
10
- from collections import defaultdict
10
+ from collections import defaultdict, namedtuple
11
11
  import argparse
12
12
  import shutil
13
13
  import subprocess
14
-
15
- version = '1.01'
14
+ try:
15
+ import multiCMD
16
+ assert float(multiCMD.version) >= 1.35
17
+ except:
18
+ import time,threading,io,sys,subprocess,select,string,re,itertools,signal
19
+ class multiCMD:
20
+ version='1.35_min_statblk'
21
+ __version__=version
22
+ COMMIT_DATE='2025-09-10'
23
+ __running_threads=set()
24
+ __variables={}
25
+ _BRACKET_RX=re.compile('\\[([^\\]]+)\\]')
26
+ _ALPHANUM=string.digits+string.ascii_letters
27
+ _ALPHA_IDX={B:A for(A,B)in enumerate(_ALPHANUM)}
28
+ class Task:
29
+ def __init__(A,command):A.command=command;A.returncode=None;A.stdout=[];A.stderr=[];A.thread=None;A.stop=False
30
+ def __iter__(A):return zip(['command','returncode','stdout','stderr'],[A.command,A.returncode,A.stdout,A.stderr])
31
+ def __repr__(A):return f"Task(command={A.command}, returncode={A.returncode}, stdout={A.stdout}, stderr={A.stderr}, stop={A.stop})"
32
+ def __str__(A):return str(dict(A))
33
+ def is_alive(A):
34
+ if A.thread is not None:return A.thread.is_alive()
35
+ return False
36
+ def _expand_piece(piece,vars_):
37
+ D=vars_;C=piece;C=C.strip()
38
+ if':'in C:E,F,G=C.partition(':');D[E]=G;return
39
+ if'-'in C:
40
+ A,F,B=(A.strip()for A in C.partition('-'));A=D.get(A,A);B=D.get(B,B)
41
+ if A.isdigit()and B.isdigit():H=max(len(A),len(B));return[f"{A:0{H}d}"for A in range(int(A),int(B)+1)]
42
+ if all(A in string.hexdigits for A in A+B):return[format(A,'x')for A in range(int(A,16),int(B,16)+1)]
43
+ try:return[multiCMD._ALPHANUM[A]for A in range(multiCMD._ALPHA_IDX[A],multiCMD._ALPHA_IDX[B]+1)]
44
+ except KeyError:pass
45
+ return[D.get(C,C)]
46
+ def _expand_ranges_fast(inStr):
47
+ D=inStr;A=[];B=0
48
+ for C in multiCMD._BRACKET_RX.finditer(D):
49
+ if C.start()>B:A.append([D[B:C.start()]])
50
+ E=[]
51
+ for G in C.group(1).split(','):
52
+ F=multiCMD._expand_piece(G,multiCMD.__variables)
53
+ if F:E.extend(F)
54
+ A.append(E or['']);B=C.end()
55
+ A.append([D[B:]]);return[''.join(A)for A in itertools.product(*A)]
56
+ def __handle_stream(stream,target,pre='',post='',quiet=False):
57
+ E=quiet;C=target
58
+ def D(current_line,target,keepLastLine=True):
59
+ A=target
60
+ if not keepLastLine:
61
+ if not E:sys.stdout.write('\r')
62
+ A.pop()
63
+ elif not E:sys.stdout.write('\n')
64
+ B=current_line.decode('utf-8',errors='backslashreplace');A.append(B)
65
+ if not E:sys.stdout.write(pre+B+post);sys.stdout.flush()
66
+ A=bytearray();B=True
67
+ for F in iter(lambda:stream.read(1),b''):
68
+ if F==b'\n':
69
+ if not B and A:D(A,C,keepLastLine=False)
70
+ elif B:D(A,C,keepLastLine=True)
71
+ A=bytearray();B=True
72
+ elif F==b'\r':D(A,C,keepLastLine=B);A=bytearray();B=False
73
+ else:A.extend(F)
74
+ if A:D(A,C,keepLastLine=B)
75
+ def int_to_color(n,brightness_threshold=500):
76
+ B=brightness_threshold;A=hash(str(n));C=A>>16&255;D=A>>8&255;E=A&255
77
+ if C+D+E<B:return multiCMD.int_to_color(A,B)
78
+ return C,D,E
79
+ def __run_command(task,sem,timeout=60,quiet=False,dry_run=False,with_stdErr=False,identity=None):
80
+ I=timeout;F=identity;E=quiet;A=task;C='';D=''
81
+ with sem:
82
+ try:
83
+ if F is not None:
84
+ if F==...:F=threading.get_ident()
85
+ P,Q,R=multiCMD.int_to_color(F);C=f"[38;2;{P};{Q};{R}m";D='\x1b[0m'
86
+ if not E:print(C+'Running command: '+' '.join(A.command)+D);print(C+'-'*100+D)
87
+ if dry_run:return A.stdout+A.stderr
88
+ B=subprocess.Popen(A.command,stdout=subprocess.PIPE,stderr=subprocess.PIPE,stdin=subprocess.PIPE);J=threading.Thread(target=multiCMD.__handle_stream,args=(B.stdout,A.stdout,C,D,E),daemon=True);J.start();K=threading.Thread(target=multiCMD.__handle_stream,args=(B.stderr,A.stderr,C,D,E),daemon=True);K.start();L=time.time();M=len(A.stdout)+len(A.stderr);time.sleep(0);H=1e-07
89
+ while B.poll()is None:
90
+ if A.stop:B.send_signal(signal.SIGINT);time.sleep(.01);B.terminate();break
91
+ if I>0:
92
+ if len(A.stdout)+len(A.stderr)!=M:L=time.time();M=len(A.stdout)+len(A.stderr)
93
+ elif time.time()-L>I:A.stderr.append('Timeout!');B.send_signal(signal.SIGINT);time.sleep(.01);B.terminate();break
94
+ time.sleep(H)
95
+ if H<.001:H*=2
96
+ A.returncode=B.poll();J.join(timeout=1);K.join(timeout=1);N,O=B.communicate()
97
+ if N:multiCMD.__handle_stream(io.BytesIO(N),A.stdout,A)
98
+ if O:multiCMD.__handle_stream(io.BytesIO(O),A.stderr,A)
99
+ if A.returncode is None:
100
+ if A.stderr and A.stderr[-1].strip().startswith('Timeout!'):A.returncode=124
101
+ elif A.stderr and A.stderr[-1].strip().startswith('Ctrl C detected, Emergency Stop!'):A.returncode=137
102
+ else:A.returncode=-1
103
+ except FileNotFoundError as G:print(f"Command / path not found: {A.command[0]}",file=sys.stderr,flush=True);A.stderr.append(str(G));A.returncode=127
104
+ except Exception as G:import traceback as S;print(f"Error running command: {A.command}",file=sys.stderr,flush=True);print(str(G).split('\n'));A.stderr.extend(str(G).split('\n'));A.stderr.extend(S.format_exc().split('\n'));A.returncode=-1
105
+ if not E:print(C+'\n'+'-'*100+D);print(C+f"Process exited with return code {A.returncode}"+D)
106
+ if with_stdErr:return A.stdout+A.stderr
107
+ else:return A.stdout
108
+ def __format_command(command,expand=False):
109
+ D=expand;A=command
110
+ if isinstance(A,str):
111
+ if D:B=multiCMD._expand_ranges_fast(A)
112
+ else:B=[A]
113
+ return[A.split()for A in B]
114
+ elif hasattr(A,'__iter__'):
115
+ C=[]
116
+ for E in A:
117
+ if isinstance(E,str):C.append(E)
118
+ else:C.append(repr(E))
119
+ if not D:return[C]
120
+ F=[multiCMD._expand_ranges_fast(A)for A in C];B=list(itertools.product(*F));return[list(A)for A in B]
121
+ else:return multiCMD.__format_command(str(A),expand=D)
122
+ def run_command(command,timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,return_code_only=False,return_object=False,wait_for_return=True,sem=None):return multiCMD.run_commands(commands=[command],timeout=timeout,max_threads=max_threads,quiet=quiet,dry_run=dry_run,with_stdErr=with_stdErr,return_code_only=return_code_only,return_object=return_object,parse=False,wait_for_return=wait_for_return,sem=sem)[0]
123
+ def run_commands(commands,timeout=0,max_threads=1,quiet=False,dry_run=False,with_stdErr=False,return_code_only=False,return_object=False,parse=False,wait_for_return=True,sem=None):
124
+ K=wait_for_return;J=dry_run;I=quiet;H=timeout;C=max_threads;B=sem;E=[]
125
+ for L in commands:E.extend(multiCMD.__format_command(L,expand=parse))
126
+ A=[multiCMD.Task(A)for A in E]
127
+ if C<1:C=len(E)
128
+ if C>1 or not K:
129
+ if not B:B=threading.Semaphore(C)
130
+ F=[threading.Thread(target=multiCMD.__run_command,args=(A,B,H,I,J,...),daemon=True)for A in A]
131
+ for(D,G)in zip(F,A):G.thread=D;D.start()
132
+ if K:
133
+ for D in F:D.join()
134
+ else:multiCMD.__running_threads.update(F)
135
+ else:
136
+ B=threading.Semaphore(1)
137
+ for G in A:multiCMD.__run_command(G,B,H,I,J,identity=None)
138
+ if return_code_only:return[A.returncode for A in A]
139
+ elif return_object:return A
140
+ elif with_stdErr:return[A.stdout+A.stderr for A in A]
141
+ else:return[A.stdout for A in A]
142
+ def pretty_format_table(data,delimiter='\t',header=None,full=False):
143
+ O=delimiter;B=header;A=data;import re;S=1.12;Z=S
144
+ def J(s):return len(re.sub('\\x1b\\[[0-?]*[ -/]*[@-~]','',s))
145
+ def L(col_widths,sep_len):A=col_widths;return sum(A)+sep_len*(len(A)-1)
146
+ def T(s,width):
147
+ A=width
148
+ if J(s)<=A:return s
149
+ if A<=0:return''
150
+ return s[:max(A-2,0)]+'..'
151
+ if not A:return''
152
+ if isinstance(A,str):A=A.strip('\n').split('\n');A=[A.split(O)for A in A]
153
+ elif isinstance(A,dict):
154
+ if isinstance(next(iter(A.values())),dict):H=[['key']+list(next(iter(A.values())).keys())];H.extend([[A]+list(B.values())for(A,B)in A.items()]);A=H
155
+ else:A=[[A]+list(B)for(A,B)in A.items()]
156
+ elif not isinstance(A,list):A=list(A)
157
+ if isinstance(A[0],dict):H=[list(A[0].keys())];H.extend([list(A.values())for A in A]);A=H
158
+ A=[[str(A)for A in A]for A in A];C=len(A[0]);U=B is not None
159
+ if not U:B=A[0];E=A[1:]
160
+ else:
161
+ if isinstance(B,str):B=B.split(O)
162
+ if len(B)<C:B=B+['']*(C-len(B))
163
+ elif len(B)>C:B=B[:C]
164
+ E=A
165
+ def V(hdr,rows_):
166
+ B=hdr;C=[0]*len(B)
167
+ for A in range(len(B)):C[A]=max(J(B[A]),*(J(B[A])for B in rows_ if A<len(B)))
168
+ return C
169
+ P=[]
170
+ for F in E:
171
+ if len(F)<C:F=F+['']*(C-len(F))
172
+ elif len(F)>C:F=F[:C]
173
+ P.append(F)
174
+ E=P;D=V(B,E);G=' | ';I='-+-';M=multiCMD.get_terminal_size()[0]
175
+ def K(hdr,rows,col_w,sep_str,hsep_str):
176
+ D=hsep_str;C=col_w;E=sep_str.join('{{:<{}}}'.format(A)for A in C);A=[];A.append(E.format(*hdr));A.append(D.join('-'*A for A in C))
177
+ for B in rows:
178
+ if not any(B):A.append(D.join('-'*A for A in C))
179
+ else:B=[T(B[A],C[A])for A in range(len(B))];A.append(E.format(*B))
180
+ return'\n'.join(A)+'\n'
181
+ if full:return K(B,E,D,G,I)
182
+ if L(D,len(G))<=M:return K(B,E,D,G,I)
183
+ G='|';I='+'
184
+ if L(D,len(G))<=M:return K(B,E,D,G,I)
185
+ W=[J(A)for A in B];X=[max(D[A]-W[A],0)for A in range(C)];N=L(D,len(G))-M
186
+ for(Y,Q)in sorted(enumerate(X),key=lambda x:-x[1]):
187
+ if N<=0:break
188
+ if Q<=0:continue
189
+ R=min(Q,N);D[Y]-=R;N-=R
190
+ return K(B,E,D,G,I)
191
+ def get_terminal_size():
192
+ try:import os;A=os.get_terminal_size()
193
+ except:
194
+ try:import fcntl,termios as C,struct as B;D=fcntl.ioctl(0,C.TIOCGWINSZ,B.pack('HHHH',0,0,0,0));A=B.unpack('HHHH',D)[:2]
195
+ except:import shutil as E;A=E.get_terminal_size(fallback=(120,30))
196
+ return A
197
+ def format_bytes(size,use_1024_bytes=None,to_int=False,to_str=False,str_format='.2f'):
198
+ H=str_format;F=to_str;C=use_1024_bytes;A=size
199
+ if to_int or isinstance(A,str):
200
+ if isinstance(A,int):return A
201
+ elif isinstance(A,str):
202
+ K=re.match('(\\d+(\\.\\d+)?)\\s*([a-zA-Z]*)',A)
203
+ if not K:
204
+ if F:return A
205
+ print("Invalid size format. Expected format: 'number [unit]', e.g., '1.5 GiB' or '1.5GiB'");print(f"Got: {A}");return 0
206
+ G,L,D=K.groups();G=float(G);D=D.strip().lower().rstrip('b')
207
+ if D.endswith('i'):C=True
208
+ elif C is None:C=False
209
+ D=D.rstrip('i')
210
+ if C:B=2**10
211
+ else:B=10**3
212
+ I={'':0,'k':1,'m':2,'g':3,'t':4,'p':5,'e':6,'z':7,'y':8}
213
+ if D not in I:
214
+ if F:return A
215
+ else:
216
+ if F:return multiCMD.format_bytes(size=int(G*B**I[D]),use_1024_bytes=C,to_str=True,str_format=H)
217
+ return int(G*B**I[D])
218
+ else:
219
+ try:return int(A)
220
+ except Exception:return 0
221
+ elif F or isinstance(A,int)or isinstance(A,float):
222
+ if isinstance(A,str):
223
+ try:A=A.rstrip('B').rstrip('b');A=float(A.lower().strip())
224
+ except Exception:return A
225
+ if C or C is None:
226
+ B=2**10;E=0;J={0:'',1:'Ki',2:'Mi',3:'Gi',4:'Ti',5:'Pi',6:'Ei',7:'Zi',8:'Yi'}
227
+ while A>B:A/=B;E+=1
228
+ return f"{A:{H}} {' '}{J[E]}".replace(' ',' ')
229
+ else:
230
+ B=10**3;E=0;J={0:'',1:'K',2:'M',3:'G',4:'T',5:'P',6:'E',7:'Z',8:'Y'}
231
+ while A>B:A/=B;E+=1
232
+ return f"{A:{H}} {' '}{J[E]}".replace(' ',' ')
233
+ else:
234
+ try:return multiCMD.format_bytes(float(A),C)
235
+ except Exception:pass
236
+ return 0
237
+ import time
238
+ try:
239
+ import functools
240
+ import typing
241
+ # Check if functiools.cache is available
242
+ # cache_decorator = functools.cache
243
+ def cache_decorator(user_function):
244
+ def _make_hashable(item):
245
+ if isinstance(item, typing.Mapping):
246
+ # Sort items so that {'a':1, 'b':2} and {'b':2, 'a':1} hash the same
247
+ return tuple(
248
+ ( _make_hashable(k), _make_hashable(v) )
249
+ for k, v in sorted(item.items(), key=lambda item: item[0])
250
+ )
251
+ if isinstance(item, (list, set, tuple)):
252
+ return tuple(_make_hashable(e) for e in item)
253
+ # Fallback: assume item is already hashable
254
+ return item
255
+ def decorating_function(user_function):
256
+ # Create the real cached function
257
+ cached_func = functools.lru_cache(maxsize=None)(user_function)
258
+ @functools.wraps(user_function)
259
+ def wrapper(*args, **kwargs):
260
+ # Convert all args/kwargs to hashable equivalents
261
+ hashable_args = tuple(_make_hashable(a) for a in args)
262
+ hashable_kwargs = {
263
+ k: _make_hashable(v) for k, v in kwargs.items()
264
+ }
265
+ # Call the lru-cached version
266
+ return cached_func(*hashable_args, **hashable_kwargs)
267
+ # Expose cache statistics and clear method
268
+ wrapper.cache_info = cached_func.cache_info
269
+ wrapper.cache_clear = cached_func.cache_clear
270
+ return wrapper
271
+ return decorating_function(user_function)
272
+ except :
273
+ import sys
274
+ # If lrucache is not available, use a dummy decorator
275
+ print('Warning: functools.lru_cache is not available, multiSSH3 will run slower without cache.',file=sys.stderr)
276
+ def cache_decorator(func):
277
+ return func
278
+
279
+ version = '1.21'
16
280
  VERSION = version
17
281
  __version__ = version
18
- COMMIT_DATE = '2025-08-27'
19
-
282
+ COMMIT_DATE = '2025-09-10'
20
283
 
21
284
  SMARTCTL_PATH = shutil.which("smartctl")
22
285
 
@@ -27,7 +290,6 @@ def read_text(path):
27
290
  except Exception:
28
291
  return None
29
292
 
30
-
31
293
  def read_int(path):
32
294
  s = read_text(path)
33
295
  if s is None:
@@ -37,8 +299,7 @@ def read_int(path):
37
299
  except Exception:
38
300
  return None
39
301
 
40
-
41
- def build_symlink_map(dir_path):
302
+ def build_symlink_dict(dir_path):
42
303
  """
43
304
  Build map: devname -> token (uuid or label string) using symlinks under
44
305
  /dev/disk/by-uuid or /dev/disk/by-label.
@@ -52,177 +313,89 @@ def build_symlink_map(dir_path):
52
313
  try:
53
314
  if os.path.islink(p):
54
315
  tgt = os.path.realpath(p)
55
- devname = os.path.basename(tgt)
56
- mapping.setdefault(devname, entry)
316
+ mapping.setdefault(tgt, entry)
57
317
  except Exception:
58
318
  continue
59
319
  except Exception:
60
320
  pass
61
321
  return mapping
62
322
 
63
-
64
- def parse_mountinfo_enhanced(uuid_rev, label_rev):
65
- """
66
- Parse /proc/self/mountinfo.
67
-
68
- Returns:
69
- - by_majmin: dict "major:minor" -> list of mounts
70
- - by_devname: dict devname -> list of mounts (resolved from source)
71
- - all_mounts: list of mount dicts
72
-
73
- Each mount dict: {mountpoint, fstype, source, majmin}
74
- """
75
- by_majmin = defaultdict(list)
76
- by_devname = defaultdict(list)
77
- all_mounts = []
78
-
79
- def resolve_source_to_devnames(src):
80
- # Returns list of candidate devnames for a given source string
81
- if not src:
82
- return []
83
- cands = []
84
- try:
85
- if src.startswith("/dev/"):
86
- real = os.path.realpath(src)
87
- dn = os.path.basename(real)
88
- if dn:
89
- cands.append(dn)
90
- elif src.startswith("UUID="):
91
- u = src[5:]
92
- dn = uuid_rev.get(u)
93
- if dn:
94
- cands.append(dn)
95
- elif src.startswith("LABEL="):
96
- l = src[6:]
97
- dn = label_rev.get(l)
98
- if dn:
99
- cands.append(dn)
100
- except Exception:
101
- pass
102
- return cands
103
-
104
- try:
105
- with open("/proc/self/mountinfo", "r", encoding="utf-8") as f:
106
- for line in f:
107
- line = line.strip()
108
- if not line:
109
- continue
110
- parts = line.split()
111
- try:
112
- majmin = parts[2]
113
- mnt_point = parts[4]
114
- dash_idx = parts.index("-")
115
- fstype = parts[dash_idx + 1]
116
- source = parts[dash_idx + 2] if len(parts) > dash_idx + 2 else ""
117
- rec = {
118
- "MOUNTPOINT": mnt_point,
119
- "FSTYPE": fstype,
120
- "SOURCE": source,
121
- "MAJMIN": majmin,
122
- }
123
- all_mounts.append(rec)
124
- by_majmin[majmin].append(rec)
125
- # Build secondary index by devname from source
126
- for dn in resolve_source_to_devnames(source):
127
- by_devname[dn].append(rec)
128
- except Exception:
129
- continue
130
- except Exception:
131
- pass
132
-
133
- return by_majmin, by_devname, all_mounts
134
-
135
-
136
- def get_statvfs_use_percent(mountpoint):
323
+ def get_statvfs_use_size(mountpoint):
137
324
  try:
138
325
  st = os.statvfs(mountpoint)
139
- if st.f_blocks == 0:
140
- return None
141
- used_pct = 100.0 * (1.0 - (st.f_bavail / float(st.f_blocks)))
142
- return int(round(used_pct))
326
+ block_size = st.f_frsize if st.f_frsize > 0 else st.f_bsize
327
+ total = st.f_blocks * block_size
328
+ avail = st.f_bavail * block_size
329
+ used = total - avail
330
+ return total, used
143
331
  except Exception:
144
- return None
332
+ return 0, 0
145
333
 
146
-
147
- def read_discard_support(sys_block_path):
148
- if not sys_block_path or not os.path.isdir(sys_block_path):
149
- return False
150
- dmbytes = read_int(os.path.join(sys_block_path, "queue", "discard_max_bytes"))
151
- dgran = read_int(os.path.join(sys_block_path, "queue", "discard_granularity"))
334
+ @cache_decorator
335
+ def read_discard_support(sysfs_block_path):
336
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
337
+ return 'N/A'
338
+ dmbytes = read_int(os.path.join(sysfs_block_path, "queue", "discard_max_bytes"))
152
339
  try:
153
- return (dmbytes or 0) > 0 or (dgran or 0) > 0
340
+ if (dmbytes or 0) > 0:
341
+ return 'Yes'
342
+ else:
343
+ return 'No'
154
344
  except Exception:
155
- return False
345
+ return 'N/A'
156
346
 
157
-
158
- def get_parent_device_sysfs(block_sysfs_path):
347
+ @cache_decorator
348
+ def get_parent_device_sysfs(sysfs_block_path):
159
349
  """
160
350
  Return the sysfs 'device' directory for this block node (resolves partition
161
351
  to its parent device as well).
162
352
  """
163
- dev_link = os.path.join(block_sysfs_path, "device")
353
+ dev_link = os.path.join(sysfs_block_path, "device")
164
354
  try:
165
355
  return os.path.realpath(dev_link)
166
356
  except Exception:
167
357
  return dev_link
168
358
 
169
-
170
- def read_model_and_serial(block_sysfs_path):
171
- if not block_sysfs_path or not os.path.isdir(block_sysfs_path):
172
- return None, None
173
- device_path = get_parent_device_sysfs(block_sysfs_path)
359
+ @cache_decorator
360
+ def read_model_and_serial(sysfs_block_path):
361
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
362
+ return '', ''
363
+ device_path = get_parent_device_sysfs(sysfs_block_path)
174
364
  model = read_text(os.path.join(device_path, "model"))
175
365
  serial = read_text(os.path.join(device_path, "serial"))
176
366
  if serial is None:
177
367
  serial = read_text(os.path.join(device_path, "wwid"))
178
368
  if model:
179
369
  model = " ".join(model.split())
370
+ else:
371
+ model = ''
180
372
  if serial:
181
373
  serial = " ".join(serial.split())
374
+ else:
375
+ serial = ''
182
376
  return model, serial
183
377
 
184
-
185
- def get_udev_props(major, minor):
186
- """
187
- Read udev properties for a block device from /run/udev/data/bMAJ:MIN
188
- Returns keys like ID_FS_TYPE, ID_FS_LABEL, ID_FS_UUID when available.
189
- """
190
- props = {}
191
- path = f"/run/udev/data/b{major}:{minor}"
192
- try:
193
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
194
- for line in f:
195
- line = line.strip()
196
- if not line or "=" not in line or line.startswith("#"):
197
- continue
198
- k, v = line.split("=", 1)
199
- props[k] = v
200
- except Exception:
201
- pass
202
- return props
203
-
204
-
205
- def choose_mount_for_dev(devname, mounts):
206
- """
207
- Choose the most relevant mount for a device:
208
- - Prefer mounts whose source resolves to /dev/<devname>.
209
- - If multiple, prefer '/' then shortest mountpoint path.
210
- - Otherwise return the first entry.
211
- """
212
- if not mounts:
213
- return None
214
-
215
- def score(m):
216
- mp = m.get("MOUNTPOINT") or "~"
217
- s = m.get("SOURCE") or ""
218
- exact = 1 if (s.startswith("/dev/") and os.path.basename(os.path.realpath(s)) == devname) else 0
219
- root = 1 if mp == "/" else 0
220
- return (exact, root, -len(mp))
221
-
222
- best = sorted(mounts, key=score, reverse=True)[0]
223
- return best
224
-
225
-
378
+ MountEntry = namedtuple("MountEntry", ["MOUNTPOINT", "FSTYPE", "OPTIONS"])
379
+ def parseMount():
380
+ rtn = multiCMD.run_command('mount',timeout=1,quiet=True)
381
+ mount_table = defaultdict(list)
382
+ for line in rtn:
383
+ device_name, _, line = line.partition(' on ')
384
+ mount_point, _, line = line.partition(' type ')
385
+ fstype, _ , options = line.partition(' (')
386
+ options = options.rstrip(')').split(',')
387
+ mount_table[device_name].append(MountEntry(mount_point, fstype, options))
388
+ return mount_table
389
+
390
+ def get_blocks():
391
+ # get entries in /sys/class/block
392
+ block_devices = []
393
+ for entry in os.listdir("/sys/class/block"):
394
+ if os.path.isdir(os.path.join("/sys/class/block", entry)):
395
+ block_devices.append(f'/dev/{entry}')
396
+ return block_devices
397
+
398
+ @cache_decorator
226
399
  def is_block_device(devpath):
227
400
  try:
228
401
  st_mode = os.stat(devpath).st_mode
@@ -230,329 +403,224 @@ def is_block_device(devpath):
230
403
  except Exception:
231
404
  return False
232
405
 
406
+ def is_partition(sysfs_block_path):
407
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
408
+ return False
409
+ return os.path.exists(os.path.join(sysfs_block_path, "partition"))
233
410
 
234
- def pretty_format_table(data, delimiter="\t", header=None):
235
- version = 1.11
236
- _ = version
237
- if not data:
238
- return ""
239
- if isinstance(data, str):
240
- data = data.strip("\n").split("\n")
241
- data = [line.split(delimiter) for line in data]
242
- elif isinstance(data, dict):
243
- if isinstance(next(iter(data.values())), dict):
244
- tempData = [["key"] + list(next(iter(data.values())).keys())]
245
- tempData.extend(
246
- [[key] + list(value.values()) for key, value in data.items()]
247
- )
248
- data = tempData
249
- else:
250
- data = [[key] + list(value) for key, value in data.items()]
251
- elif not isinstance(data, list):
252
- data = list(data)
253
- if isinstance(data[0], dict):
254
- tempData = [data[0].keys()]
255
- tempData.extend([list(item.values()) for item in data])
256
- data = tempData
257
- data = [[str(item) for item in row] for row in data]
258
- num_cols = len(data[0])
259
- col_widths = [0] * num_cols
260
- for c in range(num_cols):
261
- col_widths[c] = max(
262
- len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", row[c])) for row in data
263
- )
264
- if header:
265
- header_widths = [
266
- len(re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", col)) for col in header
267
- ]
268
- col_widths = [max(col_widths[i], header_widths[i]) for i in range(num_cols)]
269
- row_format = " | ".join("{{:<{}}}".format(width) for width in col_widths)
270
- if not header:
271
- header = data[0]
272
- outTable = []
273
- outTable.append(row_format.format(*header))
274
- outTable.append("-+-".join("-" * width for width in col_widths))
275
- for row in data[1:]:
276
- if not any(row):
277
- outTable.append("-+-".join("-" * width for width in col_widths))
278
- else:
279
- outTable.append(row_format.format(*row))
280
- else:
281
- if isinstance(header, str):
282
- header = header.split(delimiter)
283
- if len(header) < num_cols:
284
- header += [""] * (num_cols - len(header))
285
- elif len(header) > num_cols:
286
- header = header[:num_cols]
287
- outTable = []
288
- outTable.append(row_format.format(*header))
289
- outTable.append("-+-".join("-" * width for width in col_widths))
290
- for row in data:
291
- if not any(row):
292
- outTable.append("-+-".join("-" * width for width in col_widths))
293
- else:
294
- outTable.append(row_format.format(*row))
295
- return "\n".join(outTable) + "\n"
296
-
297
-
298
- def format_bytes(
299
- size, use_1024_bytes=None, to_int=False, to_str=False, str_format=".2f"
300
- ):
301
- if to_int or isinstance(size, str):
302
- if isinstance(size, int):
303
- return size
304
- elif isinstance(size, str):
305
- match = re.match(r"(\d+(\.\d+)?)\s*([a-zA-Z]*)", size)
306
- if not match:
307
- if to_str:
308
- return size
309
- print(
310
- "Invalid size format. Expected format: 'number [unit]', "
311
- "e.g., '1.5 GiB' or '1.5GiB'"
312
- )
313
- print(f"Got: {size}")
314
- return 0
315
- number, _, unit = match.groups()
316
- number = float(number)
317
- unit = unit.strip().lower().rstrip("b")
318
- if unit.endswith("i"):
319
- use_1024_bytes = True
320
- elif use_1024_bytes is None:
321
- use_1024_bytes = False
322
- unit = unit.rstrip("i")
323
- if use_1024_bytes:
324
- power = 2**10
325
- else:
326
- power = 10**3
327
- unit_labels = {
328
- "": 0,
329
- "k": 1,
330
- "m": 2,
331
- "g": 3,
332
- "t": 4,
333
- "p": 5,
334
- "e": 6,
335
- "z": 7,
336
- "y": 8,
337
- }
338
- if unit not in unit_labels:
339
- if to_str:
340
- return size
341
- else:
342
- if to_str:
343
- return format_bytes(
344
- size=int(number * (power**unit_labels[unit])),
345
- use_1024_bytes=use_1024_bytes,
346
- to_str=True,
347
- str_format=str_format,
348
- )
349
- return int(number * (power**unit_labels[unit]))
350
- else:
351
- try:
352
- return int(size)
353
- except Exception:
354
- return 0
355
- elif to_str or isinstance(size, int) or isinstance(size, float):
356
- if isinstance(size, str):
357
- try:
358
- size = size.rstrip("B").rstrip("b")
359
- size = float(size.lower().strip())
360
- except Exception:
361
- return size
362
- if use_1024_bytes or use_1024_bytes is None:
363
- power = 2**10
364
- n = 0
365
- power_labels = {
366
- 0: "",
367
- 1: "Ki",
368
- 2: "Mi",
369
- 3: "Gi",
370
- 4: "Ti",
371
- 5: "Pi",
372
- 6: "Ei",
373
- 7: "Zi",
374
- 8: "Yi",
375
- }
376
- while size > power:
377
- size /= power
378
- n += 1
379
- return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
380
- else:
381
- power = 10**3
382
- n = 0
383
- power_labels = {
384
- 0: "",
385
- 1: "K",
386
- 2: "M",
387
- 3: "G",
388
- 4: "T",
389
- 5: "P",
390
- 6: "E",
391
- 7: "Z",
392
- 8: "Y",
393
- }
394
- while size > power:
395
- size /= power
396
- n += 1
397
- return f"{size:{str_format}} {' '}{power_labels[n]}".replace(" ", " ")
398
- else:
399
- try:
400
- return format_bytes(float(size), use_1024_bytes)
401
- except Exception:
402
- pass
403
- return 0
404
-
405
-
406
- def is_partition(name):
407
- real = os.path.realpath(os.path.join("/sys/class/block", name))
408
- return os.path.exists(os.path.join(real, "partition"))
409
-
410
-
411
+ @cache_decorator
411
412
  def get_partition_parent_name(name):
412
- real = os.path.realpath(os.path.join("/sys/class/block", name))
413
- part_file = os.path.join(real, "partition")
414
- if not os.path.exists(part_file):
413
+ if not name:
415
414
  return None
416
- parent = os.path.basename(os.path.dirname(real))
417
- return parent if parent and parent != name else None
418
-
419
- def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, best_only=False, formated_only=False, show_zero_size_devices=False):
420
- # Build UUID/LABEL maps and reverse maps for resolving mount "source"
421
- uuid_map = build_symlink_map("/dev/disk/by-uuid")
422
- label_map = build_symlink_map("/dev/disk/by-label")
423
- uuid_rev = {v: k for k, v in uuid_map.items()}
424
- label_rev = {v: k for k, v in label_map.items()}
425
-
426
- # Mount info maps
427
- by_majmin, by_devname, _ = parse_mountinfo_enhanced(uuid_rev, label_rev)
428
-
429
- results = []
430
- results_by_name = {}
431
- df_stats_by_name = {} # name -> (blocks, bavail)
432
- parent_to_children = defaultdict(list)
433
-
434
- sys_class_block = "/sys/class/block"
415
+ name = os.path.basename(name)
416
+ sysfs_block_path = os.path.realpath(os.path.join('/sys/class/block', name))
417
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
418
+ return None
419
+ part_file = os.path.join(sysfs_block_path, "partition")
420
+ if not os.path.exists(part_file):
421
+ return os.path.join('/dev', name) if is_block_device(os.path.join('/dev', name)) else None
422
+ parent = os.path.basename(os.path.dirname(sysfs_block_path))
423
+ return os.path.join('/dev', parent) if parent and parent != name else None
424
+
425
+ @cache_decorator
426
+ def get_sector_size(sysfs_block_path):
427
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
428
+ return 512
429
+ if get_partition_parent_name(sysfs_block_path):
430
+ sysfs_block_path = os.path.join('/sys/class/block', os.path.basename(get_partition_parent_name(sysfs_block_path)))
431
+ sector_size = read_int(os.path.join(sysfs_block_path, "queue", "hw_sector_size"))
432
+ if sector_size is None:
433
+ sector_size = read_int(os.path.join(sysfs_block_path, "queue", "logical_block_size"))
434
+ return sector_size if sector_size else 512
435
+
436
+ def get_read_write_rate_throughput_iter(sysfs_block_path):
437
+ if not sysfs_block_path or not os.path.isdir(sysfs_block_path):
438
+ while True:
439
+ yield 0, 0
440
+ rx_path = os.path.join(sysfs_block_path, "stat")
441
+ start_time = time.monotonic()
442
+ sector_size = get_sector_size(sysfs_block_path)
443
+ previous_bytes_read = 0
444
+ previous_bytes_written = 0
435
445
  try:
436
- entries = sorted(os.listdir(sys_class_block))
446
+ with open(rx_path, "r", encoding="utf-8", errors="ignore") as f:
447
+ fields = f.read().strip().split()
448
+ if len(fields) < 7:
449
+ yield 0, 0
450
+ sectors_read = int(fields[2])
451
+ read_time = int(fields[3]) / 1000.0
452
+ sectors_written = int(fields[6])
453
+ write_time = int(fields[7]) / 1000.0
454
+ read_throughput = (sectors_read * sector_size) / read_time if read_time > 0 else 0
455
+ write_throughput = (sectors_written * sector_size) / write_time if write_time > 0 else 0
456
+ previous_bytes_read = sectors_read * sector_size
457
+ previous_bytes_written = sectors_written * sector_size
458
+ yield int(read_throughput), int(write_throughput)
437
459
  except Exception:
438
- entries = []
439
-
440
- for name in entries:
441
- block_path = os.path.join(sys_class_block, name)
442
-
443
- devnode = os.path.join("/dev", name)
444
- if not is_block_device(devnode):
445
- pass
446
-
447
- parent_name = get_partition_parent_name(name)
448
- if parent_name:
449
- parent_to_children[parent_name].append(name)
450
-
451
- majmin_str = read_text(os.path.join(block_path, "dev"))
452
- if not majmin_str or ":" not in majmin_str:
453
- continue
460
+ yield 0, 0
461
+ while True:
454
462
  try:
455
- major, minor = majmin_str.split(":", 1)
456
- major = int(major)
457
- minor = int(minor)
463
+ with open(rx_path, "r", encoding="utf-8", errors="ignore") as f:
464
+ fields = f.read().strip().split()
465
+ if len(fields) < 7:
466
+ yield 0, 0
467
+ # fields: https://www.kernel.org/doc/html/latest/block/stat.html
468
+ # 0 - reads completed successfully
469
+ # 1 - reads merged
470
+ # 2 - sectors read
471
+ # 3 - time spent reading (ms)
472
+ # 4 - writes completed
473
+ # 5 - writes merged
474
+ # 6 - sectors written
475
+ # 7 - time spent writing (ms)
476
+ # 8 - I/Os currently in progress
477
+ # 9 - time spent doing I/Os (ms)
478
+ # 10 - weighted time spent doing I/Os (ms)
479
+ sectors_read = int(fields[2])
480
+ sectors_written = int(fields[6])
481
+ bytes_read = sectors_read * sector_size
482
+ bytes_written = sectors_written * sector_size
483
+ end_time = time.monotonic()
484
+ elapsed_time = end_time - start_time
485
+ start_time = end_time
486
+ read_throughput = (bytes_read - previous_bytes_read) / elapsed_time if elapsed_time > 0 else 0
487
+ write_throughput = (bytes_written - previous_bytes_written) / elapsed_time if elapsed_time > 0 else 0
488
+ previous_bytes_read = bytes_read
489
+ previous_bytes_written = bytes_written
490
+ yield int(read_throughput), int(write_throughput)
458
491
  except Exception:
459
- continue
460
-
461
- sectors = read_int(os.path.join(block_path, "size")) or 0
462
- lb_size = (
463
- read_int(os.path.join(block_path, "queue", "logical_block_size")) or 512
464
- )
465
- size_bytes = sectors * lb_size
466
- if not show_zero_size_devices and size_bytes == 0:
467
- continue
468
-
469
- parent_block_path = os.path.join(sys_class_block, parent_name) if parent_name else None
470
- model, serial = read_model_and_serial(block_path)
471
- parent_model, parent_serial = read_model_and_serial(parent_block_path)
472
-
473
- # UUID/Label from by-uuid/by-label symlinks
474
-
475
- props = get_udev_props(major, minor=minor)
476
- # Fallback to udev props
477
- rec = {
478
- "NAME": name,
479
- "FSTYPE": props.get("ID_FS_TYPE"),
480
- "LABEL": label_map.get(name, props.get("ID_FS_LABEL")),
481
- "UUID": uuid_map.get(name, props.get("ID_FS_UUID")),
482
- "MOUNTPOINT": None,
483
- "MODEL": model if model else parent_model,
484
- "SERIAL": serial if serial else parent_serial,
485
- "DISCARD": bool(read_discard_support(block_path) or read_discard_support(parent_block_path)),
486
- "SIZE": f"{format_bytes(size_bytes, to_int=print_bytes, use_1024_bytes=use_1024)}B",
487
- "FSUSE%": None,
488
- "SMART": 'N/A',
489
- }
490
- if SMARTCTL_PATH:
491
- # if do not have read permission on the denode, set SMART to 'DENIED'
492
- if not os.access(devnode, os.R_OK):
493
- rec["SMART"] = 'DENIED'
492
+ yield 0, 0
493
+
494
+ # DRIVE_INFO = namedtuple("DRIVE_INFO",
495
+ # ["NAME", "FSTYPE", "SIZE", "FSUSEPCT", "MOUNTPOINT", "SMART","RTPT",'WTPT', "LABEL", "UUID", "MODEL", "SERIAL", "DISCARD"])
496
+ def get_drives_info(print_bytes = False, use_1024 = False, mounted_only=False, best_only=False, formated_only=False, show_zero_size_devices=False,pseudo=False,tptDict = {},full=False):
497
+ lsblk_result = multiCMD.run_command(f'lsblk -brnp -o NAME,SIZE,FSTYPE,UUID,LABEL',timeout=2,quiet=True,wait_for_return=False,return_object=True)
498
+ block_devices = get_blocks()
499
+ smart_infos = {}
500
+ for block_device in block_devices:
501
+ parent_name = get_partition_parent_name(block_device)
502
+ if parent_name:
503
+ if parent_name not in smart_infos:
504
+ smart_infos[parent_name] = multiCMD.run_command(f'{SMARTCTL_PATH} -H {parent_name}',timeout=2,quiet=True,wait_for_return=False,return_object=True)
505
+ if block_device not in tptDict:
506
+ sysfs_block_path = os.path.realpath(os.path.join('/sys/class/block', os.path.basename(block_device)))
507
+ tptDict[block_device] = get_read_write_rate_throughput_iter(sysfs_block_path)
508
+ mount_table = parseMount()
509
+ target_devices = set(block_devices)
510
+ if pseudo:
511
+ target_devices.update(mount_table.keys())
512
+ target_devices = sorted(target_devices)
513
+ uuid_dict = build_symlink_dict("/dev/disk/by-uuid")
514
+ label_dict = build_symlink_dict("/dev/disk/by-label")
515
+ fstype_dict = {}
516
+ size_dict = {}
517
+ lsblk_result.thread.join()
518
+ if lsblk_result.returncode == 0:
519
+ for line in lsblk_result.stdout:
520
+ lsblk_name, lsblk_size, lsblk_fstype, lsblk_uuid, lsblk_label = line.split(' ', 4)
521
+ # the label can be \x escaped, we need to decode it
522
+ lsblk_uuid = bytes(lsblk_uuid, "utf-8").decode("unicode_escape")
523
+ lsblk_fstype = bytes(lsblk_fstype, "utf-8").decode("unicode_escape")
524
+ lsblk_label = bytes(lsblk_label, "utf-8").decode("unicode_escape")
525
+ if lsblk_uuid:
526
+ uuid_dict[lsblk_name] = lsblk_uuid
527
+ if lsblk_fstype:
528
+ fstype_dict[lsblk_name] = lsblk_fstype
529
+ if lsblk_label:
530
+ label_dict[lsblk_name] = lsblk_label
494
531
  try:
495
- # run smartctl -H <device>
496
- outputLines = subprocess.check_output([SMARTCTL_PATH, "-H", devnode], stderr=subprocess.STDOUT).decode().splitlines()
497
- # Parse output for SMART status
498
- for line in outputLines:
532
+ size_dict[lsblk_name] = int(lsblk_size)
533
+ except Exception:
534
+ pass
535
+ output = [["NAME", "FSTYPE", "SIZE", "FSUSE%", "MOUNTPOINT", "SMART", "LABEL", "UUID", "MODEL", "SERIAL", "DISCARD","RTPT",'WTPT']]
536
+ for device_name in target_devices:
537
+ if mounted_only and device_name not in mount_table:
538
+ continue
539
+ fstype = ''
540
+ size = ''
541
+ fsusepct = ''
542
+ mountpoint = ''
543
+ smart = ''
544
+ label = ''
545
+ uuid = ''
546
+ model = ''
547
+ serial = ''
548
+ discard = ''
549
+ rtpt = ''
550
+ wtpt = ''
551
+ # fstype, size, fsuse%, mountpoint, rtpt, wtpt, lable, uuid are partition specific
552
+ # smart, model, serial, discard are device specific, and only for block devices
553
+ # fstype, size, fsuse%, mountpoint does not require block device and can have multiple values per device
554
+ if is_block_device(device_name):
555
+ parent_name = get_partition_parent_name(device_name)
556
+ parent_sysfs_path = os.path.realpath(os.path.join('/sys/class/block', os.path.basename(parent_name))) if parent_name else None
557
+ model, serial = read_model_and_serial(parent_sysfs_path)
558
+ discard = read_discard_support(parent_sysfs_path)
559
+ if parent_name in smart_infos and SMARTCTL_PATH:
560
+ smart_info_obj = smart_infos[parent_name]
561
+ smart_info_obj.thread.join()
562
+ for line in smart_info_obj.stdout:
499
563
  line = line.lower()
500
564
  if "health" in line:
501
565
  smartinfo = line.rpartition(':')[2].strip().upper()
502
- rec["SMART"] = smartinfo.replace('PASSED', 'OK')
503
- except:
504
- pass
505
- # Mount and fstype: try maj:min first, then by resolved devname
506
- mounts = by_majmin.get(majmin_str, [])
507
- if not mounts:
508
- mounts = by_devname.get(name, [])
566
+ smart = smartinfo.replace('PASSED', 'OK')
567
+ break
568
+ elif "denied" in line:
569
+ smart = 'DENIED'
570
+ break
571
+ if device_name in tptDict:
572
+ try:
573
+ rtpt, wtpt = next(tptDict[device_name])
574
+ if print_bytes:
575
+ rtpt = str(rtpt)
576
+ wtpt = str(wtpt)
577
+ else:
578
+ rtpt = multiCMD.format_bytes(rtpt, use_1024_bytes=use_1024, to_str=True,str_format='.1f') + 'B/s'
579
+ wtpt = multiCMD.format_bytes(wtpt, use_1024_bytes=use_1024, to_str=True,str_format='.1f') + 'B/s'
580
+ except Exception:
581
+ rtpt = ''
582
+ wtpt = ''
583
+ if device_name in label_dict:
584
+ label = label_dict[device_name]
585
+ if device_name in uuid_dict:
586
+ uuid = uuid_dict[device_name]
587
+ mount_points = mount_table.get(device_name, [])
509
588
  if best_only:
510
- mounts = [choose_mount_for_dev(name, mounts)] if mounts else []
511
- if not mounted_only and not mounts:
512
- if formated_only and not rec.get("FSTYPE"):
513
- continue
514
- results.append(rec)
515
- results_by_name[name] = rec
516
- for mount in mounts:
517
- rec = rec.copy()
518
- mountpoint = mount.get("MOUNTPOINT")
519
- if mount.get("FSTYPE"):
520
- rec["FSTYPE"] = mount.get("FSTYPE")
521
- if formated_only and not rec.get("FSTYPE"):
589
+ if mount_points:
590
+ mount_points = [sorted(mount_points, key=lambda x: len(x.MOUNTPOINT))[0]]
591
+ if mount_points:
592
+ for mount_entry in mount_points:
593
+ fstype = mount_entry.FSTYPE
594
+ if formated_only and not fstype:
595
+ continue
596
+ mountpoint = mount_entry.MOUNTPOINT
597
+ size_bytes, used_bytes = get_statvfs_use_size(mountpoint)
598
+ if size_bytes == 0 and not show_zero_size_devices:
599
+ continue
600
+ fsusepct = f"{int(round(100.0 * used_bytes / size_bytes))}%" if size_bytes > 0 else "N/A"
601
+ if print_bytes:
602
+ size = str(size_bytes)
603
+ else:
604
+ size = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
605
+ if not full:
606
+ device_name = device_name.lstrip('/dev/')
607
+ output.append([device_name, fstype, size, fsusepct, mountpoint, smart, label, uuid, model, serial, discard, rtpt, wtpt])
608
+ else:
609
+ if formated_only and device_name not in fstype_dict:
522
610
  continue
523
- # Use% via statvfs and collect raw stats for aggregation
524
- if mountpoint:
525
- try:
526
- st = os.statvfs(mountpoint)
527
- if st.f_blocks > 0:
528
- use_pct = 100.0 * (1.0 - (st.f_bavail / float(st.f_blocks)))
529
- rec["FSUSE%"] = f'{use_pct:.1f}%'
530
- df_stats_by_name[name] = (st.f_blocks, st.f_bavail)
531
- except Exception:
532
- pass
533
- rec["MOUNTPOINT"] = mountpoint
534
- results.append(rec)
535
- results_by_name[name] = rec
536
-
537
- # Aggregate use% for parent devices with partitions:
538
- # parent's use% = 1 - sum(bavail)/sum(blocks) over mounted partitions
539
- for parent, children in parent_to_children.items():
540
- sum_blocks = 0
541
- sum_bavail = 0
542
- for ch in children:
543
- vals = df_stats_by_name.get(ch)
544
- if not vals:
611
+ fstype = fstype_dict.get(device_name, '')
612
+ size_bytes = size_dict.get(device_name, 0)
613
+ if size_bytes == 0 and not show_zero_size_devices:
545
614
  continue
546
- b, ba = vals
547
- if b and b > 0:
548
- sum_blocks += b
549
- sum_bavail += ba if ba is not None else 0
550
- if sum_blocks > 0 and parent in results_by_name:
551
- pct = 100.0 * (1.0 - (sum_bavail / float(sum_blocks)))
552
- results_by_name[parent]["FSUSE%"] = f'{pct:.1f}%'
615
+ if print_bytes:
616
+ size = str(size_bytes)
617
+ else:
618
+ size = multiCMD.format_bytes(size_bytes, use_1024_bytes=use_1024, to_str=True) + 'B'
619
+ if not full:
620
+ device_name = device_name.lstrip('/dev/')
621
+ output.append([device_name, fstype, size, fsusepct, mountpoint, smart, label, uuid, model, serial, discard, rtpt, wtpt])
622
+ return output
553
623
 
554
- results.sort(key=lambda x: x["NAME"]) # type: ignore
555
- return results
556
624
 
557
625
  def main():
558
626
  parser = argparse.ArgumentParser(description="Gather disk and partition info for block devices.")
@@ -561,19 +629,32 @@ def main():
561
629
  parser.add_argument('-H','--si', help="Use powers of 1000 not 1024 for SIZE column", action="store_true")
562
630
  parser.add_argument('-F','-fo','--formated_only', help="Show only formated filesystems", action="store_true")
563
631
  parser.add_argument('-M','-mo','--mounted_only', help="Show only mounted filesystems", action="store_true")
564
- parser.add_argument('-B','-bo','--best_only', help="Show only best mount for each device", action="store_true")
632
+ parser.add_argument('-B','-bo','--best_only', help="Show only shortest mount point for each device", action="store_true")
633
+ parser.add_argument('-a','--full', help="Show full device information, do not collapse drive info when length > console length", action="store_true")
634
+ parser.add_argument('-P','--pseudo', help="Include pseudo file systems as well (tmpfs / nfs / cifs etc.)", action="store_true")
565
635
  parser.add_argument('--show_zero_size_devices', help="Show devices with zero size", action="store_true")
636
+ parser.add_argument('print_period', nargs='?', default=0, type=int, help="If specified as a number, repeat the output every N seconds")
566
637
  parser.add_argument('-V', '--version', action='version', version=f"%(prog)s {version} @ {COMMIT_DATE} stat drives by pan@zopyr.us")
567
638
 
568
639
  args = parser.parse_args()
569
- results = get_drives_info(print_bytes = args.bytes, use_1024 = not args.si,
570
- mounted_only=args.mounted_only, best_only=args.best_only,
571
- formated_only=args.formated_only, show_zero_size_devices=args.show_zero_size_devices)
572
- if args.json:
573
- import json
574
- print(json.dumps(results, indent=1))
575
- else:
576
- print(pretty_format_table(results))
640
+ tptDict = {}
641
+ while True:
642
+ results = get_drives_info(print_bytes = args.bytes, use_1024 = not args.si,
643
+ mounted_only=args.mounted_only, best_only=args.best_only,
644
+ formated_only=args.formated_only, show_zero_size_devices=args.show_zero_size_devices,
645
+ pseudo=args.pseudo,tptDict=tptDict,full=args.full)
646
+ if args.json:
647
+ import json
648
+ print(json.dumps(results, indent=1))
649
+ else:
650
+ print(multiCMD.pretty_format_table(results,full=args.full))
651
+ if args.print_period > 0:
652
+ try:
653
+ time.sleep(args.print_period)
654
+ except KeyboardInterrupt:
655
+ break
656
+ else:
657
+ break
577
658
 
578
659
 
579
660
  if __name__ == "__main__":
@@ -1,6 +0,0 @@
1
- statblk.py,sha256=De3O7pzb2zDInQm0XGb-wnuyZuOyTHWtayohSBK-E_0,16384
2
- statblk-1.1.dist-info/METADATA,sha256=k-ZmPI5eIRMc6TBefitRBEUsXlBFkONYeDUgImZZTqw,1411
3
- statblk-1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
4
- statblk-1.1.dist-info/entry_points.txt,sha256=JDz-sa6FIdaOckmlz9NbnhZXQaB5Yle-cTKgUQmAV40,41
5
- statblk-1.1.dist-info/top_level.txt,sha256=dBdU6_PD4tG_7uquWEs6YremqudiePASv3u3G59scf4,8
6
- statblk-1.1.dist-info/RECORD,,
File without changes