Need help for mounting smb share with Python

Hi there,

I’m trying to create a graphical interface to mount some SMB share when few applications aren’t able to access them (snap, flatpak, etc.).
While Dolphin managed shares easily, some applications like ansel (a darktable fork) is only available as a snap file, so it’s unable to open collection of photos on the server.

However, I’ve tried to create something with Claude 3.5 AI which seems right, but it cannot create the folder in /mnt. Could someone help me?

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk, messagebox
import subprocess
import os

class SMBMounter:
    def __init__(self, root):
        self.root = root
        self.root.title("Montage des partages Lagrange")

        # Configuration
        self.server = "lagrange"
        self.mount_base = "/mnt/lagrange"
        self.shares = {
            "photo": {"path": "photo", "mounted": False},
            "music": {"path": "music", "mounted": False},
            "video": {"path": "video", "mounted": False},
            "commun": {"path": "commun", "mounted": False}
        }

        # Création des points de montage au démarrage
        self.create_mount_points()

        # Création de l'interface
        self.create_widgets()

        # Vérification initiale des montages
        self.check_mounted_shares()

    def create_mount_points(self):
        """Crée les points de montage nécessaires"""
        try:
            # Création du répertoire de base
            if not os.path.exists(self.mount_base):
                subprocess.run(['sudo', 'mkdir', '-p', self.mount_base])

            # Création des sous-répertoires pour chaque partage
            for share in self.shares:
                mount_point = f"{self.mount_base}/{share}"
                if not os.path.exists(mount_point):
                    subprocess.run(['sudo', 'mkdir', '-p', mount_point])
                    subprocess.run(['sudo', 'chmod', '777', mount_point])
        except Exception as e:
            messagebox.showerror("Erreur", f"Erreur lors de la création des points de montage: {str(e)}")

    def create_widgets(self):
        # Frame principal avec padding
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # Style pour les widgets
        style = ttk.Style()
        style.configure('TCheckbutton', padding=5)
        style.configure('TButton', padding=5)

        # Variables pour les checkboxes
        self.check_vars = {}

        # Création des checkboxes pour chaque partage
        for idx, share in enumerate(self.shares):
            self.check_vars[share] = tk.BooleanVar()
            cb = ttk.Checkbutton(
                main_frame,
                text=f"Partage \\{share}",
                variable=self.check_vars[share]
            )
            cb.grid(row=idx, column=0, sticky=tk.W, pady=2)

        # Boutons avec plus d'espace
        ttk.Button(
            main_frame,
            text="Monter la sélection",
            command=self.mount_selected
        ).grid(row=len(self.shares), column=0, pady=10)

        ttk.Button(
            main_frame,
            text="Tout démonter",
            command=self.unmount_all
        ).grid(row=len(self.shares) + 1, column=0, pady=5)

    def mount_selected(self):
        """Monte les partages sélectionnés"""
        for share, var in self.check_vars.items():
            if var.get():
                mount_point = f"{self.mount_base}/{share}"

                # Commande de montage avec options étendues
                cmd = [
                    'sudo', 'mount', '-t', 'cifs',
                    f'//{self.server}/{share}',
                    mount_point,
                    '-o', 'guest,uid=$(id -u),gid=$(id -g)'
                ]

                try:
                    result = subprocess.run(cmd, capture_output=True, text=True)
                    if result.returncode == 0:
                        messagebox.showinfo("Succès", f"Partage {share} monté avec succès")
                    else:
                        messagebox.showerror("Erreur",
                            f"Erreur lors du montage de {share}\n{result.stderr}")
                except Exception as e:
                    messagebox.showerror("Erreur", str(e))

        self.check_mounted_shares()

    def unmount_all(self):
        """Démonte tous les partages"""
        for share in self.shares:
            mount_point = f"{self.mount_base}/{share}"
            try:
                subprocess.run(['sudo', 'umount', mount_point], capture_output=True)
            except Exception as e:
                messagebox.showerror("Erreur", f"Erreur lors du démontage de {share}: {str(e)}")

        self.check_mounted_shares()

    def check_mounted_shares(self):
        """Vérifie quels partages sont montés"""
        with open('/proc/mounts', 'r') as f:
            mounts = f.read()
            for share in self.shares:
                mount_point = f"{self.mount_base}/{share}"
                self.shares[share]["mounted"] = mount_point in mounts

