From 11f17c5624f9e3d30ca7f8ad51a609e32a9c632f Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sat, 18 Apr 2026 21:55:15 +0200 Subject: [PATCH 01/22] Added link to source code repository --- community_backup/webui/templates/base.html | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/community_backup/webui/templates/base.html b/community_backup/webui/templates/base.html index ffa2b06..f8c06d9 100644 --- a/community_backup/webui/templates/base.html +++ b/community_backup/webui/templates/base.html @@ -46,11 +46,20 @@ From 6f75cfaf9a8b07bc1470d9034c79cd584e0ee996 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 07:48:25 +0200 Subject: [PATCH 02/22] removed unused about.md template --- community_backup/webui/templates/about.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 community_backup/webui/templates/about.md diff --git a/community_backup/webui/templates/about.md b/community_backup/webui/templates/about.md deleted file mode 100644 index 12e7d3c..0000000 --- a/community_backup/webui/templates/about.md +++ /dev/null @@ -1,6 +0,0 @@ -# About Community Backup - -Community Backup is a service that provides backup space free of charge. - -* This is -* a list From 47a1dbc4cb4c94f588d5ba241a298c42594ce593 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 07:48:56 +0200 Subject: [PATCH 03/22] Fixed typos on landing page --- community_backup/webui/templates/landing.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community_backup/webui/templates/landing.html b/community_backup/webui/templates/landing.html index 39db761..b7a8141 100644 --- a/community_backup/webui/templates/landing.html +++ b/community_backup/webui/templates/landing.html @@ -6,7 +6,7 @@ -

This is a service offering space for backups free of charge. This is a hobby project. It comes with no garantuees for anything, especially availability of the service. This service is in a testing phase at the moment. It might be unavailalbe at times and features might change quickly.

+

This is a service offering space for backups free of charge. This is a hobby project. It comes with no garantees for anything, especially availability of the service. This service is in a testing phase at the moment. It might be unavailable at times and features might change quickly.

Currently this service offers only backups via Borg Backup.

To access this service you need an account. To register for an account you need a voucher. Vouchers are required to control the amount of users, so that there is enough space available for everyone.

