mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-04-01 09:30:29 +00:00
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:
parent
8d51adcc05
commit
ea48f9a74b
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
161
plinth/modules/storage/forms.py
Normal file
161
plinth/modules/storage/forms.py
Normal 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
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user