Compare commits

..

7 commits
0.2 ... main

14 changed files with 215 additions and 32 deletions

2
.gitignore vendored
View file

@ -207,3 +207,5 @@ tags
[._]*.un~ [._]*.un~
# End of https://www.toptal.com/developers/gitignore/api/vim,venv,python # End of https://www.toptal.com/developers/gitignore/api/vim,venv,python
community_backup/community_backup/configuration.py

109
README.md
View file

@ -4,4 +4,111 @@ A website to manage backup repositories.
## Installation ## Installation
TODO This installation guide assumes a Debian 13 system.
Install Python and several dependencies:
This example should wo
```bash
apt update && apt install python3 python3-pip python3-venv borgbackup
```
Clone the repository
git clone https://git.srvspace.net/jo/community-backup.git
Or install from a release
Create a venv
```bash
python3 -m venv venv
```
Activea the venv
```bash
source venv/bin/activate
```
Install the package and it's dependencies:
```bash
pip install -e community-backup
```
Create a configuration file in `community_backup/community_backup/community_backup/configration.py`, e.g. by copying and adjusting the `example_configuraton.py` next to that location.
The settings are explained in detail in the setting section.
Apply the migrations:
```bash
python community-backup/community_backup/manage.py migrate
```
Collect the static files:
```bash
# python community-backup/community_backup/manage.py collectstatic
130 static files copied to '/opt/community_backup/static'.
```
Point your webserver to the static files. E.g. with caddy:
```
example.backups.org {
handle /static/* {
file_server {
root /opt/community_backup/
}
}
handle {
reverse_proxy http://localhost:8000
}
}
```
Create a superuser account
```
```
## Settings
### BACKUP_USER
`BACKUP_USER` specifies the username, that is used for borg backups. This user is used for various file permissions as well as the user that clients are using to log in and push their backups.
### BACKUP_REPO_HOST
`BACKUP_REPO_HOST` is the hostname given to the user for pushing their backups to. E.g. `backup.example.com`.
### BACKUP_HOME_DIR
`BACKUP_HOME_DIR` is the home directory of the borg user. This must be a `pathlib.Path`.
### BACKUP_BORG_DIR
`BACKUP_BORG_DIR` is the directory in which the actual backups are stored. This must be a `pathlib.Path`
### BACKUP_AUTHORIZED_KEYS
`BACKUP_AUTHORIZED_KEYS` is the authorized_keys file of the SSH daemon used for `BORG_USER`. This must be a `pathlib.Path`
### DATABASES
`DATABASES` is the Django database setting. Here is an example for sqlite3.
```
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "/some/path/to/db.sqlite3",
}
}
```

View file

@ -1,2 +0,0 @@
# enter your overrides here
print("foo")

View file

@ -0,0 +1,15 @@
# enter your overrides here
from pathlib import Path
BACKUP_HOME_DIR = Path("/data/backups")
BACKUP_BORG_DIR = BACKUP_HOME_DIR / "borg"
BACKUP_AUTHORIZED_KEYS = BACKUP_HOME_DIR / ".ssh" / "authorized_keys"
BACKUP_USER = "borg"
BACKUP_REPO_HOST = "backup.example.com"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "/path/to/the/db.sqlite3",
}
}

View file

@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/6.0/ref/settings/
""" """
from pathlib import Path from pathlib import Path
from importlib import import_module
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@ -78,17 +79,6 @@ TEMPLATES = [
WSGI_APPLICATION = "community_backup.wsgi.application" WSGI_APPLICATION = "community_backup.wsgi.application"
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
@ -127,4 +117,28 @@ STATIC_URL = "static/"
TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}} TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}}
from .configuration import * STATIC_ROOT = BASE_DIR.parent.parent / "static"
# Import settings from configuration.py
try:
config_module = "community_backup.configuration"
module = import_module(config_module)
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_HOME_DIR, "The BACKUP_HOME_DIR setting is required."
assert module.BACKUP_BORG_DIR, "The BACKUP_BORG_DIR setting is required."
assert module.BACKUP_AUTHORIZED_KEYS, (
"The BACKUP_AUTHORIZED_KEYS setting is required."
)
except ModuleNotFoundError:
print(f"could not find configuration file {config_module}")
exit(1)
BACKUP_USER = module.BACKUP_USER
BACKUP_REPO_HOST = module.BACKUP_REPO_HOST
BACKUP_HOME_DIR = module.BACKUP_HOME_DIR
BACKUP_BORG_DIR = module.BACKUP_BORG_DIR
BACKUP_AUTHORIZED_KEYS = module.BACKUP_AUTHORIZED_KEYS
DATABASES = module.DATABASES

View file

