This copy of the script is no longer updated, but I'll leave it here for reference.
-
-
Save stecman/3fd04a36111874f67c484c74e15ef311 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
# List BTRFS subvolume space use information similar to df -h (with snapshot paths) | |
# | |
# Btrfsprogs is able to list and sort snapshots on a volume, but it only prints their | |
# id, not their path. This script wraps `btrfs qgroup show` to add filesystem paths | |
# to the generated table. | |
# | |
# For this to work on a BTRFS volume, you first need to enable quotas on the volume: | |
# | |
# btrfs quota enable /mnt/some-volume | |
# | |
# Note that the current version of this script does not allow sorting by path, as it | |
# passes all arugments through to btrfsprogs. If you need that and don't mind being | |
# limited to only sorting by path, see this previous version: | |
# | |
# https://gist.github.com/stecman/3fd04a36111874f67c484c74e15ef311/6690edbd6a88380a1712024bb4115969b2545509 | |
# | |
# This is based on a shell script that was too slow: | |
# https://github.com/agronick/btrfs-size | |
from __future__ import print_function | |
import subprocess | |
import sys | |
import os | |
import re | |
def get_btrfs_subvols(path): | |
"""Return a dictionary of subvolume names indexed by their subvolume ID""" | |
try: | |
raw = subprocess.check_output(["btrfs", "subvolume", "list", path]) | |
volumes = re.findall(r'^ID (\d+) .* path (.*)$', raw.decode("utf8"), re.MULTILINE) | |
return dict(volumes) | |
except subprocess.CalledProcessError as e: | |
if e.returncode != 0: | |
print("\nFailed to list subvolumes") | |
print("Is '%s' really a BTRFS volume?" % path) | |
sys.exit(1) | |
def get_data_raw(args): | |
"""Return lines of output from a call to 'btrfs qgroup show' with args appended""" | |
try: | |
# Get the lines of output, ignoring the two header lines | |
raw = subprocess.check_output(["btrfs", "qgroup", "show"] + args) | |
return raw.decode("utf8").split("\n") | |
except subprocess.CalledProcessError as e: | |
if e.returncode != 0: | |
print("\nFailed to get subvolume quotas. Have you enabled quotas on this volume?") | |
print("(You can do so with: sudo btrfs quota enable <path-to-volume>)") | |
sys.exit(1) | |
def get_qgroup_id(line): | |
"""Extract qgroup id from a line of btrfs qgroup show output | |
Returns None if the line wasn't valid | |
""" | |
id_match = re.match(r"\d+/(\d+)", line) | |
if not id_match: | |
return None | |
return id_match.group(1) | |
def guess_path_argument(argv): | |
"""Return an argument most likely to be the <path> arg for 'btrfs qgroup show' | |
This is a cheap way to pass through to btrfsprogs without duplicating the options here. | |
Currently only easier than duplication because the option/argument list is simple. | |
""" | |
# Path can't be the first argument (program) | |
args = argv[1:] | |
# Filter out arguments to options | |
# Only the sort option currently takes an argument | |
option_follows = [ | |
"--sort" | |
] | |
for text in option_follows: | |
try: | |
position = args.index(text) | |
del args[position + 1] | |
except: | |
pass | |
# Ignore options | |
args = [arg for arg in args if re.match(r"^-", arg) is None] | |
# Prefer the item at the end of the list as this is the suggested argument order | |
return args[-1] | |
# Re-run the script as root if started with a non-priveleged account | |
if os.getuid() != 0: | |
cmd = 'sudo "' + '" "'.join(sys.argv) + '"' | |
sys.exit(subprocess.call(cmd, shell=True)) | |
# Fetch command output to work with | |
output = get_data_raw(sys.argv[1:]) | |
subvols = get_btrfs_subvols(guess_path_argument(sys.argv)) | |
# Data for the new column | |
path_column = [ | |
"path", | |
"----" | |
] | |
# Iterate through all lines except for the table header | |
for index,line in enumerate(output): | |
# Ignore header rows | |
if index < 1: | |
continue | |
groupid = get_qgroup_id(line) | |
if groupid in subvols: | |
path_column.append(subvols[groupid]) | |
else: | |
path_column.append("") | |
# Find the required width for the new column | |
column_width = len(max(path_column, key=len)) + 2 | |
# Output data with extra column for path | |
for index,line in enumerate(output): | |
print(path_column[index].ljust(column_width) + output[index]) |
Seems that arguments already exists:
-s {exclusive,total} Sort output instead of using the order from btrfs
But seems there are bug:
in line 29 we see "total" argument, but in 112 - "bytes".
@MurzNN thanks, I'll update this to pass through arguments to btrfsprogs and only handle the mapping of volume ids to paths. Must have missed those options when I was looking for a solution to this originally, but it looks like --sort
has been in there for a while.
@MurzNN - updated to pass through to btrfs qgroup show
directly, without reinterpreting arguments. This makes the script simpler at the cost of not being able to sort by the path
column. The sorting options in btrfsprogs are fairly customisable though:
--sort=qgroupid,rfer,excl,max_rfer,max_excl
list qgroups sorted by specified items
you can use '+' or '-' in front of each item.
(+:ascending, -:descending, ascending default)
Sorting by path can still be achieved by piping to | sort -k1
.
Thanks for so useful script! Will be good to add options for order list via Total size, Exclusive size, and option to force fixed units (for example, Gigabytes). And maybe better to provide full GitHub project (with Issues page) instead of gist page?