Skip to content

Commit 9b0ba77

Browse files
committed
Change script to pass through to btrfs-progs for sorting
Btrfs-progs has more thorough sorting options than this script already. We now pass through to them at the expense of not being able to sort by path directly any longer. > Will be good to add options for order list via Total size, Exclusive > size, and option to force fixed units (for example, Gigabytes).
1 parent 096f480 commit 9b0ba77

File tree

1 file changed

+80
-66
lines changed

1 file changed

+80
-66
lines changed

btrfs-subvolumes.py

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,32 @@
11
#!/usr/bin/env python3
22

3-
# List BTFS subvolume space use information similar to `df -h`
3+
# List BTRFS subvolume space use information similar to df -h (with snapshot paths)
44
#
5-
# Note: this requires the "hurry.filesize" module. Install it with:
5+
# Btrfsprogs is able to list and sort snapshots on a volume, but it only prints their
6+
# id, not their path. This script wraps `btrfs qgroup show` to add filesystem paths
7+
# to the generated table.
68
#
7-
# pip install hurry.filesize
9+
# For this to work on a BTRFS volume, you first need to enable quotas on the volume:
810
#
9-
# This is rewritten from a shell script that was too slow:
11+
# btrfs quota enable /mnt/some-volume
12+
#
13+
# Note that the current version of this script does not allow sorting by path, as it
14+
# passes all arugments through to btrfsprogs. If you need that and don't mind being
15+
# limited to only sorting by path, see this previous version:
16+
#
17+
# https://gist.github.com/stecman/3fd04a36111874f67c484c74e15ef311/6690edbd6a88380a1712024bb4115969b2545509
18+
#
19+
# This is based on a shell script that was too slow:
1020
# https://github.com/agronick/btrfs-size
1121

1222
from __future__ import print_function
1323

14-
from hurry.filesize import size
15-
16-
import argparse
1724
import subprocess
1825
import sys
1926
import os
2027
import re
2128

22-
parser = argparse.ArgumentParser(description="List subvolume and snapshot sizes on BTRFS volumes")
23-
parser.add_argument("path", help="Path or UUID of the BTRFS volume to inspect")
24-
parser.add_argument(
25-
"-s",
26-
dest="sort",
27-
choices=[
28-
"exclusive",
29-
"total"
30-
],
31-
help="Sort output instead of using the order from btrfs"
32-
)
33-
34-
35-
class BtrfsQuota:
36-
def __init__(self, raw_id, total_bytes, exclusive_bytes):
37-
self.id = raw_id.split("/")[1]
38-
self.total_bytes = int(total_bytes)
39-
self.exclusive_bytes = int(exclusive_bytes)
40-
41-
def btrfs_subvols_get(path):
29+
def get_btrfs_subvols(path):
4230
"""Return a dictionary of subvolume names indexed by their subvolume ID"""
4331
try:
4432
raw = subprocess.check_output(["btrfs", "subvolume", "list", path])
@@ -52,64 +40,90 @@ def btrfs_subvols_get(path):
5240
print("Is '%s' really a BTRFS volume?" % path)
5341
sys.exit(1)
5442

55-
def btrfs_quotas_get(path):
56-
"""Return a list of BtrfsQuota objects for path"""
43+
def get_data_raw(args):
44+
"""Return lines of output from a call to 'btrfs qgroup show' with args appended"""
5745
try:
5846
# Get the lines of output, ignoring the two header lines
59-
raw = subprocess.check_output(["btrfs", "qgroup", "show", "--raw", path])
60-
lines = raw.decode("utf8").split("\n")[2:]
61-
62-
return [BtrfsQuota(*line.split()) for line in lines if line != '']
47+
raw = subprocess.check_output(["btrfs", "qgroup", "show"] + args)
48+
return raw.decode("utf8").split("\n")
6349

