storage: Directory selection form and validator

Directory selection allows to:
- select from default directory
- select from available Samba shares
- specify subdirectory
- insert custom directory

- directory validator checks: path exists, is directory, is readable, is writable
- samba: action script: include share path in share list
- create freedombox-share group inside users module  instead of samba module

Closes #1703

Signed-off-by: Veiko Aasa <veiko17@disroot.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Veiko Aasa 2019-12-05 17:13:26 +03:00 committed by James Valleroy
parent 8d51adcc05
commit ea48f9a74b
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
5 changed files with 203 additions and 5 deletions

View File

@ -163,9 +163,10 @@ def _get_shares():
config = configparser.ConfigParser()
config.read_string(output.decode())
for name in config.sections():
mount_point = _get_mount_point(config[name]['path'])
path = config[name]['path']
mount_point = _get_mount_point(path)
mount_point = os.path.normpath(mount_point)
shares.append(dict(name=name, mount_point=mount_point))
shares.append(dict(name=name, mount_point=mount_point, path=path))
return shares

View File

@ -21,6 +21,7 @@ Configuration helper for disks manager.
import argparse
import json
import os
import re
import subprocess
import sys
@ -51,6 +52,14 @@ def parse_arguments():
subparsers.add_parser('usage-info',
help='Get information about disk space usage')
subparser = subparsers.add_parser('validate-directory',
help='Validate a directory')
subparser.add_argument('--path', help='Path of the directory',
required=True)
subparser.add_argument('--check-writable', required=False, default=False,
action='store_true',
help='Check that the directory is writable')
subparsers.required = True
return parser.parse_args()
@ -316,6 +325,19 @@ def subcommand_usage_info(_):
subprocess.run(command, check=True)
def subcommand_validate_directory(arguments):
"""Validate a directory"""
directory = arguments.path
if not os.path.exists(directory):
print('ValidationError: 1')
if not os.path.isdir(directory):
print('ValidationError: 2')
if not os.access(directory, os.R_OK):
print('ValidationError: 3')
if arguments.check_writable and not os.access(directory, os.W_OK):
print('ValidationError: 4')
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()

View File

@ -28,7 +28,7 @@ from plinth import action_utils, actions
from plinth import app as app_module
from plinth import frontpage, menu
from plinth.daemon import Daemon
from plinth.modules.users import create_group, register_group
from plinth.modules.users import register_group
from plinth.modules.firewall.components import Firewall
from plinth.utils import format_lazy
@ -106,7 +106,6 @@ def init():
def setup(helper, old_version=None):
"""Install and configure the module."""
helper.install(managed_packages)
create_group('freedombox-share')
helper.call('post', actions.superuser_run, 'samba', ['setup'])
helper.call('post', app.enable)

View File

@ -0,0 +1,161 @@
#
# 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/>.
#
"""
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.forms import AppForm
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_disks()
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
add_user_to_share_group = False
service_to_restart = None
def __init__(self, username=None, check_writable=None):
if username is not None:
self.username = username
if check_writable is not None:
self.check_writable = check_writable
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_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(AppForm):
"""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 = os.path.normpath(default)
self.set_form_data()
def clean(self):
"""Clean and validate form data."""
if self.cleaned_data['is_enabled'] or not self.initial['is_enabled']:
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:
choices = choices + [(share['path'], '{0} ({1}): {2}'.format(
_('Samba share'), share['name'], share['path']))]
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

View File

@ -18,6 +18,7 @@
FreedomBox app to manage users.
"""
import grp
import subprocess
from django.utils.translation import ugettext_lazy as _
@ -27,7 +28,7 @@ from plinth import app as app_module
from plinth import cfg, menu
from plinth.utils import format_lazy
version = 2
version = 3
is_essential = True
@ -92,6 +93,7 @@ def setup(helper, old_version=None):
if not old_version:
helper.call('post', actions.superuser_run, 'users', ['first-setup'])
helper.call('post', actions.superuser_run, 'users', ['setup'])
create_group('freedombox-share')
def diagnose():
@ -148,3 +150,16 @@ def get_last_admin_user():
return admin_users[0]
return None
def add_user_to_share_group(username, service=None):
"""Add user to the freedombox-share group."""
try:
group_members = grp.getgrnam('freedombox-share').gr_mem
except KeyError:
group_members = []
if username not in group_members:
actions.superuser_run(
'users', ['add-user-to-group', username, 'freedombox-share'])
if service:
action_utils.service_restart(service)