Added Repository Quota setting

This commit is contained in:
Johannes Erwerle 2026-04-20 19:03:17 +02:00
parent 755e313c61
commit 3a4f5a0394
10 changed files with 102 additions and 2 deletions

View file

@ -134,3 +134,7 @@ Django debug setting.
### MARKDOWN_PAGE_DIR ### MARKDOWN_PAGE_DIR
`MARKDOWN_PAGE_DIR` is the directory where customized markdown files are put. This directory is required. `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`

View file

@ -21,3 +21,4 @@ SECRET_KEY = "change me!"
# DEBUG = True # DEBUG = True
MARKDOWN_PAGE_DIR = Path("./custom_md/") MARKDOWN_PAGE_DIR = Path("./custom_md/")
BACKUP_MANAGE_PY = Path("/path/to/venv/bin/python /path/to/community_backup/manage.py")

View file

@ -131,6 +131,7 @@ try:
"The BACKUP_AUTHORIZED_KEYS setting is required." "The BACKUP_AUTHORIZED_KEYS setting is required."
) )
assert module.MARKDOWN_PAGE_DIR, "The MARKDOWN_PAGE_DIR 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: except ModuleNotFoundError:
print(f"could not find configuration file {config_module}") print(f"could not find configuration file {config_module}")
@ -144,5 +145,6 @@ BACKUP_AUTHORIZED_KEYS = module.BACKUP_AUTHORIZED_KEYS
DATABASES = module.DATABASES DATABASES = module.DATABASES
SECRET_KEY = module.SECRET_KEY SECRET_KEY = module.SECRET_KEY
MARKDOWN_PAGE_DIR = module.MARKDOWN_PAGE_DIR MARKDOWN_PAGE_DIR = module.MARKDOWN_PAGE_DIR
BACKUP_MANAGE_PY = module.BACKUP_MANAGE_PY
DEBUG = getattr(module, "DEBUG", False) DEBUG = getattr(module, "DEBUG", False)

View file

@ -34,8 +34,10 @@ def sync_repos(dry_run=False):
commands = [] commands = []
for key, repositories in repos_by_key.items(): for key, repositories in repos_by_key.items():
repo_paths = [f"--restrict-to-repository {repo.path}" for repo in repositories] 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( 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: if not dry_run:

View file

@ -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"])

View file

@ -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),
),
]

View file

@ -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),
),
]

View file

@ -3,6 +3,7 @@ from django.contrib.auth.models import User
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.conf import settings from django.conf import settings
from pathlib import Path from pathlib import Path
import msgpack
from .tasks import update_user from .tasks import update_user
@ -28,6 +29,9 @@ class BorgRepository(models.Model):
) )
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
quota = models.IntegerField(default=500)
used_quota = models.IntegerField(default=-1)
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
@ -95,6 +99,21 @@ class BorgRepository(models.Model):
return output 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): class Voucher(models.Model):
used = models.BooleanField(default=False) used = models.BooleanField(default=False)

View file

@ -18,6 +18,7 @@
<th>Name</th> <th>Name</th>
<th>Key</th> <th>Key</th>
<th>Repo-URL</th> <th>Repo-URL</th>
<th>Quota (used/total)</th>
<th></th> <th></th>
</thead> </thead>
<tbody> <tbody>
@ -26,6 +27,7 @@
<td>{{ repo.name }}</td> <td>{{ repo.name }}</td>
<td><code>{{ repo.truncated_key }}</code></td> <td><code>{{ repo.truncated_key }}</code></td>
<td><code>{{ repo.repo_url }}</code></td> <td><code>{{ repo.repo_url }}</code></td>
<td>{% if repo.used_quota < 0 %}not initialized{% else %}{{ repo.used_quota }}/{{ repo.quota }} GB{% endif %}</td>
<td><a href="{% url 'borg_update' pk=repo.pk %}" class="btn btn-warning btn-sm">Edit</a> <td><a href="{% url 'borg_update' pk=repo.pk %}" class="btn btn-warning btn-sm">Edit</a>
<a href="{% url 'borg_delete' pk=repo.pk %}" class="btn btn-danger btn-sm">Delete</a></td> <a href="{% url 'borg_delete' pk=repo.pk %}" class="btn btn-danger btn-sm">Delete</a></td>
</tr> </tr>

View file

@ -10,4 +10,5 @@ dependencies = [
"django-crispy-forms>=2.6", "django-crispy-forms>=2.6",
"gunicorn>=25.3.0", "gunicorn>=25.3.0",
"markdown2>=2.5.5", "markdown2>=2.5.5",
"msgpack>=1.1.2",
] ]