├── README.md └── btrfs-subvolumes.py /README.md: -------------------------------------------------------------------------------- 1 | # BTRFS subvolume/snapshot size with paths 2 | 3 | List BTRFS subvolume space use information similar to `df -h` with paths displayed. 4 | 5 | Normally, `btrfs qgroup show` lists subvolumes, but only by id: 6 | 7 | ``` 8 | $ btrfs qgroup show /mnt/somebtrfsvol --human-readable --sort=-excl 9 | 10 | qgroupid rfer excl 11 | -------- ---- ---- 12 | 0/2317 848.55GiB 24.35GiB 13 | 0/3217 883.62GiB 16.04GiB 14 | 0/1998 840.20GiB 16.01GiB 15 | 0/489 160.81GiB 8.01GiB 16 | 0/7125 227.62GiB 2.21GiB 17 | 0/7202 230.20GiB 982.49MiB 18 | ``` 19 | 20 | This script saves looking up path information with `btrfs subvolume list` as paths 21 | are added to the output: 22 | 23 | ``` 24 | $ ./btrfs-subvolumes.py /mnt/somebtrfsvol --human-readable --sort=-excl 25 | 26 | path qgroupid rfer excl 27 | ---- -------- ---- ---- 28 | Photos/Photos.20160703 0/2317 848.55GiB 24.35GiB 29 | Photos/Photos.20161106 0/3217 883.62GiB 16.04GiB 30 | Photos/Photos.20160605 0/1998 840.20GiB 16.01GiB 31 | Archive/Archive.20160504 0/489 160.81GiB 8.01GiB 32 | timemachine/timemachine.20170716 0/7125 227.62GiB 2.21GiB 33 | timemachine/timemachine.20170723 0/7202 230.20GiB 982.49MiB 34 | ``` 35 | 36 | ## Usage 37 | 38 | For this to work on a BTRFS volume, you first need to enable quotas on the volume: 39 | 40 | ```bash 41 | btrfs quota enable /mnt/some-volume 42 | ``` 43 | 44 | All arguments are passed through to `btrfs qgroup show`, so you can get started with: 45 | 46 | ```bash 47 | ./btrfs-subvolumes.py /mnt/some-volume -h 48 | ``` 49 | 50 | ## Notes 51 | 52 | The current version of this script does not allow sorting by path as it passes 53 | all arugments through to btrfsprogs. If you need sorting by path, either submit a PR 54 | or use the original version of the script [here](https://github.com/stecman/btrfs-df/commit/096f480cad6ba5c0573d9523093195a1e33f5808). 55 | 56 | This was originally based on [a shell script that was too slow](https://github.com/agronick/btrfs-size). 57 | -------------------------------------------------------------------------------- /btrfs-subvolumes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # List BTRFS subvolume space use information similar to df -h (with snapshot paths) 4 | # 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. 8 | # 9 | # For this to work on a BTRFS volume, you first need to enable quotas on the volume: 10 | # 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: 20 | # https://github.com/agronick/btrfs-size 21 | 22 | from __future__ import print_function 23 | 24 | import subprocess 25 | import sys 26 | import os 27 | import re 28 | 29 | def get_btrfs_subvols(path): 30 | """Return a dictionary of subvolume names indexed by their subvolume ID""" 31 | try: 32 | # Get all subvolumes 33 | raw = subprocess.check_output(["btrfs", "subvolume", "list", path]) 34 | volumes = re.findall(r'^ID (\d+) .* path (.*)$', raw.decode("utf8"), re.MULTILINE) 35 | volumes = dict(volumes) 36 | 37 | # Add root volume ID (not listed as in the subvolume command) 38 | rootid = subprocess.check_output(["btrfs", "inspect-internal", "rootid", path]).decode().strip() 39 | volumes[rootid] = path 40 | 41 | return dict(volumes) 42 | 43 | except subprocess.CalledProcessError as e: 44 | if e.returncode != 0: 45 | print("\nFailed to list subvolumes") 46 | print("Is '%s' really a BTRFS volume?" % path) 47 | sys.exit(1) 48 | 49 | def get_data_raw(args): 50 | """Return lines of output from a call to 'btrfs qgroup show' with args appended""" 51 | try: 52 | # Get the lines of output, ignoring the two header lines 53 | raw = subprocess.check_output(["btrfs", "qgroup", "show"] + args) 54 | return raw.decode("utf8").split("\n") 55 | 56 | except subprocess.CalledProcessError as e: 57 | if e.returncode != 0: 58 | print("\nFailed to get subvolume quotas. Have you enabled quotas on this volume?") 59 | print("(You can do so with: sudo btrfs quota enable )") 60 | sys.exit(1) 61 | 62 | def get_qgroup_id(line): 63 | """Extract qgroup id from a line of btrfs qgroup show output 64 | Returns None if the line wasn't valid 65 | """ 66 | id_match = re.match(r"\d+/(\d+)", line) 67 | 68 | if not id_match: 69 | return None 70 | 71 | return id_match.group(1) 72 | 73 | def guess_path_argument(argv): 74 | """Return an argument most likely to be the arg for 'btrfs qgroup show' 75 | This is a cheap way to pass through to btrfsprogs without duplicating the options here. 76 | Currently only easier than duplication because the option/argument list is simple. 77 | """ 78 | # Path can't be the first argument (program) 79 | args = argv[1:] 80 | 81 | # Filter out arguments to options 82 | # Only the sort option currently takes an argument 83 | option_follows = [ 84 | "--sort" 85 | ] 86 | 87 | for text in option_follows: 88 | try: 89 | position = args.index(text) 90 | del args[position + 1] 91 | except: 92 | pass 93 | 94 | # Ignore options 95 | args = [arg for arg in args if re.match(r"^-", arg) is None] 96 | 97 | # Prefer the item at the end of the list as this is the suggested argument order 98 | return args[-1] 99 | 100 | 101 | # Re-run the script as root if started with a non-priveleged account 102 | if os.getuid() != 0: 103 | cmd = 'sudo "' + '" "'.join(sys.argv) + '"' 104 | sys.exit(subprocess.call(cmd, shell=True)) 105 | 106 | 107 | # Fetch command output to work with 108 | output = get_data_raw(sys.argv[1:]) 109 | subvols = get_btrfs_subvols(guess_path_argument(sys.argv)) 110 | 111 | # Data for the new column 112 | path_column = [ 113 | "path", 114 | "----" 115 | ] 116 | 117 | # Iterate through all lines except for the table header 118 | for index,line in enumerate(output): 119 | # Ignore header rows 120 | if index <= 1: 121 | continue 122 | 123 | groupid = get_qgroup_id(line) 124 | 125 | if groupid in subvols: 126 | path_column.append(subvols[groupid]) 127 | else: 128 | path_column.append("") 129 | 130 | # Find the required width for the new column 131 | column_width = len(max(path_column, key=len)) + 2 132 | 133 | # Output data with extra column for path 134 | for index,line in enumerate(output): 135 | if path_column[index] is "": 136 | # We can't print anything useful for qgroups that aren't associated with a path 137 | continue 138 | 139 | print(path_column[index].ljust(column_width) + output[index]) 140 | --------------------------------------------------------------------------------