@ -16,13 +16,11 @@ def sync_repos(dry_run=False):
from .models import BorgRepository from .models import BorgRepository
repos = BorgRepository.objects.all() repos = BorgRepository.objects.all()
print(repos)
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[repo.key].append(repo)
print(repos)
# create .ssh directory # create .ssh directory
ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent ssh_dir = settings.BACKUP_AUTHORIZED_KEYS.parent
if not dry_run: if not dry_run:
@ -45,10 +43,8 @@ def sync_repos(dry_run=False):
if not dry_run: if not dry_run:
authorized_keys.write_text("\n".join(commands) + "\n") authorized_keys.write_text("\n".join(commands) + "\n")
print(repos)
# remove repositories that do no longer exist # remove repositories that do no longer exist
repo_paths = {repo.path for repo in repos} repo_paths = {repo.path for repo in repos}
print(repo_paths)
for user_dir in settings.BACKUP_BORG_DIR.iterdir(): for user_dir in settings.BACKUP_BORG_DIR.iterdir():
print(user_dir) print(user_dir)
for dir in user_dir.iterdir(): for dir in user_dir.iterdir():

View file

@ -15,7 +15,7 @@ class BorgRepositoryForm(forms.ModelForm):
class RegisterUserForm(UserCreationForm): class RegisterUserForm(UserCreationForm):
email = forms.EmailField() email = forms.EmailField()
voucher = forms.CharField(help_text="You registration voucher.") voucher = forms.CharField(help_text="Your registration voucher.")
def clean_voucher(self): def clean_voucher(self):
obj = Voucher.objects.filter(code=self.cleaned_data["voucher"], used=False) obj = Voucher.objects.filter(code=self.cleaned_data["voucher"], used=False)

View file

@ -0,0 +1,39 @@
# Generated by Django 6.0.3 on 2026-04-06 19:51
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("webui", "0003_voucher"),
]
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.",
)
]
),
),
migrations.AlterField(
model_name="borgrepository",
name="name",
field=models.CharField(
max_length=100,
validators=[
django.core.validators.RegexValidator(
"[a-zA-Z0-9\\-_]+",
message="Only a-z, A-Z, 0-9, - and _ are allowed.",
)
],
),
),
]

View file

@ -48,13 +48,7 @@
<div class="row"> <div class="row">
<ul class="nav flex-column"> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url "about" %}">About</a> <a class="nav-link" href="{% url 'imprint' %}">Imprint</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Imprint</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Data Protection</a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -1,13 +1,17 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class<div class="d-flex"> <div class="d-flex">
<h1>Your Borg Repositories</h1> <h1>Your Borg Repositories</h1>
<div class="p-2 ms-auto"> <div class="p-2 ms-auto">
<a class="btn btn-primary" href="{% url 'borg_add' %}" role="button">Add Repository</a> <a class="btn btn-primary" href="{% url 'borg_add' %}" role="button">Add Repository</a>
</div> </div>
</div> </div>
<row>
<p>Create a borg repository by specifying a name and the SSH public key that is allowed to access this repository. The name is only for your convenience. To set up the repository please follow the <a href="https://borgbackup.readthedocs.io/en/stable/">BorgBackup documentation</a>. There is also a <a href="https://borgbackup.readthedocs.io/en/stable/quickstart.html">Quick start guide</a> available.
<b>Always use encrypted backups!</b></p>
</row>
<table class="table table-striped table-hover"> <table class="table table-striped table-hover">
<thead> <thead>
@ -30,4 +34,6 @@
</table> </table>
{% endblock %} {% endblock %}

View file

@ -4,4 +4,10 @@
<row class="text-center p-2"> <row class="text-center p-2">
<h1>Welcome to Community Backup!</h1> <h1>Welcome to Community Backup!</h1>
</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>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>
</row>
{% endblock %} {% endblock %}

View file

@ -17,5 +17,5 @@ urlpatterns = [
name="borg_delete", name="borg_delete",
), ),
path("register/", views.RegisterUserView.as_view(), name="register"), path("register/", views.RegisterUserView.as_view(), name="register"),
path("about/", views.AboutView.as_view(), name="about"), path("imprint/", views.ImprintView.as_view(), name="imprint"),
] ]

View file

@ -41,9 +41,14 @@ class MarkdownView(TemplateView):
return context return context
class AboutView(MarkdownView): class DataProtectionView(MarkdownView):
template_name = "markdown.html" template_name = "markdown.html"
md = "about.md" md = "dataprotection.md"
class ImprintView(MarkdownView):
template_name = "markdown.html"
md = "imprint.md"
class BorgView(LoginRequiredMixin, ListView): class BorgView(LoginRequiredMixin, ListView):

View file

@ -8,5 +8,6 @@ dependencies = [
"crispy-bootstrap5>=2026.3", "crispy-bootstrap5>=2026.3",
"django>=6.0.3", "django>=6.0.3",
"django-crispy-forms>=2.6", "django-crispy-forms>=2.6",
"gunicorn>=25.3.0",
"markdown2>=2.5.5", "markdown2>=2.5.5",
] ]