Last active
October 17, 2025 19:00
-
-
Save ernstki/f3e279e8a050c2df94e9fcfd69d67c2f to your computer and use it in GitHub Desktop.
List space used by Flatpak ~/.var/app directories for apps that are no longer installed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| ## | |
| ## flatcrap - lists orphaned Flatpak data in ~/.var/app | |
| ## | |
| ## Author: Kevin Ernst <ernstki -at- mail.uc.edu> | |
| ## Date: 16 October 2025 | |
| ## License: MPL 2.0 | |
| ## Homepage: https://gist.github.com/ernstki/f3e279e8a050c2df94e9fcfd69d67c2f | |
| ## | |
| ## Originally based on `main.py` from Flatsweep by giantpinkrobots[1], but | |
| ## that code was pretty rough (LLM-generated?), thus it was rewritten piece | |
| ## by piece until only the license remains. | |
| ## | |
| ## Note that there *are* important things in ~/.var/app/<appname> (settings | |
| ## and data), so if you think you may *ever* use a Flatpak app again, it | |
| ## would be best to leave them there unless you're confident it's been saved | |
| ## elsewhere. | |
| ## | |
| ## [1]: https://github.com/giantpinkrobots/flatsweep | |
| ## | |
| import os | |
| import glob | |
| import logging | |
| import collections | |
| from os import listdir | |
| if os.getenv('FLATCRAP_DEBUG'): | |
| logging.getLogger().setLevel(logging.DEBUG) | |
| elif os.getenv('FLATCRAP_VERBOSE'): | |
| logging.getLogger().setLevel(logging.INFO) | |
| CLEANUP_CANDIDATES = [ | |
| "/var/lib/flatpak/app", | |
| os.path.expanduser("~/.local/share/flatpak/app"), | |
| ] | |
| class FlatCrap: | |
| def __init__(self, cleanup_candidates=None): | |
| var_app_list = [] | |
| installed_apps = set() | |
| var_app_dir = os.path.expanduser("~/.var/app") | |
| self.orphaned_data_dirs = {} | |
| if cleanup_candidates is None: | |
| cleanup_candidates = CLEANUP_CANDIDATES | |
| for p in cleanup_candidates: | |
| if os.path.exists(p): | |
| installed_apps |= set(listdir(p)) | |
| logging.info("List of installed flatpaks: %s", installed_apps) | |
| if not os.access(var_app_dir, os.R_OK | os.W_OK | os.X_OK): | |
| logging.warning("User has no '~/.var/app' directory or it's " | |
| "unreadable. Nothing to do.") | |
| return | |
| for path in os.scandir(var_app_dir): | |
| if not path.is_dir(): | |
| logging.warning("Extraneous file ~/.var/app/%s", path.name) | |
| continue | |
| logging.debug("Found ~/.var/app/%s", path.name) | |
| var_app_list.append(path.name) | |
| for app in var_app_list: | |
| if app in installed_apps: | |
| logging.debug("Not considering %s as it's still installed", app) | |
| continue | |
| orphaned_app_path = os.path.join(var_app_dir, app, '**') | |
| logging.info("Considering '%s' as %s is not installed", | |
| orphaned_app_path, app) | |
| orphaned_app_data_size = 0 | |
| for path in glob.glob(orphaned_app_path, recursive=True): | |
| if not os.path.isfile(path): | |
| continue | |
| orphaned_app_data_size += os.stat(path).st_size | |
| orphan = { app: orphaned_app_data_size } | |
| logging.info("Orphaned data: %s", orphan) | |
| self.orphaned_data_dirs.update(orphan) | |
| self._sort_orphaned_data_by_size() | |
| def _sort_orphaned_data_by_size(self, reverse=True): | |
| _sorted = sorted(self.orphaned_data_dirs.items(), key=lambda x: x[1], | |
| reverse=reverse) | |
| # Since Python 3.7, I think CPython dicts remember insertion order, but | |
| # that's "an implementation detail" so we can't rely on it | |
| self.orphaned_data_dirs = collections.OrderedDict(_sorted) | |
| @classmethod | |
| def humansize(cls, bytesize): | |
| """ Divide by 1024 until we get a number < 1000 and return that """ | |
| exponent = 0 | |
| units = ['bytes', 'KB', 'MB', 'GB'] | |
| while bytesize > 1024: | |
| exponent += 1 | |
| bytesize /= 1024 | |
| return bytesize, units[exponent] | |
| @property | |
| def orphaned_data_size(self): | |
| return sum([self.orphaned_data_dirs[s] | |
| for s in self.orphaned_data_dirs]) | |
| if __name__ == '__main__': | |
| fs = FlatCrap() | |
| print(""" | |
| The following Flatpak apps are no longer installed. If you don't plan on using | |
| them again, the corresponding directories inside '~/.var/app' could be removed: | |
| """) | |
| for d in fs.orphaned_data_dirs: | |
| print("- {} ({:0.1f} {})" | |
| .format(d, *fs.humansize(fs.orphaned_data_dirs[d]))) | |
| print("\nTotal orphaned data size in ~/.var/app: {:0.1f} {}\n" | |
| .format(*fs.humansize(fs.orphaned_data_size))) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Installation
If you already use other Python programs on your system, you will probably have a
~/.local/binand it will probably already be in your path. So this will work.Log out and back in or source your login scripts if it's not immediately in your search path (check with
which flatcrap). Otherwise, it's good to know how to modifyPATHvariable on a Unix/Linux system anyway.Example output