mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-08-26 12:46:08 +00:00
Compare commits
27 Commits
a1bd01f0c1
...
020ef6ae0c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020ef6ae0c | ||
|
|
cc8fa47efe | ||
|
|
59c3b49d8b | ||
|
|
a4cfb824e8 | ||
|
|
59329169e4 | ||
|
|
df7793916c | ||
|
|
0c8cba3a13 | ||
|
|
5da5ef5f96 | ||
|
|
4ae66c034c | ||
|
|
e21ab91b21 | ||
|
|
dd0a0f56a6 | ||
|
|
a7ef60015c | ||
|
|
2208a7b210 | ||
|
|
ff7c3a53a5 | ||
|
|
7d3d930137 | ||
|
|
ad40072267 | ||
|
|
305b1f01f5 | ||
|
|
58ecf9d3e4 | ||
|
|
3cb5d1a936 | ||
|
|
8b9413c719 | ||
|
|
451e582c07 | ||
|
|
043bd44dec | ||
|
|
3558a26b2f | ||
|
|
f689e1b3cf | ||
|
|
7fb41313cd | ||
|
|
156d0b761f | ||
|
|
33d05ef5be |
@ -1064,7 +1064,7 @@ Terminal login : sudo machinectl login fbx-{distribution}
|
||||
Open a root shell : sudo machinectl shell fbx-{distribution}
|
||||
Shutdown : {script} stop {options}
|
||||
Destroy : {script} destroy {options}
|
||||
Reset : {script} destroy {options}; {script} up {options}'''
|
||||
Reset : {script} destroy {options}; {script} start {options}'''
|
||||
logger.info(message)
|
||||
|
||||
|
||||
|
||||
@ -369,7 +369,6 @@ class JSONEncoder(json.JSONEncoder):
|
||||
|
||||
def _setup_thread_storage():
|
||||
"""Setup collection of stdout/stderr from any process in this thread."""
|
||||
global thread_storage
|
||||
thread_storage.stdout = b''
|
||||
thread_storage.stderr = b''
|
||||
|
||||
@ -380,14 +379,12 @@ def _clear_thread_storage():
|
||||
Python documentation is silent on whether thread local storage will be
|
||||
cleaned up after a thread terminates.
|
||||
"""
|
||||
global thread_storage
|
||||
thread_storage.stdout = None
|
||||
thread_storage.stderr = None
|
||||
|
||||
|
||||
def get_return_value_from_exception(exception):
|
||||
"""Return the value to return from server when an exception is raised."""
|
||||
global thread_storage
|
||||
return_value = {
|
||||
'result': 'exception',
|
||||
'exception': {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
|
||||
"PO-Revision-Date: 2025-12-17 07:00+0000\n"
|
||||
"PO-Revision-Date: 2026-02-09 19:09+0000\n"
|
||||
"Last-Translator: Jiří Podhorecký <j.podhorecky@volny.cz>\n"
|
||||
"Language-Team: Czech <https://hosted.weblate.org/projects/freedombox/"
|
||||
"freedombox/cs/>\n"
|
||||
@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
|
||||
"X-Generator: Weblate 5.15.1-dev\n"
|
||||
"X-Generator: Weblate 5.16-dev\n"
|
||||
|
||||
#: plinth/config.py:103
|
||||
#, python-brace-format
|
||||
@ -9934,15 +9934,14 @@ msgid "Not configured yet."
|
||||
msgstr "Zatím není nakonfigurováno."
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:59
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Public key for this %(box_name)s:"
|
||||
#, python-format
|
||||
msgid "Endpoints for this %(box_name)s:"
|
||||
msgstr "Veřejný klíč pro tento %(box_name)s:"
|
||||
msgstr "Endpointy pro tento %(box_name)s:"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:75
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:77
|
||||
msgid "Start WireGuard Server"
|
||||
msgstr ""
|
||||
msgstr "Spustit WireGuard Server"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:81
|
||||
msgid "Add a new peer"
|
||||
@ -9981,10 +9980,8 @@ msgid "Add Connection to Server"
|
||||
msgstr "Přidat připojení k serveru"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard_add_client.html:21
|
||||
#, fuzzy
|
||||
#| msgid "IP address to use for client:"
|
||||
msgid "IP address that will be assigned to this client"
|
||||
msgstr "IP adresa, která se použije pro klienta:"
|
||||
msgstr "IP adresa, která bude přiřazena k tomuto klientovi"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard_add_client.html:31
|
||||
msgid "Add Client"
|
||||
@ -10132,14 +10129,12 @@ msgid "Server deleted."
|
||||
msgstr "Server smazán."
|
||||
|
||||
#: plinth/modules/wireguard/views.py:286
|
||||
#, fuzzy
|
||||
#| msgid "Password changed successfully."
|
||||
msgid "WireGuard server started successfully."
|
||||
msgstr "Heslo úspěšně změněno."
|
||||
msgstr "Server WireGuard se úspěšně spustil."
|
||||
|
||||
#: plinth/modules/wireguard/views.py:290
|
||||
msgid "Failed to start WireGuard server: {}"
|
||||
msgstr ""
|
||||
msgstr "Spuštění serveru WireGuard selhalo: {}"
|
||||
|
||||
#: plinth/modules/wordpress/__init__.py:20
|
||||
msgid ""
|
||||
|
||||
@ -10,8 +10,8 @@ msgstr ""
|
||||
"Project-Id-Version: FreedomBox UI\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
|
||||
"PO-Revision-Date: 2026-01-25 14:02+0000\n"
|
||||
"Last-Translator: Ettore Atalan <atalanttore@googlemail.com>\n"
|
||||
"PO-Revision-Date: 2026-02-05 11:01+0000\n"
|
||||
"Last-Translator: Dietmar <sagen@permondes.de>\n"
|
||||
"Language-Team: German <https://hosted.weblate.org/projects/freedombox/"
|
||||
"freedombox/de/>\n"
|
||||
"Language: de\n"
|
||||
@ -10156,17 +10156,14 @@ msgid "Not configured yet."
|
||||
msgstr "Noch nicht konfiguriert."
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:59
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Public key for this %(box_name)s:"
|
||||
#, python-format
|
||||
msgid "Endpoints for this %(box_name)s:"
|
||||
msgstr "Öffentlicher Schlüssel für diese %(box_name)s:"
|
||||
msgstr "Endpunkte für diese %(box_name)s:"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:75
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:77
|
||||
#, fuzzy
|
||||
#| msgid "Standard Services"
|
||||
msgid "Start WireGuard Server"
|
||||
msgstr "Standarddienste"
|
||||
msgstr "WireGuard-Server starten"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:81
|
||||
msgid "Add a new peer"
|
||||
@ -10205,10 +10202,8 @@ msgid "Add Connection to Server"
|
||||
msgstr "Verbindung zum Server hinzufügen"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard_add_client.html:21
|
||||
#, fuzzy
|
||||
#| msgid "IP address to use for client:"
|
||||
msgid "IP address that will be assigned to this client"
|
||||
msgstr "IP-Adresse, die für den Client verwendet werden soll:"
|
||||
msgstr "IP-Adresse, die diesem Client zugewiesen wird"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard_add_client.html:31
|
||||
msgid "Add Client"
|
||||
@ -10357,14 +10352,12 @@ msgid "Server deleted."
|
||||
msgstr "Server gelöscht."
|
||||
|
||||
#: plinth/modules/wireguard/views.py:286
|
||||
#, fuzzy
|
||||
#| msgid "Password changed successfully."
|
||||
msgid "WireGuard server started successfully."
|
||||
msgstr "Passwort erfolgreich geändert."
|
||||
msgstr "Der WireGuard-Server wurde erfolgreich gestartet."
|
||||
|
||||
#: plinth/modules/wireguard/views.py:290
|
||||
msgid "Failed to start WireGuard server: {}"
|
||||
msgstr ""
|
||||
msgstr "Starten des WireGuard-Servers fehlgeschlagen: {}"
|
||||
|
||||
#: plinth/modules/wordpress/__init__.py:20
|
||||
msgid ""
|
||||
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
|
||||
"PO-Revision-Date: 2026-02-03 01:07+0000\n"
|
||||
"PO-Revision-Date: 2026-02-06 23:01+0000\n"
|
||||
"Last-Translator: Pierfrancesco Passerini <p.passerini@gmail.com>\n"
|
||||
"Language-Team: Italian <https://hosted.weblate.org/projects/freedombox/"
|
||||
"freedombox/it/>\n"
|
||||
@ -10026,15 +10026,14 @@ msgid "Not configured yet."
|
||||
msgstr "Non ancora configurato."
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:59
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Public key for this %(box_name)s:"
|
||||
#, python-format
|
||||
msgid "Endpoints for this %(box_name)s:"
|
||||
msgstr "Chiave pubblica di %(box_name)s:"
|
||||
msgstr "Punti di accesso per %(box_name)s:"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:75
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:77
|
||||
msgid "Start WireGuard Server"
|
||||
msgstr ""
|
||||
msgstr "Avvia WireGuard server"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard.html:81
|
||||
msgid "Add a new peer"
|
||||
@ -10073,10 +10072,8 @@ msgid "Add Connection to Server"
|
||||
msgstr "Aggiungi Connessione a Server"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard_add_client.html:21
|
||||
#, fuzzy
|
||||
#| msgid "IP address to use for client:"
|
||||
msgid "IP address that will be assigned to this client"
|
||||
msgstr "Indirizzo IP da utilizzare sul client:"
|
||||
msgstr "Indirizzo IP che sarà assegnato al client"
|
||||
|
||||
#: plinth/modules/wireguard/templates/wireguard_add_client.html:31
|
||||
msgid "Add Client"
|
||||
@ -10224,14 +10221,12 @@ msgid "Server deleted."
|
||||
msgstr "Server cancellato."
|
||||
|
||||
#: plinth/modules/wireguard/views.py:286
|
||||
#, fuzzy
|
||||
#| msgid "Password changed successfully."
|
||||
msgid "WireGuard server started successfully."
|
||||
msgstr "La password è stata aggiornata."
|
||||
msgstr "Il server WireGuard è stato correttamente avviato."
|
||||
|
||||
#: plinth/modules/wireguard/views.py:290
|
||||
msgid "Failed to start WireGuard server: {}"
|
||||
msgstr ""
|
||||
msgstr "Avvio del server WireGuard non riuscito: {}"
|
||||
|
||||
#: plinth/modules/wordpress/__init__.py:20
|
||||
msgid ""
|
||||
|
||||
@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
|
||||
"PO-Revision-Date: 2024-07-30 01:31+0000\n"
|
||||
"Last-Translator: bittin1ddc447d824349b2 <bittin@reimu.nl>\n"
|
||||
"PO-Revision-Date: 2026-02-06 23:01+0000\n"
|
||||
"Last-Translator: Daniel Wiik <d.wiik@outlook.com>\n"
|
||||
"Language-Team: Swedish <https://hosted.weblate.org/projects/freedombox/"
|
||||
"freedombox/sv/>\n"
|
||||
"Language: sv\n"
|
||||
@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.7-dev\n"
|
||||
"X-Generator: Weblate 5.16-dev\n"
|
||||
|
||||
#: plinth/config.py:103
|
||||
#, python-brace-format
|
||||
@ -25,10 +25,9 @@ msgid "Static configuration {etc_path} is setup properly"
|
||||
msgstr "Statisk konfiguration {etc_path} är inställd korrekt"
|
||||
|
||||
#: plinth/container.py:140
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Service {service_name} is running"
|
||||
#, python-brace-format
|
||||
msgid "Container {container_name} is running"
|
||||
msgstr "Tjänsten {service_name} körs"
|
||||
msgstr "Tjänsten {container_name} körs"
|
||||
|
||||
#: plinth/context_processors.py:21 plinth/views.py:175
|
||||
msgid "FreedomBox"
|
||||
@ -72,10 +71,8 @@ msgid "Repository to backup to"
|
||||
msgstr "Repository att säkerhetskopiera till"
|
||||
|
||||
#: plinth/forms.py:62
|
||||
#, fuzzy
|
||||
#| msgid "None"
|
||||
msgid "(None)"
|
||||
msgstr "Ingen"
|
||||
msgstr "(Ingen)"
|
||||
|
||||
#: plinth/forms.py:68
|
||||
msgid "Select a domain name to be used with this application"
|
||||
@ -143,22 +140,17 @@ msgid "System is possibly under heavy load. Please retry later."
|
||||
msgstr "Systemet är möjligen under tung belastning. Försök igen senare."
|
||||
|
||||
#: plinth/middleware.py:147
|
||||
#, fuzzy, python-brace-format
|
||||
#| msgid "Page not found - %(box_name)s"
|
||||
#, python-brace-format
|
||||
msgid "Page not found: {url}"
|
||||
msgstr "Sidan hittades inte - %(box_name)s"
|
||||
msgstr "Sidan hittades inte: {url}"
|
||||
|
||||
#: plinth/middleware.py:150
|
||||
#, fuzzy
|
||||
#| msgid "Error running apt-get"
|
||||
msgid "Error running operation."
|
||||
msgstr "Fel vid körning av apt-get"
|
||||
msgstr "Fel vid körning av åtgärden."
|
||||
|
||||
#: plinth/middleware.py:152
|
||||
#, fuzzy
|
||||
#| msgid "Error running apt-get"
|
||||
msgid "Error loading page."
|
||||
msgstr "Fel vid körning av apt-get"
|
||||
msgstr "Fel vid laddning av sida."
|
||||
|
||||
#: plinth/modules/apache/__init__.py:33
|
||||
msgid "Apache HTTP Server"
|
||||
@ -210,15 +202,15 @@ msgstr "Lokalt nätverksdomän"
|
||||
|
||||
#: plinth/modules/avahi/manifest.py:14
|
||||
msgid "Auto-discovery"
|
||||
msgstr ""
|
||||
msgstr "Autoupptäck"
|
||||
|
||||
#: plinth/modules/avahi/manifest.py:14 plinth/modules/backups/manifest.py:17
|
||||
msgid "Local"
|
||||
msgstr ""
|
||||
msgstr "Lokal"
|
||||
|
||||
#: plinth/modules/avahi/manifest.py:14
|
||||
msgid "mDNS"
|
||||
msgstr ""
|
||||
msgstr "mDNS"
|
||||
|
||||
#: plinth/modules/backups/__init__.py:24
|
||||
msgid "Backups allows creating and managing backup archives."
|
||||
@ -328,6 +320,8 @@ msgid ""
|
||||
"In 24 hour format. Services may become temporarily unavailable while running "
|
||||
"backup operation at this time of the day."
|
||||
msgstr ""
|
||||
"24-timmars format. Tjänster kan bli tillfälligt otillgängliga när du kör "
|
||||
"säkerhetskopiering vid denna tidpunkt på dagen."
|
||||
|
||||
#: plinth/modules/backups/forms.py:85 plinth/modules/backups/forms.py:107
|
||||
msgid "Included apps"
|
||||
@ -373,6 +367,9 @@ msgid ""
|
||||
"file previously downloaded from the result of a successful backup on a "
|
||||
"{box_name}. It must have a .tar.gz extension."
|
||||
msgstr ""
|
||||
"Välj backup-filen för att ladda upp från den lokala datorn. Detta måste vara "
|
||||
"en fil som tidigare laddats ner från resultatet av en framgångsrik "
|
||||
"säkerhetskopia på en {box_name}. Det måste ha en .tar.gz förlängning."
|
||||
|
||||
#: plinth/modules/backups/forms.py:154
|
||||
msgid "Repository path format incorrect."
|
||||
@ -474,16 +471,12 @@ msgid "Restore"
|
||||
msgstr "Återställa"
|
||||
|
||||
#: plinth/modules/backups/manifest.py:15
|
||||
#, fuzzy
|
||||
#| msgid "Encryption"
|
||||
msgid "Encrypted"
|
||||
msgstr "Kryptering"
|
||||
msgstr "Krypterad"
|
||||
|
||||
#: plinth/modules/backups/manifest.py:16
|
||||
#, fuzzy
|
||||
#| msgid "Schedule"
|
||||
msgid "Schedules"
|
||||
msgstr "Schema"
|
||||
msgstr "Scheman"
|
||||
|
||||
#: plinth/modules/backups/manifest.py:18
|
||||
#, fuzzy
|
||||
@ -492,10 +485,8 @@ msgid "Remote"
|
||||
msgstr "Ta bort"
|
||||
|
||||
#: plinth/modules/backups/manifest.py:19
|
||||
#, fuzzy
|
||||
#| msgid "App updated"
|
||||
msgid "App data"
|
||||
msgstr "App uppdaterad"
|
||||
msgstr "App data"
|
||||
|
||||
#: plinth/modules/backups/manifest.py:20
|
||||
#: plinth/modules/homeassistant/templates/homeassistant.html:11
|
||||
@ -539,22 +530,20 @@ msgid "Repository path is neither empty nor is an existing backups repository."
|
||||
msgstr "Respositorysökvägen är varken tom eller en befintlig säkerhetskopia."
|
||||
|
||||
#: plinth/modules/backups/privileged.py:77
|
||||
#, fuzzy
|
||||
#| msgid "A share with this name already exists."
|
||||
msgid "An archive with given name already exists in the repository."
|
||||
msgstr "Det finns redan en share med det här namnet."
|
||||
msgstr "Ett arkiv med givna namn finns redan i kodförrådet."
|
||||
|
||||
#: plinth/modules/backups/privileged.py:84
|
||||
msgid "Archive with given name was not found in the repository."
|
||||
msgstr ""
|
||||
msgstr "Arkiv med givna namn hittades inte i kodförrådet."
|
||||
|
||||
#: plinth/modules/backups/privileged.py:90
|
||||
msgid "Backup system is busy with another operation."
|
||||
msgstr ""
|
||||
msgstr "Backupsystemet är upptaget med en annan process."
|
||||
|
||||
#: plinth/modules/backups/privileged.py:95
|
||||
msgid "Not enough space left on the disk or remote location."
|
||||
msgstr ""
|
||||
msgstr "Inte tillräckligt med utrymme kvar på disken eller fjärrplatsen."
|
||||
|
||||
#: plinth/modules/backups/repository.py:93
|
||||
msgid "Existing repository is not encrypted."
|
||||
@ -711,27 +700,17 @@ msgid "Restore data from"
|
||||
msgstr "Återställa data från"
|
||||
|
||||
#: plinth/modules/backups/templates/backups_upload.html:17
|
||||
#, fuzzy, python-format
|
||||
#| msgid ""
|
||||
#| "\n"
|
||||
#| " Upload a backup file downloaded from another %(box_name)s to "
|
||||
#| "restore its\n"
|
||||
#| " contents. You can choose the apps you wish to restore after "
|
||||
#| "uploading a\n"
|
||||
#| " backup file.\n"
|
||||
#| " "
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Upload a backup file downloaded from another %(box_name)s to restore its "
|
||||
"contents. You can choose the apps you wish to restore after uploading a "
|
||||
"backup file."
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Ladda upp en backup-fil som hämtats från en annan %(box_name)s för att "
|
||||
"Ladda upp en backup-fil som hämtats från en annan %(box_name)s för att "
|
||||
"återställa\n"
|
||||
" Innehållet. Du kan välja vilka appar du vill återställa efter att du "
|
||||
"laddat upp en\n"
|
||||
" backup-fil.\n"
|
||||
" "
|
||||
" backup-fil."
|
||||
|
||||
#: plinth/modules/backups/templates/backups_upload.html:31
|
||||
#, python-format
|
||||
@ -807,10 +786,8 @@ msgid "Upload and restore a backup"
|
||||
msgstr "Ladda upp och återställ en säkerhetskopia"
|
||||
|
||||
#: plinth/modules/backups/views.py:216
|
||||
#, fuzzy
|
||||
#| msgid "Logged out successfully."
|
||||
msgid "Upload successful."
|
||||
msgstr "Du har loggat ut framgångsrikt."
|
||||
msgstr "Uppladdning genomförd."
|
||||
|
||||
#: plinth/modules/backups/views.py:254
|
||||
msgid "No backup file found."
|
||||
@ -835,10 +812,8 @@ msgid "Create backup repository"
|
||||
msgstr "Skapa backup repository"
|
||||
|
||||
#: plinth/modules/backups/views.py:350
|
||||
#, fuzzy
|
||||
#| msgid "Added new remote SSH repository."
|
||||
msgid "Added new repository."
|
||||
msgstr "Lade till ett nytt remote SSH-repository."
|
||||
msgstr "Lade till nytt kodförråd."
|
||||
|
||||
#: plinth/modules/backups/views.py:364
|
||||
msgid "Create remote backup repository"
|
||||
@ -997,14 +972,12 @@ msgstr ""
|
||||
#: plinth/modules/samba/manifest.py:81 plinth/modules/sharing/manifest.py:19
|
||||
#: plinth/modules/syncthing/manifest.py:58
|
||||
#: plinth/modules/transmission/manifest.py:39
|
||||
#, fuzzy
|
||||
#| msgid "File & Snippet Sharing"
|
||||
msgid "File sharing"
|
||||
msgstr "Fil & Snippet Sharing"
|
||||
msgstr "Fildelning"
|
||||
|
||||
#: plinth/modules/bepasty/manifest.py:23
|
||||
msgid "Pastebin"
|
||||
msgstr ""
|
||||
msgstr "Pastebin"
|
||||
|
||||
#: plinth/modules/bepasty/templates/bepasty.html:12
|
||||
msgid "Manage Passwords"
|
||||
@ -1134,10 +1107,8 @@ msgstr ""
|
||||
"förfrågningar kommer att vidarebefordras"
|
||||
|
||||
#: plinth/modules/bind/manifest.py:16
|
||||
#, fuzzy
|
||||
#| msgid "Enable DNSSEC"
|
||||
msgid "DNS"
|
||||
msgstr "Aktivera DNSSEC"
|
||||
msgstr "DNS"
|
||||
|
||||
#: plinth/modules/bind/manifest.py:17 plinth/modules/mumble/manifest.py:67
|
||||
#: plinth/modules/radicale/manifest.py:91
|
||||
@ -1147,7 +1118,7 @@ msgstr "Server"
|
||||
|
||||
#: plinth/modules/bind/manifest.py:18
|
||||
msgid "Resolver"
|
||||
msgstr ""
|
||||
msgstr "Resolver"
|
||||
|
||||
#: plinth/modules/bind/templates/bind.html:11
|
||||
msgid "Serving Domains"
|
||||
@ -1256,19 +1227,15 @@ msgstr "Ett bibliotek med detta namn finns redan."
|
||||
|
||||
#: plinth/modules/calibre/manifest.py:21
|
||||
msgid "Ebook"
|
||||
msgstr ""
|
||||
msgstr "Ebok"
|
||||
|
||||
#: plinth/modules/calibre/manifest.py:21
|
||||
#, fuzzy
|
||||
#| msgid "E-book Library"
|
||||
msgid "Library"
|
||||
msgstr "E-bok Bibliotek"
|
||||
msgstr "Bibliotek"
|
||||
|
||||
#: plinth/modules/calibre/manifest.py:21
|
||||
#, fuzzy
|
||||
#| msgid "E-book Library"
|
||||
msgid "Ebook reader"
|
||||
msgstr "E-bok Bibliotek"
|
||||
msgstr "E-boksbibliotek"
|
||||
|
||||
#: plinth/modules/calibre/templates/calibre-delete-library.html:11
|
||||
#, python-format
|
||||
@ -1376,16 +1343,12 @@ msgid "Cockpit"
|
||||
msgstr "Cockpit"
|
||||
|
||||
#: plinth/modules/cockpit/manifest.py:23
|
||||
#, fuzzy
|
||||
#| msgid "Administration"
|
||||
msgid "Advanced administration"
|
||||
msgstr "Administrering"
|
||||
msgstr "Avancerad administration"
|
||||
|
||||
#: plinth/modules/cockpit/manifest.py:24
|
||||
#, fuzzy
|
||||
#| msgid "Sharing"
|
||||
msgid "Web terminal"
|
||||
msgstr "Sharing"
|
||||
msgstr "Webbterminal"
|
||||
|
||||
#: plinth/modules/cockpit/manifest.py:25 plinth/modules/storage/__init__.py:47
|
||||
#: plinth/modules/storage/__init__.py:319
|
||||
@ -1395,8 +1358,6 @@ msgid "Storage"
|
||||
msgstr "Lagring"
|
||||
|
||||
#: plinth/modules/cockpit/manifest.py:26
|
||||
#, fuzzy
|
||||
#| msgid "Networks"
|
||||
msgid "Networking"
|
||||
msgstr "Nätverk"
|
||||
|
||||
@ -1407,7 +1368,7 @@ msgstr "Tjänster"
|
||||
|
||||
#: plinth/modules/cockpit/manifest.py:28 plinth/templates/app-logs.html:9
|
||||
msgid "Logs"
|
||||
msgstr ""
|
||||
msgstr "Loggar"
|
||||
|
||||
#: plinth/modules/cockpit/manifest.py:29
|
||||
#: plinth/modules/performance/__init__.py:16
|
||||
@ -1416,16 +1377,12 @@ msgid "Performance"
|
||||
msgstr "Prestanda"
|
||||
|
||||
#: plinth/modules/config/__init__.py:20
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "Here you can set some general configuration options like hostname, domain "
|
||||
#| "name, webserver home page etc."
|
||||
msgid ""
|
||||
"Here you can set some general configuration options like webserver home page "
|
||||
"etc."
|
||||
msgstr ""
|
||||
"Här kan du ställa in några allmänna konfigurationsalternativ som värdnamn, "
|
||||
"domännamn, webserver, hemsida etc."
|
||||
"Här kan du ställa in några allmänna konfigurationsalternativ som webbserver, "
|
||||
"hemsida med mera."
|
||||
|
||||
#: plinth/modules/config/__init__.py:42
|
||||
msgid "General Configuration"
|
||||
@ -1510,13 +1467,11 @@ msgstr "Hemsida"
|
||||
|
||||
#: plinth/modules/config/manifest.py:8
|
||||
msgid "Logging"
|
||||
msgstr ""
|
||||
msgstr "Loggning"
|
||||
|
||||
#: plinth/modules/config/manifest.py:8
|
||||
#, fuzzy
|
||||
#| msgid "Advanced"
|
||||
msgid "Advanced apps"
|
||||
msgstr "Avancerat"
|
||||
msgstr "Avancerade appar"
|
||||
|
||||
#: plinth/modules/config/views.py:41
|
||||
#, python-brace-format
|
||||
@ -1573,15 +1528,15 @@ msgstr "Ogiltig lista för URI:er för STUN/TURN-servrar"
|
||||
|
||||
#: plinth/modules/coturn/manifest.py:7 plinth/modules/janus/manifest.py:16
|
||||
msgid "Video conference"
|
||||
msgstr ""
|
||||
msgstr "Videokonferens"
|
||||
|
||||
#: plinth/modules/coturn/manifest.py:7
|
||||
msgid "STUN"
|
||||
msgstr ""
|
||||
msgstr "STUN"
|
||||
|
||||
#: plinth/modules/coturn/manifest.py:7
|
||||
msgid "TURN"
|
||||
msgstr ""
|
||||
msgstr "TURN"
|
||||
|
||||
#: plinth/modules/coturn/templates/coturn.html:15
|
||||
msgid "Use the following URLs to configure your communication server:"
|
||||
@ -1625,14 +1580,10 @@ msgid "-- no time zone set --"
|
||||
msgstr "-- ingen tidszon inställd --"
|
||||
|
||||
#: plinth/modules/datetime/manifest.py:15
|
||||
#, fuzzy
|
||||
#| msgid "Network Interface"
|
||||
msgid "Network time"
|
||||
msgstr "Nätverksgränssnitt"
|
||||
msgstr "Nätverkstid"
|
||||
|
||||
#: plinth/modules/datetime/manifest.py:15
|
||||
#, fuzzy
|
||||
#| msgid "Time Zone"
|
||||
msgid "Timezone"
|
||||
msgstr "Tidszon"
|
||||
|
||||
@ -1678,24 +1629,18 @@ msgstr "Bittorrent-klient skriven i Python / PyGTK"
|
||||
|
||||
#: plinth/modules/deluge/manifest.py:21
|
||||
#: plinth/modules/transmission/manifest.py:39
|
||||
#, fuzzy
|
||||
#| msgid "Deluge BitTorrent"
|
||||
msgid "BitTorrent"
|
||||
msgstr "Deluge BitTorrent"
|
||||
msgstr "BitTorrent"
|
||||
|
||||
#: plinth/modules/deluge/manifest.py:21 plinth/modules/roundcube/manifest.py:23
|
||||
#: plinth/modules/transmission/manifest.py:39
|
||||
#, fuzzy
|
||||
#| msgid "Launch web client"
|
||||
msgid "Web client"
|
||||
msgstr "Starta webbklient"
|
||||
msgstr "Webklient"
|
||||
|
||||
#: plinth/modules/deluge/manifest.py:21 plinth/modules/syncthing/manifest.py:58
|
||||
#: plinth/modules/transmission/manifest.py:39
|
||||
#, fuzzy
|
||||
#| msgid "I2P"
|
||||
msgid "P2P"
|
||||
msgstr "I2P"
|
||||
msgstr "P2P"
|
||||
|
||||
#: plinth/modules/diagnostics/__init__.py:32
|
||||
msgid ""
|
||||
@ -1711,6 +1656,8 @@ msgid ""
|
||||
"This app also shows the <a href=\"{logs_url}\">logs</a> for {box_name} "
|
||||
"services."
|
||||
msgstr ""
|
||||
"Denna app visar också <a href=\"{logs_url}\">loggar</a> för {box_name} "
|
||||
"tjänster."
|
||||
|
||||
#: plinth/modules/diagnostics/__init__.py:60
|
||||
#: plinth/modules/diagnostics/__init__.py:255
|
||||
@ -1719,7 +1666,7 @@ msgstr "Diagnostik"
|
||||
|
||||
#: plinth/modules/diagnostics/__init__.py:115
|
||||
msgid "skipped"
|
||||
msgstr ""
|
||||
msgstr "Hoppat över"
|
||||
|
||||
#: plinth/modules/diagnostics/__init__.py:116
|
||||
msgid "passed"
|
||||
@ -1806,7 +1753,7 @@ msgstr "Om problem upptäcks, försök att reparera dem automatiskt."
|
||||
|
||||
#: plinth/modules/diagnostics/manifest.py:10
|
||||
msgid "Detect problems"
|
||||
msgstr ""
|
||||
msgstr "Finn problem"
|
||||
|
||||
#: plinth/modules/diagnostics/manifest.py:10
|
||||
msgid "Repair"
|
||||
@ -1814,7 +1761,7 @@ msgstr "Reparera"
|
||||
|
||||
#: plinth/modules/diagnostics/manifest.py:10
|
||||
msgid "Daily"
|
||||
msgstr ""
|
||||
msgstr "Daglig"
|
||||
|
||||
#: plinth/modules/diagnostics/templates/diagnostics.html:11
|
||||
msgid "Diagnostics Run"
|
||||
@ -1865,33 +1812,31 @@ msgstr "Resultat"
|
||||
#: plinth/modules/diagnostics/templates/diagnostics_full.html:53
|
||||
#, python-format
|
||||
msgid "%(number)s passed"
|
||||
msgstr ""
|
||||
msgstr "%(number)s passerade"
|
||||
|
||||
#: plinth/modules/diagnostics/templates/diagnostics_full.html:57
|
||||
#, python-format
|
||||
msgid "%(number)s failed"
|
||||
msgstr ""
|
||||
msgstr "%(number)s misslyckades"
|
||||
|
||||
#: plinth/modules/diagnostics/templates/diagnostics_full.html:61
|
||||
#, python-format
|
||||
msgid "%(number)s warnings"
|
||||
msgstr ""
|
||||
msgstr "%(number)s varningar"
|
||||
|
||||
#: plinth/modules/diagnostics/templates/diagnostics_full.html:65
|
||||
#, python-format
|
||||
msgid "%(number)s errors"
|
||||
msgstr ""
|
||||
msgstr "%(number)s fel"
|
||||
|
||||
#: plinth/modules/diagnostics/templates/diagnostics_full.html:69
|
||||
#, python-format
|
||||
msgid "%(number)s skipped"
|
||||
msgstr ""
|
||||
msgstr "%(number)s skippade"
|
||||
|
||||
#: plinth/modules/diagnostics/templates/diagnostics_full.html:111
|
||||
#, fuzzy
|
||||
#| msgid "deluge-web is running"
|
||||
msgid "Running..."
|
||||
msgstr "Deluges webbgränssnitt är aktiverat"
|
||||
msgstr "Kör..."
|
||||
|
||||
#: plinth/modules/diagnostics/templates/diagnostics_results.html:11
|
||||
msgid "Test"
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""FreedomBox app to manage backup archives."""
|
||||
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils.text import get_valid_filename
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -14,9 +16,10 @@ from django.utils.translation import gettext_noop
|
||||
|
||||
from plinth import app as app_module
|
||||
from plinth import cfg, glib, menu
|
||||
from plinth.modules.names.components import DomainName
|
||||
from plinth.package import Packages
|
||||
|
||||
from . import api, manifest, privileged
|
||||
from . import api, errors, manifest, privileged
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -132,9 +135,77 @@ def restore_from_upload(path, app_ids=None):
|
||||
create_subvolume=False, backup_file=path)
|
||||
|
||||
|
||||
def get_known_hosts_path():
|
||||
def get_known_hosts_path() -> Path:
|
||||
"""Return the path to the known hosts file."""
|
||||
return pathlib.Path(cfg.data_dir) / '.ssh' / 'known_hosts'
|
||||
return Path(cfg.data_dir) / '.ssh' / 'known_hosts'
|
||||
|
||||
|
||||
def get_ssh_client_auth_key_paths() -> tuple[Path, Path]:
|
||||
"""Return the paths to the SSH client public key and private key."""
|
||||
key_path = Path(cfg.data_dir) / '.ssh' / 'id_ed25519'
|
||||
pubkey_path = key_path.with_suffix('.pub')
|
||||
return pubkey_path, key_path
|
||||
|
||||
|
||||
def generate_ssh_client_auth_key():
|
||||
"""Generate SSH client authentication keypair, if needed."""
|
||||
_, key_path = get_ssh_client_auth_key_paths()
|
||||
key_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if key_path.exists():
|
||||
logger.info('SSH client key %s for FreedomBox service already exists',
|
||||
key_path)
|
||||
return
|
||||
|
||||
domain_name = DomainName.list_names()[0]
|
||||
logger.info('Generating SSH client key %s for FreedomBox service',
|
||||
key_path)
|
||||
subprocess.run([
|
||||
'ssh-keygen', '-t', 'ed25519', '-N', '', '-C',
|
||||
f'freedombox@{domain_name}', '-f',
|
||||
str(key_path)
|
||||
], stdout=subprocess.DEVNULL, check=True)
|
||||
|
||||
|
||||
def get_ssh_client_public_key() -> str:
|
||||
"""Get SSH client public key for FreedomBox service."""
|
||||
pubkey_path, _ = get_ssh_client_auth_key_paths()
|
||||
with pubkey_path.open('r') as pubkey_file:
|
||||
pubkey = pubkey_file.read()
|
||||
|
||||
return pubkey
|
||||
|
||||
|
||||
def copy_ssh_client_public_key(pubkey_path: str, hostname: str, username: str,
|
||||
password: str):
|
||||
"""Copy the SSH client public key to the remote server.
|
||||
|
||||
Returns whether the copy was successful, and any error message.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env['SSHPASS'] = str(password)
|
||||
with raise_ssh_error():
|
||||
try:
|
||||
subprocess.run([
|
||||
'sshpass', '-e', 'ssh-copy-id', '-i', pubkey_path,
|
||||
f'{username}@{hostname}'
|
||||
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True,
|
||||
env=env)
|
||||
logger.info("Copied SSH client public key to remote host's "
|
||||
"authorized keys.")
|
||||
except subprocess.CalledProcessError as exception:
|
||||
logger.warning('Failed to copy SSH client public key: %s',
|
||||
exception.stderr)
|
||||
raise
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def raise_ssh_error() -> collections.abc.Generator[None]:
|
||||
"""Convert subprocess error to SshError."""
|
||||
try:
|
||||
yield
|
||||
except subprocess.CalledProcessError as exception:
|
||||
raise errors.SshError(exception.returncode, exception.cmd,
|
||||
exception.output, exception.stderr)
|
||||
|
||||
|
||||
def is_ssh_hostkey_verified(hostname):
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import subprocess
|
||||
|
||||
from plinth.errors import PlinthError
|
||||
|
||||
|
||||
class BorgError(PlinthError):
|
||||
"""Generic borg errors"""
|
||||
"""Generic borg errors."""
|
||||
|
||||
|
||||
class BorgRepositoryDoesNotExistError(BorgError):
|
||||
"""Borg access to a repository works but the repository does not exist"""
|
||||
"""Borg access to a repository works but the repository does not exist."""
|
||||
|
||||
|
||||
class SshError(subprocess.CalledProcessError):
|
||||
"""Error when running an SSH command."""
|
||||
|
||||
|
||||
class SshfsError(PlinthError):
|
||||
"""Generic sshfs errors"""
|
||||
"""Generic sshfs errors."""
|
||||
|
||||
|
||||
class BorgRepositoryExists(BorgError):
|
||||
|
||||
@ -184,7 +184,7 @@ class EncryptedBackupsMixin(forms.Form):
|
||||
choices=[('repokey', _('Key in Repository')), ('none', _('None'))])
|
||||
encryption_passphrase = forms.CharField(
|
||||
label=_('Passphrase'),
|
||||
help_text=_('Passphrase; Only needed when using encryption.'),
|
||||
help_text=_('Only needed when using encryption.'),
|
||||
widget=forms.PasswordInput(), required=False)
|
||||
confirm_encryption_passphrase = forms.CharField(
|
||||
label=_('Confirm Passphrase'), help_text=_('Repeat the passphrase.'),
|
||||
@ -251,13 +251,30 @@ class AddRemoteRepositoryForm(EncryptedBackupsMixin, forms.Form):
|
||||
help_text=_('Path of a new or existing repository. Example: '
|
||||
'<i>user@host:~/path/to/repo/</i>'),
|
||||
validators=[repository_validator])
|
||||
ssh_auth_type = forms.ChoiceField(
|
||||
label=_('SSH Authentication Type'),
|
||||
help_text=_('Choose how to authenticate to the remote SSH server.'),
|
||||
widget=forms.RadioSelect(),
|
||||
choices=[('key_auth', _('Key-based Authentication')),
|
||||
('password_auth', _('Password-based Authentication'))])
|
||||
ssh_password = forms.CharField(
|
||||
label=_('SSH server password'), strip=True,
|
||||
help_text=_('Password of the SSH Server.<br />'
|
||||
'SSH key-based authentication is not yet possible.'),
|
||||
widget=forms.PasswordInput(), required=False)
|
||||
label=_('SSH server password'), widget=forms.PasswordInput(),
|
||||
strip=True, help_text=_('Required for password-based authentication.'),
|
||||
required=False)
|
||||
|
||||
field_order = ['repository', 'ssh_password'] + encryption_fields
|
||||
field_order = ['repository', 'ssh_auth_type', 'ssh_password'
|
||||
] + encryption_fields
|
||||
|
||||
def clean(self):
|
||||
"""Perform additional checks on form data."""
|
||||
super().clean()
|
||||
ssh_password = self.cleaned_data.get('ssh_password')
|
||||
if self.cleaned_data.get(
|
||||
'ssh_auth_type') == 'password_auth' and not ssh_password:
|
||||
raise forms.ValidationError(
|
||||
_('SSH password is needed for password-based authentication.'))
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def clean_repository(self):
|
||||
"""Validate repository form field."""
|
||||
|
||||
@ -49,7 +49,7 @@ KNOWN_ERRORS = [
|
||||
'raise_as': errors.BorgRepositoryDoesNotExistError,
|
||||
},
|
||||
{
|
||||
'errors': ['passphrase supplied in .* is incorrect'],
|
||||
'errors': ['[Pp]assphrase supplied in .* is incorrect'],
|
||||
'message': _('Incorrect encryption passphrase'),
|
||||
'raise_as': errors.BorgError,
|
||||
},
|
||||
@ -131,8 +131,7 @@ def _reraise_known_errors(err):
|
||||
|
||||
@reraise_known_errors
|
||||
@privileged
|
||||
def mount(mountpoint: str, remote_path: str, ssh_keyfile: str | None = None,
|
||||
password: secret_str | None = None,
|
||||
def mount(mountpoint: str, remote_path: str, ssh_keyfile: str,
|
||||
user_known_hosts_file: str = '/dev/null'):
|
||||
"""Mount a remote ssh path via sshfs."""
|
||||
try:
|
||||
@ -156,16 +155,9 @@ def mount(mountpoint: str, remote_path: str, ssh_keyfile: str | None = None,
|
||||
'sshfs', remote_path, mountpoint, '-o',
|
||||
f'UserKnownHostsFile={user_known_hosts_file}', '-o',
|
||||
'StrictHostKeyChecking=yes', '-o', 'reconnect', '-o',
|
||||
'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3'
|
||||
'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3', '-o',
|
||||
'IdentityFile=' + ssh_keyfile
|
||||
]
|
||||
if ssh_keyfile:
|
||||
cmd += ['-o', 'IdentityFile=' + ssh_keyfile]
|
||||
else:
|
||||
if not password:
|
||||
raise ValueError('mount requires either a password or ssh_keyfile')
|
||||
cmd += ['-o', 'password_stdin']
|
||||
input_ = password.encode()
|
||||
|
||||
action_utils.run(cmd, check=True, timeout=TIMEOUT, input=input_)
|
||||
|
||||
|
||||
|
||||
@ -13,7 +13,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
from plinth import cfg
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import (_backup_handler, api, errors, get_known_hosts_path, privileged,
|
||||
from . import (_backup_handler, api, copy_ssh_client_public_key, errors,
|
||||
generate_ssh_client_auth_key, get_known_hosts_path,
|
||||
get_ssh_client_auth_key_paths, privileged, raise_ssh_error,
|
||||
restore_archive_handler, split_path, store)
|
||||
from .schedule import Schedule
|
||||
|
||||
@ -143,6 +145,9 @@ class BaseBorgRepository(abc.ABC):
|
||||
privileged.delete_archive(archive_path,
|
||||
self._get_encryption_passpharse())
|
||||
|
||||
def migrate_credentials(self) -> None:
|
||||
"""Migrate any credentials."""
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize / create a borg repository."""
|
||||
encryption = 'none'
|
||||
@ -315,11 +320,27 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
self._umount_ignore_errors()
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
def hostname(self) -> str:
|
||||
"""Return hostname from the remote path."""
|
||||
_, hostname, _ = split_path(self._path)
|
||||
return hostname.split('%')[0] # XXX: Likely incorrect to split
|
||||
|
||||
@property
|
||||
def username(self) -> str:
|
||||
"""Return username from the remote path."""
|
||||
username, _, _ = split_path(self._path)
|
||||
return username
|
||||
|
||||
@property
|
||||
def ssh_password(self) -> str | None:
|
||||
"""Return SSH password if it is stored, otherwise None."""
|
||||
return self.credentials.get('ssh_password')
|
||||
|
||||
@property
|
||||
def ssh_keyfile(self) -> str | None:
|
||||
"""Return path to SSH client key if stored, otherwise None."""
|
||||
return self.credentials.get('ssh_keyfile')
|
||||
|
||||
@property
|
||||
def _mountpoint(self):
|
||||
"""Return the local mount point where repository is to be mounted."""
|
||||
@ -330,8 +351,22 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
"""Return whether remote path is mounted locally."""
|
||||
return privileged.is_mounted(self._mountpoint)
|
||||
|
||||
def migrate_credentials(self) -> None:
|
||||
"""Add SSH keyfile credential and delete stored password."""
|
||||
if not self.ssh_password:
|
||||
return
|
||||
|
||||
pubkey_path, keyfile_path = get_ssh_client_auth_key_paths()
|
||||
generate_ssh_client_auth_key()
|
||||
copy_ssh_client_public_key(str(pubkey_path), self.hostname,
|
||||
self.username, self.ssh_password)
|
||||
self.credentials['ssh_keyfile'] = str(keyfile_path)
|
||||
self.credentials.pop('ssh_password', None)
|
||||
self.save()
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the repository after mounting the target directory."""
|
||||
self.migrate_credentials()
|
||||
self._ensure_remote_directory()
|
||||
self.mount()
|
||||
super().initialize()
|
||||
@ -341,17 +376,11 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
if self.is_mounted:
|
||||
return
|
||||
|
||||
self.migrate_credentials()
|
||||
known_hosts_path = get_known_hosts_path()
|
||||
kwargs = {'user_known_hosts_file': str(known_hosts_path)}
|
||||
if 'ssh_password' in self.credentials and self.credentials[
|
||||
'ssh_password']:
|
||||
kwargs['password'] = self.credentials['ssh_password']
|
||||
|
||||
if 'ssh_keyfile' in self.credentials and self.credentials[
|
||||
'ssh_keyfile']:
|
||||
kwargs['ssh_keyfile'] = self.credentials['ssh_keyfile']
|
||||
|
||||
privileged.mount(self._mountpoint, self._path, **kwargs)
|
||||
privileged.mount(self._mountpoint, self._path,
|
||||
ssh_keyfile=self.credentials['ssh_keyfile'],
|
||||
user_known_hosts_file=str(known_hosts_path))
|
||||
|
||||
def umount(self):
|
||||
"""Unmount the remote path that was mounted locally using sshfs."""
|
||||
@ -391,16 +420,16 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
if dir_path[0] == '~':
|
||||
dir_path = '.' + dir_path[1:]
|
||||
|
||||
password = self.credentials['ssh_password']
|
||||
|
||||
# Ensure remote directory exists, check contents
|
||||
env = {'SSHPASS': password}
|
||||
known_hosts_path = str(get_known_hosts_path())
|
||||
subprocess.run([
|
||||
'sshpass', '-e', 'ssh', '-o',
|
||||
f'UserKnownHostsFile={known_hosts_path}', f'{username}@{hostname}',
|
||||
'mkdir', '-p', dir_path
|
||||
], check=True, env=env)
|
||||
with raise_ssh_error():
|
||||
subprocess.run([
|
||||
'ssh', '-i',
|
||||
str(self.ssh_keyfile), '-o',
|
||||
f'UserKnownHostsFile={known_hosts_path}', '-o',
|
||||
'BatchMode=yes', f'{username}@{hostname}', 'mkdir', '-p',
|
||||
dir_path
|
||||
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
|
||||
|
||||
def get_repositories():
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
/**
|
||||
* @licstart The following is the entire license notice for the JavaScript
|
||||
* code in this page.
|
||||
*
|
||||
* This file is part of FreedomBox.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @licend The above is the entire license notice for the JavaScript code
|
||||
* in this page.
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const keyAuth = document.getElementById('id_ssh_auth_type_0');
|
||||
const passwordAuth = document.getElementById('id_ssh_auth_type_1');
|
||||
const sshPasswordField = document.getElementById('id_ssh_password');
|
||||
const encryptionType = document.getElementById('id_encryption');
|
||||
const encryptionPassphraseField = document.getElementById('id_encryption_passphrase');
|
||||
const encryptionConfirmPassphraseField = document.getElementById('id_confirm_encryption_passphrase');
|
||||
|
||||
function handleAuthTypeChange() {
|
||||
if (passwordAuth.checked) {
|
||||
sshPasswordField.parentElement.parentElement.style.display = 'block';
|
||||
} else {
|
||||
sshPasswordField.parentElement.parentElement.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function handleEncryptionTypeChange() {
|
||||
let display = 'none';
|
||||
if (encryptionType.value === "repokey") {
|
||||
display = 'block';
|
||||
}
|
||||
|
||||
encryptionPassphraseField.parentElement.parentElement.style.display = display;
|
||||
encryptionConfirmPassphraseField.parentElement.parentElement.style.display = display;
|
||||
}
|
||||
|
||||
keyAuth.addEventListener('change', handleAuthTypeChange);
|
||||
passwordAuth.addEventListener('change', handleAuthTypeChange);
|
||||
encryptionType.addEventListener('change', handleEncryptionTypeChange);
|
||||
|
||||
handleAuthTypeChange();
|
||||
handleEncryptionTypeChange();
|
||||
});
|
||||
@ -5,15 +5,125 @@
|
||||
|
||||
{% load bootstrap %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block page_js %}
|
||||
<script type="text/javascript" src="{% static 'backups/backups_add_remote_repository.js' %}"
|
||||
defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{{ title }}</h3>
|
||||
|
||||
<form class="form" method="post">
|
||||
<form class="form form-add-remote-repository" method="post">
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
<a class="close" data-dismiss="alert">×</a>
|
||||
{% for non_field_error in form.non_field_errors %}
|
||||
{{ non_field_error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% csrf_token %}
|
||||
|
||||
{{ form|bootstrap }}
|
||||
<div class="form-group{% if form.repository.errors %}
|
||||
has-error{% endif %}">
|
||||
<label class="control-label fs-4 fw-medium" for="id_repository">
|
||||
{{ form.repository.label }}
|
||||
</label>
|
||||
<div>
|
||||
<input type="text" name="repository" class="form-control"
|
||||
required id="id_repository">
|
||||
{% for error in form.repository.errors %}
|
||||
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
<p class="help-block">
|
||||
{{ form.repository.help_text|safe }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group{% if form.ssh_auth_type.errors %}
|
||||
has-error{% endif %}">
|
||||
<label class="control-label fs-4 fw-medium">
|
||||
{{ form.ssh_auth_type.label }}
|
||||
</label>
|
||||
<p class="help-block">
|
||||
{{ form.ssh_auth_type.help_text|safe }}
|
||||
</p>
|
||||
{% for error in form.ssh_auth_type.errors %}
|
||||
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
{{ form.ssh_auth_type.0.tag }}
|
||||
{{ form.ssh_auth_type.0.choice_label }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
The following SSH client public key must be added to the
|
||||
authorized keys list on the remote machine for {{ box_name }} to
|
||||
be able to connect to the remote machine:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<pre>{{ ssh_client_public_key }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
{{ form.ssh_auth_type.1.tag }}
|
||||
{{ form.ssh_auth_type.1.choice_label }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
{{ box_name }} service will attempt to connect using the provided
|
||||
password. If successful, then the public key will be automatically
|
||||
added to the authorized keys list, so that future connections do
|
||||
not need the password.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
{% with _=form.ssh_password|add_input_classes %}
|
||||
{% include "bootstrapform/field.html" with field=form.ssh_password %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group{% if form.encryption.errors %}
|
||||
has-error{% endif %}">
|
||||
<label class="control-label fs-4 fw-medium" for="id_encryption">
|
||||
{{ form.encryption.label }}
|
||||
</label>
|
||||
<div>
|
||||
<select name="encryption" class="form-control" id="id_encryption">
|
||||
<option value="repokey">{{ form.encryption.0.choice_label }}</option>
|
||||
<option value="none">{{ form.encryption.1.choice_label }}</option>
|
||||
</select>
|
||||
{% for error in form.encryption.errors %}
|
||||
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
<p class="help-block">
|
||||
{{ form.encryption.help_text|safe }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with _=form.encryption_passphrase|add_input_classes %}
|
||||
{% include "bootstrapform/field.html" with field=form.encryption_passphrase %}
|
||||
{% endwith %}
|
||||
|
||||
{% with _=form.confirm_encryption_passphrase|add_input_classes %}
|
||||
{% include "bootstrapform/field.html" with field=form.confirm_encryption_passphrase %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning d-flex align-items-center" role="alert">
|
||||
<div class="me-2">
|
||||
@ -24,8 +134,8 @@
|
||||
{% blocktrans trimmed %}
|
||||
The credentials for this repository are stored on your {{ box_name }}.
|
||||
<br />
|
||||
To restore a backup on a new {{ box_name }} you need the SSH credentials
|
||||
and, if chosen, the encryption passphrase.
|
||||
To restore a backup on a new {{ box_name }} you need the SSH
|
||||
credentials and, if chosen, the encryption passphrase.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table" id="archives-list">
|
||||
<div class="table-responsive repository" data-repository-name="{{ repository.name }}">
|
||||
<table class="table" class="archives-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">
|
||||
@ -23,7 +23,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{{ repository.name }}
|
||||
<span class="repository-name">{{ repository.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-end">
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<form class="form" method="post">
|
||||
<form class="form form-remove-location" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<input type="submit" class="btn btn-danger"
|
||||
@ -33,4 +33,3 @@
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
<h3>{{ title }}</h3>
|
||||
|
||||
<form class="form" method="post">
|
||||
<form class="form form-verify-ssh-hostkey" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.ssh_public_key|length == 0 %}
|
||||
|
||||
@ -15,6 +15,8 @@ from plinth.tests import functional
|
||||
|
||||
pytestmark = [pytest.mark.system, pytest.mark.backups]
|
||||
|
||||
REMOTE_PATH = 'tester@localhost:~/backups'
|
||||
|
||||
|
||||
@pytest.fixture(scope='module', autouse=True)
|
||||
def fixture_background(session_browser):
|
||||
@ -66,6 +68,22 @@ def test_set_schedule(session_browser):
|
||||
monthly=30, run_at=15, without_app='firewall')
|
||||
|
||||
|
||||
def test_remote_backup_location(session_browser):
|
||||
"""Test remote backup location operations."""
|
||||
_add_remote_backup_location(session_browser)
|
||||
assert _has_remote_backup_location(session_browser)
|
||||
|
||||
_remove_remote_backup_location(session_browser)
|
||||
assert not _has_remote_backup_location(session_browser)
|
||||
|
||||
# Add it again without providing SSH password.
|
||||
_add_remote_backup_location(session_browser, False)
|
||||
assert _has_remote_backup_location(session_browser)
|
||||
|
||||
_remove_remote_backup_location(session_browser)
|
||||
assert not _has_remote_backup_location(session_browser)
|
||||
|
||||
|
||||
def _assert_main_page_is_shown(session_browser):
|
||||
assert (session_browser.url.endswith('/plinth/'))
|
||||
|
||||
@ -172,6 +190,7 @@ def _download_file_logged_in(browser, url, suffix=''):
|
||||
|
||||
|
||||
def _download(browser, archive_name=None):
|
||||
"""Download a backup archive to a temporary file on disk."""
|
||||
functional.nav_to_module(browser, 'backups')
|
||||
href = f'/plinth/sys/backups/root/download/{archive_name}/'
|
||||
url = functional.base_url + href
|
||||
@ -180,11 +199,13 @@ def _download(browser, archive_name=None):
|
||||
|
||||
|
||||
def _open_main_page(browser):
|
||||
"""Open the FreedomBox interface main page."""
|
||||
with functional.wait_for_page_update(browser):
|
||||
browser.links.find_by_href('/plinth/').first.click()
|
||||
|
||||
|
||||
def _upload_and_restore(browser, app_name, downloaded_file_path):
|
||||
"""Upload a backup archive from the disk and perform restore operation."""
|
||||
functional.nav_to_module(browser, 'backups')
|
||||
with functional.wait_for_page_update(browser):
|
||||
browser.links.find_by_href('/plinth/sys/backups/upload/').first.click()
|
||||
@ -197,3 +218,52 @@ def _upload_and_restore(browser, app_name, downloaded_file_path):
|
||||
with functional.wait_for_page_update(browser,
|
||||
expected_url='/plinth/sys/backups/'):
|
||||
functional.submit(browser, form_class='form-restore')
|
||||
|
||||
|
||||
def _has_remote_backup_location(browser) -> bool:
|
||||
"""Return whether atleast one remote backup location is configured."""
|
||||
functional.nav_to_module(browser, 'backups')
|
||||
return browser.is_element_present_by_css(
|
||||
f'.repository[data-repository-name="{REMOTE_PATH}"]')
|
||||
|
||||
|
||||
def _add_remote_backup_location(browser, ssh_use_password=True):
|
||||
"""Add a remote backup location."""
|
||||
if _has_remote_backup_location(browser):
|
||||
_remove_remote_backup_location(browser)
|
||||
|
||||
browser.links.find_by_href(
|
||||
'/plinth/sys/backups/repositories/add-remote/').first.click()
|
||||
browser.find_by_name('repository').fill(REMOTE_PATH)
|
||||
password = functional.get_password(
|
||||
functional.config['DEFAULT']['username'])
|
||||
if ssh_use_password:
|
||||
browser.find_by_id('id_ssh_auth_type_1').check()
|
||||
browser.find_by_name('ssh_password').fill(password)
|
||||
else:
|
||||
browser.find_by_id('id_ssh_auth_type_0').check()
|
||||
|
||||
browser.choose('id_encryption', 'repokey')
|
||||
browser.find_by_name('encryption_passphrase').fill(password)
|
||||
browser.find_by_name('confirm_encryption_passphrase').fill(password)
|
||||
functional.submit(browser, form_class='form-add-remote-repository')
|
||||
|
||||
assert browser.is_text_present('Added new remote SSH repository.')
|
||||
|
||||
if 'ssh-verify' in browser.url:
|
||||
_verify_host_key(browser)
|
||||
|
||||
|
||||
def _remove_remote_backup_location(browser):
|
||||
"""Remove the remote backup location with known remote path."""
|
||||
repository = browser.find_by_css(
|
||||
f'.repository[data-repository-name="{REMOTE_PATH}"]').first
|
||||
repository.find_by_css('.repository-remove').first.click()
|
||||
functional.submit(browser, form_class='form-remove-location')
|
||||
|
||||
|
||||
def _verify_host_key(browser):
|
||||
"""Verify the remote location's SSH host key."""
|
||||
browser.find_by_name('ssh_public_key').first.click()
|
||||
functional.submit(browser, form_class='form-verify-ssh-hostkey')
|
||||
assert browser.is_text_present('SSH host verified.')
|
||||
|
||||
@ -6,7 +6,6 @@ Views for the backups app.
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from urllib.parse import unquote
|
||||
|
||||
from django.contrib import messages
|
||||
@ -24,7 +23,9 @@ from plinth.errors import PlinthError
|
||||
from plinth.modules import backups, storage
|
||||
from plinth.views import AppView
|
||||
|
||||
from . import (SESSION_PATH_VARIABLE, api, errors, forms, get_known_hosts_path,
|
||||
from . import (SESSION_PATH_VARIABLE, api, errors, forms,
|
||||
generate_ssh_client_auth_key, get_known_hosts_path,
|
||||
get_ssh_client_auth_key_paths, get_ssh_client_public_key,
|
||||
is_ssh_hostkey_verified, privileged)
|
||||
from .decorators import delete_tmp_backup_file
|
||||
from .repository import (BorgRepository, SshBorgRepository, get_instance,
|
||||
@ -38,6 +39,19 @@ def handle_common_errors(request: HttpRequest):
|
||||
"""If any known Borg exceptions occur, show proper error messages."""
|
||||
try:
|
||||
yield
|
||||
except errors.SshError as exception:
|
||||
if exception.returncode in (6, 7):
|
||||
message = _('SSH host public key could not be verified.')
|
||||
elif (exception.returncode == 5
|
||||
or 'Permission denied' in exception.stderr.decode()):
|
||||
message = _('Authentication to remote server failed.')
|
||||
else:
|
||||
message = _(
|
||||
'Error establishing connection to server: {} {} {}').format(
|
||||
str(exception), exception.stdout.decode(),
|
||||
exception.stderr.decode())
|
||||
|
||||
messages.error(request, message)
|
||||
except errors.BorgError as exception:
|
||||
messages.error(request, exception.args[0])
|
||||
|
||||
@ -344,11 +358,11 @@ class AddRepositoryView(FormView):
|
||||
encryption_passphrase = None
|
||||
|
||||
credentials = {'encryption_passphrase': encryption_passphrase}
|
||||
repository = BorgRepository(path, credentials)
|
||||
with handle_common_errors(self.request):
|
||||
repository = BorgRepository(path, credentials)
|
||||
if _save_repository(self.request, repository):
|
||||
messages.success(self.request, _('Added new repository.'))
|
||||
return super().form_valid(form)
|
||||
_save_repository(self.request, repository)
|
||||
messages.success(self.request, _('Added new repository.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
return redirect(reverse_lazy('backups:add-repository'))
|
||||
|
||||
@ -358,10 +372,19 @@ class AddRemoteRepositoryView(FormView):
|
||||
form_class = forms.AddRemoteRepositoryForm
|
||||
template_name = 'backups_add_remote_repository.html'
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""Handle GET requests.
|
||||
|
||||
Generate SSH client authentication key if necessary.
|
||||
"""
|
||||
generate_ssh_client_auth_key()
|
||||
return super().get(*args, kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Return additional context for rendering the template."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _('Create remote backup repository')
|
||||
context['ssh_client_public_key'] = get_ssh_client_public_key()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -374,10 +397,13 @@ class AddRemoteRepositoryView(FormView):
|
||||
if form.cleaned_data.get('encryption') == 'none':
|
||||
encryption_passphrase = None
|
||||
|
||||
credentials = {
|
||||
'ssh_password': form.cleaned_data.get('ssh_password'),
|
||||
'encryption_passphrase': encryption_passphrase
|
||||
}
|
||||
credentials = {'encryption_passphrase': encryption_passphrase}
|
||||
if form.cleaned_data.get('ssh_auth_type') == 'password_auth':
|
||||
credentials['ssh_password'] = form.cleaned_data.get('ssh_password')
|
||||
else:
|
||||
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
|
||||
credentials['ssh_keyfile'] = str(key_path)
|
||||
|
||||
with handle_common_errors(self.request):
|
||||
repository = SshBorgRepository(path, credentials)
|
||||
repository.verfied = False
|
||||
@ -426,27 +452,28 @@ class VerifySshHostkeyView(FormView):
|
||||
with known_hosts_path.open('a', encoding='utf-8') as known_hosts_file:
|
||||
known_hosts_file.write(ssh_public_key + '\n')
|
||||
|
||||
def _save_repository_and_redirect(self):
|
||||
"""Save the repository and redirect according to the result."""
|
||||
with handle_common_errors(self.request):
|
||||
_save_repository(self.request, self._get_repository())
|
||||
return redirect(reverse_lazy('backups:index'))
|
||||
|
||||
return redirect(reverse_lazy('backups:add-remote-repository'))
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""Skip this view if host is already verified."""
|
||||
if not is_ssh_hostkey_verified(self._get_repository().hostname):
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
messages.success(self.request, _('SSH host already verified.'))
|
||||
if _save_repository(self.request, self._get_repository()):
|
||||
return redirect(reverse_lazy('backups:index'))
|
||||
|
||||
return redirect(reverse_lazy('backups:add-remote-repository'))
|
||||
return self._save_repository_and_redirect()
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Create and store the repository."""
|
||||
ssh_public_key = form.cleaned_data['ssh_public_key']
|
||||
with handle_common_errors(self.request):
|
||||
self._add_ssh_hostkey(ssh_public_key)
|
||||
messages.success(self.request, _('SSH host verified.'))
|
||||
if _save_repository(self.request, self._get_repository()):
|
||||
return redirect(reverse_lazy('backups:index'))
|
||||
|
||||
return redirect(reverse_lazy('backups:add-remote-repository'))
|
||||
self._add_ssh_hostkey(ssh_public_key)
|
||||
messages.success(self.request, _('SSH host verified.'))
|
||||
return self._save_repository_and_redirect()
|
||||
|
||||
|
||||
def _save_repository(request, repository):
|
||||
@ -455,29 +482,16 @@ def _save_repository(request, repository):
|
||||
repository.initialize()
|
||||
repository.verified = True
|
||||
repository.save()
|
||||
return True
|
||||
except subprocess.CalledProcessError as exception:
|
||||
if exception.returncode in (6, 7):
|
||||
message = _('SSH host public key could not be verified.')
|
||||
elif exception.returncode == 5:
|
||||
message = _('Authentication to remote server failed.')
|
||||
else:
|
||||
message = _('Error establishing connection to server: {}').format(
|
||||
str(exception))
|
||||
except Exception as exception:
|
||||
message = str(exception)
|
||||
logger.exception('Error adding repository: %s', exception)
|
||||
except Exception:
|
||||
# Remove the repository so that the user can have another go at
|
||||
# creating it.
|
||||
try:
|
||||
repository.remove()
|
||||
messages.error(request, _('Repository removed.'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
messages.error(request, message)
|
||||
# Remove the repository so that the user can have another go at
|
||||
# creating it.
|
||||
try:
|
||||
repository.remove()
|
||||
messages.error(request, _('Repository removed.'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return False
|
||||
raise
|
||||
|
||||
|
||||
class RemoveRepositoryView(TemplateView):
|
||||
@ -522,7 +536,8 @@ def mount_repository(request, uuid):
|
||||
|
||||
repository = SshBorgRepository.load(uuid)
|
||||
try:
|
||||
repository.mount()
|
||||
with handle_common_errors(request):
|
||||
repository.mount()
|
||||
except Exception as err:
|
||||
msg = "%s: %s" % (_('Mounting failed'), str(err))
|
||||
messages.error(request, msg)
|
||||
|
||||
@ -102,6 +102,7 @@ class ShowClientView(SuccessMessageMixin, TemplateView):
|
||||
context['endpoints'] = [
|
||||
domain + ':' + str(server_info['listen_port'])
|
||||
for domain in domains
|
||||
if not domain.endswith('.local')
|
||||
]
|
||||
return context
|
||||
|
||||
|
||||
@ -260,7 +260,6 @@ def _shutdown_server() -> None:
|
||||
This must be run in a thread separate from the server.serve_forever()
|
||||
otherwise it will deadlock waiting for the shutdown to complete.
|
||||
"""
|
||||
global _server
|
||||
logger.info('SIGTERM received, shutting down the server.')
|
||||
if _server:
|
||||
_server.shutdown()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user