From 79bc9a92bed623d2263d2639140971283ee041e2 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 08:34:57 +0200 Subject: [PATCH 04/22] added version number to the footer --- community_backup/community_backup/context_processors.py | 7 +++++++ community_backup/community_backup/settings.py | 3 +++ community_backup/webui/templates/base.html | 7 +++++++ pyproject.toml | 2 +- 4 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 community_backup/community_backup/context_processors.py diff --git a/community_backup/community_backup/context_processors.py b/community_backup/community_backup/context_processors.py new file mode 100644 index 0000000..a6321ea --- /dev/null +++ b/community_backup/community_backup/context_processors.py @@ -0,0 +1,7 @@ +from django.conf import settings + + +def release_version(_request): + return { + "RELEASE_VERSION": settings.RELEASE_VERSION, + } diff --git a/community_backup/community_backup/settings.py b/community_backup/community_backup/settings.py index b8c735f..2e420e3 100644 --- a/community_backup/community_backup/settings.py +++ b/community_backup/community_backup/settings.py @@ -66,6 +66,7 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "community_backup.context_processors.release_version", ], }, }, @@ -114,6 +115,8 @@ TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBacken STATIC_ROOT = BASE_DIR.parent.parent / "static" +RELEASE_VERSION = "0.5" + # Import settings from configuration.py try: config_module = "community_backup.configuration" diff --git a/community_backup/webui/templates/base.html b/community_backup/webui/templates/base.html index f8c06d9..f030569 100644 --- a/community_backup/webui/templates/base.html +++ b/community_backup/webui/templates/base.html @@ -60,6 +60,13 @@ + diff --git a/pyproject.toml b/pyproject.toml index 412eb3e..8a678d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "community_backup" -version = "0.1.0" +version = "0.5.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From 2f0db972deb5ed8168ce19bc44bb2778b46d114f Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 09:09:15 +0200 Subject: [PATCH 05/22] moved custom Markdown pages to configurable directory. --- README.md | 14 ++++++++++++++ .../community_backup/example_configuration.py | 2 ++ community_backup/community_backup/settings.py | 2 ++ community_backup/webui/views.py | 6 +++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6bf868d..f5b384d 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,16 @@ Create a superuser account ``` +## Custom pages + +Some pages are specific to your installation. E.g. an imprint. + +Those files are markdown files that are rendered as HTML. You have to supply them and put them into the `MARKDOWN_PAGE_DIR`. + +The following files are expected: + +* `imprint.md` with your imprint information. + ## Settings ### BACKUP_USER @@ -120,3 +130,7 @@ The secret key used by django for session cookies and other things. ### DEBUG Django debug setting. + +### MARKDOWN_PAGE_DIR + +`MARKDOWN_PAGE_DIR` is the directory where customized markdown files are put. This directory is required. diff --git a/community_backup/community_backup/example_configuration.py b/community_backup/community_backup/example_configuration.py index cf06f1c..1ddb2ac 100644 --- a/community_backup/community_backup/example_configuration.py +++ b/community_backup/community_backup/example_configuration.py @@ -19,3 +19,5 @@ SECRET_KEY = "change me!" # SECURITY WARNING: don't run with debug turned on in production! # DEBUG = True + +MARKDOWN_PAGE_DIR = Path("./custom_md/") diff --git a/community_backup/community_backup/settings.py b/community_backup/community_backup/settings.py index 2e420e3..40d17a7 100644 --- a/community_backup/community_backup/settings.py +++ b/community_backup/community_backup/settings.py @@ -130,6 +130,7 @@ try: assert module.BACKUP_AUTHORIZED_KEYS, ( "The BACKUP_AUTHORIZED_KEYS setting is required." ) + assert module.MARKDOWN_PAGE_DIR, "The MARKDOWN_PAGE_DIR setting is required." except ModuleNotFoundError: print(f"could not find configuration file {config_module}") @@ -142,5 +143,6 @@ BACKUP_BORG_DIR = module.BACKUP_BORG_DIR BACKUP_AUTHORIZED_KEYS = module.BACKUP_AUTHORIZED_KEYS DATABASES = module.DATABASES SECRET_KEY = module.SECRET_KEY +MARKDOWN_PAGE_DIR = module.MARKDOWN_PAGE_DIR DEBUG = getattr(module, "DEBUG", False) diff --git a/community_backup/webui/views.py b/community_backup/webui/views.py index f265477..249f861 100644 --- a/community_backup/webui/views.py +++ b/community_backup/webui/views.py @@ -2,13 +2,12 @@ from django.shortcuts import get_object_or_404 from django.views.generic.base import TemplateView from django.views.generic import ListView, FormView from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.forms import BaseUserCreationForm from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin -from django.template.loader import get_template - from django.core.exceptions import ValidationError import markdown2 @@ -36,7 +35,8 @@ class MarkdownView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - source = get_template(self.md).template.source + + source = (settings.MARKDOWN_PAGE_DIR / self.md).read_text() context["markdown_content"] = markdown2.markdown(source) return context From 942b6581602cba72a6374b7a78c75380c41126dc Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 09:10:44 +0200 Subject: [PATCH 06/22] Release 0.6 --- 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 40d17a7..f404ca3 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.5" +RELEASE_VERSION = "0.6" # Import settings from configuration.py try: diff --git a/pyproject.toml b/pyproject.toml index 8a678d3..526d5d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "community_backup" -version = "0.5.0" +version = "0.6" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From 8b46b747deeb1411ca6033d59154a3d8b04fd953 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 20:30:55 +0200 Subject: [PATCH 07/22] Fix typo in landing page --- community_backup/webui/templates/landing.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community_backup/webui/templates/landing.html b/community_backup/webui/templates/landing.html index b7a8141..a9960f9 100644 --- a/community_backup/webui/templates/landing.html +++ b/community_backup/webui/templates/landing.html @@ -6,7 +6,7 @@ -

This is a service offering space for backups free of charge. This is a hobby project. It comes with no garantees for anything, especially availability of the service. This service is in a testing phase at the moment. It might be unavailable at times and features might change quickly.

+

This is a service offering space for backups free of charge. This is a hobby project. It comes with no guarantees for anything, especially availability of the service. This service is in a testing phase at the moment. It might be unavailable at times and features might change quickly.

Currently this service offers only backups via Borg Backup.

To access this service you need an account. To register for an account you need a voucher. Vouchers are required to control the amount of users, so that there is enough space available for everyone.