if __name__ == "__main__":
    root = tk.Tk()
    app = SMBMounter(root)
    root.mainloop()

Heya,

Not sure how relevant this is still:
https://askubuntu.com/questions/1033344/how-to-give-snaps-access-to-somedir

But is seems snaps are confined to the homedrive of the user. The post lists some options to get around this that could be incorporated in the script. There is a ansel-git in the AUR that could negate the need for a snap.

1 Like

Perhaps you can use the idea presented in → [root tip] [Utility Script] GIO mount samba share

1 Like

Well, that’s a perfect example. I’ve adapted it to my Python script and it works like a charmed.

Here is the full code, hope it would be useful to everyone. Shares are in ~/SMBLinks/

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk, messagebox
import subprocess
import os
import getpass
from pathlib import Path

class SMBMounter:
    def __init__(self, root):
        self.root = root
        self.root.title("SMB Share Mounter")

        # Configuration
        self.default_server = "lagrange"
        self.server = self.default_server
        self.username = getpass.getuser()
        self.smb_links = Path.home() / "SMBLinks"
        self.credentials_dir = Path.home() / ".credentials"

        # Default shares if detection fails
        self.default_shares = {
            "photo": {"mounted": False},
            "music": {"mounted": False},
            "video": {"mounted": False},
            "commun": {"mounted": False}
        }

        # Create main container
        self.main_container = ttk.Frame(self.root, padding="10")
        self.main_container.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # Create server input form
        self.create_server_form()

        # Initialize shares as empty
        self.shares = {}

    def create_server_form(self):
        """Creates the server input form"""
        form_frame = ttk.Frame(self.main_container)
        form_frame.grid(row=0, column=0, pady=10, sticky=(tk.W, tk.E))

        # Server label and entry
        ttk.Label(
            form_frame,
            text="Server:",
            font=('Helvetica', 10)
        ).pack(side=tk.LEFT, padx=5)

        self.server_entry = ttk.Entry(form_frame, width=30)
        self.server_entry.pack(side=tk.LEFT, padx=5)
        self.server_entry.insert(0, self.default_server)

        # Connect button
        ttk.Button(
            form_frame,
            text="Connect",
            command=self.connect_to_server
        ).pack(side=tk.LEFT, padx=5)

    def connect_to_server(self):
        """Connects to the specified server and detects shares"""
        new_server = self.server_entry.get().strip()
        if not new_server:
            messagebox.showerror("Error", "Please enter a server name")
            return

        self.server = new_server

        # Clear previous widgets if they exist
        if hasattr(self, 'main_frame'):
            self.main_frame.destroy()

        # Detect shares on the new server
        self.shares = self.detect_shares()

        # Create necessary directories
        self.setup_directories()

        # Create interface
        self.create_widgets()

        # Check mount status
        self.check_mounted_shares()

    def setup_directories(self):
        """Creates necessary directories"""
        self.smb_links.mkdir(parents=True, exist_ok=True)
        self.credentials_dir.mkdir(parents=True, exist_ok=True)
        self.credentials_dir.chmod(0o700)

    def detect_shares(self):
        """Detects available shares on the server"""
        shares = {}
        try:
            cmd = ["smbclient", "-L", self.server, "-N"]
            result = subprocess.run(cmd, capture_output=True, text=True)

            if result.returncode == 0:
                lines = result.stdout.split('\n')
                for line in lines:
                    if "Disk" in line and not "$" in line:
                        share_name = line.split()[0].strip()
                        shares[share_name] = {"mounted": False}

                if not shares:
                    messagebox.showwarning(
                        "Warning",
                        f"No shares found on {self.server}\nUsing default shares list"
                    )
                    shares = self.default_shares.copy()
            else:
                messagebox.showerror(
                    "Error",
                    f"Unable to list shares on {self.server}:\n{result.stderr}\nUsing default shares list"
                )
                shares = self.default_shares.copy()
        except Exception as e:
            messagebox.showerror(
                "Error",
                f"Error while detecting shares: {str(e)}\nUsing default shares list"
            )
            shares = self.default_shares.copy()

        return shares

    def create_widgets(self):
        """Creates the main interface widgets"""
        self.main_frame = ttk.Frame(self.main_container)
        self.main_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))

        # Header with refresh button
        header_frame = ttk.Frame(self.main_frame)
        header_frame.grid(row=0, column=0, columnspan=2, pady=10)

        ttk.Label(
            header_frame,
            text=f"Available shares on {self.server}",
            font=('Helvetica', 10, 'bold')
        ).pack(side=tk.LEFT, padx=5)

        ttk.Button(
            header_frame,
            text="↻",
            width=3,
            command=self.refresh_shares
        ).pack(side=tk.LEFT, padx=5)

        # Checkboxes for shares
        self.check_vars = {}
        for idx, share in enumerate(self.shares, start=1):
            self.check_vars[share] = tk.BooleanVar()
            cb = ttk.Checkbutton(
                self.main_frame,
                text=f"Share \\{share}",
                variable=self.check_vars[share]
            )
            cb.grid(row=idx, column=0, sticky=tk.W, pady=2)

        # Action buttons
        btn_frame = ttk.Frame(self.main_frame)
        btn_frame.grid(row=len(self.shares) + 2, column=0, columnspan=2, pady=10)

        ttk.Button(
            btn_frame,
            text="Mount Selected",
            command=self.mount_selected
        ).grid(row=0, column=0, padx=5)

        # Add new Unmount Selected button
        ttk.Button(
            btn_frame,
            text="Unmount Selected",
            command=self.unmount_selected
        ).grid(row=0, column=1, padx=5)

        ttk.Button(
            btn_frame,
            text="Unmount All",
            command=self.unmount_all
        ).grid(row=0, column=2, padx=5)

    def refresh_shares(self):
        """Refreshes the list of available shares"""
        # Save current mount states
        mounted_states = {share: self.shares[share]["mounted"]
                         for share in self.shares if share in self.check_vars}

        # Detect shares again
        self.shares = self.detect_shares()

        # Recreate widgets
        if hasattr(self, 'main_frame'):
            self.main_frame.destroy()

        self.create_widgets()

        # Restore mount states
        for share, mounted in mounted_states.items():
            if share in self.check_vars:
                self.check_vars[share].set(mounted)

    def mount_selected(self):
        """Mounts selected shares"""
        for share, var in self.check_vars.items():
            if var.get():
                self.mount_share(share)
        self.check_mounted_shares()

    def mount_share(self, share):
        """Mounts a specific share"""
        try:
            cmd = ["gio", "mount", f"smb://{self.server}/{share}"]
            result = subprocess.run(cmd, capture_output=True, text=True)

            if result.returncode == 0:
                endpoint = Path(f"/run/user/{os.getuid()}/gvfs/smb-share:server={self.server},share={share}")
                link_path = self.smb_links / share

                if endpoint.exists():
                    if link_path.exists():
                        link_path.unlink()
                    link_path.symlink_to(endpoint)

                messagebox.showinfo("Success", f"Share {share} mounted successfully")
                return True
            else:
                messagebox.showerror("Error", f"Error mounting {share}\n{result.stderr}")
                return False

        except Exception as e:
            messagebox.showerror("Error", str(e))
            return False

    def unmount_selected(self):
        """Unmounts only the selected shares"""
        for share, var in self.check_vars.items():
            if var.get():
                self.unmount_share(share)
        self.check_mounted_shares()

    def unmount_share(self, share):
        """Unmounts a specific share"""
        try:
            # Remove symbolic link
            link_path = self.smb_links / share
            if link_path.exists():
                link_path.unlink()

            # Unmount share
            cmd = ["gio", "mount", "-u", f"smb://{self.server}/{share}"]
            subprocess.run(cmd, capture_output=True, check=True)
            return True
        except Exception as e:
            messagebox.showerror("Error", f"Error unmounting {share}: {str(e)}")
            return False

    def check_mounted_shares(self):
        """Checks which shares are currently mounted"""
        for share in self.shares:
            link_path = self.smb_links / share
            self.shares[share]["mounted"] = link_path.exists()

if __name__ == "__main__":
    root = tk.Tk()
    app = SMBMounter(root)
    root.mainloop()


2 Likes

This topic was automatically closed 3 days after the last reply. New replies are no longer allowed.