6450
except subprocess.CalledProcessError as e:
6551
if e.returncode != 0:
6652
print("\nFailed to get subvolume quotas. Have you enabled quotas on this volume?")
67-
print("(You can do so with: sudo btrfs quota enable '%s')" % path)
53+
print("(You can do so with: sudo btrfs quota enable <path-to-volume>)")
6854
sys.exit(1)
6955

70-
def print_table(subvols, quotas):
71-
# Get the column size right for the path
72-
max_path_length = 0
73-
for path in subvols.values():
74-
max_path_length = max(len(path), max_path_length)
56+
def get_qgroup_id(line):
57+
"""Extract qgroup id from a line of btrfs qgroup show output
58+
Returns None if the line wasn't valid
59+
"""
60+
id_match = re.match(r"\d+/(\d+)", line)
61+
62+
if not id_match:
63+
return None
7564

76-
template = "{path:<{path_len}}{bytes:>16}{exclusive:>16}"
65+
return id_match.group(1)
7766

78-
# Print table header
79-
header = template.format(
80-
path="Subvolume",
81-
bytes="Total size",
82-
exclusive="Exclusive size",
83-
path_len=max_path_length
84-
)
67+
def guess_path_argument(argv):
68+
"""Return an argument most likely to be the <path> arg for 'btrfs qgroup show'
69+
This is a cheap way to pass through to btrfsprogs without duplicating the options here.
70+
Currently only easier than duplication because the option/argument list is simple.
71+
"""
72+
# Path can't be the first argument (program)
73+
args = argv[1:]
8574

86-
print(header)
87-
print("-" * len(header))
75+
# Filter out arguments to options
76+
# Only the sort option currently takes an argument
77+
option_follows = [
78+
"--sort"
79+
]
8880

89-
# Print table rows
90-
for quota in quotas:
91-
if quota.id in subvols:
92-
print(template.format(
93-
path_len=max_path_length,
94-
path=subvols[quota.id],
95-
bytes=size(quota.total_bytes),
96-
exclusive=size(quota.exclusive_bytes)
97-
))
81+
for text in option_follows:
82+
try:
83+
position = args.index(text)
84+
del args[position + 1]
85+
except:
86+
pass
87+
88+
# Ignore options
89+
args = [arg for arg in args if re.match(r"^-", arg) is None]
90+
91+
# Prefer the item at the end of the list as this is the suggested argument order
92+
return args[-1]
9893

9994

10095
# Re-run the script as root if started with a non-priveleged account
10196
if os.getuid() != 0:
10297
cmd = 'sudo "' + '" "'.join(sys.argv) + '"'
10398
sys.exit(subprocess.call(cmd, shell=True))
10499

105-
args = parser.parse_args()
106100

107-
subvols = btrfs_subvols_get(args.path)
108-
quotas = btrfs_quotas_get(args.path)
101+
# Fetch command output to work with
102+
output = get_data_raw(sys.argv[1:])
103+
subvols = get_btrfs_subvols(guess_path_argument(sys.argv))
104+
105+
# Data for the new column
106+
path_column = [
107+
"path",
108+
"----"
109+
]
110+
111+
# Iterate through all lines except for the table header
112+
for index,line in enumerate(output):
113+
# Ignore header rows
114+
if index < 1:
115+
continue
116+
117+
groupid = get_qgroup_id(line)
118+
119+
if groupid in subvols:
120+
path_column.append(subvols[groupid])
121+
else:
122+
path_column.append("")
109123

110-
if args.sort == "exclusive":
111-
quotas.sort(key=lambda q: q.exclusive_bytes, reverse=True)
112-
elif args.sort == "bytes":
113-
quotas.sort(key=lambda q: q.total_bytes, reverse=True)
124+
# Find the required width for the new column
125+
column_width = len(max(path_column, key=len)) + 2
114126

115-
print_table(subvols, quotas)
127+
# Output data with extra column for path
128+
for index,line in enumerate(output):
129+
print(path_column[index].ljust(column_width) + output[index])

0 commit comments

Comments
 (0)