From 02fad76482e5e18032a22680133028f8b1ebc264 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 20:34:07 +0200 Subject: [PATCH 08/22] reworked SSH Key handling --- community_backup/tests/__init__.py | 0 community_backup/tests/test_models.py | 54 +++++++++++++++++++ community_backup/webui/deployments.py | 4 +- community_backup/webui/forms.py | 2 +- community_backup/webui/models.py | 45 ++++++++++++++-- .../webui/templates/borg_list.html | 2 +- 6 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 community_backup/tests/__init__.py create mode 100644 community_backup/tests/test_models.py diff --git a/community_backup/tests/__init__.py b/community_backup/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/community_backup/tests/test_models.py b/community_backup/tests/test_models.py new file mode 100644 index 0000000..74e2aa4 --- /dev/null +++ b/community_backup/tests/test_models.py @@ -0,0 +1,54 @@ +from django.test import TestCase +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from webui.models import BorgRepository + + +class BorgRepositoryTestCase(TestCase): + def test_ssh_key_regex(self): + user = User.objects.create(username="foo") + + # good repo + repo = BorgRepository.objects.create( + name="test1", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + + repo.full_clean() + + # good repo, key without comment + repo = BorgRepository.objects.create( + name="test2", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3", + user=user, + ) + repo.full_clean() + + # bad repo (wrong key) + repo = BorgRepository.objects.create(name="test3", key="Foo", user=user) + self.assertRaises(ValidationError, repo.full_clean) + + # bad repo (multiple valid keys ) + repo = BorgRepository.objects.create( + name="test4", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + self.assertRaises(ValidationError, repo.full_clean) + + # bad repo, leading whitespace + repo = BorgRepository.objects.create( + name="test5", + key=" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris", + user=user, + ) + self.assertRaises(ValidationError, repo.full_clean) + + # good repo, with whitespace in commment + repo = BorgRepository.objects.create( + name="test6", + key="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOYOkeGl40Bss9LMmreCtzq+uXw4IQ/E5SKsBRcKAfF3 jo@hubris whitespace in comment", + user=user, + ) + repo.full_clean() diff --git a/community_backup/webui/deployments.py b/community_backup/webui/deployments.py index b56d1bc..527dda3 100644 --- a/community_backup/webui/deployments.py +++ b/community_backup/webui/deployments.py @@ -8,8 +8,6 @@ from django.conf import settings logger = getLogger("deployments") -# sync users - def sync_repos(dry_run=False): """Synchronize the repos""" @@ -19,7 +17,7 @@ def sync_repos(dry_run=False): repos_by_key = defaultdict(list) for repo in repos: - repos_by_key[repo.key].append(repo) + repos_by_key[f"{repo.key_type} {repo.key_value}"].append(repo) # create .ssh directory ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent diff --git a/community_backup/webui/forms.py b/community_backup/webui/forms.py index 99e774c..3ac0cb0 100644 --- a/community_backup/webui/forms.py +++ b/community_backup/webui/forms.py @@ -10,7 +10,7 @@ class BorgRepositoryForm(forms.ModelForm): class Meta: fields = ["name", "key", "user"] model = BorgRepository - widgets = {"user": forms.HiddenInput()} + widgets = {"user": forms.HiddenInput(), "key": forms.TextInput()} class RegisterUserForm(UserCreationForm): diff --git a/community_backup/webui/models.py b/community_backup/webui/models.py index 75c1962..d04785a 100644 --- a/community_backup/webui/models.py +++ b/community_backup/webui/models.py @@ -21,7 +21,7 @@ class BorgRepository(models.Model): key = models.TextField( validators=[ RegexValidator( - r"(ssh\-rsa|ecdsa\-sha2\-nistp256|ssh\-ed25519) ([a-zA-Z0-9\+/=]+) (\S*)", + r"^(ssh\-rsa|ecdsa\-sha2\-nistp256|ssh\-ed25519) ([a-zA-Z0-9\+/=]+)( ([\S ]*))?$", message="not a valid SSH public key.", ), ] @@ -46,9 +46,9 @@ class BorgRepository(models.Model): def repo_url(self) -> str: return f"{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}:{self.user.pk}/{self.pk}" - def save(self): + def save(self, *args, **kwargs): - super().save() + super().save(*args, **kwargs) update_user.enqueue() def delete(self): @@ -56,6 +56,45 @@ class BorgRepository(models.Model): super().delete() update_user.enqueue() + @property + def key_type(self) -> str: + return self.key.split(" ", 1)[0].strip() + + @property + def key_value(self) -> str: + return self.key.split(" ", 2)[1].strip() + + @property + def key_comment(self) -> str | None: + splits = self.key.split(" ", 2) + if len(splits) == 3: + return splits[2].strip() + return None + + def truncated_key(self, length: int = 60): + """ + Returns a truncated version of the key for display purposes. + The Key type and the comment are retained as much as possible. + """ + + # check how long the truncated part of the key may be at most. + max_len = length - len(self.key_type) - 1 + if self.key_comment: + max_len = max_len - len(self.key_comment) - 1 + + if len(self.key_value) > max_len: + max_len -= 1 # remove one character if we add an ellipsis + trunc_key = self.key_value[:max_len] + "…" + else: + trunc_key = self.key_value + + output = f"{self.key_type} {trunc_key}" + + if self.key_comment: + output += f" {self.key_comment}" + + return output + 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 b193fcd..cd508d4 100644 --- a/community_backup/webui/templates/borg_list.html +++ b/community_backup/webui/templates/borg_list.html @@ -24,7 +24,7 @@ {% for repo in object_list %} {{ repo.name }} - {{ repo.key | truncatechars:40 }} + {{ repo.truncated_key }} {{ repo.repo_url}} Edit Delete From 3e84c036f7f26bedd27158bb44a2ca111d0c0b1a Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Sun, 19 Apr 2026 20:35:07 +0200 Subject: [PATCH 09/22] release version 0.7 --- 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 f404ca3..154ea3c 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.6" +RELEASE_VERSION = "0.7" # Import settings from configuration.py try: diff --git a/pyproject.toml b/pyproject.toml index 526d5d5..9e552ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "community_backup" -version = "0.6" +version = "0.7" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" From 755e313c61941322051a876d04880ef9db702500 Mon Sep 17 00:00:00 2001 From: Johannes Erwerle Date: Mon, 20 Apr 2026 14:08:02 +0200 Subject: [PATCH 10/22] 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 11/22] 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 12/22] 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 13/22] 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 14/22] 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 15/22] 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 `