diff --git a/actions/samba b/actions/samba
index 034584671..3cadf7444 100755
--- a/actions/samba
+++ b/actions/samba
@@ -20,14 +20,16 @@ Configuration helper for samba.
"""
import argparse
+import configparser
+import json
import os
import shutil
import subprocess
import augeas
from plinth import action_utils
+from plinth.modules.samba.manifest import SHARES_CONF_BACKUP_FILE, SHARES_PATH
-SHARES_PATH = '/var/lib/samba/shares'
DEFAULT_FILE = '/etc/default/samba'
CONF_PATH = '/etc/samba/smb-freedombox.conf'
@@ -35,9 +37,10 @@ CONF = r'''
#
# This file is managed and overwritten by Plinth. If you wish to manage
# Samba yourself, disable Samba in Plinth, remove this file and remove
-# the --configfile parameter in /etc/default/samba
+# line with --configfile parameter in /etc/default/samba.
#
-# To view configured samba shares use command `net conf list`
+# Configuration parameters which differ from Debian default configuration
+# are commented. To view configured samba shares use command `net conf list`.
#
[global]
@@ -53,6 +56,8 @@ CONF = r'''
passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .
pam password change = yes
map to guest = bad user
+ # connection inactivity timeout in minutes
+ deadtime = 5
# enable registry based shares
registry shares = yes
''' # noqa: E501
@@ -65,27 +70,85 @@ def parse_arguments():
subparsers.add_parser('setup', help='Configure samba after install')
+ subparsers.add_parser('get-shares', help='Get configured samba shares')
+
+ subparser = subparsers.add_parser('add-share', help='Add new samba share')
+ subparser.add_argument('--mount-point', help='Path of the mount point',
+ required=True)
+ subparser.add_argument('--windows-filesystem', required=False,
+ default=False, action='store_true',
+ help='Path is Windows filesystem')
+
+ subparser = subparsers.add_parser(
+ 'delete-share', help='Delete a samba share configuration')
+ subparser.add_argument('--mount-point', help='Path of the mount point',
+ required=True)
+
+ subparsers.add_parser('dump-shares',
+ help='Dump share configuration to file')
+ subparsers.add_parser('restore-shares',
+ help='Restore share configuration from file')
+
subparsers.required = True
return parser.parse_args()
-def _share_conf(parameters, **kwargs):
- """Run samba registry edit command."""
+def _conf_command(parameters, **kwargs):
+ """Run samba configuration registry command."""
subprocess.check_call(['net', 'conf'] + parameters, **kwargs)
-def _create_open_share(name, path):
- """Create an open samba share."""
+def _create_share(mount_point, windows_filesystem=False):
+ """Create a samba share."""
+ open_share_path = os.path.join(mount_point, SHARES_PATH, 'open_share')
+ os.makedirs(open_share_path, exist_ok=True)
+
+ # FAT and NTFS partitions don't support chown and chmod
+ if not windows_filesystem:
+ shutil.chown(open_share_path, group='sambashare')
+ os.chmod(open_share_path, 0o2775)
+
+ share_name = _create_share_name(mount_point)
+ _define_open_share(share_name, open_share_path, windows_filesystem)
+
+
+def _define_open_share(name, path, windows_filesystem=False):
+ """Define an open samba share."""
try:
- _share_conf(['delshare', name], stderr=subprocess.DEVNULL)
+ _conf_command(['delshare', name], stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError:
pass
- _share_conf(['addshare', name, path, 'writeable=y', 'guest_ok=y'])
- _share_conf(['setparm', name, 'force group', 'sambashare'])
- _share_conf(['setparm', name, 'inherit permissions', 'yes'])
+ _conf_command(['addshare', name, path, 'writeable=y', 'guest_ok=y'])
+ if not windows_filesystem:
+ _conf_command(['setparm', name, 'force group', 'sambashare'])
+ _conf_command(['setparm', name, 'inherit permissions', 'yes'])
-def _use_config_file(conf):
+def _create_share_name(mount_point):
+ """Create a share name."""
+ share_name = os.path.basename(mount_point)
+ if not share_name:
+ share_name = "disk"
+
+ return share_name
+
+
+def _get_shares():
+ """Get shares"""
+ shares = []
+ command = ['net', 'conf', 'list']
+ output = subprocess.check_output(command)
+ config = configparser.ConfigParser()
+ config.read_string(output.decode())
+ for name in config.sections():
+ mount_point = config[name]['path'].split(SHARES_PATH)[0]
+ mount_point = os.path.normpath(mount_point)
+ shares.append(dict(name=name, mount_point=mount_point))
+
+ return shares
+
+
+def _use_config_file(conf_file):
"""Set samba configuration file location."""
aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
@@ -94,41 +157,74 @@ def _use_config_file(conf):
aug.load()
aug.set('/files' + DEFAULT_FILE + '/SMBDOPTIONS',
- '--configfile={0}'.format(conf))
+ '--configfile={0}'.format(conf_file))
aug.save()
+def _validate_mount_point(path):
+ """Validate that given path string is a mount point."""
+ if path != '/':
+ parent_path = os.path.dirname(path)
+ if os.stat(path).st_dev == os.stat(parent_path).st_dev:
+ raise RuntimeError('Path "{0}" is not a mount point.'.format(path))
+
+
+def subcommand_add_share(arguments):
+ """Create a samba share."""
+ mount_point = os.path.normpath(arguments.mount_point)
+ _validate_mount_point(mount_point)
+ _create_share(mount_point, arguments.windows_filesystem)
+
+
+def subcommand_delete_share(arguments):
+ """Delete a samba share configuration."""
+ mount_point = os.path.normpath(arguments.mount_point)
+ shares = _get_shares()
+ for share in shares:
+ if share['mount_point'] == mount_point:
+ _conf_command(['delshare', share['name']])
+ # restart samba to disconnect all users
+ action_utils.service_restart('smbd')
+ break
+ else:
+ raise RuntimeError(
+ 'Mount path "{0}" is not shared.'.format(mount_point))
+
+
+def subcommand_get_shares(_):
+ """Get samba shares."""
+ print(json.dumps(_get_shares()))
+
+
def subcommand_setup(_):
- """Configure samba after install."""
- try:
- os.mkdir(SHARES_PATH)
- except FileExistsError:
- pass
-
- open_share_path = os.path.join(SHARES_PATH, 'open_share')
- try:
- os.mkdir(open_share_path)
- except FileExistsError:
- pass
- # set folder group writable, 2 turns on the setGID bit
- #
- # TODO: some filesystems doesn't support chown and chmod
- # (and it is not needed if mounted with correct parameters)
- shutil.chown(open_share_path, group='sambashare')
- os.chmod(open_share_path, 0o2775)
-
- # use custom samba config file
+ """Configure samba. Use custom samba config file."""
with open(CONF_PATH, 'w') as file_handle:
file_handle.write(CONF)
_use_config_file(CONF_PATH)
- _create_open_share('freedombox-open-share', open_share_path)
- action_utils.service_restart('smbd')
+ if action_utils.service_is_running('smbd'):
+ action_utils.service_restart('smbd')
+
+
+def subcommand_dump_shares(_):
+ """Dump registy share configuration."""
+ os.makedirs(os.path.dirname(SHARES_CONF_BACKUP_FILE), exist_ok=True)
+ with open(SHARES_CONF_BACKUP_FILE, 'w') as backup_file:
+ command = ['net', 'conf', 'list']
+ subprocess.run(command, stdout=backup_file, check=True)
+
+
+def subcommand_restore_shares(_):
+ """Restore registy share configuration."""
+ if not os.path.exists(SHARES_CONF_BACKUP_FILE):
+ raise RuntimeError(
+ 'Backup file {0} does not exist.'.format(SHARES_CONF_BACKUP_FILE))
+ _conf_command(['drop'])
+ _conf_command(['import', SHARES_CONF_BACKUP_FILE])
def main():
"""Parse arguments and perform all duties."""
arguments = parse_arguments()
-
subcommand = arguments.subcommand.replace('-', '_')
subcommand_method = globals()['subcommand_' + subcommand]
subcommand_method(arguments)
diff --git a/plinth/modules/samba/__init__.py b/plinth/modules/samba/__init__.py
index 4ec03b8be..f18d946a4 100644
--- a/plinth/modules/samba/__init__.py
+++ b/plinth/modules/samba/__init__.py
@@ -18,6 +18,7 @@
FreedomBox app to configure samba.
"""
+import json
import socket
from django.urls import reverse_lazy
@@ -34,7 +35,7 @@ from .manifest import backup, clients # noqa, pylint: disable=unused-import
version = 1
-managed_services = ['smbd']
+managed_services = ['smbd', 'nmbd']
managed_packages = ['samba']
@@ -43,12 +44,13 @@ name = _('Samba')
short_description = _('Samba File Sharing')
description = [
- _('Samba file sharing allows to share files between computers in your '
- 'local network. '),
+ _('Samba allows to share files and folders between computers in your '
+ 'local network.'),
format_lazy(
- _('If enabled, Samba share will be available at \\\\{hostname} on '
- 'Windows and smb://{hostname} on Linux and Mac'),
- hostname=socket.gethostname()),
+ _('After installation, you can choose which disks to use for sharing. '
+ 'Enabled {hostname} shares are open to everyone in your local '
+ 'network and are accessible under Network section in the file '
+ 'manager.'), hostname=socket.gethostname().upper())
]
clients = clients
@@ -81,6 +83,9 @@ class SambaApp(app_module.App):
daemon = Daemon('daemon-samba', managed_services[0])
self.add(daemon)
+ daemon_nmbd = Daemon('daemon-samba-nmbd', managed_services[1])
+ self.add(daemon_nmbd)
+
def init():
"""Initialize the module."""
@@ -111,3 +116,35 @@ def diagnose():
results.append(action_utils.diagnose_port_listening(445, 'tcp6'))
return results
+
+
+def add_share(mount_point, filesystem):
+ """Add a share."""
+ command = ['add-share', '--mount-point', mount_point]
+ if filesystem in ['ntfs', 'vfat']:
+ command = command + ['--windows-filesystem']
+ actions.superuser_run('samba', command)
+
+
+def delete_share(mount_point):
+ """Delete a share."""
+ command = ['delete-share', '--mount-point', mount_point]
+ actions.superuser_run('samba', command)
+
+
+def get_shares():
+ """Get defined shares."""
+ output = actions.superuser_run('samba', ['get-shares'])
+
+ return json.loads(output)
+
+
+def backup_pre(packet):
+ """Save registry share configuration."""
+ actions.superuser_run('samba', ['dump-shares'])
+
+
+def restore_post(packet):
+ """Restore configuration."""
+ actions.superuser_run('samba', ['setup'])
+ actions.superuser_run('samba', ['restore-config'])
diff --git a/plinth/modules/samba/manifest.py b/plinth/modules/samba/manifest.py
index 8275de1da..8f50e3263 100644
--- a/plinth/modules/samba/manifest.py
+++ b/plinth/modules/samba/manifest.py
@@ -14,10 +14,22 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see
+ {% blocktrans trimmed %} + NB! Selecting device does not share the whole disk, only the folder + FreedomBox/shares/open_share will be shared on that disk. + {% endblocktrans %} +
+| + | {% trans "Device" %} | +{% trans "Label" %} | +{% trans "Mount Point" %} | +{% trans "Type" %} | +{% trans "Used" %} | +
|---|---|---|---|---|---|
| + {% if disk.mount_point in shared_mounts %} + + {% else %} + + {% endif %} + | +{{ disk.device }} | +{{ disk.label|default_if_none:"" }} | +{{ disk.mount_point }} | +{{ disk.filesystem_type }} | +
+
+ {% if disk.percent_used < 75 %}
+
+
+ {{ disk.used_str }} / {{ disk.size_str }}
+ |
+
+ {% blocktrans trimmed %} + If the disk is plugged back in, sharing will be automatically enabled. + {% endblocktrans %} +
+| {% trans "Share name" %} | +{% trans "Action" %} | +
|---|---|
| {{ share.name }} | ++ + | +