Compare commits

...

23 commits
0.4 ... main

Author SHA1 Message Date
a0b69596c8 bumped version to 0.12 2026-06-05 11:13:11 +02:00
71e72fb98b Added command to bulk-create vouchers 2026-06-05 11:11:55 +02:00
a942a0b8c6 bumped version to 0.11 2026-05-13 00:06:20 +02:00
95fde8e58f added systemd service and timer for quota update 2026-05-13 00:05:04 +02:00
ea9346d25d Added SSH public keys to the borg repository listing 2026-05-13 00:01:04 +02:00
d0b7158423 improved overflows in the borg repository table 2026-05-12 15:17:06 +02:00
528d7b9e4b bumped version to 0.10 2026-05-06 10:23:53 +02:00
72fee741e6 added option to add links to the footer 2026-05-06 10:23:17 +02:00
68533d9983 Release 0.9 2026-04-25 17:00:37 +02:00
fb76fffdba moved the repo URL to the new URL format 2026-04-25 13:54:13 +02:00
4bfab4d699 Release 0.8 2026-04-20 19:04:09 +02:00
3a4f5a0394 Added Repository Quota setting 2026-04-20 19:03:17 +02:00
755e313c61 added missing migrations 2026-04-20 14:08:02 +02:00
3e84c036f7 release version 0.7 2026-04-19 20:35:07 +02:00
02fad76482 reworked SSH Key handling 2026-04-19 20:34:07 +02:00
8b46b747de Fix typo in landing page 2026-04-19 20:30:55 +02:00
942b658160 Release 0.6 2026-04-19 09:10:44 +02:00
2f0db972de moved custom Markdown pages to configurable directory. 2026-04-19 09:09:15 +02:00
79bc9a92be added version number to the footer 2026-04-19 08:34:57 +02:00
47a1dbc4cb Fixed typos on landing page 2026-04-19 07:48:56 +02:00
6f75cfaf9a removed unused about.md template 2026-04-19 07:48:35 +02:00
11f17c5624 Added link to source code repository 2026-04-18 21:55:15 +02:00
46e9e0fcfa moved SECRET_KEY and DEBUG variables from settings to configuration. 2026-04-18 17:51:53 +02:00
22 changed files with 448 additions and 32 deletions

View file

@ -78,6 +78,24 @@ 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
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 ## Settings
### BACKUP_USER ### BACKUP_USER
@ -112,3 +130,49 @@ DATABASES = {
} }
} }
``` ```
### SECRET_KEY
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.
### 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

@ -0,0 +1,19 @@
from django.conf import settings
def release_version(_request):
return {
"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

@ -13,3 +13,27 @@ DATABASES = {
"NAME": "/path/to/the/db.sqlite3", "NAME": "/path/to/the/db.sqlite3",
} }
} }
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "change me!"
# SECURITY WARNING: don't run with debug turned on in production!
# DEBUG = True
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

@ -20,11 +20,6 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-_53=1gp5m=j+ienpcguini#xczk^d+&jx*#@z+2837+_-0=+$z"
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
"*", "*",
@ -71,6 +66,9 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"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.additional_footer_nav_items",
"community_backup.context_processors.borg_server_pubkeys",
], ],
}, },
}, },
@ -119,11 +117,14 @@ 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"
# Import settings from configuration.py # Import settings from configuration.py
try: try:
config_module = "community_backup.configuration" config_module = "community_backup.configuration"
module = import_module(config_module) module = import_module(config_module)
assert module.SECRET_KEY, "The Django SECRET is required."
assert module.BACKUP_USER, "The BACKUP_USER setting is required." assert module.BACKUP_USER, "The BACKUP_USER setting is required."
assert module.BACKUP_REPO_HOST, "The BACKUP_REPO_HOST setting is required." assert module.BACKUP_REPO_HOST, "The BACKUP_REPO_HOST setting is required."
assert module.BACKUP_HOME_DIR, "The BACKUP_HOME_DIR setting is required." assert module.BACKUP_HOME_DIR, "The BACKUP_HOME_DIR setting is required."
@ -131,6 +132,8 @@ try:
assert module.BACKUP_AUTHORIZED_KEYS, ( assert module.BACKUP_AUTHORIZED_KEYS, (
"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.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}")
@ -142,3 +145,10 @@ BACKUP_HOME_DIR = module.BACKUP_HOME_DIR
BACKUP_BORG_DIR = module.BACKUP_BORG_DIR BACKUP_BORG_DIR = module.BACKUP_BORG_DIR
BACKUP_AUTHORIZED_KEYS = module.BACKUP_AUTHORIZED_KEYS BACKUP_AUTHORIZED_KEYS = module.BACKUP_AUTHORIZED_KEYS
DATABASES = module.DATABASES 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)
ADDITIONAL_FOOTER_NAV_ITEMS = getattr(module, "ADDITIONAL_FOOTER_NAV_ITEMS", list())
BORG_SERVER_PUBKEYS = getattr(module, "BORG_SERVER_PUBKEYS", list())

