Sunil Mohan Adapa 10b46f1968
storage: Use UDisks information as primary source
Rename get_disks() to get_mounts() and use it in for backups and samba shares.

Create a new get_disks() similar to get_mounts() but use df information only for
showing free space. This inverts the importance of 'df' and UDisks. Use UDisks
as primary source of information for showing list of disks and then use df to
fill in the free space information.

- Retrieve all the mount points of a device and return them as part of
get_disks() in an extra 'mount_points' property.

- For storage listing, this fixes showing up of /.snapshots as separate disk and
showing of vboxsf, network mounts etc. Only shows mounts that are related to
block devices.

- Update various uses of get_disks() within storage module to use
'mounts_points' instead of 'mount_point' to be accurate in cases where there are
multiple mounts for a given device. Use get_mounts() where appropriate instead.

- Display all the mount points against a devices in multiple lines.

- Also show devices that are not currently mounted.

Tests performed:

- Filling up a disk shows a disk space warning properly. Warning contains the
  free disk space correctly.

- Calling get_root_device(get_disks()) return the correct root device.

- In Deluge, the download directory contains a list of all samba current shares.
  If a disk with samba share is unmouted, it does not show up in the list.

- In the Samba app page, all disks are shown properly. Root disk is shown as
  'disk'. All other mount points such as .snapshots and /vagrant also show up.

- In the Samba app page, unavailable shares list shows up when a disk with a
  share is unmounted.

- Upload a backup, warning on the form shows available disk space properly.

- When adding a backup location. The list includes all mount points. Duplicated
  mount points are not shown. Root disk is not shown in the list. When all the
  disks are used up for backup location, a warning that no additional disks are
  available is shown.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2020-06-24 07:23:29 -04:00

158 lines
5.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Forms for directory selection.
"""
import json
import os
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from plinth import actions, module_loader
from plinth.modules import storage
def get_available_samba_shares():
"""Get available samba shares."""
available_shares = []
if is_module_enabled('samba'):
samba_shares = json.loads(
actions.superuser_run('samba', ['get-shares']))
if samba_shares:
disks = storage.get_mounts()
for share in samba_shares:
for disk in disks:
if share['mount_point'] == disk['mount_point']:
available_shares.append(share)
break
return available_shares
def is_module_enabled(name):
"""Check whether a module is enabled."""
if name in module_loader.loaded_modules:
module = module_loader.loaded_modules['samba']
if module.setup_helper.get_state(
) != 'needs-setup' and module.app.is_enabled():
return True
return False
class DirectoryValidator:
username = None
check_writable = False
check_creatable = False
add_user_to_share_group = False
service_to_restart = None
def __init__(self, username=None, check_writable=None,
check_creatable=None):
if username is not None:
self.username = username
if check_writable is not None:
self.check_writable = check_writable
if check_creatable is not None:
self.check_creatable = check_creatable
def __call__(self, value):
"""Validate a directory."""
if not value.startswith('/'):
raise ValidationError(_('Invalid directory name.'), 'invalid')
command = ['validate-directory', '--path', value]
if self.check_creatable:
command.append('--check-creatable')
elif self.check_writable:
command.append('--check-writable')
if self.username:
output = actions.run_as_user('storage', command,
become_user=self.username)
else:
output = actions.run('storage', command)
if 'ValidationError' in output:
error_nr = int(output.strip().split()[1])
if error_nr == 1:
raise ValidationError(_('Directory does not exist.'),
'invalid')
elif error_nr == 2:
raise ValidationError(_('Path is not a directory.'), 'invalid')
elif error_nr == 3:
raise ValidationError(
_('Directory is not readable by the user.'), 'invalid')
elif error_nr == 4:
raise ValidationError(
_('Directory is not writable by the user.'), 'invalid')
class DirectorySelectForm(forms.Form):
"""Directory selection form."""
storage_dir = forms.ChoiceField(choices=[], label=_('Directory'),
required=True)
storage_subdir = forms.CharField(label=_('Subdirectory (optional)'),
required=False)
def __init__(self, title=None, default='/', validator=DirectoryValidator,
*args, **kwargs):
super().__init__(*args, **kwargs)
if title:
self.fields['storage_dir'].label = title
self.validator = validator
self.default = default
self.set_form_data()
def clean(self):
"""Clean and validate form data."""
storage_dir = self.cleaned_data['storage_dir']
storage_subdir = self.cleaned_data['storage_subdir']
if storage_dir != '/':
storage_subdir = storage_subdir.lstrip('/')
storage_path = os.path.realpath(
os.path.join(storage_dir, storage_subdir))
if self.validator:
self.validator(storage_path)
self.cleaned_data.update({'storage_path': storage_path})
def get_initial(self, choices):
"""Get initial form data."""
initial_selection = ()
subdir = ''
storage_path = self.initial['storage_path']
for choice in choices:
if storage_path.startswith(choice[0]):
initial_selection = choice
subdir = storage_path.split(choice[0], 1)[1].strip('/')
if choice[0] == '/':
subdir = '/' + subdir
break
return (initial_selection, subdir)
def set_form_data(self):
"""Set initial form data."""
choices = []
if self.default:
choices = choices + [(self.default, '{0}: {1}'.format(
_('Default'), self.default))]
available_shares = get_available_samba_shares()
for share in available_shares:
if share['share_type'] != 'home':
share_type = _('Share')
if share['share_type'] == 'group':
share_type = _('Group Share')
elif share['share_type'] == 'open':
share_type = _('Open Share')
selection_text = 'Samba {0} ({1}): {2}'.format(
share_type, share['name'], share['path'])
choices = choices + [(share['path'], selection_text)]
choices = choices + [('/', _('Other directory (specify below)'))]
initial_value, subdir = self.get_initial(choices)
self.fields['storage_dir'].choices = choices
self.initial['storage_dir'] = initial_value
self.initial['storage_subdir'] = subdir