Implement df command using os.statvfs

pull/226/head
Andrei Makeev 2024-04-15 18:20:41 +01:00
rodzic c8a2cd7d20
commit 1a7b59222e
3 zmienionych plików z 337 dodań i 1 usunięć

Wyświetl plik

@ -378,6 +378,25 @@ and destination, it will only be copied if the source is newer than the
destination.
df
--
::
usage: df [-b|-h|-H]
Report file system space usage
optional arguments:
--help show this help message and exit
-b, --bytes Prints sizes in bytes
-h, --human-readable Prints sizes in a human-readable format using power of 1024
-H, --si Prints sizes in a human-readable format using power of 1000
Gets filesystem available space based on statvfs. Granularity is limited
to filesystem block size.
echo
----

242
rshell/dfutils.py 100644
Wyświetl plik

@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""Implements tooling for formatting df command columns"""
from enum import Enum
def convert_bytes(size, unit = '', always_append_unit = False):
"""Converts size in bytes to closest power of 1024: Ki, Mi, Gi, etc.
"""
single_unit = '' if always_append_unit else unit
appendix = unit if always_append_unit else ''
for x in [single_unit, 'Ki', 'Mi', 'Gi', 'Ti']:
if size < 1024.0:
return "%3.1f%s%s" % (size, x, appendix)
size /= 1024.0
return size
def convert_bytes_si(size, unit = '', always_append_unit = False):
"""Converts size in bytes to closest power of 1000: K, M, G, etc.
"""
single_unit = '' if always_append_unit else unit
appendix = unit if always_append_unit else ''
for x in [single_unit, 'K', 'M', 'G', 'T']:
if size < 1000.0:
return "%3.1f%s%s" % (size, x, appendix)
size /= 1000.0
return size
class DByteFormat(Enum):
"""Enum for selecting the formatting for size in bytes
"""
BYTES = 1
"""Output in bytes
"""
HUMAN = 2
"""Output in human readable format: powers of 1024
"""
HUMAN_SI = 3
"""Output in human readable format: powers of 1000
"""
class DfColumn:
def title(self):
pass
def formatted(statvfs, dev_name, dir):
pass
class DfNumColumn(DfColumn):
def __init__(self, num_format = '{:d}'):
self.num_format = num_format
def formatted(self, statvfs, dev_name, dir):
value = self.get_num_value(statvfs)
return self.num_format.format(value)
def get_num_value(self, statvfs):
pass
class DfByteColumn(DfNumColumn):
def __init__(self, byte_format):
self.byte_format = byte_format
super().__init__('{:d}B')
def formatted(self, statvfs, dev_name, dir):
value = self.get_num_value(statvfs)
if self.byte_format == DByteFormat.HUMAN:
return convert_bytes(value, 'B')
elif self.byte_format == DByteFormat.HUMAN_SI:
return convert_bytes_si(value, 'B')
else: # fallback to bytes as default
return super().formatted(statvfs, dev_name, dir)
class DfFilesystem(DfColumn):
def title(self):
return 'Filesystem'
def formatted(self, statvfs, dev_name, dir):
return '{:s}@{:s}'.format(dir[:-1], dev_name)
# format: /${dir_name}/@${device_name}
# e.g. /flash@pyboard
class DfMountedOn(DfColumn):
def title(self):
return 'Mounted on'
def formatted(self, statvfs, dev_name, dir):
return '/{}{}'.format(dev_name, dir)[:-1]
# format: /${device_name}/${dir_name}
# e.g. /pyboard/flash
class DfNumBlocks(DfNumColumn):
def title(self):
return 'Blocks'
def get_num_value(self, statvfs):
return statvfs[2]
# f_blocks
class DfBlockSize(DfNumColumn):
def title(self):
return 'Block size'
def get_num_value(self, statvfs):
return statvfs[1]
# f_frsize
class DfUsedBlocks(DfNumColumn):
def title(self):
return 'Used'
def get_num_value(self, statvfs):
return statvfs[2] - statvfs[3]
# f_blocks - f_used
class DfAvailBlocks(DfNumColumn):
def title(self):
return 'Available'
def get_num_value(self, statvfs):
return statvfs[4]
# f_bavail
class DfCapacityBlocks(DfNumColumn):
def __init__(self):
super().__init__('{:.0f}%')
def title(self):
return 'Capacity'
def get_num_value(self, statvfs):
return 100 * (statvfs[2] - statvfs[3]) / statvfs[2] if statvfs[2] > 0 else 0
# 100 * (f_blocks - f_used) / f_blocks
# or 0 if 0 blocks
class DfSizeBytes(DfByteColumn):
def __init__(self, byte_format):
super().__init__(byte_format)
def title(self):
return 'Size'
def get_num_value(self, statvfs):
return statvfs[1] * statvfs[2]
# f_frsize * f_blocks
class DfUsedBytes(DfByteColumn):
def __init__(self, byte_format):
super().__init__(byte_format)
def title(self):
return 'Used'
def get_num_value(self, statvfs):
return statvfs[1] * (statvfs[2] - statvfs[3])
# f_frsize * (f_blocks - f_used)
class DfAvailBytes(DfByteColumn):
def __init__(self, byte_format):
super().__init__(byte_format)
def title(self):
return 'Available'
def get_num_value(self, statvfs):
return statvfs[1] * statvfs[4]
# f_frsize * f_bavail
class DfCapacityBytes(DfNumColumn):
def __init__(self):
super().__init__('{:.0f}%')
def title(self):
return 'Capacity'
def get_num_value(self, statvfs):
return 100 * (statvfs[2] - statvfs[3]) / statvfs[2] if statvfs[2] > 0 else 0
# 100 * (f_blocks - f_used) / f_blocks
# or 0 if 0 blocks
def create_byte_sizes_columns(byte_format):
"""Returns standard set of columns for df command output
in bytes in different formats
"""
return [
DfFilesystem(),
DfSizeBytes(byte_format),
DfUsedBytes(byte_format),
DfAvailBytes(byte_format),
DfCapacityBytes(),
DfMountedOn(),
]
def create_block_sizes_columns():
"""Returns standard set of columns for df command output
in blocks
"""
return [
DfFilesystem(),
DfBlockSize(),
DfNumBlocks(),
DfUsedBlocks(),
DfAvailBlocks(),
DfCapacityBlocks(),
DfMountedOn(),
]

Wyświetl plik

@ -20,6 +20,7 @@
import sys
try:
import rshell.dfutils as dfutils
from rshell.getch import getch
from rshell.pyboard import Pyboard, PyboardError
from rshell.version import __version__
@ -713,6 +714,15 @@ def eval_str(string):
return output
def get_vfs_stats(filename):
"""Returns filesystem statistics."""
import os
try:
return os.statvfs(filename)
except OSError:
return -1
def get_filesize(filename):
"""Returns the size of a file, in bytes."""
import os
@ -2006,7 +2016,8 @@ class Shell(cmd.Cmd):
parser = argparse.ArgumentParser(
prog=command,
usage='\n'.join(usage),
description='\n'.join(description)
description='\n'.join(description),
conflict_handler='resolve'
)
for args, kwargs in argparse_args:
parser.add_argument(*args, **kwargs)
@ -2581,6 +2592,70 @@ class Shell(cmd.Cmd):
if len(files) > 0:
print_cols(sorted(files), self.print, self.columns)
argparse_df = (
add_arg(
'-b', '--bytes',
dest='byte_sizes',
action='store_true',
help='Prints sizes in bytes',
default=False
),
add_arg(
'-h', '--human-readable',
dest='human_readable',
action='store_true',
help='Prints sizes in a human-readable format using power of 1024',
default=False
),
add_arg(
'-H', '--si',
dest='human_readable_si',
action='store_true',
help='Prints sizes in a human-readable format using power of 1000',
default=False
),
)
def complete_df(self, text, line, begidx, endidx):
return self.filename_complete(text, line, begidx, endidx)
def do_df(self, line):
"""df [-b|-h|-H]
Report file system space usage
"""
args = self.line_to_args(line)
if args.byte_sizes:
columns = dfutils.create_byte_sizes_columns(dfutils.DByteFormat.BYTES)
elif args.human_readable:
columns = dfutils.create_byte_sizes_columns(dfutils.DByteFormat.HUMAN)
elif args.human_readable_si:
columns = dfutils.create_byte_sizes_columns(dfutils.DByteFormat.HUMAN_SI)
else:
columns = dfutils.create_block_sizes_columns()
table = []
widths = [len(col.title()) for col in columns]
with DEV_LOCK:
for dev in DEVS:
for dir in dev.root_dirs:
stats = dev.remote_eval(get_vfs_stats, dir)
row = [col.formatted(stats, dev.name, dir) for col in columns]
table.append(row)
widths = [max(len(val), widths[i]) for i, val in enumerate(row)]
col_formatters = []
for i, width in enumerate(widths):
# first and last row should be aligned to the left, others to the right
alignment = '<' if i == 0 or i == len(widths) - 1 else '>'
col_formatters.append('{:' + alignment + str(width) + 's}')
row_f = " ".join(col_formatters)
print(row_f.format(*[col.title() for col in columns]))
for row in table:
print(row_f.format(*row))
def complete_mkdir(self, text, line, begidx, endidx):
return self.filename_complete(text, line, begidx, endidx)