View file

@ -0,0 +1,9 @@
[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

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

View file

View file

@ -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()

View file

@ -8,8 +8,6 @@ from django.conf import settings
logger = getLogger("deployments") logger = getLogger("deployments")
# sync users
def sync_repos(dry_run=False): def sync_repos(dry_run=False):
"""Synchronize the repos""" """Synchronize the repos"""
@ -19,7 +17,7 @@ def sync_repos(dry_run=False):
repos_by_key = defaultdict(list) repos_by_key = defaultdict(list)
for repo in repos: 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 # create .ssh directory
ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent
@ -36,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

@ -10,7 +10,7 @@ class BorgRepositoryForm(forms.ModelForm):
class Meta: class Meta:
fields = ["name", "key", "user"] fields = ["name", "key", "user"]
model = BorgRepository model = BorgRepository
widgets = {"user": forms.HiddenInput()} widgets = {"user": forms.HiddenInput(), "key": forms.TextInput()}
class RegisterUserForm(UserCreationForm): class RegisterUserForm(UserCreationForm):

View file

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

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

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

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
@ -21,13 +22,16 @@ class BorgRepository(models.Model):
key = models.TextField( key = models.TextField(
validators=[ validators=[
RegexValidator( 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.", message="not a valid SSH public key.",
), ),
] ]
) )
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(
@ -44,11 +48,11 @@ class BorgRepository(models.Model):
@property @property
def repo_url(self) -> str: 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): def save(self, *args, **kwargs):
super().save() super().save(*args, **kwargs)
update_user.enqueue() update_user.enqueue()
def delete(self): def delete(self):
@ -56,6 +60,60 @@ class BorgRepository(models.Model):
super().delete() super().delete()
update_user.enqueue() 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
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

@ -1,6 +0,0 @@
# About Community Backup
Community Backup is a service that provides backup space free of charge.
* This is
* a list

View file

@ -46,12 +46,39 @@
<footer class="bg-body-tertiary"> <footer class="bg-body-tertiary">
<div class="container p-2"> <div class="container p-2">
<div class="row"> <div class="row">
<div class="col-3">
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'imprint' %}">Imprint</a> <a class="nav-link" href="{% url 'imprint' %}">Imprint</a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="col-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="https://git.srvspace.net/jo/community-backup">Source Code</a>
</li>
</ul>
</div>
<div class="col-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link disabled">Community Backup v{{ RELEASE_VERSION }}</a>
</li>
</ul>
</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>
</footer> </footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

View file

@ -18,14 +18,16 @@
<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><code>{{ repo.key | truncatechars:40 }}</code></td> <td style="word-break: break-all"><code>{{ repo.truncated_key }}</code></td>
<td><code>{{ repo.repo_url}}</code></td> <td style="word-break: break-all"><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>
@ -33,6 +35,21 @@
</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

@ -6,7 +6,7 @@
</row> </row>
<row> <row>
<p>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.</p> <p>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.</p>
<p>Currently this service offers only backups via <a href="https://www.borgbackup.org/">Borg Backup</a>.</p> <p>Currently this service offers only backups via <a href="https://www.borgbackup.org/">Borg Backup</a>.</p>
<p>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.</p> <p>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.</p>
</row> </row>

View file

@ -2,13 +2,12 @@ from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic import ListView, FormView from django.views.generic import ListView, FormView
from django.views.generic.edit import CreateView, UpdateView, DeleteView 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.models import User
from django.contrib.auth.forms import BaseUserCreationForm from django.contrib.auth.forms import BaseUserCreationForm
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from django.template.loader import get_template
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import markdown2 import markdown2
@ -36,7 +35,8 @@ class MarkdownView(TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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) context["markdown_content"] = markdown2.markdown(source)
return context return context

View file

@ -1,6 +1,6 @@
[project] [project]
name = "community_backup" name = "community_backup"
version = "0.1.0" version = "0.12"
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,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",
] ]