#!/usr/bin/env python3 import os import subprocess import sys import tempfile from pathlib import Path import click # btrfs cannot shrink smaller than 256 MiB SMALLEST_SIZE = 256 * 1024 * 1024 @click.command() @click.argument( 'btrfs_img', type=click.Path(exists=True, dir_okay=False, file_okay=True, path_type=Path), ) def cli(btrfs_img: Path): """ Shrinks a Btrfs image // I cannot believe that Btrfs is over 15 years old, // yet there is no resize2fs tool which can shrink a disk image // to minimum size. // It cannot even tell you how much should be the right size, // it just randomly fails after which you have to umount and mount again. // So we have to make a loop which tries to shrink it until it fails. // Also, WONTFIX bugs like how instead of telling you that // minimum fs size is 256 MB, it says "ERROR: unable to resize - Invalid argument" // https://bugzilla.kernel.org/show_bug.cgi?id=118111 """ if os.geteuid() != 0: sys.exit(' needs sudo') current_dir = Path.cwd() mnt_dir = Path(tempfile.mkdtemp(dir=current_dir, prefix='tmp_shrink_')) subprocess.run(['mount', '-t', 'btrfs', btrfs_img, mnt_dir], check=True) # shink until max. 10 MB left or reached SMALLEST_SIZE or failure while True: # needs to start with a balancing # https://btrfs.readthedocs.io/en/latest/Balance.html # https://marc.merlins.org/perso/btrfs/post_2014-05-04_Fixing-Btrfs-Filesystem-Full-Problems.html do_balancing(mnt_dir) free_bytes = get_usage(mnt_dir, 'Device unallocated') device_size = get_usage(mnt_dir, 'Device size') shrink_idea = free_bytes * 0.7 # workaround for the SMALLEST_SIZE limit if device_size - free_bytes < SMALLEST_SIZE: shrink_idea = (device_size - SMALLEST_SIZE) * 0.7 # stop if 10 MB left if shrink_idea < 10_000_000: break # stop if process error if not do_shrink(mnt_dir, shrink_idea): break total_size = get_usage(mnt_dir, 'Device size') subprocess.run(['umount', mnt_dir]) mnt_dir.rmdir() subprocess.run(['truncate', '-s', str(total_size), btrfs_img]) print(f'Truncated {btrfs_img} to {total_size//1_000_000} MB size') print('shrink_btrfs.py DONE') def get_usage(mnt: Path, key: str): p = subprocess.run( ['btrfs', 'filesystem', 'usage', '-b', mnt], text=True, capture_output=True, check=True ) for line in p.stdout.splitlines(): if f'{key}:' not in line: continue free = int(line.split(':')[1]) return free def do_shrink(mnt: Path, delta_size: float): delta_size = int(delta_size) print(f'Trying to shrink by {delta_size//1_000_000} MB') p = subprocess.run(['btrfs', 'filesystem', 'resize', str(-delta_size), mnt]) return p.returncode == 0 def do_balancing(mnt: Path): print('Starting btrfs balancing') p = subprocess.run( ['btrfs', 'balance', 'start', '-dusage=100', mnt], capture_output=True, text=True ) if p.returncode: print(f'Balance error: {p.stdout} {p.stderr}') print('Balancing done') if __name__ == '__main__': cli()