#!/usr/bin/env bash # ========================================================== # ProxMenux - Global Share Functions (reusable) # File: scripts/global/share_common.func # ========================================================== if [[ -n "${__PROXMENUX_SHARE_COMMON__}" ]]; then return 0 fi __PROXMENUX_SHARE_COMMON__=1 : "${PROXMENUX_DEFAULT_SHARE_GROUP:=sharedfiles}" : "${PROXMENUX_SHARE_MAP_DB:=/usr/local/share/proxmenux/share-map.db}" mkdir -p "$(dirname "$PROXMENUX_SHARE_MAP_DB")" 2>/dev/null || true touch "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true pmx_share_map_get() { local key="$1" awk -F'=' -v k="$key" '$1==k {print $2}' "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null | tail -n1 } pmx_share_map_set() { local key="$1" val="$2" sed -i "\|^${key}=|d" "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true echo "${key}=${val}" >> "$PROXMENUX_SHARE_MAP_DB" } pmx_choose_or_create_group__() { local default_group="${1:-$PROXMENUX_DEFAULT_SHARE_GROUP}" local choice group_name choice=$(whiptail --title "$(translate "Shared Group")" --menu "$(translate "Choose a group policy for this shared directory:")" 18 78 6 \ "1" "$(translate "Use sharefiles group default (recommended)")" \ "2" "$(translate "Create a new group for isolation")" \ "3" "$(translate "Select an existing group")" \ 3>&1 1>&2 2>&3) || { echo ""; return 1; } case "$choice" in 1) pmx_ensure_host_group "$default_group" >/dev/null || return 1 echo "$default_group" ;; 2) group_name=$(whiptail --inputbox "$(translate "Enter new group name:")" 10 70 "sharedfiles-project" --title "$(translate "New Group")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$group_name" ]] && { msg_error "$(translate "Group name cannot be empty.")"; echo ""; return 1; } pmx_ensure_host_group "$group_name" >/dev/null || return 1 echo "$group_name" ;; 3) groups=$(getent group | awk -F: '$3 >= 1000 && $1 != "nogroup" && $1 !~ /^pve/ {print $1 ":" $3}') if [[ -z "$groups" ]]; then whiptail --title "$(translate "Groups")" --msgbox "$(translate "No user groups found.")" 8 60 echo ""; return 1 fi menu_args=() while IFS=: read -r gname gid; do menu_args+=("$gname" "GID=$gid") done <<< "$groups" group_name=$(whiptail --title "$(translate "Existing Groups")" --menu "$(translate "Select an existing group:")" 20 70 12 \ "${menu_args[@]}" \ 3>&1 1>&2 2>&3) || { echo ""; return 1; } pmx_ensure_host_group "$group_name" >/dev/null || return 1 echo "$group_name" ;; *) echo ""; return 1;; esac } pmx_ensure_host_group__() { local group_name="$1" local suggested_gid="$2" if ! getent group "$group_name" >/dev/null 2>&1; then if [[ -n "$suggested_gid" ]]; then if getent group "$suggested_gid" >/dev/null 2>&1; then msg_error "$(translate "GID already in use:") $suggested_gid" return 1 fi groupadd -g "$suggested_gid" "$group_name" >/dev/null 2>&1 else groupadd "$group_name" >/dev/null 2>&1 fi if [[ $? -eq 0 ]]; then msg_ok "$(translate "Group created:") $group_name" else msg_error "$(translate "Failed to create group:") $group_name" return 1 fi fi local gid gid="$(getent group "$group_name" | cut -d: -f3)" if [[ -z "$gid" ]]; then msg_error "$(translate "Failed to resolve group GID for") $group_name" return 1 fi echo "$gid" return 0 } pmx_choose_or_create_group() { local default_group="${1:-$PROXMENUX_DEFAULT_SHARE_GROUP}" local choice group_name groups menu_args gid_min # Detect GID_MIN (fallback 1000) gid_min="$(awk '/^\s*GID_MIN\s+[0-9]+/ {print $2}' /etc/login.defs 2>/dev/null | tail -n1)" [[ -z "$gid_min" ]] && gid_min=1000 choice=$(whiptail --title "$(translate "Shared Group")" \ --menu "$(translate "Choose a group policy for this shared directory:")" 18 78 6 \ "1" "$(translate "Use default group:") $default_group $(translate "(recommended)")" \ "2" "$(translate "Create a new group for isolation")" \ "3" "$(translate "Select an existing group")" \ 3>&1 1>&2 2>&3) || { echo ""; return 1; } case "$choice" in 1) # Ensure exists; auto GID >= 101000 if creation is needed pmx_ensure_host_group "$default_group" >/dev/null || { echo ""; return 1; } echo "$default_group" ;; 2) group_name=$(whiptail --inputbox "$(translate "Enter new group name:")" 10 70 "sharedfiles-project" \ --title "$(translate "New Group")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } if [[ -z "$group_name" ]]; then msg_error "$(translate "Group name cannot be empty.")" echo ""; return 1 fi # POSIX-ish validation: start with letter/_ ; then letters/digits/_/- if ! [[ "$group_name" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]]; then msg_error "$(translate "Invalid group name. Use letters, digits, underscore or hyphen, and start with a letter or underscore.")" echo ""; return 1 fi pmx_ensure_host_group "$group_name" >/dev/null || { echo ""; return 1; } echo "$group_name" ;; 3) # Build list of real user groups (>= GID_MIN), exclude nogroup and pve* groups=$(getent group | awk -F: -v MIN="$gid_min" ' $3 >= MIN && $1 != "nogroup" && $1 !~ /^pve/ {print $0} ' | sort -t: -k1,1) if [[ -z "$groups" ]]; then whiptail --title "$(translate "Groups")" --msgbox "$(translate "No user groups found.")" 8 60 echo ""; return 1 fi menu_args=() while IFS=: read -r gname _ gid members; do menu_args+=("$gname" "GID=$gid") done <<< "$groups" group_name=$(whiptail --title "$(translate "Existing Groups")" \ --menu "$(translate "Select an existing group:")" 20 70 12 \ "${menu_args[@]}" 3>&1 1>&2 2>&3) || { echo ""; return 1; } # Ensure (no-op if exists) pmx_ensure_host_group "$group_name" >/dev/null || { echo ""; return 1; } echo "$group_name" ;; *) echo ""; return 1 ;; esac } pmx_ensure_host_group() { local group_name="$1" local suggested_gid="${2:-}" local base_gid=101000 local new_gid gid # Si ya existe, devuelve su GID if getent group "$group_name" >/dev/null 2>&1; then gid="$(getent group "$group_name" | cut -d: -f3)" echo "$gid" return 0 fi if [[ -n "$suggested_gid" ]]; then # Verifica que el GID sugerido esté libre if getent group "$suggested_gid" >/dev/null 2>&1; then msg_error "$(translate "GID already in use:") $suggested_gid" echo "" return 1 fi if ! groupadd -g "$suggested_gid" "$group_name" >/dev/null 2>&1; then msg_error "$(translate "Failed to create group:") $group_name" echo "" return 1 fi msg_ok "$(translate "Group created:") $group_name" else # Busca el primer GID libre >= 101000 new_gid="$base_gid" while getent group "$new_gid" >/dev/null 2>&1; do new_gid=$((new_gid+1)) done if ! groupadd -g "$new_gid" "$group_name" >/dev/null 2>&1; then msg_error "$(translate "Failed to create group:") $group_name" echo "" return 1 fi msg_ok "$(translate "Group created:") $group_name" fi gid="$(getent group "$group_name" | cut -d: -f3)" if [[ -z "$gid" ]]; then msg_error "$(translate "Failed to resolve group GID for") $group_name" echo "" return 1 fi echo "$gid" return 0 } pmx_prepare_host_shared_dir() { local dir="$1" group_name="$2" [[ -z "$dir" || -z "$group_name" ]] && { msg_error "$(translate "Internal error: missing arguments in pmx_prepare_host_shared_dir")"; return 1; } if [[ ! -d "$dir" ]]; then if mkdir -p "$dir" 2>/dev/null; then msg_ok "$(translate "Created directory on host:") $dir" else msg_error "$(translate "Failed to create directory on host:") $dir" return 1 fi fi chown -R root:"$group_name" "$dir" 2>/dev/null || true chmod -R 2775 "$dir" 2>/dev/null || true if command -v setfacl >/dev/null 2>&1; then setfacl -R -m d:g:"$group_name":rwx -m d:o::rx -m g:"$group_name":rwx "$dir" 2>/dev/null || true msg_ok "$(translate "Default ACLs applied for group inheritance.")" else msg_warn "$(translate "setfacl not found; default ACLs were not applied.")" fi return 0 } pmx_select_host_mount_point_() { local title="${1:-$(translate "Select Mount Point")}" local default_path="${2:-/mnt/shared}" local choice folder_name result while true; do choice=$(whiptail --title "$title" --menu "$(translate "Where do you want the host folder?")" 16 76 4 \ "mnt" "$(translate "Create folder in /mnt")" \ "srv" "$(translate "Create folder in /srv")" \ "media" "$(translate "Create folder in /media")" \ "custom" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } case "$choice" in mnt) folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 70 "shared" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$folder_name" ]] && continue echo "/mnt/$folder_name"; return 0;; srv) folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /srv:")" 10 70 "shared" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$folder_name" ]] && continue echo "/srv/$folder_name"; return 0;; media) folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /media:")" 10 70 "shared" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$folder_name" ]] && continue echo "/media/$folder_name"; return 0;; custom) result=$(whiptail --inputbox "$(translate "Enter full path:")" 10 80 "$default_path" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$result" ]] && continue echo "$result"; return 0;; esac done } pmx_select_host_mount_point__() { local title="${1:-$(translate "Select Mount Point")}" local default_path="${2:-/mnt/shared}" local choice folder_name result while true; do choice=$(whiptail --title "$title" --menu "$(translate "Where do you want the host folder?")" 16 76 4 \ "mnt" "$(translate "Create folder in /mnt")" \ "srv" "$(translate "Create folder in /srv")" \ "media" "$(translate "Create folder in /media")" \ "custom" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } case "$choice" in mnt) folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 70 "$(basename "$default_path")" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$folder_name" ]] && continue echo "/mnt/$folder_name"; return 0;; srv) folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /srv:")" 10 70 "$(basename "$default_path")" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$folder_name" ]] && continue echo "/srv/$folder_name"; return 0;; media) folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /media:")" 10 70 "$(basename "$default_path")" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$folder_name" ]] && continue echo "/media/$folder_name"; return 0;; custom) result=$(whiptail --inputbox "$(translate "Enter full path:")" 10 80 "$default_path" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$result" ]] && continue echo "$result"; return 0;; esac done } pmx_select_host_mount_point() { local title="${1:-$(translate "Select Mount Point")}" local default_path="${2:-/mnt/shared}" local context="${3:-local}" local choice folder_name result existing_dirs mount_point while true; do choice=$(whiptail --title "$title" --menu "$(translate "Where do you want the host folder?")" 16 76 3 \ "1" "$(translate "Create new folder in /mnt")" \ "2" "$(translate "Use existing folder")" \ "3" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } case "$choice" in 1) folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 70 "$(basename "$default_path")" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$folder_name" ]] && continue mount_point="/mnt/$folder_name" echo "$mount_point"; return 0 ;; 2) existing_dirs=($(ls -1d /mnt/*/ 2>/dev/null | sed 's:/$::')) if [[ ${#existing_dirs[@]} -eq 0 ]]; then whiptail --msgbox "$(translate "No existing folders found in /mnt")" 8 60 continue fi mount_point=$(whiptail --title "$(translate "Select Existing Folder")" \ --menu "$(translate "Choose a folder in /mnt:")" 20 70 10 \ $(for d in "${existing_dirs[@]}"; do echo "$d" "$(basename "$d")"; done) \ 3>&1 1>&2 2>&3) || continue if [[ "$context" =~ ^(nfs|samba)$ ]] && [[ -n "$(ls -A "$mount_point" 2>/dev/null)" ]]; then whiptail --yesno "$(translate "Warning: The selected folder is not empty. Files may not be accessible once the network share is mounted. Proceed anyway?")" 12 70 || continue fi echo "$mount_point"; return 0 ;; 3) result=$(whiptail --inputbox "$(translate "Enter full path:")" 10 80 "$default_path" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } [[ -z "$result" ]] && continue echo "$result"; return 0 ;; esac done } select_host_directory_() { local method choice result method=$(whiptail --title "$(translate "Select Host Directory")" --menu "$(translate "How do you want to select the host folder to mount?")" 15 70 4 \ "mnt" "$(translate "Select from /mnt directories")" \ "srv" "$(translate "Select from /srv directories")" \ "media" "$(translate "Select from /media directories")" \ "manual" "$(translate "Enter path manually")" 3>&1 1>&2 2>&3) || return 1 case "$method" in mnt|srv|media) local base_path="/$method" local host_dirs=("$base_path"/*) local options=() for dir in "${host_dirs[@]}"; do if [[ -d "$dir" ]]; then options+=("$dir" "$(basename "$dir")") fi done if [[ ${#options[@]} -eq 0 ]]; then msg_error "$(translate "No directories found in") $base_path" return 1 fi result=$(whiptail --title "$(translate "Select Host Folder")" \ --menu "$(translate "Select the folder to mount:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3) ;; manual) result=$(whiptail --title "$(translate "Enter Path")" \ --inputbox "$(translate "Enter the full path to the host folder:")" 10 70 "/mnt/" 3>&1 1>&2 2>&3) ;; esac if [[ -z "$result" ]]; then return 1 fi if [[ ! -d "$result" ]]; then msg_error "$(translate "The selected path is not a valid directory:") $result" return 1 fi echo "$result" } select_host_directory() { local method choice result method=$(whiptail --title "$(translate "Select Host Directory")" --menu "$(translate "How do you want to select the host folder to mount?")" 15 70 4 \ "mnt" "$(translate "Select from /mnt directories")" \ "manual" "$(translate "Enter path manually")" 3>&1 1>&2 2>&3) || return 1 case "$method" in mnt|srv|media) local base_path="/$method" local host_dirs=("$base_path"/*) local options=() for dir in "${host_dirs[@]}"; do if [[ -d "$dir" ]]; then options+=("$dir" "$(basename "$dir")") fi done if [[ ${#options[@]} -eq 0 ]]; then msg_error "$(translate "No directories found in") $base_path" return 1 fi result=$(whiptail --title "$(translate "Select Host Folder")" \ --menu "$(translate "Select the folder to mount:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3) ;; manual) result=$(whiptail --title "$(translate "Enter Path")" \ --inputbox "$(translate "Enter the full path to the host folder:")" 10 70 "/mnt/" 3>&1 1>&2 2>&3) ;; esac if [[ -z "$result" ]]; then return 1 fi if [[ ! -d "$result" ]]; then msg_error "$(translate "The selected path is not a valid directory:") $result" return 1 fi echo "$result" } select_lxc_container() { local ct_list ctid ct_status ct_list=$(pct list | awk 'NR>1 {print $1, $2, $3}') if [[ -z "$ct_list" ]]; then dialog --title "$(translate "Error")" \ --msgbox "$(translate "No LXC containers available")" 8 50 return 1 fi local options=() while read -r id name status; do if [[ -n "$id" ]]; then options+=("$id" "$name ($status)") fi done <<< "$ct_list" ctid=$(dialog --title "$(translate "Select LXC Container")" \ --menu "$(translate "Select container:")" 20 70 12 \ "${options[@]}" 3>&1 1>&2 2>&3) if [[ -z "$ctid" ]]; then return 1 fi echo "$ctid" return 0 }