mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +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 = configparser.ConfigParser()
|
||||||
config.read_string(output.decode())
|
config.read_string(output.decode())
|
||||||
for name in config.sections():
|
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)
|
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
|
return shares
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ Configuration helper for disks manager.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@ -51,6 +52,14 @@ def parse_arguments():
|
|||||||
subparsers.add_parser('usage-info',
|
subparsers.add_parser('usage-info',
|
||||||
help='Get information about disk space usage')
|
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
|
subparsers.required = True
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
@ -316,6 +325,19 @@ def subcommand_usage_info(_):
|
|||||||
subprocess.run(command, check=True)
|
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():
|
def main():
|
||||||
"""Parse arguments and perform all duties."""
|
"""Parse arguments and perform all duties."""
|
||||||
arguments = parse_arguments()
|
arguments = parse_arguments()
|
||||||
|
|||||||
@ -28,7 +28,7 @@ from plinth import action_utils, actions
|
|||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
from plinth import frontpage, menu
|
from plinth import frontpage, menu
|
||||||
from plinth.daemon import Daemon
|
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.modules.firewall.components import Firewall
|
||||||
from plinth.utils import format_lazy
|
from plinth.utils import format_lazy
|
||||||
|
|
||||||
@ -106,7 +106,6 @@ def init():
|
|||||||
def setup(helper, old_version=None):
|
def setup(helper, old_version=None):
|
||||||
"""Install and configure the module."""
|
"""Install and configure the module."""
|
||||||
helper.install(managed_packages)
|
helper.install(managed_packages)
|
||||||
create_group('freedombox-share')
|
|
||||||
helper.call('post', actions.superuser_run, 'samba', ['setup'])
|
helper.call('post', actions.superuser_run, 'samba', ['setup'])
|
||||||
helper.call('post', app.enable)
|
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.
|
FreedomBox app to manage users.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import grp
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
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 import cfg, menu
|
||||||
from plinth.utils import format_lazy
|
from plinth.utils import format_lazy
|
||||||
|
|
||||||
version = 2
|
version = 3
|
||||||
|
|
||||||
is_essential = True
|
is_essential = True
|
||||||
|
|
||||||
@ -92,6 +93,7 @@ def setup(helper, old_version=None):
|
|||||||
if not old_version:
|
if not old_version:
|
||||||
helper.call('post', actions.superuser_run, 'users', ['first-setup'])
|
helper.call('post', actions.superuser_run, 'users', ['first-setup'])
|
||||||
helper.call('post', actions.superuser_run, 'users', ['setup'])
|
helper.call('post', actions.superuser_run, 'users', ['setup'])
|
||||||
|
create_group('freedombox-share')
|
||||||
|
|
||||||
|
|
||||||
def diagnose():
|
def diagnose():
|
||||||
@ -148,3 +150,16 @@ def get_last_admin_user():
|
|||||||
return admin_users[0]
|
return admin_users[0]
|
||||||
|
|
||||||
return None
|
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