diff --git a/actions/backups b/actions/backups
index cb539e7c0..a4d612948 100755
--- a/actions/backups
+++ b/actions/backups
@@ -27,9 +27,10 @@ import subprocess
import sys
import tarfile
-from plinth.errors import ActionError
from plinth.modules.backups import MANIFESTS_FOLDER, REPOSITORY
+TIMEOUT = 5
+
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
@@ -38,7 +39,8 @@ def parse_arguments():
subparsers.add_parser(
'setup', help='Create repository if it does not already exist')
- subparsers.add_parser('info', help='Show repository information')
+ info = subparsers.add_parser('info', help='Show repository information')
+ info.add_argument('--repository', help='Repository path', required=True)
subparsers.add_parser('list', help='List repository contents')
create = subparsers.add_parser('create', help='Create archive')
@@ -94,9 +96,9 @@ def subcommand_setup(_):
subprocess.run(['borg', 'init', '--encryption', 'none', REPOSITORY])
-def subcommand_info(_):
+def subcommand_info(arguments):
"""Show repository information."""
- subprocess.run(['borg', 'info', '--json', REPOSITORY], check=True)
+ run(['borg', 'info', '--json', arguments.repository])
def subcommand_list(_):
@@ -189,7 +191,7 @@ def _get_apps_of_manifest(manifest):
elif type(manifest) is dict and 'apps' in manifest:
apps = manifest['apps']
else:
- raise ActionError('Unknown manifest format')
+ raise RuntimeError('Unknown manifest format')
return apps
@@ -236,6 +238,29 @@ def subcommand_restore_exported_archive(arguments):
break
+def read_password():
+ """Read the password from stdin."""
+ if sys.stdin.isatty():
+ return ''
+ else:
+ return ''.join(sys.stdin)
+
+
+def run(cmd):
+ """Pass provided passwords on to borg"""
+ env = dict(os.environ, BORG_RELOCATED_REPO_ACCESS_IS_OK='yes')
+ password = read_password()
+ timeout = None
+ if password:
+ env['SSHPASS'] = password
+ env['BORG_RSH'] = 'sshpass -e ssh -o StrictHostKeyChecking=no'
+ else:
+ # When no password is given ssh might ask for a password and get stuck.
+ # Use timeout to abort early.
+ timeout = TIMEOUT
+ subprocess.run(cmd, check=True, env=env, timeout=timeout)
+
+
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
diff --git a/plinth/modules/backups/__init__.py b/plinth/modules/backups/__init__.py
index 5f5095bc2..d7f3b0a7a 100644
--- a/plinth/modules/backups/__init__.py
+++ b/plinth/modules/backups/__init__.py
@@ -59,8 +59,12 @@ def setup(helper, old_version=None):
helper.call('post', actions.superuser_run, 'backups', ['setup'])
-def get_info():
- output = actions.superuser_run('backups', ['info'])
+def get_info(repository, password=None):
+ args = ['backups', ['info', '--repository', repository]]
+ kwargs = {}
+ if password is not None:
+ kwargs['input'] = password.encode()
+ output = actions.superuser_run(*args, **kwargs)
return json.loads(output)
diff --git a/plinth/modules/backups/forms.py b/plinth/modules/backups/forms.py
index 6d61aa85e..0a42ff07b 100644
--- a/plinth/modules/backups/forms.py
+++ b/plinth/modules/backups/forms.py
@@ -76,38 +76,52 @@ class RestoreForm(forms.Form):
class UploadForm(forms.Form):
- file = forms.FileField(label=_('Upload File'), required=True,
- validators=[FileExtensionValidator(['gz'],
- 'Backup files have to be in .tar.gz format')],
- help_text=_('Select the backup file you want to upload'))
+ file = forms.FileField(
+ label=_('Upload File'),
+ required=True,
+ validators=[FileExtensionValidator(['gz'],
+ 'Backup files have to be in .tar.gz format')],
+ help_text=_('Select the backup file you want to upload'))
class CreateRepositoryForm(forms.Form):
repository = forms.CharField(
- label=_('Repository path'), strip=True,
- help_text=_('Path of the new repository.'))
+ label=_('SSH Repository Path'), strip=True,
+ help_text=_('Path of the new repository. Example: '
+ 'user@host/path/to/repo/'))
+ ssh_password = forms.CharField(
+ label=_('SSH password'), strip=True,
+ help_text=_('Password of the SSH Server.
'
+ 'If you have set up SSH key-based authentication you can '
+ 'omit the password.'),
+ required=False)
encryption = forms.ChoiceField(
label=_('Encryption'),
- help_text=format_lazy(_('"Key in Repository" means that a '
- 'password-protected key is stored with the backup.
'
- 'You need this password to restore a backup!')),
+ help_text=format_lazy(
+ _('"Key in Repository" means that a '
+ 'password-protected key is stored with the backup.
'
+ 'You need this password to restore a backup!')),
choices=[('repokey', 'Key in Repository'), ('none', 'None')]
)
passphrase = forms.CharField(
label=_('Passphrase'),
help_text=_('Passphrase; Only needed when using encryption.'),
- widget=forms.PasswordInput()
+ widget=forms.PasswordInput(),
+ required=False
)
confirm_passphrase = forms.CharField(
label=_('Confirm Passphrase'),
help_text=_('Repeat the passphrase.'),
- widget=forms.PasswordInput()
+ widget=forms.PasswordInput(),
+ required=False
)
store_passphrase = forms.BooleanField(
label=_('Store passphrase on FreedomBox'),
- help_text=format_lazy(_('Store the passphrase on your {box_name}.'
- '
You need to store the passphrase if you want to run '
- 'recurrent backups.'), box_name=_(cfg.box_name)),
+ help_text=format_lazy(
+ _('Store the passphrase on your {box_name}.'
+ '
You need to store the passphrase if you want to run '
+ 'recurrent backups.'), box_name=_(cfg.box_name)),
required=False
)
diff --git a/plinth/modules/backups/templates/backups_create_repository.html b/plinth/modules/backups/templates/backups_create_repository.html
new file mode 100644
index 000000000..0b4be62eb
--- /dev/null
+++ b/plinth/modules/backups/templates/backups_create_repository.html
@@ -0,0 +1,40 @@
+{% extends "base.html" %}
+{% comment %}
+#
+# 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