diff --git a/actions/backups b/actions/backups
index 36d164cc6..bb6a39e9e 100755
--- a/actions/backups
+++ b/actions/backups
@@ -27,7 +27,7 @@ import subprocess
import sys
import tarfile
-from plinth.modules.backups import MANIFESTS_FOLDER, REPOSITORY
+from plinth.modules.backups import MANIFESTS_FOLDER
TIMEOUT = 5
@@ -41,28 +41,14 @@ def parse_arguments():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
- subparsers.add_parser(
+ setup = subparsers.add_parser(
'setup', help='Create repository if it does not already exist')
- info = subparsers.add_parser('info', help='Show repository information')
- info.add_argument('--repository', help='Repository path', required=True)
-
init = subparsers.add_parser('init', help='Initialize a repository')
- init.add_argument('--repository', help='Repository path', required=True)
init.add_argument('--encryption', help='Enryption of the repository',
required=True)
- # TODO: ssh commands should be in SSH or a separate action script
- mount = subparsers.add_parser('mount', help='mount an ssh filesystem')
- mount.add_argument('--mountpoint', help='Local mountpoint', required=True)
- umount = subparsers.add_parser('umount',
- help='unmount an ssh filesystem')
- umount.add_argument('--mountpoint', help='Mountpoint to unmount',
- required=True)
- is_mounted = subparsers.add_parser('is-mounted',
- help='Check whether an sshfs is mouned')
- is_mounted.add_argument('--mountpoint', help='Mountpoint to check',
- required=True)
+ info = subparsers.add_parser('info', help='Show repository information')
list_repo = subparsers.add_parser('list-repo',
help='List repository contents')
@@ -77,16 +63,25 @@ def parse_arguments():
export_help = 'Export archive contents as tar on stdout'
export_tar = subparsers.add_parser('export-tar', help=export_help)
- export_tar.add_argument('--name', help='Archive name)',
- required=True)
get_archive_apps = subparsers.add_parser(
'get-archive-apps',
help='Get list of apps included in archive')
- # TODO: add parameters to missing commands (export etc)
+ # SSH mount actions
+ mount = subparsers.add_parser('mount', help='mount an ssh filesystem')
+ mount.add_argument('--mountpoint', help='Local mountpoint', required=True)
+ umount = subparsers.add_parser('umount',
+ help='unmount an ssh filesystem')
+ umount.add_argument('--mountpoint', help='Mountpoint to unmount',
+ required=True)
+ is_mounted = subparsers.add_parser('is-mounted',
+ help='Check whether an sshfs is mouned')
+ is_mounted.add_argument('--mountpoint', help='Mountpoint to check',
+ required=True)
+
for cmd in [info, init, list_repo, create_archive, delete_archive,
- get_archive_apps, mount]:
+ export_tar, get_archive_apps, mount, setup]:
cmd.add_argument('--path', help='Repository or Archive path',
required=False) # TODO: set required to True!
cmd.add_argument('--ssh-keyfile', help='Path of private ssh key',
@@ -117,26 +112,26 @@ def parse_arguments():
return parser.parse_args()
-def subcommand_setup(_):
+def subcommand_setup(arguments):
"""Create repository if it does not already exist."""
- # TODO: use init()
+ env = get_env(arguments)
try:
- subprocess.run(['borg', 'info', REPOSITORY], check=True)
+ run(['borg', 'info', arguments.path], check=True, env=env)
except:
- path = os.path.dirname(REPOSITORY)
+ path = os.path.dirname(arguments.path)
if not os.path.exists(path):
os.makedirs(path)
- run(['borg', 'init', '--encryption', 'none', REPOSITORY])
+ init(arguments.path, 'none', env=env)
-def init(repository, encryption, env=None):
+def init(path, encryption, env=None):
"""Initialize a local or remote borg repository"""
# TODO: verify that the repository does not exist?
# TODO: does remote borg also create folders if they don't exist?
if encryption != 'none' and 'BORG_PASSPHRASE' not in env:
raise ValueError('No encryption passphrase provided')
- cmd = ['borg', 'init', '--encryption', encryption, repository]
+ cmd = ['borg', 'init', '--encryption', encryption, path]
run(cmd, env=env)
@@ -171,13 +166,13 @@ def get_sshfs_env(arguments):
def subcommand_init(arguments):
env = get_env(arguments)
- init(arguments.repository, arguments.encryption, env=env)
+ init(arguments.path, arguments.encryption, env=env)
def subcommand_info(arguments):
"""Show repository information."""
env = get_env(arguments)
- run(['borg', 'info', '--json', arguments.repository], env=env)
+ run(['borg', 'info', '--json', arguments.path], env=env)
def subcommand_mount(arguments):
@@ -286,7 +281,9 @@ def _extract(archive_path, destination, locations=None):
def subcommand_export_tar(arguments):
"""Export archive contents as tar stream on stdout."""
- run(['borg', 'export-tar', REPOSITORY + '::' + arguments.name, '-'])
+ env = get_env(arguments)
+ run(['borg', 'export-tar', arguments.path, '-'],
+ env=env)
def _read_archive_file(archive, filepath):
@@ -385,14 +382,14 @@ def read_password():
return ''.join(sys.stdin)
-def run(cmd, env=None):
+def run(cmd, env=None, check=True):
"""Wrap the command with ssh password or keyfile authentication"""
# If the remote server asks for a password but no password is
# provided, we get stuck at asking the password.
timeout = None
if env and 'BORG_RSH' in env and 'SSHPASS' not in env:
timeout = TIMEOUT
- subprocess.run(cmd, check=True, env=env, timeout=timeout)
+ subprocess.run(cmd, check=check, env=env, timeout=timeout)
def main():
diff --git a/plinth/modules/backups/__init__.py b/plinth/modules/backups/__init__.py
index d4c710a1c..da0cb887c 100644
--- a/plinth/modules/backups/__init__.py
+++ b/plinth/modules/backups/__init__.py
@@ -25,10 +25,8 @@ from django.utils.text import get_valid_filename
from django.utils.translation import ugettext_lazy as _
from plinth import actions, cfg
-from plinth.errors import ActionError
from plinth.menu import main_menu
from plinth.utils import format_lazy
-from .errors import BorgError, BorgRepositoryDoesNotExistError
from . import api
@@ -45,30 +43,11 @@ description = [
service = None
MANIFESTS_FOLDER = '/var/lib/plinth/backups-manifests/'
-REPOSITORY = '/var/lib/freedombox/borgbackup'
+ROOT_REPOSITORY = '/var/lib/freedombox/borgbackup'
+ROOT_REPOSITORY_NAME = format_lazy(_('{box_name} storage'),
+ box_name=cfg.box_name)
# session variable name that stores when a backup file should be deleted
SESSION_PATH_VARIABLE = 'fbx-backups-upload-path'
-# known errors that come up when remotely accessing a borg repository
-# 'errors' are error strings to look for in the stacktrace.
-ACCESS_PARAMS = ['ssh_keyfile', 'ssh_password', 'encryption_passphrase']
-# kvstore key for storing remote locations
-REMOTE_LOCATIONS_KEY = 'remote_locations'
-KNOWN_ERRORS = [{
- "errors": ["subprocess.TimeoutExpired"],
- "message": _("Connection refused - make sure you provided correct "
- "credentials and the server is running."),
- "raise_as": BorgError,
- },
- {
- "errors": ["Connection refused"],
- "message": _("Connection refused"),
- "raise_as": BorgError,
- },
- {
- "errors": ["not a valid repository", "does not exist"],
- "message": _("Repository not found"),
- "raise_as": BorgRepositoryDoesNotExistError,
- }]
def init():
@@ -80,67 +59,8 @@ def init():
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
- helper.call('post', actions.superuser_run, 'backups', ['setup'])
-
-
-def get_info(repository, access_params=None):
- cmd = ['info', '--repository', repository]
- output = run(cmd, access_params)
- return json.loads(output)
-
-
-def list_archives(repository, access_params=None):
- try:
- output = run(['list-repo', '--path', repository], access_params)
- except ActionError as err:
- reraise_known_error(err)
- else:
- return json.loads(output)['archives']
-
-
-def get_root_location_content():
- """
- Get information about the root backup location in the same format
- that the remote repositories use
- """
- return {
- 'name': format_lazy(_('{box_name} storage'), box_name=cfg.box_name),
- 'mounted': True,
- 'archives': list_archives(REPOSITORY),
- 'type': 'rootfs',
- }
-
-
-def get_archive(name):
- # TODO: can't we get this archive directly?
- for archive in list_archives():
- if archive['name'] == name:
- return archive
-
- return None
-
-
-def reraise_known_error(err):
- """Look whether the caught error is known and re-raise it accordingly"""
- caught_error = str(err)
- for known_error in KNOWN_ERRORS:
- for error in known_error["errors"]:
- if error in caught_error:
- raise known_error["raise_as"](known_error["message"])
- else:
- raise err
-
-
-def test_connection(repository, access_params=None):
- """
- Test connecting to a local or remote borg repository.
- Tries to detect (and throw) some known ssh or borg errors.
- Returns 'borg info' information otherwise.
- """
- try:
- return get_info(repository, access_params)
- except ActionError as err:
- reraise_known_error(err)
+ helper.call('post', actions.superuser_run, 'backups', ['setup', '--path',
+ ROOT_REPOSITORY])
def _backup_handler(packet):
@@ -168,32 +88,6 @@ def _backup_handler(packet):
paths)
-def create_archive(name, app_names, access_params=None):
- api.backup_apps(_backup_handler, app_names, name,
- access_params=access_params)
-
-
-def create_repository(repository, encryption, access_params=None):
- cmd = ['init', '--repository', repository, '--encryption', encryption]
- run(cmd, access_params=access_params)
-
-
-def delete_archive(path):
- actions.superuser_run('backups', ['delete-archive', '--path', path])
-
-
-def get_archive_path(archive_name):
- """Get path of an archive"""
- return "::".join([REPOSITORY, archive_name])
-
-
-def get_archive_apps(path):
- """Get list of apps included in an archive."""
- output = actions.superuser_run('backups',
- ['get-archive-apps', '--path', path])
- return output.splitlines()
-
-
def get_exported_archive_apps(path):
"""Get list of apps included in exported archive file."""
arguments = ['get-exported-archive-apps', '--path', path]
@@ -229,34 +123,3 @@ def restore(archive_path, apps=None):
"""Restore files from a backup archive."""
api.restore_apps(_restore_archive_handler, app_names=apps,
create_subvolume=False, backup_file=archive_path)
-
-
-def get_args(params):
- args = []
- kwargs = {}
- if 'ssh_password' in params and params['ssh_password'] is not None:
- kwargs['input'] = params['ssh_password'].encode()
- if 'ssh_keyfile' in params and params['ssh_keyfile'] is not None:
- args += ['--ssh-keyfile', params['ssh_keyfile']]
- if 'encryption_passphrase' in params and \
- params['encryption_passphrase'] is not None:
- args += ['--encryption-passphrase', params['encryption_passphrase']]
- return (args, kwargs)
-
-
-def run(arguments, access_params=None, superuser=True):
- """Run a given command, appending (kw)args as necessary"""
- command = 'backups'
- kwargs = {}
- if access_params:
- for key in access_params.keys():
- if key not in ACCESS_PARAMS:
- raise ValueError('Unknown access parameter: %s' % key)
-
- append_arguments, kwargs = get_args(access_params)
- arguments += append_arguments
-
- if superuser:
- return actions.superuser_run(command, arguments, **kwargs)
- else:
- return actions.run(command, arguments, **kwargs)
diff --git a/plinth/modules/backups/location.py b/plinth/modules/backups/location.py
deleted file mode 100644
index 6c637c607..000000000
--- a/plinth/modules/backups/location.py
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# 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 .
-#
-"""
-Manage borg backup locations
-"""
-
-
-class Location(object):
- def __init__(self, uuid):
- pass
diff --git a/plinth/modules/backups/network_storage.py b/plinth/modules/backups/network_storage.py
new file mode 100644
index 000000000..174433775
--- /dev/null
+++ b/plinth/modules/backups/network_storage.py
@@ -0,0 +1,68 @@
+#
+# 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 .
+#
+"""
+Manage remote (network) storage storages in plinths' KVStore.
+"""
+
+import json
+from uuid import uuid1
+
+from plinth import kvstore
+
+# kvstore key for network storage
+NETWORK_STORAGE_KEY = 'network_storage'
+REQUIRED_FIELDS = ['path', 'storage_type', 'added_by_module']
+
+
+def get_storages(storage_type=None):
+ """Get list of network storage storages"""
+ storages = kvstore.get_default(NETWORK_STORAGE_KEY, [])
+ if storages:
+ storages = json.loads(storages)
+ if storage_type:
+ storages = [storage for storage in storages if 'type' in storage
+ and storage['type'] == storage_type]
+ return storages
+
+
+def get(uuid):
+ storages = get_storages()
+ return list(filter(lambda storage: storage['uuid'] == uuid,
+ storages))[0]
+
+
+def update_or_add(storage):
+ """Update an existing or create a new network location"""
+ for field in REQUIRED_FIELDS:
+ assert field in storage
+ storages = get_storages()
+ if 'uuid' in storage:
+ # Replace the existing storage
+ storages = [_storage if _storage['uuid'] != storage['uuid'] else
+ storage for _storage in storages]
+ else:
+ storage['uuid'] = str(uuid1())
+ storages.append(storage)
+ kvstore.set(NETWORK_STORAGE_KEY, json.dumps(storages))
+
+
+def delete(uuid):
+ """Remove a network storage from kvstore"""
+ storages = get_storages()
+ storages = list(filter(lambda storage: storage['uuid'] != uuid,
+ storages))
+ kvstore.set(NETWORK_STORAGE_KEY, json.dumps(storages))
diff --git a/plinth/modules/backups/remote_locations.py b/plinth/modules/backups/remote_locations.py
deleted file mode 100644
index 8261c41d6..000000000
--- a/plinth/modules/backups/remote_locations.py
+++ /dev/null
@@ -1,180 +0,0 @@
-#
-# 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 .
-#
-"""
-Manage remote storage locations
-"""
-
-import json
-import logging
-import os
-from uuid import uuid1
-
-from django.utils.translation import ugettext as _
-
-from plinth import kvstore
-from plinth.errors import ActionError
-
-from . import sshfs, list_archives, reraise_known_error, REMOTE_LOCATIONS_KEY
-from .errors import BorgError
-
-logger = logging.getLogger(__name__)
-MOUNTPOINT = '/media/'
-
-
-def add(path, repotype, encryption, access_params, store_passwords, added_by):
- locations = get_locations()
- location = {
- 'uuid': str(uuid1()),
- 'path': path,
- 'type': repotype,
- 'encryption': encryption,
- 'added_by': added_by
- }
- if store_passwords:
- if 'encryption_passphrase' in access_params:
- location['encryption_passphrase'] = \
- access_params['encryption_passphrase']
- if 'ssh_password' in access_params:
- location['ssh_password'] = access_params['ssh_password']
- locations.append(location)
- kvstore.set(REMOTE_LOCATIONS_KEY, json.dumps(locations))
-
-
-def delete(uuid):
- """Umount a location, remove it from kvstore and unlink the mountpoint"""
- locations = get_locations()
- location = get_location(uuid)
- mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
- locations = list(filter(lambda location: location['uuid'] != uuid,
- locations))
- kvstore.set(REMOTE_LOCATIONS_KEY, json.dumps(locations))
- if os.path.exists(mountpoint):
- try:
- sshfs.umount(mountpoint)
- except ActionError:
- pass
- try:
- os.unlink(mountpoint)
- except Exception as err:
- logger.error(err)
-
-
-def get_locations_content(uuid=None):
- """
- Get archives of one or all locations.
- returns: {
- uuid: {
- 'path': path,
- 'type': type,
- 'archives': [],
- 'error': error_message,
- }
- }
- """
- locations = {}
- for location in get_locations():
- mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
- new_location = {
- 'name': location['path'],
- 'mounted': uuid_is_mounted(location['uuid']),
- }
- if new_location['mounted']:
- try:
- new_location['archives'] = list_archives(mountpoint)
- except BorgError as err:
- new_location['error'] = str(err)
- except Exception as err:
- logger.error(err)
- new_location['error'] = _("Access failed")
- locations[location['uuid']] = new_location
-
- return locations
-
-
-def get_locations(location_type=None):
- """Get list of all locations"""
- # TODO: hold locations in memory?
- locations = kvstore.get_default(REMOTE_LOCATIONS_KEY, [])
- if locations:
- locations = json.loads(locations)
- if location_type:
- locations = [location for location in locations if 'type' in location
- and location['type'] == location_type]
- return locations
-
-
-def get_location(uuid):
- locations = get_locations()
- return list(filter(lambda location: location['uuid'] == uuid,
- locations))[0]
-
-
-def _mount_locations(uuid=None):
- locations = get_locations(location_type='ssh')
- for location in locations:
- _mount_location(location)
-
-
-def _mount_location(location):
- # TODO: shouldn't I just store and query the access_params as they are?
- # but encryption_passphrase is not an ssh access_param..
- mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
- is_mounted = False
- if sshfs.is_mounted(mountpoint):
- is_mounted = True
- else:
- access_params = _get_access_params(location)
- # TODO: use actual feedback of sshfs.mount
- try:
- sshfs.mount(location['path'], mountpoint, access_params)
- except Exception as err:
- reraise_known_error(err)
- is_mounted = True
- return is_mounted
-
-
-def _umount_location(location):
- mountpoint = os.path.join(MOUNTPOINT, location['uuid'])
- return sshfs.umount(mountpoint)
-
-
-def _get_access_params(location):
- keys = ['encryption_passphrase', 'ssh_keyfile', 'ssh_password']
- access_params = {key: location[key] for key in keys if key in location}
- if location['type'] == 'ssh':
- if 'ssh_keyfile' not in location and 'ssh_password' not in \
- location:
- raise ValueError('Missing credentials')
- return access_params
-
-
-def mount_uuid(uuid):
- location = get_location(uuid)
- mounted = False
- if location:
- mounted = _mount_location(location)
- return mounted
-
-
-def umount_uuid(uuid):
- location = get_location(uuid)
- return _umount_location(location)
-
-
-def uuid_is_mounted(uuid):
- mountpoint = os.path.join(MOUNTPOINT, uuid)
- return sshfs.is_mounted(mountpoint)
diff --git a/plinth/modules/backups/repository.py b/plinth/modules/backups/repository.py
new file mode 100644
index 000000000..db0a8df27
--- /dev/null
+++ b/plinth/modules/backups/repository.py
@@ -0,0 +1,266 @@
+#
+# 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 .
+#
+"""
+Remote and local Borg backup repositories
+"""
+
+import json
+import os
+import logging
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth import actions
+from plinth.errors import ActionError
+
+from . import api, network_storage, _backup_handler, ROOT_REPOSITORY_NAME
+from .errors import BorgError, BorgRepositoryDoesNotExistError
+
+logger = logging.getLogger(__name__)
+
+SSHFS_MOUNTPOINT = '/media/'
+# known errors that come up when remotely accessing a borg repository
+# 'errors' are error strings to look for in the stacktrace.
+KNOWN_ERRORS = [{
+ "errors": ["subprocess.TimeoutExpired"],
+ "message": _("Connection refused - make sure you provided correct "
+ "credentials and the server is running."),
+ "raise_as": BorgError,
+ },
+ {
+ "errors": ["Connection refused"],
+ "message": _("Connection refused"),
+ "raise_as": BorgError,
+ },
+ {
+ "errors": ["not a valid repository", "does not exist"],
+ "message": _("Repository not found"),
+ "raise_as": BorgRepositoryDoesNotExistError,
+ }]
+
+
+class BorgRepository(object):
+ """Borg repository on the root filesystem"""
+ command = 'backups'
+ storage_type = 'root'
+ name = ROOT_REPOSITORY_NAME
+ is_mounted = True
+
+ def __init__(self, path):
+ self.path = path
+
+ def get_info(self):
+ output = self._run(['info', '--path', self.path])
+ return json.loads(output)
+
+ def list_archives(self):
+ output = self._run(['list-repo', '--path', self.path])
+ return json.loads(output)['archives']
+
+ def get_view_content(self):
+ """Get archives with additional information as needed by the view"""
+ return {
+ 'name': self.name,
+ 'mounted': self.is_mounted,
+ 'archives': self.list_archives(),
+ 'type': self.storage_type,
+ }
+
+ def delete_archive(self, archive_name):
+ archive_path = self.get_archive_path(archive_name)
+ self._run(['delete-archive', '--path', archive_path])
+
+ def remove(self):
+ """Remove a borg repository"""
+ raise NotImplementedError
+
+ def create_archive(self, app_names, archive_name):
+ api.backup_apps(_backup_handler, app_names=app_names,
+ label=archive_name),
+
+ def download_archive(self, name):
+ pass
+
+ def get_archive(self, name):
+ # TODO: can't we get this archive directly?
+ for archive in self.list_archives():
+ if archive['name'] == name:
+ return archive
+
+ return None
+
+ def get_archive_apps(self, archive_name):
+ """Get list of apps included in an archive."""
+ archive_path = self.get_archive_path(archive_name)
+ output = self._run(['get-archive-apps', '--path', archive_path])
+ return output.splitlines()
+
+ def restore_archive(self):
+ pass
+
+ def get_archive_path(self, archive_name):
+ return "::".join(self.path, archive_name)
+
+ def _run(self, arguments, superuser=True):
+ """Run a backups action script command."""
+ try:
+ if superuser:
+ return actions.superuser_run(self.command, arguments)
+ else:
+ return actions.run(self.command, arguments)
+ except ActionError as err:
+ self.reraise_known_error(err)
+
+ def reraise_known_error(self, err):
+ """Look whether the caught error is known and reraise it accordingly"""
+ caught_error = str(err)
+ for known_error in KNOWN_ERRORS:
+ for error in known_error["errors"]:
+ if error in caught_error:
+ raise known_error["raise_as"](known_error["message"])
+ else:
+ raise err
+
+
+class SshBorgRepository(BorgRepository):
+ KNOWN_CREDENTIALS = ['ssh_keyfile', 'ssh_password',
+ 'encryption_passphrase']
+ storage_type = 'ssh'
+
+ def __init__(self, uuid=None, path=None, credentials=None, **kwargs):
+ """
+ Provide a uuid to instanciate an existing repository,
+ or 'path' and 'credentials' for a new repository.
+ """
+ if uuid:
+ self.uuid = uuid
+ # If all data are given, instanciate right away.
+ if path and credentials:
+ self.path = path
+ self.credentials = credentials
+ else:
+ self._load_from_kvstore()
+ # No uuid given: new instance.
+ elif path and credentials:
+ self.path = path
+ self.credentials = credentials
+ else:
+ raise ValueError('Invalid arguments.')
+
+ @property
+ def mountpoint(self):
+ return os.path.join(SSHFS_MOUNTPOINT, self.uuid)
+
+ @property
+ def name(self):
+ return self.path
+
+ @property
+ def is_mounted(self):
+ output = self._run(['is-mounted', '--mountpoint', self.mountpoint])
+ return json.loads(output)
+
+ def _load_from_kvstore(self):
+ storage = network_storage.get(self.uuid)
+ self.credentials = storage['credentials']
+ self.path = storage['path']
+
+ def _get_network_storage_format(self):
+ storage = {
+ 'path': self.path,
+ 'credentials': self.credentials,
+ 'storage_type': self.storage_type,
+ 'added_by_module': 'backups'
+ }
+ if hasattr(self, 'uuid'):
+ storage['uuid'] = self.uuid
+ return storage
+
+ def create_archive(self, app_names, archive_name):
+ api.backup_apps(_backup_handler, app_names=app_names,
+ label=archive_name, credentials=self.credentials)
+
+ def create_repository(self, encryption):
+ cmd = ['init', '--path', self.path, '--encryption', encryption]
+ self._run(cmd)
+
+ def save(self):
+ storage = self._get_network_storage_format()
+ self.uuid = network_storage.update_or_add(storage)
+
+ def mount(self):
+ cmd = ['mount', '--mountpoint', self.mountpoint, '--path', self.path]
+ self._run(cmd)
+
+ def umount(self):
+ self._run(['umount', '--mountpoint', self.mountpoint],
+ use_credentials=False)
+
+ def remove(self):
+ """Remove a repository from the kvstore and delete its mountpoint"""
+ network_storage.delete(self.uuid)
+ try:
+ if os.path.exists(self.mountpoint):
+ try:
+ self.umount()
+ except ActionError:
+ pass
+ if not os.listdir(self.mountpoint):
+ os.rmdir(self.mountpoint)
+ except Exception as err:
+ logger.error(err)
+
+ def _run(self, arguments, superuser=True, use_credentials=True):
+ """Run a backups action script command.
+
+ This automatically passes on self.credentials to the backups script
+ via environment variables or input, except if you set use_credentials
+ to False.
+ """
+ for key in self.credentials.keys():
+ if key not in self.KNOWN_CREDENTIALS:
+ raise ValueError('Unknown credentials: %s' % key)
+
+ kwargs = {}
+ if use_credentials:
+ if 'ssh_password' in self.credentials and \
+ self.credentials['ssh_password'] is not None:
+ kwargs['input'] = self.credentials['ssh_password'].encode()
+ if 'ssh_keyfile' in self.credentials and \
+ self.credentials['ssh_keyfile'] is not None:
+ arguments += ['--ssh-keyfile', self.credentials['ssh_keyfile']]
+ if 'encryption_passphrase' in self.credentials and \
+ self.credentials['encryption_passphrase'] is not None:
+ arguments += ['--encryption-passphrase',
+ self.credentials['encryption_passphrase']]
+
+ try:
+ if superuser:
+ return actions.superuser_run(self.command, arguments, **kwargs)
+ else:
+ return actions.run(self.command, arguments, **kwargs)
+ except ActionError as err:
+ self.reraise_known_error(err)
+
+
+def get_ssh_repositories():
+ """Get all SSH Repositories including the archive content"""
+ repositories = {}
+ for storage in network_storage.get_storages():
+ repository = SshBorgRepository(**storage)
+ repositories[storage['uuid']] = repository.get_view_content()
+ return repositories
diff --git a/plinth/modules/backups/sshfs.py b/plinth/modules/backups/sshfs.py
deleted file mode 100644
index d3aa32b24..000000000
--- a/plinth/modules/backups/sshfs.py
+++ /dev/null
@@ -1,38 +0,0 @@
-#
-# 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 .
-#
-
-"""
-Utilities to work with sshfs.
-"""
-
-import json
-
-from plinth.modules.backups import run
-
-
-def mount(remote_path, mountpoint, access_params):
- run(['mount', '--mountpoint', mountpoint, '--path', remote_path],
- access_params)
-
-
-def umount(mountpoint):
- run(['umount', '--mountpoint', mountpoint])
-
-
-def is_mounted(mountpoint):
- output = run(['is-mounted', '--mountpoint', mountpoint])
- return json.loads(output)
diff --git a/plinth/modules/backups/templates/backups.html b/plinth/modules/backups/templates/backups.html
index 58387a92b..a103ae4bc 100644
--- a/plinth/modules/backups/templates/backups.html
+++ b/plinth/modules/backups/templates/backups.html
@@ -52,17 +52,17 @@
{% trans 'Existing backups' %}
- {% include "backups_location.inc" with location=root_location uuid='root' editable=False %}
+ {% include "backups_repository.inc" with repository=root_repository uuid='root' editable=False %}
- {% for uuid,location in remote_locations.items %}
- {% include "backups_location.inc" with editable=True %}
+ {% for uuid,repository in ssh_repositories.items %}
+ {% include "backups_repository.inc" with editable=True %}
{% endfor %}
+ href="{% url 'backups:repository-add' %}">
{% trans '+ Add Remote Repository' %}
diff --git a/plinth/modules/backups/templates/backups_location.inc b/plinth/modules/backups/templates/backups_repository.inc
similarity index 85%
rename from plinth/modules/backups/templates/backups_location.inc
rename to plinth/modules/backups/templates/backups_repository.inc
index 0e4f191ea..e0fb7f43c 100644
--- a/plinth/modules/backups/templates/backups_location.inc
+++ b/plinth/modules/backups/templates/backups_repository.inc
@@ -25,16 +25,16 @@
|
- {{ location.name }}
+ {{ repository.name }}
{% if editable %}
- {% if location.mounted %}
+ {% if repository.mounted %}
- |