#!/bin/bash # maravento.com # ################################################################################ # # remote-mount.sh - Nautilus/Caja script to mount/unmount SMB (and FUSE/SFTP) # Improved: mount SMB with progressive SMB version, clean Unmount list, elevation support. # Added: Save/Load connection profiles # ################################################################################ set -euo pipefail # check no-root if [ "$(id -u)" == "0" ]; then echo "[ERROR] This script should not be run as root." exit 1 fi echo "Start Mountman. Wait..." # --- Helpers --- err() { zenity --error --title="Error" --text="$1" exit 1 } info() { zenity --info --title="$2" --text="$1" } # --- Dependency check (minimal) --- for cmd in zenity mount umount; do if ! command -v "$cmd" &>/dev/null; then zenity --error --title="Missing dependency" --text="Command '$cmd' not found. Please install it." exit 1 fi done # --- Elevation command (pkexec preferred, fallback to sudo) --- ELEVATE_CMD="" if command -v pkexec &>/dev/null; then ELEVATE_CMD="pkexec" elif command -v sudo &>/dev/null; then ELEVATE_CMD="sudo -E" else err "Neither pkexec nor sudo found. Cannot elevate privileges." fi # --- Function to check if resource is already mounted --- check_resource_mounted() { local resource="$1" local protocol="$2" case "$protocol" in "smb") mount | grep -qF "//$resource" && return 0 ;; "sftp") mount | grep -qF "@$resource" && return 0 ;; esac return 1 } # --- Main menu --- ACTION=$(zenity --list --radiolist \ --title="Remote Mount" \ --text="Choose an action:" \ --column="Select" --column="Action" \ TRUE "Mount" FALSE "Unmount") || exit 0 # === Unmount Section (improved & clean) === if [[ "$ACTION" == "Unmount" ]]; then MAP=() # Read /proc/mounts: source mountpoint fstype ... while read -r SRC MNT FSTYPE _; do # Skip pseudo filesystems case "$FSTYPE" in proc|sysfs|tmpfs|devtmpfs|devpts|securityfs|cgroup|pstore|mqueue|hugetlbfs|tracefs|overlay|autofs) continue ;; esac # skip portal doc mounts and gvfs transient mounts if [[ "$MNT" =~ ^/run/user/[0-9]+/doc ]] || [[ "$MNT" =~ ^/run/user/[0-9]+/gvfs ]]; then continue fi # Accept common real FS types and network FS case "$FSTYPE" in cifs|smbfs|fuse*|fuseblk|ext4|xfs|btrfs|ntfs|vfat|iso9660) MAP+=("$MNT" "$SRC" "$FSTYPE") ;; *) continue ;; esac done < /proc/mounts if [[ ${#MAP[@]} -eq 0 ]]; then info "No mountpoints found to unmount." "Unmount" exit 0 fi SEL=$(zenity --list \ --title="Unmount" \ --text="Select a mounted directory to unmount:" \ --column="Mountpoint" --column="Source" --column="Type" \ "${MAP[@]}") || exit 0 # Zenity returns selected row with columns separated by | TARGET=$(printf '%s' "$SEL" | cut -d'|' -f1 | sed 's/[[:space:]]*$//') if [[ -z "$TARGET" ]]; then err "No valid mountpoint selected." fi TMPERR=$(mktemp) || err "Failed to create temporary file." if $ELEVATE_CMD umount -f "$TARGET" 2> "$TMPERR"; then info "Unmounted successfully:\n$TARGET" "Unmount" rm -f "$TMPERR" else ERR=$(< "$TMPERR") zenity --error --title="Unmount failed" --width=600 --text="Failed to unmount:\n$TARGET\n\n$ERR" rm -f "$TMPERR" exit 1 fi exit 0 fi # === Mount Section === # --- Ask if user wants to load a saved connection --- LOAD_CONNECTION=$(zenity --question \ --title="Load Connection" \ --text="Do you want to load a saved connection profile?" \ --ok-label="Yes" --cancel-label="No" 2>/dev/null && echo "yes" || echo "no") if [[ "$LOAD_CONNECTION" == "yes" ]]; then # User wants to load a connection file CONN_FILE=$(zenity --file-selection \ --title="Select connection profile" \ --file-filter="Connection files (*.conn)|*.conn" \ --file-filter="All files|*") || exit 0 if [[ ! -f "$CONN_FILE" ]]; then err "Connection file not found." fi # Read connection file using a safe key=value parser (no code execution) while IFS= read -r line; do [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue key="${line%%=*}" value="${line#*=}" key="${key// /}" value="${value%\"}" value="${value#\"}" case "$key" in PROTOCOL|SMB_HOST|SMB_SHARE|SMB_USER|SMB_PASS|SFTP_HOST|SFTP_USER|SFTP_PATH|LOCAL_DIR|FUSE_CMD) printf -v "$key" '%s' "$value" ;; esac done < "$CONN_FILE" CREDS_FILE="${CONN_FILE%.conn}.creds" if [[ -f "$CREDS_FILE" ]]; then while IFS= read -r line; do [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue key="${line%%=*}" value="${line#*=}" key="${key// /}" case "$key" in username) SMB_USER="$value" ;; password) SMB_PASS="$value" ;; esac done < "$CREDS_FILE" fi if [[ -z "${PROTOCOL:-}" ]]; then err "Invalid connection file: PROTOCOL not defined." fi if [[ -n "${LOCAL_DIR:-}" && "$LOCAL_DIR" != /* ]]; then err "Invalid connection file: LOCAL_DIR must be an absolute path." fi else # Normal flow: ask for protocol PROTOCOL=$(zenity --list --radiolist \ --title="Protocol" \ --text="Select protocol:" \ --column="Select" --column="Protocol" \ TRUE "smb" FALSE "sftp" FALSE "fuse") || exit 0 fi # --- SMB Mount (smart, no creds-file prompt) --- if [[ "$PROTOCOL" == "smb" ]]; then # If not loaded from file, ask for connection details if [[ "$LOAD_CONNECTION" != "yes" ]]; then SMB_HOST=$(zenity --entry --title="SMB Host" --text="Enter SMB host (e.g. 192.168.0.10):") || exit 0 SMB_SHARE=$(zenity --entry --title="Share" --text="Enter share name:") || exit 0 SMB_USER="" SMB_PASS="" SMB_USER=$(zenity --entry --title="Username" --text="Username (leave blank for guest):" 2>/dev/null) || true SMB_PASS=$(zenity --entry --hide-text --title="Password" --text="Password (leave blank for guest):" 2>/dev/null) || true LOCAL_DIR=$(zenity --file-selection --directory --title="Select local mount point") || exit 0 # Ask if user wants to save this connection SAVE_CONNECTION=$(zenity --question \ --title="Save Connection" \ --text="Do you want to save this connection profile?" \ --ok-label="Yes" --cancel-label="No" 2>/dev/null && echo "yes" || echo "no") if [[ "$SAVE_CONNECTION" == "yes" ]]; then SAVE_FILE=$(zenity --file-selection --save \ --title="Save connection profile" \ --filename="smb-connection.conn" \ --file-filter="Connection files (*.conn)|*.conn") || true if [[ -n "$SAVE_FILE" ]]; then [[ "$SAVE_FILE" != *.conn ]] && SAVE_FILE="${SAVE_FILE}.conn" CREDS_FILE="${SAVE_FILE%.conn}.creds" (umask 177; cat > "$CREDS_FILE" < "$SAVE_FILE" </dev/null || true sleep 1 else exit 0 fi fi MOUNT_CREDS_FILE="" _CREDS_TEMP=false if [[ -z "${SMB_USER:-}" ]]; then BASE_OPTS="guest,uid=$(id -u),gid=$(id -g),nounix,noserverino,_netdev,nofail" else # Never pass the password as a mount option (visible in ps aux / /proc//cmdline). # Use the saved credentials file if available, otherwise write a temp one (mode 600). if [[ -n "${CREDS_FILE:-}" ]] && [[ -f "$CREDS_FILE" ]]; then MOUNT_CREDS_FILE="$CREDS_FILE" else if [[ -z "${SMB_PASS:-}" ]]; then SMB_PASS=$(zenity --entry --hide-text --title="Password" \ --text="Credentials file not found for '$SMB_USER'. Enter password (leave blank for guest):" \ 2>/dev/null) || true fi MOUNT_CREDS_FILE=$(mktemp) chmod 600 "$MOUNT_CREDS_FILE" printf 'username=%s\npassword=%s\n' "$SMB_USER" "$SMB_PASS" > "$MOUNT_CREDS_FILE" _CREDS_TEMP=true fi BASE_OPTS="credentials=$MOUNT_CREDS_FILE,uid=$(id -u),gid=$(id -g),file_mode=0644,dir_mode=0755,sec=ntlmssp,nounix,noserverino,_netdev,nofail" fi SMB_VERSIONS=("3.1.1" "3.0" "2.0" "1.0") SUCCESS=0 TMPERR=$(mktemp) || err "Failed to create temporary file." for VER in "${SMB_VERSIONS[@]}"; do MOUNT_OPTS="$BASE_OPTS,vers=$VER" # provide minimal environment so mount.cifs behaves correctly if $ELEVATE_CMD env HOME="$HOME" mount -t cifs "//$SMB_HOST/$SMB_SHARE" "$LOCAL_DIR" -o "$MOUNT_OPTS" 2> "$TMPERR"; then SUCCESS=1 break fi done # Remove temp credentials file regardless of mount outcome [[ "$_CREDS_TEMP" == "true" ]] && [[ -n "$MOUNT_CREDS_FILE" ]] && rm -f "$MOUNT_CREDS_FILE" if [[ $SUCCESS -eq 1 ]]; then info "Mounted successfully:\n//$SMB_HOST/$SMB_SHARE -> $LOCAL_DIR\n\n(Protocol version: SMB ${VER:-unknown})" "Mounted" xdg-open "$LOCAL_DIR" >/dev/null 2>&1 & else ERR=$(< "$TMPERR") zenity --error --title="Mount failed" --width=600 --text="Mount failed for //$SMB_HOST/$SMB_SHARE.\n\n$ERR" rm -f "$TMPERR" exit 1 fi rm -f "$TMPERR" exit 0 fi # --- SFTP mount using sshfs (FUSE) --- if [[ "$PROTOCOL" == "sftp" ]]; then # If not loaded from file, ask for connection details if [[ "$LOAD_CONNECTION" != "yes" ]]; then SFTP_HOST=$(zenity --entry --title="SFTP Host" --text="Enter host (e.g. 192.168.0.10):") || exit 0 SFTP_USER="" SFTP_PATH="" SFTP_USER=$(zenity --entry --title="Username" --text="Enter username (optional):" 2>/dev/null) || true SFTP_PATH=$(zenity --entry --title="Remote Path" --text="Enter remote path (optional, e.g. /home/user):" 2>/dev/null) || true LOCAL_DIR=$(zenity --file-selection --directory --title="Select local mount point") || exit 0 # Ask if user wants to save this connection SAVE_CONNECTION=$(zenity --question \ --title="Save Connection" \ --text="Do you want to save this connection profile?" \ --ok-label="Yes" --cancel-label="No" 2>/dev/null && echo "yes" || echo "no") if [[ "$SAVE_CONNECTION" == "yes" ]]; then SAVE_FILE=$(zenity --file-selection --save \ --title="Save connection profile" \ --filename="sftp-connection.conn" \ --file-filter="Connection files (*.conn)|*.conn") || true if [[ -n "$SAVE_FILE" ]]; then # Ensure .conn extension [[ "$SAVE_FILE" != *.conn ]] && SAVE_FILE="${SAVE_FILE}.conn" cat > "$SAVE_FILE" </dev/null; then err "sshfs not installed. Please install sshfs to mount SFTP shares." fi # Check if SFTP host is already mounted if check_resource_mounted "$SFTP_HOST" "sftp"; then MOUNT_INFO=$(mount | grep -F "@$SFTP_HOST") if ! zenity --question \ --title="SFTP Already Mounted" \ --text="This SFTP host is already mounted:\n\n$MOUNT_INFO\n\nDo you want to continue?" \ --ok-label="Continue" --cancel-label="Cancel"; then exit 0 fi fi mkdir -p "$LOCAL_DIR" || err "Failed to create mount point: $LOCAL_DIR" # Check if mount point is already in use if mountpoint -q "$LOCAL_DIR"; then CURRENT_SRC=$(mount | grep -F "$LOCAL_DIR" | awk '{print $1}') if zenity --question \ --title="Mount Point In Use" \ --text="$LOCAL_DIR is already mounted with:\n$CURRENT_SRC\n\nUnmount it first?" \ --ok-label="Unmount" --cancel-label="Cancel"; then fusermount -u "$LOCAL_DIR" 2>/dev/null || true sleep 1 else exit 0 fi fi TARGET="$SFTP_HOST" [[ -n "${SFTP_USER:-}" ]] && TARGET="$SFTP_USER@$SFTP_HOST" [[ -n "${SFTP_PATH:-}" ]] && TARGET="$TARGET:$SFTP_PATH" if sshfs "$TARGET" "$LOCAL_DIR" -o reconnect,ServerAliveInterval=15,uid=$(id -u),gid=$(id -g); then info "Mounted:\n$TARGET -> $LOCAL_DIR" "Mounted" xdg-open "$LOCAL_DIR" >/dev/null 2>&1 & else err "Failed to mount $TARGET via sshfs." fi exit 0 fi # --- Generic FUSE command entry (advanced) --- if [[ "$PROTOCOL" == "fuse" ]]; then # If not loaded from file, ask for connection details if [[ "$LOAD_CONNECTION" != "yes" ]]; then FUSE_CMD="" FUSE_CMD=$(zenity --entry --title="FUSE Command" --text="Enter full FUSE command (example: sshfs user@host:/path):") || exit 0 LOCAL_DIR=$(zenity --file-selection --directory --title="Select local mount point") || exit 0 # Ask if user wants to save this connection SAVE_CONNECTION=$(zenity --question \ --title="Save Connection" \ --text="Do you want to save this connection profile?" \ --ok-label="Yes" --cancel-label="No" 2>/dev/null && echo "yes" || echo "no") if [[ "$SAVE_CONNECTION" == "yes" ]]; then SAVE_FILE=$(zenity --file-selection --save \ --title="Save connection profile" \ --filename="fuse-connection.conn" \ --file-filter="Connection files (*.conn)|*.conn") || true if [[ -n "$SAVE_FILE" ]]; then # Ensure .conn extension [[ "$SAVE_FILE" != *.conn ]] && SAVE_FILE="${SAVE_FILE}.conn" cat > "$SAVE_FILE" </dev/null || true sleep 1 else exit 0 fi fi TMPERR=$(mktemp) || err "Failed to create temporary file." read -ra FUSE_ARGS <<< "$FUSE_CMD" if $ELEVATE_CMD "${FUSE_ARGS[@]}" "$LOCAL_DIR" 2> "$TMPERR"; then info "Mounted via FUSE command." "Mounted" else ERR=$(< "$TMPERR") zenity --error --title="FUSE mount failed" --width=600 --text="Failed to run FUSE command.\n\n$ERR" rm -f "$TMPERR" exit 1 fi rm -f "$TMPERR" exit 0 fi # If reached here, nothing matched (shouldn't happen) exit 0