Compare commits

..

No commits in common. "main" and "0.7" have entirely different histories.
main ... 0.7

16 changed files with 6 additions and 280 deletions

View file

@ -78,14 +78,6 @@ Create a superuser account
``` ```
### Repository Quota Updates
The repository quotas in the UI are updated after each backup.
To update the quota during an operation the `update_used_quota` management command can be executed on a regular basis.
There are example configs for systemd-timers in `contrib/communitybackup-quota-update.service` and `contrib/communitybackup-quota-update.timer`. Adjust the paths and the timers to your needs.
## Custom pages ## Custom pages
Some pages are specific to your installation. E.g. an imprint. Some pages are specific to your installation. E.g. an imprint.
@ -142,37 +134,3 @@ 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`
### 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 `<li class=nav-item>`. So usually you want to continue the proper Bootstrap styling in the lst. E.g.:
```python
ADDITIONAL_FOOTER_NAV_ITEMS = ["""<a class="nav-link" href="https://example.com/>Example Link</a>""",]
```
### BORG_SERVER_PUBKEYS
A list of the SSH public keys and their hashes to verify the server a User is connecting to.
This is a list of tuples, containing the key and it's hash.
```python
BORG_SERVER_PUBKEYS = [
(
"ecdsa-sha2-nistp256 AAAAASDJIASKJDASD root@example.com",
"256 SHA256:sTbOK9NvP1uUEixgUT8KUiYrY8J/DbK+jR39lwcT8Zw root@example.com (ECDSA)",
),
(
"ssh-ed25519 AAAAasdiwdkjasdijwklajsdijasd root@example.com",
"256 SHA256:hPbWwRxNr1mFHZKYjcysnay1cQGQsOmDBvkA3Pzo4YY root@example.com (ED25519)",
),
(
"ssh-rsa AAAABasdlkjasdiualksjd root@example.com",
"3072 SHA256:deuPTR8Hcc1LP7DHqAp91EINdLBQoco2IeMldIahamQ root@example.com (RSA)",
),
]
```

View file

@ -5,15 +5,3 @@ def release_version(_request):
return { return {
"RELEASE_VERSION": settings.RELEASE_VERSION, "RELEASE_VERSION": settings.RELEASE_VERSION,
} }
def additional_footer_nav_items(_request):
return {
"ADDITIONAL_FOOTER_NAV_ITEMS": settings.ADDITIONAL_FOOTER_NAV_ITEMS,
}
def borg_server_pubkeys(_request):
return {
"BORG_SERVER_PUBKEYS": settings.BORG_SERVER_PUBKEYS,
}

View file

@ -21,19 +21,3 @@ 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")
# BORG_SERVER_PUBKEYS = [
# (
# "ecdsa-sha2-nistp256 AAAAASDJIASKJDASD root@example.com",
# "256 SHA256:sTbOK9NvP1uUEixgUT8KUiYrY8J/DbK+jR39lwcT8Zw root@example.com (ECDSA)",
# ),
# (
# "ssh-ed25519 AAAAasdiwdkjasdijwklajsdijasd root@example.com",
# "256 SHA256:hPbWwRxNr1mFHZKYjcysnay1cQGQsOmDBvkA3Pzo4YY root@example.com (ED25519)",
# ),
# (
# "ssh-rsa AAAABasdlkjasdiualksjd root@example.com",
# "3072 SHA256:deuPTR8Hcc1LP7DHqAp91EINdLBQoco2IeMldIahamQ root@example.com (RSA)",
# ),
# ]

View file

@ -67,8 +67,6 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"community_backup.context_processors.release_version", "community_backup.context_processors.release_version",
"community_backup.context_processors.additional_footer_nav_items",
"community_backup.context_processors.borg_server_pubkeys",
], ],
}, },
}, },
@ -117,7 +115,7 @@ TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBacken
STATIC_ROOT = BASE_DIR.parent.parent / "static" STATIC_ROOT = BASE_DIR.parent.parent / "static"
RELEASE_VERSION = "0.12" RELEASE_VERSION = "0.7"
# Import settings from configuration.py # Import settings from configuration.py
try: try:
@ -133,7 +131,6 @@ 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}")
@ -147,8 +144,5 @@ 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)
ADDITIONAL_FOOTER_NAV_ITEMS = getattr(module, "ADDITIONAL_FOOTER_NAV_ITEMS", list())
BORG_SERVER_PUBKEYS = getattr(module, "BORG_SERVER_PUBKEYS", list())

View file

@ -1,9 +0,0 @@
[Unit]
Description=Update the used quota
[Service]
User=borg
Group=borg
WorkingDirectory=/opt/community_backup/community-backup/community_backup/
ExecStart=/opt/community_backup/venv/bin/python manage.py update_used_quota
PrivateTmp=true

View file

@ -1,9 +0,0 @@
[Unit]
Description=Update the used quota on a regular basis
[Timer]
OnCalendar=*-*-* *:0/5:00
RandomizedDelaySec=120
[Install]
WantedBy=timers.target

View file

@ -34,10 +34,8 @@ 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={quota}G;{settings.BACKUP_MANAGE_PY} update_used_quota --repository-id {" ".join(ids)}",restrict {key}""" f"""command="cd {str(settings.BACKUP_BORG_DIR)}; borg serve {" ".join(repo_paths)} --storage-quota=500G",restrict {key}"""
) )
if not dry_run: if not dry_run:

View file

@ -1,39 +0,0 @@
from django.core.management.base import BaseCommand
from random import choices
from ...models import Voucher
class Command(BaseCommand):
help = "bulk-add vouchers with a given prefix and generate a latex output to render them"
def add_arguments(self, parser):
parser.add_argument("prefix", type=str)
parser.add_argument("amount", type=int)
def handle(self, *args, **options):
unabmigous_characters = "abcdefghijkmnopqrstuvwxyzACDEFHJKLMNPQRTUVWXY1234679"
vouchers = set()
while len(vouchers) < options["amount"]:
random_part = "".join(choices(unabmigous_characters, k=5))
vouchers.add(f"{options['prefix']}-{random_part}")
Voucher.objects.bulk_create(Voucher(code=v) for v in vouchers)
header = r"""\begin{document}
\begin{center}
"""
output = header
even = False
for v in vouchers:
output += f"\\voucher{{{v}}}"
if even:
output += "\\newline\n"
else:
output += "\n"
even = not even
output += r"""\end{center}
\end{document}"""
print(output)

View file

@ -1,24 +0,0 @@
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):
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"))
for repo in qs:
repo.refresh_quota()
BorgRepository.objects.bulk_update(qs, ["used_quota"])

View file

@ -1,26 +0,0 @@
# 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.",
)
]
),
),
]

View file

@ -1,23 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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,7 +3,6 @@ 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
@ -29,9 +28,6 @@ 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(
@ -48,7 +44,7 @@ class BorgRepository(models.Model):
@property @property
def repo_url(self) -> str: def repo_url(self) -> str:
return f"ssh://{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}/./{self.user.pk}/{self.pk}" return f"{settings.BACKUP_USER}@{settings.BACKUP_REPO_HOST}:{self.user.pk}/{self.pk}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -99,21 +95,6 @@ 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

@ -67,17 +67,6 @@
</li> </li>
</ul> </ul>
</div> </div>
{% if ADDITIONAL_FOOTER_NAV_ITEMS %}
<div class="col-3">
<ul class="nav flex-column">
{% for item in ADDITIONAL_FOOTER_NAV_ITEMS %}
<li class="nav-item">
{{item|safe}}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -18,16 +18,14 @@
<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>
{% for repo in object_list %} {% for repo in object_list %}
<tr> <tr>
<td>{{ repo.name }}</td> <td>{{ repo.name }}</td>
<td style="word-break: break-all"><code>{{ repo.truncated_key }}</code></td> <td><code>{{ repo.truncated_key }}</code></td>
<td style="word-break: break-all"><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>
@ -35,21 +33,6 @@
</tbody> </tbody>
</table> </table>
{% if BORG_SERVER_PUBKEYS %}
<div class="card">
<div class="card-header">
<h4>Borg Server Public Keys</h4>
</div>
<ul class="list-group list-group-flush">
{% for key, fingerprint in BORG_SERVER_PUBKEYS %}
<li class="list-group-item">
<span style="word-break: break-all"><code>{{ key }}</code></span><br>
<span style="word-break: break-all"><code>{{ fingerprint }}</code></span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}

View file

@ -1,6 +1,6 @@
[project] [project]
name = "community_backup" name = "community_backup"
version = "0.12" version = "0.7"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
@ -10,5 +10,4 @@ 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",
] ]