From 755e313c61941322051a876d04880ef9db702500 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Mon, 20 Apr 2026 14:08:02 +0200 Subject: [PATCH 01/13] added missing migrations --- .../0005_alter_borgrepository_key.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 community_backup/webui/migrations/0005_alter_borgrepository_key.py diff --git a/community_backup/webui/migrations/0005_alter_borgrepository_key.py b/community_backup/webui/migrations/0005_alter_borgrepository_key.py new file mode 100644 index 0000000..8f7f15c --- /dev/null +++ b/community_backup/webui/migrations/0005_alter_borgrepository_key.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.3 on 2026-04-20 12:07 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("webui", "0004_alter_borgrepository_key_alter_borgrepository_name"), + ] + + operations = [ + migrations.AlterField( + model_name="borgrepository", + name="key", + field=models.TextField( + validators=[ + django.core.validators.RegexValidator( + "^(ssh\\-rsa|ecdsa\\-sha2\\-nistp256|ssh\\-ed25519) ([a-zA-Z0-9\\+/=]+)( ([\\S ]*))?$", + message="not a valid SSH public key.", + ) + ] + ), + ), + ] From 3a4f5a03945dfca36efe2a9ac62a0f8081c09685 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Mon, 20 Apr 2026 19:03:17 +0200 Subject: [PATCH 02/13] Added Repository Quota setting --- README.md | 4 +++ .../community_backup/example_configuration.py | 1 + community_backup/community_backup/settings.py | 2 ++ community_backup/webui/deployments.py | 4 ++- .../management/commands/update_used_quota.py | 28 +++++++++++++++++++ ...ository_quota_borgrepository_used_quota.py | 23 +++++++++++++++ .../0007_alter_borgrepository_used_quota.py | 18 ++++++++++++ community_backup/webui/models.py | 19 +++++++++++++ .../webui/templates/borg_list.html | 4 ++- pyproject.toml | 1 + 10 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 community_backup/webui/management/commands/update_used_quota.py create mode 100644 community_backup/webui/migrations/0006_borgrepository_quota_borgrepository_used_quota.py create mode 100644 community_backup/webui/migrations/0007_alter_borgrepository_used_quota.py diff --git a/README.md b/README.md index f5b384d..8209c94 100644 --- a/README.md +++ b/README.md @@ -134,3 +134,7 @@ Django debug setting. ### MARKDOWN_PAGE_DIR `MARKDOWN_PAGE_DIR` is the directory where customized markdown files are put. This directory is required. + +### BACKUP_MANAGE_PY + +`BACKUP_MANAGE_PY` is the command to run the `manage.py` file, including e.g. a Python interpreter, venv, etc. This must be a `pathlib.Path` diff --git a/community_backup/community_backup/example_configuration.py b/community_backup/community_backup/example_configuration.py index 1ddb2ac..6ad812f 100644 --- a/community_backup/community_backup/example_configuration.py +++ b/community_backup/community_backup/example_configuration.py @@ -21,3 +21,4 @@ SECRET_KEY = "change me!" # DEBUG = True MARKDOWN_PAGE_DIR = Path("./custom_md/") +BACKUP_MANAGE_PY = Path("/path/to/venv/bin/python /path/to/community_backup/manage.py") diff --git a/community_backup/community_backup/settings.py b/community_backup/community_backup/settings.py index 154ea3c..0a7f6d1 100644 --- a/community_backup/community_backup/settings.py +++ b/community_backup/community_backup/settings.py @@ -131,6 +131,7 @@ try: "The BACKUP_AUTHORIZED_KEYS setting is required." ) assert module.MARKDOWN_PAGE_DIR, "The MARKDOWN_PAGE_DIR setting is required." + assert module.BACKUP_MANAGE_PY, "The BACKUP_MANAGE_PY setting is required" except ModuleNotFoundError: print(f"could not find configuration file {config_module}") @@ -144,5 +145,6 @@ BACKUP_AUTHORIZED_KEYS = module.BACKUP_AUTHORIZED_KEYS DATABASES = module.DATABASES SECRET_KEY = module.SECRET_KEY MARKDOWN_PAGE_DIR = module.MARKDOWN_PAGE_DIR +BACKUP_MANAGE_PY = module.BACKUP_MANAGE_PY DEBUG = getattr(module, "DEBUG", False) diff --git a/community_backup/webui/deployments.py b/community_backup/webui/deployments.py index 527dda3..1b73a2e 100644 --- a/community_backup/webui/deployments.py +++ b/community_backup/webui/deployments.py @@ -34,8 +34,10 @@ def sync_repos(dry_run=False): commands = [] for key, repositories in repos_by_key.items(): repo_paths = [f"--restrict-to-repository {repo.path}" for repo in repositories] + quota = max(repo.quota for repo in repositories) + ids = [str(repo.id) for repo in repositories] commands.append( - f"""command="cd {str(settings.BACKUP_BORG_DIR)}; borg serve {" ".join(repo_paths)} --storage-quota=500G",restrict {key}""" + f"""command="cd {str(settings.BACKUP_BORG_DIR)}; borg serve {" ".join(repo_paths)} --storage-quota={quota}G;{settings.BACKUP_MANAGE_PY} update_used_quota --repository-id {" ".join(ids)}",restrict {key}""" ) if not dry_run: diff --git a/community_backup/webui/management/commands/update_used_quota.py b/community_backup/webui/management/commands/update_used_quota.py new file mode 100644 index 0000000..b19e661 --- /dev/null +++ b/community_backup/webui/management/commands/update_used_quota.py @@ -0,0 +1,28 @@ +from django.core.management.base import BaseCommand +from ...models import BorgRepository + + +class Command(BaseCommand): + help = "Update the used quota values" + + def add_arguments(self, parser): + parser.add_argument("--user-id", type=int) + parser.add_argument("--repository-id", type=int, nargs="+") + + def handle(self, *args, **options): + print(options) + + qs = BorgRepository.objects.all() + + if options.get("user_id"): + qs = qs.filter(user__pk=options.get("user_id")) + + if options.get("repository_id"): + qs = qs.filter(pk__in=options.get("repository_id")) + + print(qs) + + for repo in qs: + repo.refresh_quota() + + BorgRepository.objects.bulk_update(qs, ["used_quota"]) diff --git a/community_backup/webui/migrations/0006_borgrepository_quota_borgrepository_used_quota.py b/community_backup/webui/migrations/0006_borgrepository_quota_borgrepository_used_quota.py new file mode 100644 index 0000000..83c9faa --- /dev/null +++ b/community_backup/webui/migrations/0006_borgrepository_quota_borgrepository_used_quota.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.3 on 2026-04-20 15:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("webui", "0005_alter_borgrepository_key"), + ] + + operations = [ + migrations.AddField( + model_name="borgrepository", + name="quota", + field=models.IntegerField(default=500), + ), + migrations.AddField( + model_name="borgrepository", + name="used_quota", + field=models.IntegerField(default=0), + ), + ] diff --git a/community_backup/webui/migrations/0007_alter_borgrepository_used_quota.py b/community_backup/webui/migrations/0007_alter_borgrepository_used_quota.py new file mode 100644 index 0000000..0c8cca9 --- /dev/null +++ b/community_backup/webui/migrations/0007_alter_borgrepository_used_quota.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.3 on 2026-04-20 17:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("webui", "0006_borgrepository_quota_borgrepository_used_quota"), + ] + + operations = [ + migrations.AlterField( + model_name="borgrepository", + name="used_quota", + field=models.IntegerField(default=-1), + ), + ] diff --git a/community_backup/webui/models.py b/community_backup/webui/models.py index d04785a..4acc2f4 100644 --- a/community_backup/webui/models.py +++ b/community_backup/webui/models.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.core.validators import RegexValidator from django.conf import settings from pathlib import Path +import msgpack from .tasks import update_user @@ -28,6 +29,9 @@ class BorgRepository(models.Model): ) user = models.ForeignKey(User, on_delete=models.CASCADE) + quota = models.IntegerField(default=500) + used_quota = models.IntegerField(default=-1) + class Meta: constraints = [ models.UniqueConstraint( @@ -95,6 +99,21 @@ class BorgRepository(models.Model): return output + def refresh_quota(self): + hints_files = self.path.glob("hints.*") + + try: + hint = next(hints_files) + data = hint.open(mode="rb").read() + + unpacked = msgpack.unpackb(data, strict_map_key=False, raw=True) + quota_used = unpacked[b"storage_quota_use"] + + self.used_quota = int(quota_used / 10**9) + except StopIteration: + # No hints file found, therefore the repo is probably not initialized + self.used_quota = -1 + class Voucher(models.Model): used = models.BooleanField(default=False) diff --git a/community_backup/webui/templates/borg_list.html b/community_backup/webui/templates/borg_list.html index cd508d4..f2e550b 100644 --- a/community_backup/webui/templates/borg_list.html +++ b/community_backup/webui/templates/borg_list.html @@ -18,6 +18,7 @@ Name Key Repo-URL + Quota (used/total) @@ -25,7 +26,8 @@ {{ repo.name }} {{ repo.truncated_key }} - {{ repo.repo_url}} + {{ repo.repo_url }} + {% if repo.used_quota < 0 %}not initialized{% else %}{{ repo.used_quota }}/{{ repo.quota }} GB{% endif %} Edit Delete diff --git a/pyproject.toml b/pyproject.toml index 9e552ec..203799e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,4 +10,5 @@ dependencies = [ "django-crispy-forms>=2.6", "gunicorn>=25.3.0", "markdown2>=2.5.5", + "msgpack>=1.1.2", ] From 4bfab4d6993f9f34a7ce0cfc3e9e4e96a40bcf3c Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Mon, 20 Apr 2026 19:04:09 +0200 Subject: [PATCH 03/13] Release 0.8 --- community_backup/community_backup/settings.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/community_backup/community_backup/settings.py b/community_backup/community_backup/settings.py index 0a7f6d1..c766e6a 100644 --- a/community_backup/community_backup/settings.py +++ b/community_backup/community_backup/settings.py @@ -115,7 +115,7 @@ TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBacken STATIC_ROOT = BASE_DIR.parent.parent / "static" -RELEASE_VERSION = "0.7" +RELEASE_VERSION = "0.8" # Import settings from configuration.py try: diff --git a/pyproject.toml b/pyproject.toml index 203799e..8ffcd76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "community_backup" -version = "0.7" +version = "0.8" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From fb76fffdba5a809c0cd38f1aa095115887829c10 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sat, 25 Apr 2026 13:54:13 +0200 Subject: [PATCH 04/13] moved the repo URL to the new URL format --- community_backup/webui/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community_backup/webui/models.py b/community_backup/webui/models.py index 4acc2f4..36e338f 100644 --- a/community_backup/webui/models.py +++ b/community_backup/webui/models.py @@ -48,7 +48,7 @@ class BorgRepository(models.Model): @property def repo_url(self) -> str: - return f"{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}:{self.user.pk}/{self.pk}" + return f"ssh://{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}/./{self.user.pk}/{self.pk}" def save(self, *args, **kwargs): From 68533d9983584cbd98276fef86b8917bb129686c Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sat, 25 Apr 2026 17:00:37 +0200 Subject: [PATCH 05/13] Release 0.9 --- community_backup/community_backup/settings.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/community_backup/community_backup/settings.py b/community_backup/community_backup/settings.py index c766e6a..004da17 100644 --- a/community_backup/community_backup/settings.py +++ b/community_backup/community_backup/settings.py @@ -115,7 +115,7 @@ TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBacken STATIC_ROOT = BASE_DIR.parent.parent / "static" -RELEASE_VERSION = "0.8" +RELEASE_VERSION = "0.9" # Import settings from configuration.py try: diff --git a/pyproject.toml b/pyproject.toml index 8ffcd76..eb5c918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "community_backup" -version = "0.8" +version = "0.9" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From 72fee741e66501c2f1f69afd8f37ef421afbc1d1 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Wed, 6 May 2026 10:23:17 +0200 Subject: [PATCH 06/13] added option to add links to the footer --- README.md | 8 ++++++++ .../community_backup/context_processors.py | 5 +++++ community_backup/community_backup/settings.py | 2 ++ community_backup/webui/templates/base.html | 11 +++++++++++ 4 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 8209c94..6d5a710 100644 --- a/README.md +++ b/README.md @@ -138,3 +138,11 @@ Django debug setting. ### BACKUP_MANAGE_PY `BACKUP_MANAGE_PY` is the command to run the `manage.py` file, including e.g. a Python interpreter, venv, etc. This must be a `pathlib.Path` + +### ADDITIONAL_FOOTER_NAV_ITEMS + +`ADDITIONAL_FOOTER_NAV_ITEMS` allows you to add additional links to the footer. This is a `list[str]`. Each item gets rendered as is into a `