#!/usr/bin/env bash # ========================================================== # ProxMenux - Global Share Functions (reusable) # File: scripts/global/share_common.func # ========================================================== # All console-visible strings MUST be wrapped with $(translate "..."). # Comments and code are in English, as requested. # Requires: utils.sh (show_proxmenux_logo, msg_info, msg_ok, msg_warn, msg_error, msg_title, etc.) # ========================================================== # Guard against multiple sourcing if [[ -n "${__PROXMENUX_SHARE_COMMON__}" ]]; then return 0 fi __PROXMENUX_SHARE_COMMON__=1 # Default group name used for shared folders (can be overridden by caller) : "${PROXMENUX_DEFAULT_SHARE_GROUP:=sharedfiles}" # Where to store simple mappings (dir -> group) to remember choices (optional) : "${PROXMENUX_SHARE_MAP_DB:=/usr/local/share/proxmenux/share-map.db}" # Ensure the mapping DB exists (best-effort) mkdir -p "$(dirname "$PROXMENUX_SHARE_MAP_DB")" 2>/dev/null || true touch "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true # Read mapping: returns group name for a given path pmx_share_map_get() { # comments in English # Usage: pmx_share_map_get "/mnt/myshare" local key="$1" awk -F'=' -v k="$key" '$1==k {print $2}' "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null | tail -n1 } # Write mapping: persist association between directory and group pmx_share_map_set() { # comments in English # Usage: pmx_share_map_set "/mnt/myshare" "sharedfiles" local key="$1" val="$2" # Remove any existing line for this key sed -i "\|^${key}=|d" "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true echo "${key}=${val}" >> "$PROXMENUX_SHARE_MAP_DB" } # Select container from available LXC containers select_container() { local containers_raw containers_formatted=() containers_raw=$(pct list 2>/dev/null | awk 'NR>1 {print $1 " " $3}') if [[ -z "$containers_raw" ]]; then msg_error "$(translate 'No containers available in Proxmox.')" return 1 fi # Format containers for whiptail menu while read -r line; do if [[ -n "$line" ]]; then local ctid=$(echo "$line" | awk '{print $1}') local name=$(echo "$line" | awk '{print $2}') containers_formatted+=("$ctid" "$name") fi done <<< "$containers_raw" if [[ ${#containers_formatted[@]} -eq 0 ]]; then msg_error "$(translate 'No containers available in Proxmox.')" return 1 fi local container_id container_id=$(whiptail --title "$(translate 'Select Container')" \ --menu "$(translate 'Select the LXC container:')" 20 70 10 \ "${containers_formatted[@]}" 3>&1 1>&2 2>&3) if [[ -z "$container_id" ]]; then msg_error "$(translate 'No container selected.')" return 1 fi if ! pct list 2>/dev/null | awk 'NR>1 {print $1}' | grep -qw "$container_id"; then msg_error "$(translate 'Container with ID') $container_id $(translate 'does not exist.')" return 1 fi msg_ok "$(translate 'Container selected:') $container_id" echo "$container_id" } # Validate container ID and handle running state validate_container_id() { local ctid="$1" if [[ -z "$ctid" ]]; then msg_error "$(translate 'Container ID not defined. Make sure to select a container first.')" return 1 fi local status status=$(pct status "$ctid" 2>/dev/null | awk '{print $2}') if [[ "$status" == "running" ]]; then if whiptail --yesno "$(translate 'Container is running. Stop it to apply mount configuration?')" 10 70 --title "$(translate 'Container Running')"; then msg_info "$(translate 'Stopping the container before applying configuration...')" if pct stop "$ctid" 2>/dev/null; then msg_ok "$(translate 'Container stopped.')" else msg_error "$(translate 'Failed to stop container.')" return 1 fi else msg_warn "$(translate 'Mount will be applied to config but requires container restart.')" fi fi return 0 } # Get container UID shift (for unprivileged containers) pmx_get_container_uid_shift() { local ctid="$1" local conf="/etc/pve/lxc/${ctid}.conf" if [[ ! -f "$conf" ]]; then echo "0" return 0 fi # Get UID shift from lxc.idmap configuration local uid_shift uid_shift=$(grep "^lxc.idmap" "$conf" | grep 'u 0' | awk '{print $5}' | head -1) # Default to 0 for privileged containers, 100000 for unprivileged if [[ -z "$uid_shift" ]]; then local unpriv unpriv=$(awk '/^unprivileged:/ {print $2}' "$conf" 2>/dev/null) if [[ "$unpriv" == "1" ]]; then echo "100000" else echo "0" fi else echo "$uid_shift" fi } # Get container users and their host mappings pmx_get_container_users_mapping() { local ctid="$1" local uid_shift uid_shift=$(pmx_get_container_uid_shift "$ctid") # Get all users from container and filter root or UID >= 1000 pct exec "$ctid" -- getent passwd 2>/dev/null | while IFS=: read -r username _ uid gid _ home _; do if [[ "$uid" -eq 0 ]] || [[ "$uid" -ge 1000 ]]; then local real_uid=$((uid_shift + uid)) local real_gid=$((uid_shift + gid)) echo "$username:$uid:$gid:$real_uid:$real_gid" fi done } # Check if container is unprivileged pmx_is_container_unprivileged() { local ctid="$1" local conf="/etc/pve/lxc/${ctid}.conf" if [[ ! -f "$conf" ]]; then echo "false" return 0 fi local unpriv unpriv=$(awk '/^unprivileged:/ {print $2}' "$conf" 2>/dev/null) if [[ "$unpriv" == "1" ]]; then echo "true" else echo "false" fi } # Select existing host directory for mounting pmx_select_existing_host_directory() { local title="${1:-$(translate "Select Host Directory")}" local choice folder_path result while true; do choice=$(whiptail --title "$title" --menu "$(translate "Select existing directory location:")" 16 76 4 \ "mnt" "$(translate "Browse /mnt directories")" \ "srv" "$(translate "Browse /srv directories")" \ "media" "$(translate "Browse /media directories")" \ "custom" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } case "$choice" in mnt) folder_path=$(pmx_browse_directory "/mnt") [[ -n "$folder_path" ]] && { echo "$folder_path"; return 0; } ;; srv) folder_path=$(pmx_browse_directory "/srv") [[ -n "$folder_path" ]] && { echo "$folder_path"; return 0; } ;; media) folder_path=$(pmx_browse_directory "/media") [[ -n "$folder_path" ]] && { echo "$folder_path"; return 0; } ;; custom) result=$(whiptail --inputbox "$(translate "Enter full directory path:")" 10 80 "/mnt/shared" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || continue if [[ -d "$result" ]]; then echo "$result"; return 0 else msg_error "$(translate "Directory does not exist:") $result" fi ;; esac done } # Browse directories in a given path pmx_browse_directory() { local base_path="$1" local dirs=() local dir_list="" if [[ ! -d "$base_path" ]]; then msg_error "$(translate "Base path does not exist:") $base_path" return 1 fi # Get directories in base path while IFS= read -r -d '' dir; do dir_name=$(basename "$dir") dirs+=("$dir" "$dir_name") done < <(find "$base_path" -maxdepth 1 -type d -not -path "$base_path" -print0 2>/dev/null | sort -z) if [[ ${#dirs[@]} -eq 0 ]]; then msg_warn "$(translate "No directories found in") $base_path" return 1 fi # Add option to use base path itself dirs=("$base_path" "$(translate "Use this directory") ($base_path)" "${dirs[@]}") local selected_dir selected_dir=$(whiptail --title "$(translate "Select Directory")" \ --menu "$(translate "Choose directory from") $base_path:" 20 80 10 "${dirs[@]}" 3>&1 1>&2 2>&3) [[ -n "$selected_dir" ]] && echo "$selected_dir" } # Ask user to reuse an existing group or create a new one pmx_choose_or_create_group() { # comments in English # OUT: echoes chosen group name 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:")" 16 78 3 \ "reuse" "$(translate "Use existing group (recommended)")" \ "new" "$(translate "Create a new group for isolation")" \ "custom" "$(translate "Enter a custom existing group name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; } case "$choice" in reuse) # Ensure default group exists or create it pmx_ensure_host_group "$default_group" >/dev/null || return 1 echo "$default_group" ;; new) 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" ;; custom) group_name=$(whiptail --inputbox "$(translate "Enter existing group name:")" 10 70 "$default_group" --title "$(translate "Group Name")" 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" ;; *) echo ""; return 1;; esac } # Ensure group exists on host and return its GID pmx_ensure_host_group() { # comments in English # IN: group name # OUT: echoes GID to stdout, return 0 on success local group_name="$1" if ! getent group "$group_name" >/dev/null 2>&1; then if groupadd "$group_name" >/dev/null 2>&1; 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 } # Prepare directory on host with shared permissions (root:group, 2775, default ACLs) pmx_prepare_host_shared_dir() { # comments in English # IN: dir path, group name 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 } # Common selector for host mountpoint base (used by SMB/NFS/Local) pmx_select_host_mount_point() { # comments in English # OUT: echoes selected mount point path 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 } # Compute next free mp index for a CT config (mp0..mpN) pmx_compute_next_mp_index() { # comments in English # IN: CTID local ctid="$1" conf="/etc/pve/lxc/${ctid}.conf" [[ ! -f "$conf" ]] && { echo "0"; return 0; } local used idx next=0 used=$(awk -F: '/^mp[0-9]+:/ {print $1}' "$conf" | sed 's/mp//' | sort -n) for idx in $used; do [[ "$idx" -ge "$next" ]] && next=$((idx+1)) done echo "$next" } # Add bind mount host->CT (unified approach for privileged and unprivileged) pmx_add_bind_mount_to_ct() { # comments in English # IN: CTID, host_path, ct_path local ctid="$1" host_path="$2" ct_path="$3" local conf="/etc/pve/lxc/${ctid}.conf" [[ -z "$ctid" || -z "$host_path" || -z "$ct_path" ]] && { msg_error "$(translate "Internal error: missing arguments in pmx_add_bind_mount_to_ct")"; return 1; } [[ ! -f "$conf" ]] && { msg_error "$(translate "Container config not found:") $conf"; return 1; } # Avoid duplicates if grep -qE "^mp[0-9]+:\s*${host_path}," "$conf"; then msg_warn "$(translate "This host path is already present in CT config.")" return 0 fi local running mpidx mpkey running=$(pct status "$ctid" 2>/dev/null | awk '{print $2}') mpidx="$(pmx_compute_next_mp_index "$ctid")" mpkey="mp${mpidx}" # Check if container is unprivileged local is_unprivileged is_unprivileged=$(pmx_is_container_unprivileged "$ctid") if [[ "$is_unprivileged" == "true" ]]; then msg_info "$(translate "Configuring mount for unprivileged container...")" # For unprivileged containers, append to config; requires restart echo "${mpkey}: ${host_path},mp=${ct_path}" >> "$conf" msg_ok "$(translate "Bind mount appended to config (unprivileged).")" if [[ "$running" == "running" ]]; then msg_warn "$(translate "Restart the container to apply the mount.")" fi else msg_info "$(translate "Configuring mount for privileged container...")" # Privileged: try hot-apply via pct set; fallback to config if [[ "$running" == "running" ]]; then if pct set "$ctid" -mp${mpidx} "${host_path},mp=${ct_path},create=dir" >/dev/null 2>&1; then msg_ok "$(translate "Bind mount applied live to running container (privileged).")" else echo "${mpkey}: ${host_path},mp=${ct_path},create=dir" >> "$conf" msg_warn "$(translate "pct set failed; entry appended to config.")" fi else echo "${mpkey}: ${host_path},mp=${ct_path},create=dir" >> "$conf" msg_ok "$(translate "Bind mount appended to config (privileged).")" fi fi return 0 } # Ensure same shared group (name + GID) inside CT and add CT users to it pmx_sync_group_in_ct() { # comments in English # IN: CTID, group_name, host_gid local ctid="$1" group_name="$2" host_gid="$3" local uid_shift uid_shift=$(pmx_get_container_uid_shift "$ctid") # Calculate the GID inside the container local ct_gid if [[ "$uid_shift" -eq 0 ]]; then # Privileged container - use same GID ct_gid="$host_gid" msg_info "$(translate "Privileged container - using host GID:") $ct_gid" else # Unprivileged container - map the GID ct_gid=$((host_gid - uid_shift)) if [[ $ct_gid -lt 0 ]]; then # If mapped GID would be negative, use a safe GID ct_gid=1000 msg_warn "$(translate "GID mapping resulted in negative value, using GID 1000 inside container")" fi msg_info "$(translate "Unprivileged container - mapped GID inside CT:") $ct_gid" fi # Create or ensure group exists inside container pct exec "$ctid" -- sh -lc " getent group ${group_name} >/dev/null || \ (addgroup --gid ${ct_gid} ${group_name} 2>/dev/null || groupadd -g ${ct_gid} ${group_name} 2>/dev/null) " >/dev/null 2>&1 # Get container users and add them to the group local users_mapping users_mapping=$(pmx_get_container_users_mapping "$ctid") if [[ -n "$users_mapping" ]]; then while IFS=: read -r username ct_uid ct_gid_orig real_uid real_gid; do if [[ -n "$username" ]]; then pct exec "$ctid" -- sh -lc "usermod -aG ${group_name} \"$username\" 2>/dev/null || true" >/dev/null 2>&1 msg_ok "$(translate "Added user to group inside CT:") $username -> $group_name" fi done <<< "$users_mapping" fi msg_ok "$(translate "Group synchronized inside CT.")" } # Ensure target path exists inside CT with group+2775 pmx_prepare_ct_target_path() { # comments in English # IN: CTID, ct_path, group_name local ctid="$1" ct_path="$2" group_name="$3" # Create directory and set permissions pct exec "$ctid" -- sh -lc "mkdir -p \"$ct_path\"" >/dev/null 2>&1 || true pct exec "$ctid" -- sh -lc "chgrp \"$group_name\" \"$ct_path\" 2>/dev/null && chmod 2775 \"$ct_path\" 2>/dev/null" >/dev/null 2>&1 || true msg_ok "$(translate "Prepared CT target path with group and 2775.")" }