Compare commits
No commits in common. "main" and "0.7" have entirely different histories.
16 changed files with 6 additions and 280 deletions
42
README.md
42
README.md
|
|
@ -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
|
||||
|
||||
Some pages are specific to your installation. E.g. an imprint.
|
||||
|
|
@ -142,37 +134,3 @@ 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)",
|
||||
),
|
||||
]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -5,15 +5,3 @@ 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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,19 +21,3 @@ 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")
|
||||
|
||||
# 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)",
|
||||
# ),
|
||||
# ]
|
||||
|
|
|
|||
|
|
@ -67,8 +67,6 @@ TEMPLATES = [
|
|||
"django.contrib.auth.context_processors.auth",
|
||||
"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",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
@ -117,7 +115,7 @@ TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBacken
|
|||
|
||||
STATIC_ROOT = BASE_DIR.parent.parent / "static"
|
||||
|
||||
RELEASE_VERSION = "0.12"
|
||||
RELEASE_VERSION = "0.7"
|
||||
|
||||
# Import settings from configuration.py
|
||||
try:
|
||||
|
|
@ -133,7 +131,6 @@ 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}")
|
||||
|
|
@ -147,8 +144,5 @@ 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)
|
||||
ADDITIONAL_FOOTER_NAV_ITEMS = getattr(module, "ADDITIONAL_FOOTER_NAV_ITEMS", list())
|
||||
BORG_SERVER_PUBKEYS = getattr(module, "BORG_SERVER_PUBKEYS", list())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -34,10 +34,8 @@ 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={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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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"])
|
||||
|
|
@ -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.",
|
||||
)
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -3,7 +3,6 @@ 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
|
||||
|
|
@ -29,9 +28,6 @@ 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(
|
||||
|
|
@ -48,7 +44,7 @@ class BorgRepository(models.Model):
|
|||
|
||||
@property
|
||||
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):
|
||||
|
||||
|
|
@ -99,21 +95,6 @@ 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)
|
||||
|
|
|
|||
|
|
@ -67,17 +67,6 @@
|
|||
</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>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -18,16 +18,14 @@
|
|||
<th>Name</th>
|
||||
<th>Key</th>
|
||||
<th>Repo-URL</th>
|
||||
<th>Quota (used/total)</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for repo in object_list %}
|
||||
<tr>
|
||||
<td>{{ repo.name }}</td>
|
||||
<td style="word-break: break-all"><code>{{ repo.truncated_key }}</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><code>{{ repo.truncated_key }}</code></td>
|
||||
<td><code>{{ repo.repo_url}}</code></td>
|
||||
<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>
|
||||
</tr>
|
||||
|
|
@ -35,21 +33,6 @@
|
|||
</tbody>
|
||||
</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 %}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "community_backup"
|
||||
version = "0.12"
|
||||
version = "0.7"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
|
|
@ -10,5 +10,4 @@ dependencies = [
|
|||
"django-crispy-forms>=2.6",
|
||||
"gunicorn>=25.3.0",
|
||||
"markdown2>=2.5.5",
|
||||
"msgpack>=1.1.2",
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue