mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-02-17 20:16:48 +01:00
Merge pull request #1660 from master3395/v2.5.5-dev
CyberPanel Plugin Store Added upgrade button UI Added revert version button in Grid and Table views Implemented local timezone display for cache expiry Fixed revert plugin JavaScript function
This commit is contained in:
582
cyberpanel.sh
582
cyberpanel.sh
@@ -81,7 +81,12 @@ detect_os() {
|
||||
fi
|
||||
|
||||
# Detect OS
|
||||
if echo $OUTPUT | grep -q "AlmaLinux 9" ; then
|
||||
if echo $OUTPUT | grep -q "AlmaLinux 10" ; then
|
||||
SERVER_OS="AlmaLinux10"
|
||||
OS_FAMILY="rhel"
|
||||
PACKAGE_MANAGER="dnf"
|
||||
print_status "Detected: AlmaLinux 10"
|
||||
elif echo $OUTPUT | grep -q "AlmaLinux 9" ; then
|
||||
SERVER_OS="AlmaLinux9"
|
||||
OS_FAMILY="rhel"
|
||||
PACKAGE_MANAGER="dnf"
|
||||
@@ -133,7 +138,7 @@ detect_os() {
|
||||
print_status "Detected: Debian GNU/Linux 11"
|
||||
else
|
||||
print_status "ERROR: Unsupported OS detected"
|
||||
print_status "Supported OS: AlmaLinux 8/9, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04, Debian 11/12"
|
||||
print_status "Supported OS: AlmaLinux 8/9/10, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04, Debian 11/12"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -477,8 +482,8 @@ install_dependencies() {
|
||||
echo ""
|
||||
|
||||
echo "Step 3/4: Installing core packages..."
|
||||
if [ "$SERVER_OS" = "AlmaLinux9" ] || [ "$SERVER_OS" = "CentOS9" ] || [ "$SERVER_OS" = "RockyLinux9" ]; then
|
||||
# AlmaLinux 9 / CentOS 9 / Rocky Linux 9
|
||||
if [ "$SERVER_OS" = "AlmaLinux9" ] || [ "$SERVER_OS" = "AlmaLinux10" ] || [ "$SERVER_OS" = "CentOS9" ] || [ "$SERVER_OS" = "RockyLinux9" ]; then
|
||||
# AlmaLinux 9/10 / CentOS 9 / Rocky Linux 9
|
||||
$PACKAGE_MANAGER install -y ImageMagick gd libicu oniguruma python3 python3-pip python3-devel 2>/dev/null || true
|
||||
$PACKAGE_MANAGER install -y aspell 2>/dev/null || print_status "WARNING: aspell not available, skipping..."
|
||||
$PACKAGE_MANAGER install -y libc-client-devel 2>/dev/null || print_status "WARNING: libc-client-devel not available, skipping..."
|
||||
@@ -609,18 +614,180 @@ install_cyberpanel_direct() {
|
||||
mkdir -p "$temp_dir"
|
||||
cd "$temp_dir" || return 1
|
||||
|
||||
# CRITICAL: Disable MariaDB 12.1 repository and add dnf exclude if MariaDB 10.x is installed
|
||||
# This must be done BEFORE Pre_Install_Setup_Repository runs
|
||||
if command -v rpm >/dev/null 2>&1; then
|
||||
# Check if MariaDB 10.x is installed
|
||||
if rpm -qa | grep -qiE "^(mariadb-server|mysql-server|MariaDB-server)" 2>/dev/null; then
|
||||
local mariadb_version=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
|
||||
if [ -n "$mariadb_version" ]; then
|
||||
local major_ver=$(echo "$mariadb_version" | cut -d. -f1)
|
||||
local minor_ver=$(echo "$mariadb_version" | cut -d. -f2)
|
||||
|
||||
# Check if it's MariaDB 10.x (major version < 12)
|
||||
if [ "$major_ver" -lt 12 ]; then
|
||||
print_status "MariaDB $mariadb_version detected, adding dnf exclude to prevent upgrade attempts"
|
||||
|
||||
# Add MariaDB-server to dnf excludes (multiple formats for compatibility)
|
||||
local dnf_conf="/etc/dnf/dnf.conf"
|
||||
local exclude_added=false
|
||||
|
||||
if [ -f "$dnf_conf" ]; then
|
||||
# Check if [main] section exists
|
||||
if grep -q "^\[main\]" "$dnf_conf" 2>/dev/null; then
|
||||
# [main] section exists, add exclude there
|
||||
if ! grep -q "exclude=.*MariaDB-server" "$dnf_conf" 2>/dev/null; then
|
||||
if grep -q "^exclude=" "$dnf_conf" 2>/dev/null; then
|
||||
# Append to existing exclude line in [main] section
|
||||
sed -i '/^\[main\]/,/^\[/ { /^exclude=/ s/$/ MariaDB-server*/ }' "$dnf_conf"
|
||||
else
|
||||
# Add new exclude line after [main]
|
||||
sed -i '/^\[main\]/a exclude=MariaDB-server*' "$dnf_conf"
|
||||
fi
|
||||
exclude_added=true
|
||||
fi
|
||||
else
|
||||
# No [main] section, add it with exclude
|
||||
if ! grep -q "exclude=.*MariaDB-server" "$dnf_conf" 2>/dev/null; then
|
||||
echo "" >> "$dnf_conf"
|
||||
echo "[main]" >> "$dnf_conf"
|
||||
echo "exclude=MariaDB-server*" >> "$dnf_conf"
|
||||
exclude_added=true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Create dnf.conf with exclude
|
||||
echo "[main]" > "$dnf_conf"
|
||||
echo "exclude=MariaDB-server*" >> "$dnf_conf"
|
||||
exclude_added=true
|
||||
fi
|
||||
|
||||
if [ "$exclude_added" = true ]; then
|
||||
print_status "Added MariaDB-server* to dnf excludes in $dnf_conf"
|
||||
fi
|
||||
|
||||
# Also add to yum.conf for compatibility
|
||||
local yum_conf="/etc/yum.conf"
|
||||
if [ -f "$yum_conf" ]; then
|
||||
if ! grep -q "exclude=.*MariaDB-server" "$yum_conf" 2>/dev/null; then
|
||||
if grep -q "^exclude=" "$yum_conf" 2>/dev/null; then
|
||||
sed -i 's/^exclude=\(.*\)/exclude=\1 MariaDB-server*/' "$yum_conf"
|
||||
else
|
||||
echo "exclude=MariaDB-server*" >> "$yum_conf"
|
||||
fi
|
||||
print_status "Added MariaDB-server* to yum excludes"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create a function to disable MariaDB repositories (will be called after repository setup)
|
||||
disable_mariadb_repos() {
|
||||
local repo_files=(
|
||||
"/etc/yum.repos.d/mariadb-main.repo"
|
||||
"/etc/yum.repos.d/mariadb.repo"
|
||||
"/etc/yum.repos.d/mariadb-12.1.repo"
|
||||
)
|
||||
|
||||
# Also check for any mariadb repo files
|
||||
while IFS= read -r repo_file; do
|
||||
repo_files+=("$repo_file")
|
||||
done < <(find /etc/yum.repos.d -name "*mariadb*.repo" 2>/dev/null)
|
||||
|
||||
for repo_file in "${repo_files[@]}"; do
|
||||
if [ -f "$repo_file" ] && [ -n "$repo_file" ]; then
|
||||
# First, try to disable by setting enabled=0
|
||||
sed -i 's/^enabled\s*=\s*1/enabled=0/g' "$repo_file" 2>/dev/null
|
||||
|
||||
# If file contains MariaDB 12.1 references, disable or remove it
|
||||
if grep -qi "mariadb.*12\|12.*mariadb\|mariadb-main" "$repo_file" 2>/dev/null; then
|
||||
# Try to add enabled=0 to each [mariadb...] section
|
||||
python3 -c "
|
||||
import re
|
||||
import sys
|
||||
try:
|
||||
with open('$repo_file', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace enabled=1 with enabled=0
|
||||
content = re.sub(r'(enabled\s*=\s*)1', r'\g<1>0', content, flags=re.IGNORECASE)
|
||||
|
||||
# Add enabled=0 after [mariadb...] sections if not present
|
||||
lines = content.split('\n')
|
||||
new_lines = []
|
||||
in_mariadb_section = False
|
||||
has_enabled = False
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
new_lines.append(line)
|
||||
if re.match(r'^\s*\[.*mariadb.*\]', line, re.IGNORECASE):
|
||||
in_mariadb_section = True
|
||||
has_enabled = False
|
||||
elif in_mariadb_section:
|
||||
if re.match(r'^\s*enabled\s*=', line, re.IGNORECASE):
|
||||
has_enabled = True
|
||||
elif re.match(r'^\s*\[', line) and not re.match(r'^\s*\[.*mariadb.*\]', line, re.IGNORECASE):
|
||||
if not has_enabled:
|
||||
new_lines.insert(-1, 'enabled=0')
|
||||
in_mariadb_section = False
|
||||
has_enabled = False
|
||||
|
||||
if in_mariadb_section and not has_enabled:
|
||||
new_lines.append('enabled=0')
|
||||
|
||||
with open('$repo_file', 'w') as f:
|
||||
f.write('\n'.join(new_lines))
|
||||
except:
|
||||
# Fallback: just rename the file
|
||||
import os
|
||||
os.rename('$repo_file', '${repo_file}.disabled')
|
||||
" 2>/dev/null || \
|
||||
# Fallback: rename the file to disable it
|
||||
mv "$repo_file" "${repo_file}.disabled" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Export function so it can be called from installer
|
||||
export -f disable_mariadb_repos
|
||||
export MARIADB_VERSION="$mariadb_version"
|
||||
|
||||
# Also set up a background process to monitor and disable repos
|
||||
(
|
||||
while [ ! -f /tmp/cyberpanel_install_complete ]; do
|
||||
sleep 2
|
||||
if [ -f /etc/yum.repos.d/mariadb-main.repo ] || [ -f /etc/yum.repos.d/mariadb.repo ]; then
|
||||
disable_mariadb_repos
|
||||
fi
|
||||
done
|
||||
) &
|
||||
local monitor_pid=$!
|
||||
echo "$monitor_pid" > /tmp/cyberpanel_repo_monitor.pid
|
||||
print_status "Started background process to monitor and disable MariaDB repositories"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Download the working CyberPanel installation files
|
||||
# Use master3395 fork which has our fixes
|
||||
# Try to download the actual installer script (install/install.py) from the repository
|
||||
echo "Downloading from: https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh"
|
||||
# Try development branch first, fallback to stable
|
||||
|
||||
# First, try to download the repository archive to get the correct installer
|
||||
local archive_url="https://github.com/master3395/cyberpanel/archive/v2.5.5-dev.tar.gz"
|
||||
local installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/v2.5.5-dev/cyberpanel.sh"
|
||||
|
||||
# Test if the development branch exists
|
||||
if ! curl -s --head "$installer_url" | grep -q "200 OK"; then
|
||||
echo " Development branch not available, falling back to stable"
|
||||
installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh"
|
||||
else
|
||||
# Test if the development branch archive exists
|
||||
if curl -s --head "$archive_url" | grep -q "200 OK"; then
|
||||
echo " Using development branch (v2.5.5-dev) from master3395/cyberpanel"
|
||||
else
|
||||
echo " Development branch archive not available, trying installer script directly..."
|
||||
# Test if the installer script exists
|
||||
if ! curl -s --head "$installer_url" | grep -q "200 OK"; then
|
||||
echo " Development branch not available, falling back to stable"
|
||||
installer_url="https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh"
|
||||
archive_url="https://github.com/master3395/cyberpanel/archive/stable.tar.gz"
|
||||
fi
|
||||
fi
|
||||
|
||||
curl --silent -o cyberpanel_installer.sh "$installer_url" 2>/dev/null
|
||||
@@ -629,6 +796,71 @@ install_cyberpanel_direct() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# CRITICAL: Patch the installer script to skip MariaDB installation if 10.x is already installed
|
||||
if [ -n "$MARIADB_VERSION" ] && [ "$major_ver" -lt 12 ] 2>/dev/null; then
|
||||
print_status "Patching installer script to skip MariaDB installation..."
|
||||
|
||||
# Create a backup
|
||||
cp cyberpanel_installer.sh cyberpanel_installer.sh.backup
|
||||
|
||||
# Use Python to properly patch the installer script
|
||||
python3 -c "
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open('cyberpanel_installer.sh', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
|
||||
# Pattern: Add --exclude=MariaDB-server* to dnf/yum install commands that install mariadb-server
|
||||
# Match: (dnf|yum) install [flags] [packages including mariadb-server]
|
||||
def add_exclude(match):
|
||||
cmd = match.group(0)
|
||||
# Check if --exclude is already present
|
||||
if '--exclude=MariaDB-server' in cmd:
|
||||
return cmd
|
||||
# Add --exclude=MariaDB-server* after install and flags, before packages
|
||||
return re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', cmd, flags=re.IGNORECASE)
|
||||
|
||||
# Find all dnf/yum install commands that mention mariadb-server
|
||||
content = re.sub(
|
||||
r'(?:dnf|yum)\s+install[^;]*?mariadb-server[^;]*',
|
||||
add_exclude,
|
||||
content,
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
|
||||
# Also handle MariaDB-server (capitalized)
|
||||
content = re.sub(
|
||||
r'(?:dnf|yum)\s+install[^;]*?MariaDB-server[^;]*',
|
||||
add_exclude,
|
||||
content,
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
|
||||
# Only write if content changed
|
||||
if content != original_content:
|
||||
with open('cyberpanel_installer.sh', 'w') as f:
|
||||
f.write(content)
|
||||
print('Installer script patched successfully')
|
||||
else:
|
||||
print('No changes needed in installer script')
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error patching installer script: {e}')
|
||||
sys.exit(1)
|
||||
" 2>/dev/null && print_status "Installer script patched successfully" || {
|
||||
# Fallback: Simple sed-based patching if Python fails
|
||||
sed -i 's/\(dnf\|yum\) install\([^;]*\)mariadb-server/\1 install\2--exclude=MariaDB-server* mariadb-server/gi' cyberpanel_installer.sh 2>/dev/null
|
||||
sed -i 's/\(dnf\|yum\) install\([^;]*\)MariaDB-server/\1 install\2--exclude=MariaDB-server* MariaDB-server/gi' cyberpanel_installer.sh 2>/dev/null
|
||||
print_status "Installer script patched (fallback method)"
|
||||
}
|
||||
|
||||
print_status "Installer script patched to exclude MariaDB-server from installation"
|
||||
fi
|
||||
|
||||
# Make script executable and verify
|
||||
chmod 755 cyberpanel_installer.sh 2>/dev/null || true
|
||||
if [ ! -x "cyberpanel_installer.sh" ]; then
|
||||
@@ -657,13 +889,27 @@ install_cyberpanel_direct() {
|
||||
|
||||
# Copy install directory to current location
|
||||
if [ "$installer_url" = "https://raw.githubusercontent.com/master3395/cyberpanel/stable/cyberpanel.sh" ]; then
|
||||
cp -r cyberpanel-stable/install . 2>/dev/null || true
|
||||
cp -r cyberpanel-stable/install.sh . 2>/dev/null || true
|
||||
if [ -d "cyberpanel-stable" ]; then
|
||||
cp -r cyberpanel-stable/install . 2>/dev/null || true
|
||||
cp -r cyberpanel-stable/install.sh . 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
cp -r cyberpanel-v2.5.5-dev/install . 2>/dev/null || true
|
||||
cp -r cyberpanel-v2.5.5-dev/install.sh . 2>/dev/null || true
|
||||
if [ -d "cyberpanel-v2.5.5-dev" ]; then
|
||||
cp -r cyberpanel-v2.5.5-dev/install . 2>/dev/null || true
|
||||
cp -r cyberpanel-v2.5.5-dev/install.sh . 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify install directory was copied
|
||||
if [ ! -d "install" ]; then
|
||||
print_status "ERROR: install directory not found after extraction"
|
||||
print_status "Archive contents:"
|
||||
ls -la 2>/dev/null | head -20
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_status "Verified install directory exists"
|
||||
|
||||
echo " ✓ CyberPanel installation files downloaded"
|
||||
echo " 🔄 Starting CyberPanel installation..."
|
||||
echo ""
|
||||
@@ -700,27 +946,301 @@ install_cyberpanel_direct() {
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run installer and show live output, capturing the password
|
||||
# Use bash to execute to avoid permission issues
|
||||
local installer_script="cyberpanel_installer.sh"
|
||||
if [ ! -f "$installer_script" ]; then
|
||||
print_status "ERROR: cyberpanel_installer.sh not found in current directory: $(pwd)"
|
||||
return 1
|
||||
# CRITICAL: Use install/install.py directly instead of cyberpanel_installer.sh
|
||||
# The cyberpanel_installer.sh is the old wrapper that doesn't support auto-install
|
||||
# install/install.py is the actual installer that we can control
|
||||
local installer_py="install/install.py"
|
||||
if [ -f "$installer_py" ]; then
|
||||
print_status "Using install/install.py directly for installation (non-interactive mode)"
|
||||
|
||||
# CRITICAL: Patch install.py to exclude MariaDB-server from dnf/yum commands
|
||||
if [ -n "$MARIADB_VERSION" ] && [ "$major_ver" -lt 12 ] 2>/dev/null; then
|
||||
print_status "Patching install.py to exclude MariaDB-server from installation commands..."
|
||||
|
||||
# Create backup
|
||||
cp "$installer_py" "${installer_py}.backup" 2>/dev/null || true
|
||||
|
||||
# Patch install.py to add --exclude=MariaDB-server* to dnf/yum install commands
|
||||
python3 -c "
|
||||
import re
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open('$installer_py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
|
||||
# Pattern: Add --exclude=MariaDB-server* to dnf/yum install commands that install mariadb-server
|
||||
def add_exclude(match):
|
||||
cmd = match.group(0)
|
||||
# Check if --exclude is already present
|
||||
if '--exclude=MariaDB-server' in cmd:
|
||||
return cmd
|
||||
# Add --exclude=MariaDB-server* after install and flags, before packages
|
||||
return re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', cmd, flags=re.IGNORECASE)
|
||||
|
||||
# Find all dnf/yum install commands that mention mariadb-server
|
||||
content = re.sub(
|
||||
r'(?:dnf|yum)\s+install[^;]*?mariadb-server[^;]*',
|
||||
add_exclude,
|
||||
content,
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
|
||||
# Also handle MariaDB-server (capitalized) and in Python strings
|
||||
content = re.sub(
|
||||
r'(\"|\')(?:dnf|yum)\s+install[^\"]*?mariadb-server[^\"]*(\"|\')',
|
||||
lambda m: m.group(1) + re.sub(r'((?:dnf|yum)\s+install\s+(?:-[^\s]+\s+)*)', r'\1--exclude=MariaDB-server* ', m.group(0)[1:-1], flags=re.IGNORECASE) + m.group(2),
|
||||
content,
|
||||
flags=re.IGNORECASE | re.MULTILINE
|
||||
)
|
||||
|
||||
# Only write if content changed
|
||||
if content != original_content:
|
||||
with open('$installer_py', 'w') as f:
|
||||
f.write(content)
|
||||
print('install.py patched successfully')
|
||||
else:
|
||||
print('No changes needed in install.py')
|
||||
|
||||
except Exception as e:
|
||||
print(f'Error patching install.py: {e}')
|
||||
sys.exit(1)
|
||||
" 2>/dev/null && print_status "install.py patched successfully" || {
|
||||
# Fallback: Simple sed-based patching if Python fails
|
||||
sed -i 's/\(dnf\|yum\) install\([^;]*\)mariadb-server/\1 install\2--exclude=MariaDB-server* mariadb-server/gi' "$installer_py" 2>/dev/null
|
||||
sed -i 's/\(dnf\|yum\) install\([^;]*\)MariaDB-server/\1 install\2--exclude=MariaDB-server* MariaDB-server/gi' "$installer_py" 2>/dev/null
|
||||
print_status "install.py patched (fallback method)"
|
||||
}
|
||||
fi
|
||||
|
||||
# If MariaDB 10.x is installed, disable repositories right before running installer
|
||||
if [ -n "$MARIADB_VERSION" ] && [ -f /tmp/cyberpanel_repo_monitor.pid ]; then
|
||||
# Call the disable function one more time before installer runs
|
||||
if type disable_mariadb_repos >/dev/null 2>&1; then
|
||||
disable_mariadb_repos
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get server IP address (required by install.py)
|
||||
local server_ip
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
server_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://icanhazip.com 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ -z "$server_ip" ]; then
|
||||
# Fallback: try to get IP from network interfaces
|
||||
server_ip=$(ip route get 8.8.8.8 2>/dev/null | awk '{print $7; exit}' || \
|
||||
hostname -I 2>/dev/null | awk '{print $1}' || \
|
||||
echo "127.0.0.1")
|
||||
fi
|
||||
|
||||
if [ -z "$server_ip" ] || [ "$server_ip" = "127.0.0.1" ]; then
|
||||
print_status "WARNING: Could not detect public IP, using 127.0.0.1"
|
||||
server_ip="127.0.0.1"
|
||||
fi
|
||||
|
||||
print_status "Detected server IP: $server_ip"
|
||||
|
||||
# CRITICAL: Install Python MySQL dependencies before running install.py
|
||||
# installCyberPanel.py requires MySQLdb (mysqlclient) which needs development headers
|
||||
echo ""
|
||||
echo "==============================================================================================================="
|
||||
echo "Installing Python MySQL dependencies (required for installCyberPanel.py)..."
|
||||
echo "==============================================================================================================="
|
||||
print_status "Installing Python MySQL dependencies..."
|
||||
|
||||
# Detect OS for package installation
|
||||
local os_family=""
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
almalinux|rocky|centos|rhel|fedora)
|
||||
os_family="rhel"
|
||||
print_status "Detected RHEL-based OS: $ID"
|
||||
;;
|
||||
ubuntu|debian)
|
||||
os_family="debian"
|
||||
print_status "Detected Debian-based OS: $ID"
|
||||
;;
|
||||
*)
|
||||
print_status "Unknown OS ID: $ID, defaulting to RHEL-based"
|
||||
os_family="rhel"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
print_status "WARNING: /etc/os-release not found, defaulting to RHEL-based"
|
||||
os_family="rhel"
|
||||
fi
|
||||
|
||||
# Install MariaDB/MySQL development headers and Python mysqlclient
|
||||
if [ "$os_family" = "rhel" ]; then
|
||||
# RHEL-based (AlmaLinux, Rocky, CentOS, RHEL)
|
||||
print_status "Installing MariaDB development headers for RHEL-based system..."
|
||||
|
||||
# Try to install mariadb-devel (works with MariaDB 10.x and 12.x)
|
||||
# NOTE: We need mariadb-devel even if we excluded MariaDB-server
|
||||
# The exclude only applies to MariaDB-server, not development packages
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
# For AlmaLinux 9/10 and newer - show output for debugging
|
||||
print_status "Attempting to install mariadb-devel (development headers only, not server)..."
|
||||
# Temporarily remove exclude for devel packages if needed
|
||||
local dnf_exclude_backup=""
|
||||
if [ -f /etc/dnf/dnf.conf ] && grep -q "exclude=.*MariaDB" /etc/dnf/dnf.conf; then
|
||||
# Check if exclude is too broad
|
||||
if grep -q "exclude=.*MariaDB-server.*MariaDB-devel" /etc/dnf/dnf.conf || \
|
||||
grep -q "exclude=.*MariaDB\*" /etc/dnf/dnf.conf; then
|
||||
print_status "Temporarily adjusting dnf exclude to allow mariadb-devel installation..."
|
||||
# We only want to exclude MariaDB-server, not devel packages
|
||||
sed -i 's/exclude=\(.*\)MariaDB-server\(.*\)MariaDB-devel\(.*\)/exclude=\1MariaDB-server\2\3/' /etc/dnf/dnf.conf 2>/dev/null || true
|
||||
sed -i 's/exclude=\(.*\)MariaDB\*\(.*\)/exclude=\1MariaDB-server*\2/' /etc/dnf/dnf.conf 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if dnf install -y --allowerasing --skip-broken --nobest \
|
||||
mariadb-devel pkgconfig gcc python3-devel python3-pip; then
|
||||
print_status "✓ Successfully installed mariadb-devel"
|
||||
elif dnf install -y --allowerasing --skip-broken --nobest \
|
||||
mysql-devel pkgconfig gcc python3-devel python3-pip; then
|
||||
print_status "✓ Successfully installed mysql-devel"
|
||||
elif dnf install -y --allowerasing --skip-broken --nobest \
|
||||
mariadb-connector-c-devel pkgconfig gcc python3-devel python3-pip; then
|
||||
print_status "✓ Successfully installed mariadb-connector-c-devel"
|
||||
else
|
||||
print_status "⚠️ WARNING: Failed to install MariaDB development headers"
|
||||
print_status "This may cause MySQLdb installation to fail"
|
||||
fi
|
||||
else
|
||||
# For older systems with yum
|
||||
print_status "Using yum to install mariadb-devel..."
|
||||
if yum install -y mariadb-devel pkgconfig gcc python3-devel python3-pip; then
|
||||
print_status "✓ Successfully installed mariadb-devel"
|
||||
elif yum install -y mysql-devel pkgconfig gcc python3-devel python3-pip; then
|
||||
print_status "✓ Successfully installed mysql-devel"
|
||||
else
|
||||
print_status "⚠️ WARNING: Failed to install MariaDB development headers"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install mysqlclient Python package
|
||||
print_status "Installing mysqlclient Python package..."
|
||||
python3 -m pip install --upgrade pip setuptools wheel 2>&1 | grep -v "already satisfied" || true
|
||||
if python3 -m pip install mysqlclient 2>&1; then
|
||||
print_status "✓ Successfully installed mysqlclient"
|
||||
else
|
||||
# If pip install fails, try with build dependencies
|
||||
print_status "Retrying mysqlclient installation with build dependencies..."
|
||||
python3 -m pip install --no-cache-dir mysqlclient 2>&1 || {
|
||||
print_status "⚠️ WARNING: Failed to install mysqlclient, trying alternative method..."
|
||||
# Try installing from source
|
||||
python3 -m pip install --no-binary mysqlclient mysqlclient 2>&1 || true
|
||||
}
|
||||
fi
|
||||
|
||||
elif [ "$os_family" = "debian" ]; then
|
||||
# Debian-based (Ubuntu, Debian)
|
||||
print_status "Installing MariaDB development headers for Debian-based system..."
|
||||
apt-get update -y
|
||||
if apt-get install -y libmariadb-dev libmariadb-dev-compat pkg-config build-essential python3-dev python3-pip; then
|
||||
print_status "✓ Successfully installed MariaDB development headers"
|
||||
elif apt-get install -y default-libmysqlclient-dev pkg-config build-essential python3-dev python3-pip; then
|
||||
print_status "✓ Successfully installed MySQL development headers"
|
||||
else
|
||||
print_status "⚠️ WARNING: Failed to install MariaDB/MySQL development headers"
|
||||
fi
|
||||
|
||||
# Install mysqlclient Python package
|
||||
print_status "Installing mysqlclient Python package..."
|
||||
python3 -m pip install --upgrade pip setuptools wheel 2>&1 | grep -v "already satisfied" || true
|
||||
if python3 -m pip install mysqlclient 2>&1; then
|
||||
print_status "✓ Successfully installed mysqlclient"
|
||||
else
|
||||
print_status "Retrying mysqlclient installation with build dependencies..."
|
||||
python3 -m pip install --no-cache-dir mysqlclient 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify MySQLdb is available
|
||||
print_status "Verifying MySQLdb module availability..."
|
||||
if python3 -c "import MySQLdb; print('MySQLdb version:', MySQLdb.__version__)" 2>&1; then
|
||||
print_status "✓ MySQLdb module is available and working"
|
||||
else
|
||||
print_status "⚠️ WARNING: MySQLdb module not available"
|
||||
print_status "Attempting to diagnose the issue..."
|
||||
python3 -c "import sys; print('Python path:', sys.path)" 2>&1 || true
|
||||
python3 -m pip list | grep -i mysql || print_status "No MySQL-related packages found in pip list"
|
||||
print_status "Attempting to continue anyway, but installation may fail..."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Build installer arguments based on user preferences
|
||||
# install.py requires publicip as first positional argument
|
||||
local install_args=("$server_ip")
|
||||
|
||||
# Add optional arguments based on user preferences
|
||||
# Default: OpenLiteSpeed, Full installation (postfix, powerdns, ftp), Local MySQL
|
||||
# These match what the user selected in the interactive prompts
|
||||
install_args+=("--postfix" "ON")
|
||||
install_args+=("--powerdns" "ON")
|
||||
install_args+=("--ftp" "ON")
|
||||
install_args+=("--remotemysql" "OFF")
|
||||
|
||||
if [ "$DEBUG_MODE" = true ]; then
|
||||
# Note: install.py doesn't have --debug, but we can set it via environment
|
||||
export DEBUG_MODE=true
|
||||
fi
|
||||
|
||||
# Run the Python installer directly
|
||||
if [ "$DEBUG_MODE" = true ]; then
|
||||
python3 "$installer_py" "${install_args[@]}" 2>&1 | tee /var/log/CyberPanel/install_output.log
|
||||
else
|
||||
python3 "$installer_py" "${install_args[@]}" 2>&1 | tee /var/log/CyberPanel/install_output.log
|
||||
fi
|
||||
else
|
||||
# Fallback to cyberpanel_installer.sh if install.py not found
|
||||
print_status "WARNING: install/install.py not found, using cyberpanel_installer.sh (may be interactive)"
|
||||
|
||||
local installer_script="cyberpanel_installer.sh"
|
||||
if [ ! -f "$installer_script" ]; then
|
||||
print_status "ERROR: cyberpanel_installer.sh not found in current directory: $(pwd)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get absolute path to installer script
|
||||
local installer_path
|
||||
if [[ "$installer_script" = /* ]]; then
|
||||
installer_path="$installer_script"
|
||||
else
|
||||
installer_path="$(pwd)/$installer_script"
|
||||
fi
|
||||
|
||||
# If MariaDB 10.x is installed, disable repositories right before running installer
|
||||
if [ -n "$MARIADB_VERSION" ] && [ -f /tmp/cyberpanel_repo_monitor.pid ]; then
|
||||
# Call the disable function one more time before installer runs
|
||||
if type disable_mariadb_repos >/dev/null 2>&1; then
|
||||
disable_mariadb_repos
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$DEBUG_MODE" = true ]; then
|
||||
bash "$installer_path" --debug 2>&1 | tee /var/log/CyberPanel/install_output.log
|
||||
else
|
||||
bash "$installer_path" 2>&1 | tee /var/log/CyberPanel/install_output.log
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get absolute path to installer script
|
||||
local installer_path
|
||||
if [[ "$installer_script" = /* ]]; then
|
||||
installer_path="$installer_script"
|
||||
else
|
||||
installer_path="$(pwd)/$installer_script"
|
||||
fi
|
||||
local install_exit_code=${PIPESTATUS[0]}
|
||||
|
||||
if [ "$DEBUG_MODE" = true ]; then
|
||||
bash "$installer_path" --debug 2>&1 | tee /var/log/CyberPanel/install_output.log
|
||||
else
|
||||
bash "$installer_path" 2>&1 | tee /var/log/CyberPanel/install_output.log
|
||||
# Stop the repository monitor
|
||||
if [ -f /tmp/cyberpanel_repo_monitor.pid ]; then
|
||||
local monitor_pid=$(cat /tmp/cyberpanel_repo_monitor.pid 2>/dev/null)
|
||||
if [ -n "$monitor_pid" ] && kill -0 "$monitor_pid" 2>/dev/null; then
|
||||
kill "$monitor_pid" 2>/dev/null
|
||||
fi
|
||||
rm -f /tmp/cyberpanel_repo_monitor.pid
|
||||
fi
|
||||
touch /tmp/cyberpanel_install_complete 2>/dev/null || true
|
||||
|
||||
local install_exit_code=${PIPESTATUS[0]}
|
||||
|
||||
|
||||
@@ -345,40 +345,67 @@ class preFlightsChecks:
|
||||
self.stdOut(f"Successfully installed alternative: {alt_package}", 1)
|
||||
break
|
||||
|
||||
# Install MariaDB with enhanced AlmaLinux 9.6 support
|
||||
self.stdOut("Installing MariaDB for AlmaLinux 9.6...", 1)
|
||||
# Disable MariaDB 12.1 repository if MariaDB 10.x is already installed
|
||||
# This prevents upgrade attempts in Pre_Install_Required_Components
|
||||
self.disableMariaDB12RepositoryIfNeeded()
|
||||
|
||||
# Try multiple installation methods for maximum compatibility
|
||||
mariadb_commands = [
|
||||
"dnf install -y mariadb-server mariadb-devel mariadb-client --skip-broken --nobest",
|
||||
"dnf install -y mariadb-server mariadb-devel mariadb-client --allowerasing",
|
||||
"dnf install -y mariadb-server mariadb-devel --skip-broken --nobest --allowerasing",
|
||||
"dnf install -y mariadb-server --skip-broken --nobest --allowerasing"
|
||||
]
|
||||
# CRITICAL: Remove conflicting MariaDB compat packages before installation
|
||||
# These packages from MariaDB 12.1 can conflict with MariaDB 10.11
|
||||
self.stdOut("Removing conflicting MariaDB compat packages...", 1)
|
||||
try:
|
||||
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
|
||||
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
|
||||
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
|
||||
for line in (r.stdout or "").strip().splitlines():
|
||||
pkg = (line.strip().split() or [""])[0]
|
||||
if pkg and "MariaDB-server-compat" in pkg:
|
||||
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
|
||||
self.stdOut("Removed conflicting MariaDB compat packages", 1)
|
||||
except Exception as e:
|
||||
self.stdOut("Warning: Could not remove compat packages: " + str(e), 0)
|
||||
|
||||
mariadb_installed = False
|
||||
for cmd in mariadb_commands:
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode == 0:
|
||||
# Check if MariaDB is already installed before attempting installation
|
||||
is_installed, installed_version, major_minor = self.checkExistingMariaDB()
|
||||
|
||||
if is_installed:
|
||||
self.stdOut(f"MariaDB/MySQL is already installed (version: {installed_version}), skipping installation", 1)
|
||||
mariadb_installed = True
|
||||
else:
|
||||
# Install MariaDB with enhanced AlmaLinux 9.6 support
|
||||
self.stdOut("Installing MariaDB for AlmaLinux 9.6...", 1)
|
||||
|
||||
# Try multiple installation methods for maximum compatibility
|
||||
mariadb_commands = [
|
||||
"dnf install -y mariadb-server mariadb-devel mariadb-client --skip-broken --nobest",
|
||||
"dnf install -y mariadb-server mariadb-devel mariadb-client --allowerasing",
|
||||
"dnf install -y mariadb-server mariadb-devel --skip-broken --nobest --allowerasing",
|
||||
"dnf install -y mariadb-server --skip-broken --nobest --allowerasing"
|
||||
]
|
||||
|
||||
mariadb_installed = False
|
||||
for cmd in mariadb_commands:
|
||||
try:
|
||||
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode == 0:
|
||||
mariadb_installed = True
|
||||
self.stdOut(f"MariaDB installed successfully with command: {cmd}", 1)
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
self.stdOut(f"Timeout installing MariaDB with command: {cmd}", 0)
|
||||
continue
|
||||
except Exception as e:
|
||||
self.stdOut(f"Error installing MariaDB with command: {cmd} - {str(e)}", 0)
|
||||
continue
|
||||
|
||||
if not mariadb_installed:
|
||||
self.stdOut("MariaDB installation failed, trying MySQL as fallback...", 0)
|
||||
try:
|
||||
command = "dnf install -y mysql-server mysql-devel --skip-broken --nobest --allowerasing"
|
||||
self.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
self.stdOut("MySQL installed as fallback for MariaDB", 1)
|
||||
mariadb_installed = True
|
||||
self.stdOut(f"MariaDB installed successfully with command: {cmd}", 1)
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
self.stdOut(f"Timeout installing MariaDB with command: {cmd}", 0)
|
||||
continue
|
||||
except Exception as e:
|
||||
self.stdOut(f"Error installing MariaDB with command: {cmd} - {str(e)}", 0)
|
||||
continue
|
||||
|
||||
if not mariadb_installed:
|
||||
self.stdOut("MariaDB installation failed, trying MySQL as fallback...", 0)
|
||||
try:
|
||||
command = "dnf install -y mysql-server mysql-devel --skip-broken --nobest --allowerasing"
|
||||
self.call(command, self.distro, command, command, 1, 0, os.EX_OSERR)
|
||||
self.stdOut("MySQL installed as fallback for MariaDB", 1)
|
||||
except:
|
||||
self.stdOut("Both MariaDB and MySQL installation failed", 0)
|
||||
except:
|
||||
self.stdOut("Both MariaDB and MySQL installation failed", 0)
|
||||
|
||||
# Install additional required packages
|
||||
self.stdOut("Installing additional required packages...", 1)
|
||||
@@ -1238,6 +1265,13 @@ class preFlightsChecks:
|
||||
self.stdOut("Installing custom binaries...", 1)
|
||||
|
||||
try:
|
||||
# Ensure /usr/local/lsws/bin exists (dnf openlitespeed may use different layout)
|
||||
ols_bin_dir = os.path.dirname(OLS_BINARY_PATH)
|
||||
os.makedirs(ols_bin_dir, mode=0o755, exist_ok=True)
|
||||
ols_base = os.path.dirname(ols_bin_dir)
|
||||
if not os.path.isdir(ols_base):
|
||||
os.makedirs(ols_base, mode=0o755, exist_ok=True)
|
||||
|
||||
# Make binary executable before moving
|
||||
os.chmod(tmp_binary, 0o755)
|
||||
|
||||
@@ -1451,6 +1485,124 @@ module cyberpanel_ols {
|
||||
except:
|
||||
return False
|
||||
|
||||
def disableMariaDB12RepositoryIfNeeded(self):
|
||||
"""Disable MariaDB 12.1 repository if MariaDB 10.x is already installed to prevent upgrade attempts"""
|
||||
try:
|
||||
is_installed, installed_version, major_minor = self.checkExistingMariaDB()
|
||||
|
||||
if is_installed and major_minor and major_minor != "unknown":
|
||||
try:
|
||||
major_ver = float(major_minor)
|
||||
if major_ver < 12.0:
|
||||
# MariaDB 10.x is installed, disable 12.1 repository to prevent upgrade attempts
|
||||
self.stdOut(f"MariaDB {installed_version} detected, disabling MariaDB 12.1 repository to prevent upgrade conflicts", 1)
|
||||
logging.InstallLog.writeToFile(f"MariaDB {installed_version} detected, disabling MariaDB 12.1 repository")
|
||||
|
||||
# Disable MariaDB 12.1 repository - check all possible repo file locations
|
||||
repo_files = [
|
||||
'/etc/yum.repos.d/mariadb-main.repo',
|
||||
'/etc/yum.repos.d/mariadb.repo',
|
||||
'/etc/yum.repos.d/mariadb-12.1.repo',
|
||||
'/etc/yum.repos.d/mariadb-main.repo.bak'
|
||||
]
|
||||
|
||||
# Also check for any mariadb repo files
|
||||
import glob
|
||||
repo_files.extend(glob.glob('/etc/yum.repos.d/*mariadb*.repo'))
|
||||
|
||||
disabled_any = False
|
||||
for repo_file in repo_files:
|
||||
if os.path.exists(repo_file):
|
||||
try:
|
||||
# Read the file
|
||||
with open(repo_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Modify the file to disable all MariaDB repositories
|
||||
modified = False
|
||||
new_lines = []
|
||||
in_mariadb_section = False
|
||||
|
||||
for line in lines:
|
||||
# Check if we're entering a MariaDB repository section
|
||||
if line.strip().startswith('[') and 'mariadb' in line.lower():
|
||||
in_mariadb_section = True
|
||||
new_lines.append(line)
|
||||
# Add enabled=0 if not already present
|
||||
if 'enabled' not in line.lower():
|
||||
new_lines.append('enabled=0\n')
|
||||
modified = True
|
||||
elif in_mariadb_section:
|
||||
# If we see enabled=1, change it to enabled=0
|
||||
if line.strip().startswith('enabled=') and 'enabled=0' not in line.lower():
|
||||
new_lines.append('enabled=0\n')
|
||||
modified = True
|
||||
elif line.strip().startswith('['):
|
||||
# New section, exit MariaDB section
|
||||
in_mariadb_section = False
|
||||
new_lines.append(line)
|
||||
else:
|
||||
new_lines.append(line)
|
||||
else:
|
||||
new_lines.append(line)
|
||||
|
||||
# Write back if modified
|
||||
if modified:
|
||||
with open(repo_file, 'w') as f:
|
||||
f.writelines(new_lines)
|
||||
self.stdOut(f"Disabled MariaDB repository in {repo_file}", 1)
|
||||
logging.InstallLog.writeToFile(f"Disabled MariaDB repository in {repo_file}")
|
||||
disabled_any = True
|
||||
|
||||
except Exception as e:
|
||||
self.stdOut(f"Warning: Could not disable repository {repo_file}: {e}", 1)
|
||||
logging.InstallLog.writeToFile(f"Warning: Could not disable repository {repo_file}: {e}")
|
||||
|
||||
# Always exclude MariaDB-server from dnf/yum operations to prevent upgrades
|
||||
try:
|
||||
# Add exclude to dnf.conf
|
||||
dnf_conf = '/etc/dnf/dnf.conf'
|
||||
exclude_line = 'exclude=MariaDB-server'
|
||||
|
||||
if os.path.exists(dnf_conf):
|
||||
with open(dnf_conf, 'r') as f:
|
||||
dnf_content = f.read()
|
||||
|
||||
# Check if exclude line already exists
|
||||
if exclude_line not in dnf_content:
|
||||
# Check if there's already an exclude line
|
||||
if 'exclude=' in dnf_content:
|
||||
# Append to existing exclude line
|
||||
dnf_content = re.sub(r'(exclude=.*)', r'\1 MariaDB-server', dnf_content)
|
||||
else:
|
||||
# Add new exclude line
|
||||
dnf_content = dnf_content.rstrip() + '\n' + exclude_line + '\n'
|
||||
|
||||
with open(dnf_conf, 'w') as f:
|
||||
f.write(dnf_content)
|
||||
self.stdOut("Added MariaDB-server to dnf excludes to prevent upgrade", 1)
|
||||
logging.InstallLog.writeToFile("Added MariaDB-server to dnf excludes")
|
||||
else:
|
||||
# Create dnf.conf with exclude
|
||||
with open(dnf_conf, 'w') as f:
|
||||
f.write('[main]\n')
|
||||
f.write(exclude_line + '\n')
|
||||
self.stdOut("Created dnf.conf with MariaDB-server exclude", 1)
|
||||
logging.InstallLog.writeToFile("Created dnf.conf with MariaDB-server exclude")
|
||||
except Exception as e:
|
||||
self.stdOut(f"Warning: Could not add exclude to dnf.conf: {e}", 1)
|
||||
logging.InstallLog.writeToFile(f"Warning: Could not add exclude to dnf.conf: {e}")
|
||||
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
self.stdOut(f"Warning: Error checking MariaDB repository: {e}", 1)
|
||||
logging.InstallLog.writeToFile(f"Warning: Error checking MariaDB repository: {e}")
|
||||
return False
|
||||
|
||||
def checkExistingMariaDB(self):
|
||||
"""Check if MariaDB/MySQL is already installed and return version info"""
|
||||
try:
|
||||
@@ -1659,7 +1811,42 @@ module cyberpanel_ols {
|
||||
|
||||
else:
|
||||
# RHEL-based MariaDB installation
|
||||
command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=12.1'
|
||||
# CRITICAL: Remove conflicting MariaDB compat packages first
|
||||
# These packages from MariaDB 12.1 can conflict with MariaDB 10.11
|
||||
self.stdOut("Removing conflicting MariaDB compat packages...", 1)
|
||||
try:
|
||||
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
|
||||
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
|
||||
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
|
||||
for line in (r.stdout or "").strip().splitlines():
|
||||
pkg = (line.strip().split() or [""])[0]
|
||||
if pkg and "MariaDB-server-compat" in pkg:
|
||||
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
|
||||
self.stdOut("Removed conflicting MariaDB compat packages", 1)
|
||||
except Exception as e:
|
||||
self.stdOut("Warning: Could not remove compat packages: " + str(e), 0)
|
||||
|
||||
# Check if MariaDB is already installed before setting up repository
|
||||
is_installed, installed_version, major_minor = self.checkExistingMariaDB()
|
||||
|
||||
if is_installed:
|
||||
self.stdOut(f"MariaDB/MySQL is already installed (version: {installed_version}), skipping installation", 1)
|
||||
# Don't set up 12.1 repository if 10.x is installed to avoid upgrade issues
|
||||
if major_minor and major_minor != "unknown":
|
||||
try:
|
||||
major_ver = float(major_minor)
|
||||
if major_ver < 12.0:
|
||||
self.stdOut("Skipping MariaDB 12.1 repository setup to avoid upgrade conflicts", 1)
|
||||
self.stdOut("Using existing MariaDB installation", 1)
|
||||
self.startMariaDB()
|
||||
self.changeMYSQLRootPassword()
|
||||
self.fixMariaDB()
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Set up MariaDB 12.1 repository only if not already installed
|
||||
command = 'curl -LsS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version=12.1'
|
||||
self.call(command, self.distro, command, command, 1, 1, os.EX_OSERR, True)
|
||||
|
||||
command = 'dnf install mariadb-server mariadb-devel mariadb-client-utils -y'
|
||||
@@ -6246,6 +6433,25 @@ def main():
|
||||
|
||||
# Apply OS-specific fixes early in the installation process
|
||||
checks.apply_os_specific_fixes()
|
||||
|
||||
# CRITICAL: Disable MariaDB 12.1 repository and add dnf exclude BEFORE any MariaDB installation attempts
|
||||
# This must run before Pre_Install_Required_Components tries to install MariaDB
|
||||
checks.disableMariaDB12RepositoryIfNeeded()
|
||||
|
||||
# CRITICAL: Remove MariaDB-server-compat* before ANY MariaDB installation
|
||||
# This package conflicts with MariaDB 10.11 and must be removed early
|
||||
preFlightsChecks.stdOut("Removing conflicting MariaDB-server-compat packages...", 1)
|
||||
try:
|
||||
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
|
||||
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
|
||||
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
|
||||
for line in (r.stdout or "").strip().splitlines():
|
||||
pkg = (line.strip().split() or [""])[0]
|
||||
if pkg and "MariaDB-server-compat" in pkg:
|
||||
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
|
||||
preFlightsChecks.stdOut("MariaDB compat cleanup completed", 1)
|
||||
except Exception as e:
|
||||
preFlightsChecks.stdOut("Warning: compat cleanup: " + str(e), 0)
|
||||
|
||||
# Ensure MySQL password file is created early to prevent FileNotFoundError
|
||||
checks.ensure_mysql_password_file()
|
||||
@@ -6273,6 +6479,10 @@ def main():
|
||||
# Apply AlmaLinux 9 comprehensive fixes first if needed
|
||||
if checks.is_almalinux9():
|
||||
checks.fix_almalinux9_comprehensive()
|
||||
|
||||
# Disable MariaDB 12.1 repository if MariaDB 10.x is already installed
|
||||
# This prevents upgrade attempts in Pre_Install_Required_Components
|
||||
checks.disableMariaDB12RepositoryIfNeeded()
|
||||
|
||||
# Install core services in the correct order
|
||||
checks.installLiteSpeed(ent, serial)
|
||||
|
||||
@@ -556,14 +556,31 @@ def call(command, distro, bracket, message, log=0, do_exit=0, code=os.EX_OK, she
|
||||
os._exit(code)
|
||||
return False
|
||||
|
||||
# CRITICAL: Use shell=True for commands with shell metacharacters
|
||||
# Avoids "No matching repo to modify: 2>/dev/null, true, ||" when shlex.split splits them
|
||||
if not shell and any(x in command for x in (' || ', ' 2>/dev', ' 2>', ' | ', '; true', '|| true')):
|
||||
shell = True
|
||||
|
||||
# CRITICAL: For mysql/mariadb commands, always use shell=True and full binary path
|
||||
# This fixes "No such file or directory: 'mysql'" when run via shlex.split
|
||||
if not shell and ('mysql' in command or 'mariadb' in command):
|
||||
import re
|
||||
mysql_bin = '/usr/bin/mariadb' if os.path.exists('/usr/bin/mariadb') else '/usr/bin/mysql'
|
||||
if not os.path.exists(mysql_bin):
|
||||
mysql_bin = '/usr/bin/mysql'
|
||||
# Replace only leading "mysql" or "mariadb" (executable), not "mysql" in SQL like "use mysql;"
|
||||
if re.match(r'^\s*(sudo\s+)?(mysql|mariadb)\s', command):
|
||||
command = re.sub(r'^(\s*)(?:sudo\s+)?(mysql|mariadb)(\s)', r'\g<1>' + mysql_bin + r'\g<3>', command, count=1)
|
||||
shell = True
|
||||
|
||||
finalMessage = 'Running: %s' % (message)
|
||||
stdOut(finalMessage, log)
|
||||
count = 0
|
||||
while True:
|
||||
if shell == False:
|
||||
res = subprocess.call(shlex.split(command))
|
||||
else:
|
||||
if shell:
|
||||
res = subprocess.call(command, shell=True)
|
||||
else:
|
||||
res = subprocess.call(shlex.split(command))
|
||||
|
||||
if resFailed(distro, res):
|
||||
count = count + 1
|
||||
|
||||
@@ -4299,6 +4299,19 @@ echo $oConfig->Save() ? 'Done' : 'Error';
|
||||
Upgrade.stdOut("Applying AlmaLinux 9 MariaDB fixes...", 1)
|
||||
|
||||
try:
|
||||
# CRITICAL: Remove MariaDB-server-compat* before any MariaDB install (conflicts with 10.11)
|
||||
Upgrade.stdOut("Removing conflicting MariaDB-server-compat packages...", 1)
|
||||
try:
|
||||
subprocess.run("rpm -e --nodeps MariaDB-server-compat-12.1.2-1.el9.noarch 2>/dev/null; true", shell=True, timeout=30)
|
||||
subprocess.run("dnf remove -y 'MariaDB-server-compat*' 2>/dev/null || true", shell=True, timeout=60)
|
||||
r = subprocess.run("rpm -qa 2>/dev/null | grep -i MariaDB-server-compat", shell=True, capture_output=True, text=True, timeout=30)
|
||||
for line in (r.stdout or "").strip().splitlines():
|
||||
pkg = (line.strip().split() or [""])[0]
|
||||
if pkg and "MariaDB-server-compat" in pkg:
|
||||
subprocess.run(["rpm", "-e", "--nodeps", pkg], timeout=30)
|
||||
except Exception as e:
|
||||
Upgrade.stdOut("Warning: compat cleanup: " + str(e), 0)
|
||||
|
||||
# Disable problematic MariaDB MaxScale repository
|
||||
Upgrade.stdOut("Disabling problematic MariaDB MaxScale repository...", 1)
|
||||
command = "dnf config-manager --disable mariadb-maxscale 2>/dev/null || true"
|
||||
@@ -4320,9 +4333,9 @@ echo $oConfig->Save() ? 'Done' : 'Error';
|
||||
command = "dnf clean all"
|
||||
subprocess.run(command, shell=True, capture_output=True)
|
||||
|
||||
# Install MariaDB from official repository
|
||||
# Install MariaDB 10.11 from official repository (avoid 12.1 compat conflicts)
|
||||
Upgrade.stdOut("Setting up official MariaDB repository...", 1)
|
||||
command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='12.1'"
|
||||
command = "curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | bash -s -- --mariadb-server-version='10.11'"
|
||||
result = subprocess.run(command, shell=True, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
Upgrade.stdOut(f"Warning: MariaDB repo setup failed: {result.stderr}", 0)
|
||||
|
||||
@@ -709,6 +709,32 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-upgrade {
|
||||
padding: 8px 16px;
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.btn-upgrade:hover:not(:disabled) {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(245,158,11,0.3);
|
||||
}
|
||||
|
||||
.btn-upgrade:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-secondary, #f8f9ff);
|
||||
@@ -772,6 +798,17 @@
|
||||
box-shadow: 0 4px 8px rgba(88,86,214,0.3);
|
||||
}
|
||||
|
||||
.btn-revert {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-revert:hover:not(:disabled) {
|
||||
background: #5a6268;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(108,117,125,0.3);
|
||||
}
|
||||
|
||||
.btn-uninstall {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
@@ -1042,6 +1079,9 @@
|
||||
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn-action btn-revert btn-small" onclick="showRevertDialog('{{ plugin.plugin_dir }}')" title="{% trans 'Revert to Previous Version' %}">
|
||||
<i class="fas fa-undo"></i> {% trans "Revert Version" %}
|
||||
</button>
|
||||
<button class="btn-action btn-uninstall btn-small" onclick="uninstallPlugin('{{ plugin.plugin_dir }}')">
|
||||
<i class="fas fa-trash"></i> {% trans "Uninstall" %}
|
||||
</button>
|
||||
@@ -1123,6 +1163,9 @@
|
||||
<i class="fas fa-toggle-off"></i> {% trans "Activate" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button class="btn-action btn-revert" onclick="showRevertDialog('{{ plugin.plugin_dir }}')" title="{% trans 'Revert to Previous Version' %}">
|
||||
<i class="fas fa-undo"></i> {% trans "Revert Version" %}
|
||||
</button>
|
||||
<button class="btn-action btn-uninstall" onclick="uninstallPlugin('{{ plugin.plugin_dir }}')">
|
||||
<i class="fas fa-trash"></i> {% trans "Uninstall" %}
|
||||
</button>
|
||||
@@ -1194,6 +1237,17 @@
|
||||
|
||||
<!-- CyberPanel Plugin Store (always available) -->
|
||||
<div id="storeView" style="display: {% if not plugins %}block{% else %}none{% endif %};">
|
||||
<!-- Loading Indicator -->
|
||||
<div id="storeLoading" class="store-loading" style="display: {% if not plugins %}block{% else %}none{% endif %};">
|
||||
<i class="fas fa-spinner fa-spin"></i> {% trans "Loading plugins from store..." %}
|
||||
</div>
|
||||
|
||||
<!-- Store Error -->
|
||||
<div id="storeError" class="alert alert-danger" style="display: none;">
|
||||
<i class="fas fa-exclamation-circle alert-icon"></i>
|
||||
<span id="storeErrorText"></span>
|
||||
</div>
|
||||
|
||||
<!-- Notice Section (similar to CMSMS Module Manager) -->
|
||||
<div class="store-notice" id="pluginStoreNotice">
|
||||
<div class="notice-header">
|
||||
@@ -1208,6 +1262,9 @@
|
||||
<i class="fas fa-info-circle" style="color: #5856d6;"></i>
|
||||
<strong>{% trans "Cache Information:" %}</strong>
|
||||
{% trans "Plugin store data is cached for 1 hour to improve performance and reduce GitHub API rate limits. New plugins may take up to 1 hour to appear after being published." %}
|
||||
{% if cache_expiry_timestamp %}
|
||||
<br><strong>{% trans "Next cache update:" %}</strong> <span id="cacheExpiryTime" style="font-family: monospace;" data-timestamp="{{ cache_expiry_timestamp }}">{% trans "Calculating..." %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="warning-text">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
@@ -1410,12 +1467,20 @@ function displayStorePlugins() {
|
||||
<i class="${iconClass}"></i>
|
||||
</div>`;
|
||||
|
||||
// Action column - Store view only shows Install/Installed (no Deactivate/Uninstall)
|
||||
// Action column - Store view only shows Install/Installed/Upgrade (no Deactivate/Uninstall)
|
||||
// NOTE: Store view should NOT show Deactivate/Uninstall buttons - users manage from Grid/Table views
|
||||
let actionHtml = '';
|
||||
if (plugin.installed) {
|
||||
// Show "Installed" text
|
||||
actionHtml = '<span class="status-installed">Installed</span>';
|
||||
// Check if update is available
|
||||
if (plugin.update_available) {
|
||||
// Show Upgrade button
|
||||
actionHtml = `<button class="btn-action btn-upgrade" onclick="upgradePlugin('${plugin.plugin_dir}', '${plugin.installed_version || 'Unknown'}', '${plugin.version || 'Unknown'}')">
|
||||
<i class="fas fa-arrow-up"></i> Upgrade
|
||||
</button>`;
|
||||
} else {
|
||||
// Show "Installed" text
|
||||
actionHtml = '<span class="status-installed">Installed</span>';
|
||||
}
|
||||
} else {
|
||||
// Show Install button
|
||||
actionHtml = `<button class="btn-action btn-install" onclick="installFromStore('${plugin.plugin_dir}')">
|
||||
@@ -1486,6 +1551,83 @@ function filterByLetter(letter) {
|
||||
displayStorePlugins();
|
||||
}
|
||||
|
||||
function upgradePlugin(pluginName, currentVersion, newVersion) {
|
||||
// Show confirmation dialog with backup warning
|
||||
const message = `⚠️ WARNING: Plugin Upgrade\n\n` +
|
||||
`You are about to upgrade ${pluginName} from version ${currentVersion} to ${newVersion}.\n\n` +
|
||||
`⚠️ IMPORTANT: You could lose data during the upgrade process.\n\n` +
|
||||
`Please ensure you have backed up:\n` +
|
||||
`• Plugin configuration files\n` +
|
||||
`• Plugin data and databases\n` +
|
||||
`• Any custom modifications\n\n` +
|
||||
`Do you want to continue with the upgrade?`;
|
||||
|
||||
if (!confirm(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Double confirmation
|
||||
if (!confirm(`Final confirmation: Upgrade ${pluginName} now?\n\nThis action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = event.target.closest('.btn-upgrade') || event.target;
|
||||
const originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Upgrading...';
|
||||
|
||||
fetch(`/plugins/api/store/upgrade/${pluginName}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken'),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Upgrade Successful!',
|
||||
text: data.message || `Plugin ${pluginName} upgraded successfully`,
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
alert('Success: ' + (data.message || `Plugin ${pluginName} upgraded successfully`));
|
||||
}
|
||||
// Reload page after short delay to show success message
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Upgrade Failed!',
|
||||
text: data.error || 'Failed to upgrade plugin',
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Failed to upgrade plugin'));
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error!',
|
||||
text: 'Failed to upgrade plugin: ' + error.message,
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
alert('Error: Failed to upgrade plugin - ' + error.message);
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
function installFromStore(pluginName) {
|
||||
if (!confirm(`Install ${pluginName} from the CyberPanel Plugin Store?`)) {
|
||||
return;
|
||||
@@ -1659,6 +1801,165 @@ function installPlugin(pluginName) {
|
||||
});
|
||||
}
|
||||
|
||||
function showRevertDialog(pluginName) {
|
||||
// Fetch available backups
|
||||
fetch(`/plugins/api/backups/${pluginName}/`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error!',
|
||||
text: data.error || 'Failed to load backups',
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Failed to load backups'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.backups || data.backups.length === 0) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'No Backups Available',
|
||||
text: `No backups found for ${pluginName}. Backups are automatically created before upgrades.`,
|
||||
type: 'info'
|
||||
});
|
||||
} else {
|
||||
alert(`No backups found for ${pluginName}. Backups are automatically created before upgrades.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show backup selection dialog
|
||||
let backupList = 'Available backups:\n\n';
|
||||
data.backups.forEach((backup, index) => {
|
||||
const version = backup.version || 'unknown';
|
||||
const timestamp = backup.timestamp || backup.created_at || 'unknown';
|
||||
backupList += `${index + 1}. Version ${version} (${timestamp})\n`;
|
||||
});
|
||||
|
||||
backupList += '\n⚠️ WARNING: Reverting will replace the current plugin version with the selected backup.\n';
|
||||
backupList += 'This action cannot be undone. Continue?';
|
||||
|
||||
if (!confirm(backupList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask which backup to restore (for now, restore the most recent)
|
||||
// In a more advanced UI, you could show a dropdown
|
||||
const selectedBackup = data.backups[0]; // Most recent backup
|
||||
|
||||
if (!confirm(`Revert ${pluginName} to version ${selectedBackup.version}?\n\nThis will replace the current installation.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform revert
|
||||
revertPlugin(pluginName, selectedBackup.backup_path);
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMessage = error && error.message ? error.message : (error ? String(error) : 'Unknown error');
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error!',
|
||||
text: 'Failed to load backups: ' + errorMessage,
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
alert('Error: Failed to load backups - ' + errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function revertPlugin(pluginName, backupPath) {
|
||||
// Find the revert button for this plugin (if it exists)
|
||||
let btn = null;
|
||||
let originalText = '';
|
||||
|
||||
// Try to find button by looking for onclick attribute containing the plugin name
|
||||
const revertButtons = document.querySelectorAll('.btn-revert');
|
||||
for (let button of revertButtons) {
|
||||
const onclickAttr = button.getAttribute('onclick') || '';
|
||||
if (onclickAttr.includes(pluginName)) {
|
||||
btn = button;
|
||||
originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Reverting...';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Show loading notification if button not found
|
||||
if (!btn) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Reverting...',
|
||||
text: `Reverting ${pluginName} to previous version...`,
|
||||
type: 'info'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetch(`/plugins/api/revert/${pluginName}/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken'),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
backup_path: backupPath
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Revert Successful!',
|
||||
text: data.message || `Plugin ${pluginName} reverted successfully`,
|
||||
type: 'success'
|
||||
});
|
||||
} else {
|
||||
alert('Success: ' + (data.message || `Plugin ${pluginName} reverted successfully`));
|
||||
}
|
||||
// Reload page after short delay
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Revert Failed!',
|
||||
text: data.error || 'Failed to revert plugin',
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Failed to revert plugin'));
|
||||
}
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (typeof PNotify !== 'undefined') {
|
||||
new PNotify({
|
||||
title: 'Error!',
|
||||
text: 'Failed to revert plugin: ' + error.message,
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
alert('Error: Failed to revert plugin - ' + error.message);
|
||||
}
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallPlugin(pluginName) {
|
||||
if (!confirm(`Are you sure you want to uninstall ${pluginName}? All data from this plugin will be deleted.`)) {
|
||||
return;
|
||||
@@ -1848,7 +2149,63 @@ if (localStorage.getItem('pluginStoreNoticeDismissed') === 'true') {
|
||||
}
|
||||
|
||||
// Initialize view on page load
|
||||
// Convert cache expiry timestamp to local time
|
||||
function updateCacheExpiryTime() {
|
||||
const expiryElement = document.getElementById('cacheExpiryTime');
|
||||
if (!expiryElement) return;
|
||||
|
||||
const timestamp = expiryElement.getAttribute('data-timestamp');
|
||||
if (!timestamp) return;
|
||||
|
||||
try {
|
||||
// Convert Unix timestamp (seconds) to milliseconds for JavaScript Date
|
||||
const timestampMs = parseFloat(timestamp) * 1000;
|
||||
const expiryDate = new Date(timestampMs);
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(expiryDate.getTime())) {
|
||||
expiryElement.textContent = 'Invalid timestamp';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user's locale preferences
|
||||
const locale = navigator.language || navigator.userLanguage || 'en-US';
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Format date and time according to user's locale
|
||||
const dateOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
};
|
||||
|
||||
const timeOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
};
|
||||
|
||||
// Format date and time separately for better readability
|
||||
const dateStr = expiryDate.toLocaleDateString(locale, dateOptions);
|
||||
const timeStr = expiryDate.toLocaleTimeString(locale, timeOptions);
|
||||
|
||||
// Combine with timezone abbreviation
|
||||
const formatted = dateStr + ' ' + timeStr;
|
||||
|
||||
// Display with timezone info
|
||||
expiryElement.textContent = formatted;
|
||||
expiryElement.title = 'Local time: ' + formatted + ' | Timezone: ' + timezone;
|
||||
} catch (e) {
|
||||
console.error('Error formatting cache expiry time:', e);
|
||||
expiryElement.textContent = 'Error calculating time';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Update cache expiry time to local timezone
|
||||
updateCacheExpiryTime();
|
||||
|
||||
// Default to grid view if plugins exist, otherwise show store
|
||||
const gridView = document.getElementById('gridView');
|
||||
const storeView = document.getElementById('storeView');
|
||||
@@ -1858,6 +2215,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// No plugins installed, show store by default
|
||||
toggleView('store');
|
||||
}
|
||||
|
||||
// Load store plugins if store view is visible (either from toggleView or already displayed)
|
||||
setTimeout(function() {
|
||||
const storeViewCheck = document.getElementById('storeView');
|
||||
if (storeViewCheck && storeViewCheck.style.display !== 'none' && storePlugins.length === 0) {
|
||||
loadPluginStore();
|
||||
}
|
||||
}, 100); // Small delay to ensure DOM is ready
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -10,5 +10,8 @@ urlpatterns = [
|
||||
path('api/disable/<str:plugin_name>/', views.disable_plugin, name='disable_plugin'),
|
||||
path('api/store/plugins/', views.fetch_plugin_store, name='fetch_plugin_store'),
|
||||
path('api/store/install/<str:plugin_name>/', views.install_from_store, name='install_from_store'),
|
||||
path('api/store/upgrade/<str:plugin_name>/', views.upgrade_plugin, name='upgrade_plugin'),
|
||||
path('api/backups/<str:plugin_name>/', views.get_plugin_backups, name='get_plugin_backups'),
|
||||
path('api/revert/<str:plugin_name>/', views.revert_plugin, name='revert_plugin'),
|
||||
path('<str:plugin_name>/help/', views.plugin_help, name='plugin_help'),
|
||||
]
|
||||
|
||||
@@ -26,11 +26,15 @@ PLUGIN_STATE_DIR = '/home/cyberpanel/plugin_states'
|
||||
# Plugin store cache configuration
|
||||
PLUGIN_STORE_CACHE_DIR = '/home/cyberpanel/plugin_store_cache'
|
||||
PLUGIN_STORE_CACHE_FILE = os.path.join(PLUGIN_STORE_CACHE_DIR, 'plugins_cache.json')
|
||||
PLUGIN_STORE_CACHE_DURATION = 3600 # Cache for 1 hour (3600 seconds)
|
||||
PLUGIN_STORE_CACHE_DURATION = 3600 # Base cache duration: 1 hour (3600 seconds)
|
||||
PLUGIN_STORE_CACHE_RANDOM_OFFSET = 600 # Random offset: ±10 minutes (600 seconds) to prevent simultaneous requests
|
||||
GITHUB_REPO_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/contents'
|
||||
GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/master3395/cyberpanel-plugins/main'
|
||||
GITHUB_COMMITS_API = 'https://api.github.com/repos/master3395/cyberpanel-plugins/commits'
|
||||
|
||||
# Plugin backup configuration
|
||||
PLUGIN_BACKUP_DIR = '/home/cyberpanel/plugin_backups'
|
||||
|
||||
def _get_plugin_state_file(plugin_name):
|
||||
"""Get the path to the plugin state file"""
|
||||
if not os.path.exists(PLUGIN_STATE_DIR):
|
||||
@@ -364,9 +368,13 @@ def installed(request):
|
||||
for p in pluginList:
|
||||
logging.writeToFile(f" - {p.get('plugin_dir')}: installed={p.get('installed')}, enabled={p.get('enabled')}")
|
||||
|
||||
# Get cache expiry timestamp for display (will be converted to local time in browser)
|
||||
cache_expiry_timestamp, _ = _get_cache_expiry_time()
|
||||
|
||||
proc = httpProc(request, 'pluginHolder/plugins.html',
|
||||
{'plugins': pluginList, 'error_plugins': errorPlugins,
|
||||
'installed_count': installed_count, 'active_count': active_count}, 'admin')
|
||||
'installed_count': installed_count, 'active_count': active_count,
|
||||
'cache_expiry_timestamp': cache_expiry_timestamp}, 'admin')
|
||||
return proc.render()
|
||||
|
||||
@csrf_exempt
|
||||
@@ -604,6 +612,37 @@ def _ensure_cache_dir():
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error creating cache directory: {str(e)}")
|
||||
|
||||
def _get_cache_expiry_time():
|
||||
"""Get the cache expiry time (when cache will be updated next)
|
||||
|
||||
Returns:
|
||||
tuple: (expiry_timestamp, expiry_datetime_string) or (None, None) if no cache
|
||||
expiry_timestamp is Unix timestamp for JavaScript conversion to local time
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(PLUGIN_STORE_CACHE_FILE):
|
||||
return None, None
|
||||
|
||||
# Try to read stored expiry time from cache metadata
|
||||
try:
|
||||
with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f:
|
||||
cache_data = json.load(f)
|
||||
stored_expiry = cache_data.get('expiry_timestamp')
|
||||
if stored_expiry:
|
||||
# Return timestamp for JavaScript to convert to local time
|
||||
return stored_expiry, None
|
||||
except:
|
||||
pass # Fall back to calculation if metadata not found
|
||||
|
||||
# Fallback: calculate from file modification time (for old cache files)
|
||||
cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE)
|
||||
expiry_timestamp = cache_mtime + PLUGIN_STORE_CACHE_DURATION
|
||||
|
||||
return expiry_timestamp, None
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error getting cache expiry time: {str(e)}")
|
||||
return None, None
|
||||
|
||||
def _get_cached_plugins(allow_expired=False):
|
||||
"""Get plugins from cache if available and not expired
|
||||
|
||||
@@ -614,22 +653,32 @@ def _get_cached_plugins(allow_expired=False):
|
||||
if not os.path.exists(PLUGIN_STORE_CACHE_FILE):
|
||||
return None
|
||||
|
||||
# Check if cache is expired
|
||||
cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE)
|
||||
cache_age = time.time() - cache_mtime
|
||||
# Read cache file to get stored expiry time
|
||||
with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f:
|
||||
cache_data = json.load(f)
|
||||
|
||||
if cache_age > PLUGIN_STORE_CACHE_DURATION:
|
||||
# Check expiry using stored timestamp if available, otherwise fall back to file mtime
|
||||
current_time = time.time()
|
||||
stored_expiry = cache_data.get('expiry_timestamp')
|
||||
|
||||
if stored_expiry:
|
||||
# Use stored expiry time (with randomization)
|
||||
cache_age = current_time - (stored_expiry - cache_data.get('cache_duration', PLUGIN_STORE_CACHE_DURATION))
|
||||
is_expired = current_time >= stored_expiry
|
||||
else:
|
||||
# Fallback for old cache files without expiry metadata
|
||||
cache_mtime = os.path.getmtime(PLUGIN_STORE_CACHE_FILE)
|
||||
cache_age = current_time - cache_mtime
|
||||
is_expired = cache_age > PLUGIN_STORE_CACHE_DURATION
|
||||
|
||||
if is_expired:
|
||||
if not allow_expired:
|
||||
logging.writeToFile(f"Plugin store cache expired (age: {cache_age:.0f}s)")
|
||||
return None
|
||||
else:
|
||||
logging.writeToFile(f"Using expired cache as fallback (age: {cache_age:.0f}s)")
|
||||
|
||||
# Read cache file
|
||||
with open(PLUGIN_STORE_CACHE_FILE, 'r', encoding='utf-8') as f:
|
||||
cache_data = json.load(f)
|
||||
|
||||
if not allow_expired or cache_age <= PLUGIN_STORE_CACHE_DURATION:
|
||||
if not allow_expired or not is_expired:
|
||||
logging.writeToFile(f"Using cached plugin store data (age: {cache_age:.0f}s)")
|
||||
return cache_data.get('plugins', [])
|
||||
except Exception as e:
|
||||
@@ -637,20 +686,213 @@ def _get_cached_plugins(allow_expired=False):
|
||||
return None
|
||||
|
||||
def _save_plugins_cache(plugins):
|
||||
"""Save plugins to cache"""
|
||||
"""Save plugins to cache with randomized expiry time"""
|
||||
try:
|
||||
_ensure_cache_dir()
|
||||
|
||||
# Generate random cache duration to prevent simultaneous requests from all CyberPanel instances
|
||||
# Base duration ± random offset (e.g., 1 hour ± 10 minutes)
|
||||
import random
|
||||
random_offset = random.randint(-PLUGIN_STORE_CACHE_RANDOM_OFFSET, PLUGIN_STORE_CACHE_RANDOM_OFFSET)
|
||||
actual_cache_duration = PLUGIN_STORE_CACHE_DURATION + random_offset
|
||||
|
||||
# Calculate expiry timestamp
|
||||
current_time = time.time()
|
||||
expiry_timestamp = current_time + actual_cache_duration
|
||||
|
||||
cache_data = {
|
||||
'plugins': plugins,
|
||||
'cached_at': datetime.now().isoformat(),
|
||||
'cache_duration': PLUGIN_STORE_CACHE_DURATION
|
||||
'expiry_timestamp': expiry_timestamp,
|
||||
'cache_duration': actual_cache_duration,
|
||||
'base_duration': PLUGIN_STORE_CACHE_DURATION,
|
||||
'random_offset': random_offset
|
||||
}
|
||||
|
||||
with open(PLUGIN_STORE_CACHE_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(cache_data, f, indent=2, ensure_ascii=False)
|
||||
logging.writeToFile("Plugin store cache saved successfully")
|
||||
|
||||
expiry_datetime = datetime.fromtimestamp(expiry_timestamp)
|
||||
logging.writeToFile(f"Plugin store cache saved successfully. Expires at: {expiry_datetime.strftime('%Y-%m-%d %H:%M:%S')} (duration: {actual_cache_duration}s, offset: {random_offset:+d}s)")
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error saving plugin store cache: {str(e)}")
|
||||
|
||||
def _compare_versions(version1, version2):
|
||||
"""
|
||||
Compare two version strings (semantic versioning)
|
||||
Returns: 1 if version1 > version2, -1 if version1 < version2, 0 if equal
|
||||
"""
|
||||
try:
|
||||
# Split versions into parts
|
||||
v1_parts = [int(x) for x in version1.split('.')]
|
||||
v2_parts = [int(x) for x in version2.split('.')]
|
||||
|
||||
# Pad shorter version with zeros
|
||||
max_len = max(len(v1_parts), len(v2_parts))
|
||||
v1_parts.extend([0] * (max_len - len(v1_parts)))
|
||||
v2_parts.extend([0] * (max_len - len(v2_parts)))
|
||||
|
||||
# Compare each part
|
||||
for v1, v2 in zip(v1_parts, v2_parts):
|
||||
if v1 > v2:
|
||||
return 1
|
||||
elif v1 < v2:
|
||||
return -1
|
||||
return 0
|
||||
except:
|
||||
# Fallback to string comparison if parsing fails
|
||||
if version1 > version2:
|
||||
return 1
|
||||
elif version1 < version2:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def _get_installed_version(plugin_dir, plugin_install_dir):
|
||||
"""Get installed version of a plugin from meta.xml"""
|
||||
installed_path = os.path.join(plugin_install_dir, plugin_dir)
|
||||
meta_path = os.path.join(installed_path, 'meta.xml')
|
||||
|
||||
if os.path.exists(meta_path):
|
||||
try:
|
||||
pluginMetaData = ElementTree.parse(meta_path)
|
||||
root = pluginMetaData.getroot()
|
||||
version_elem = root.find('version')
|
||||
if version_elem is not None and version_elem.text:
|
||||
return version_elem.text.strip()
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error reading version from {meta_path}: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
def _create_plugin_backup(plugin_name, plugin_install_dir='/usr/local/CyberCP'):
|
||||
"""
|
||||
Create a backup of a plugin before upgrade
|
||||
Returns: (backup_path, backup_info) or (None, None) on failure
|
||||
"""
|
||||
try:
|
||||
# Ensure backup directory exists
|
||||
if not os.path.exists(PLUGIN_BACKUP_DIR):
|
||||
os.makedirs(PLUGIN_BACKUP_DIR, mode=0o755)
|
||||
|
||||
plugin_path = os.path.join(plugin_install_dir, plugin_name)
|
||||
if not os.path.exists(plugin_path):
|
||||
return None, None
|
||||
|
||||
# Get current version
|
||||
installed_version = _get_installed_version(plugin_name, plugin_install_dir) or 'unknown'
|
||||
|
||||
# Create backup directory with timestamp
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_name = f"{plugin_name}_v{installed_version}_{timestamp}"
|
||||
backup_path = os.path.join(PLUGIN_BACKUP_DIR, backup_name)
|
||||
|
||||
# Copy plugin directory
|
||||
import shutil
|
||||
shutil.copytree(plugin_path, backup_path)
|
||||
|
||||
# Create backup metadata
|
||||
backup_info = {
|
||||
'plugin_name': plugin_name,
|
||||
'version': installed_version,
|
||||
'timestamp': timestamp,
|
||||
'backup_path': backup_path,
|
||||
'created_at': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Save metadata as JSON
|
||||
metadata_file = os.path.join(backup_path, '.backup_metadata.json')
|
||||
with open(metadata_file, 'w') as f:
|
||||
json.dump(backup_info, f, indent=2)
|
||||
|
||||
logging.writeToFile(f"Created backup for {plugin_name} version {installed_version} at {backup_path}")
|
||||
|
||||
return backup_path, backup_info
|
||||
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error creating backup for {plugin_name}: {str(e)}")
|
||||
return None, None
|
||||
|
||||
def _get_plugin_backups(plugin_name):
|
||||
"""Get list of available backups for a plugin"""
|
||||
backups = []
|
||||
|
||||
if not os.path.exists(PLUGIN_BACKUP_DIR):
|
||||
return backups
|
||||
|
||||
try:
|
||||
for item in os.listdir(PLUGIN_BACKUP_DIR):
|
||||
if item.startswith(plugin_name + '_'):
|
||||
backup_path = os.path.join(PLUGIN_BACKUP_DIR, item)
|
||||
if os.path.isdir(backup_path):
|
||||
metadata_file = os.path.join(backup_path, '.backup_metadata.json')
|
||||
if os.path.exists(metadata_file):
|
||||
try:
|
||||
with open(metadata_file, 'r') as f:
|
||||
backup_info = json.load(f)
|
||||
backups.append(backup_info)
|
||||
except:
|
||||
# Fallback: parse from directory name
|
||||
parts = item.split('_')
|
||||
if len(parts) >= 3:
|
||||
version = parts[1].replace('v', '')
|
||||
timestamp = '_'.join(parts[2:])
|
||||
backups.append({
|
||||
'plugin_name': plugin_name,
|
||||
'version': version,
|
||||
'timestamp': timestamp,
|
||||
'backup_path': backup_path,
|
||||
'created_at': timestamp
|
||||
})
|
||||
else:
|
||||
# No metadata, try to parse from directory name
|
||||
parts = item.split('_')
|
||||
if len(parts) >= 3:
|
||||
version = parts[1].replace('v', '')
|
||||
timestamp = '_'.join(parts[2:])
|
||||
backups.append({
|
||||
'plugin_name': plugin_name,
|
||||
'version': version,
|
||||
'timestamp': timestamp,
|
||||
'backup_path': backup_path,
|
||||
'created_at': timestamp
|
||||
})
|
||||
|
||||
# Sort by timestamp (newest first)
|
||||
backups.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
||||
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error listing backups for {plugin_name}: {str(e)}")
|
||||
|
||||
return backups
|
||||
|
||||
def _restore_plugin_from_backup(plugin_name, backup_path):
|
||||
"""Restore a plugin from a backup"""
|
||||
try:
|
||||
plugin_install_dir = '/usr/local/CyberCP'
|
||||
plugin_path = os.path.join(plugin_install_dir, plugin_name)
|
||||
|
||||
# Remove current plugin installation
|
||||
if os.path.exists(plugin_path):
|
||||
import shutil
|
||||
shutil.rmtree(plugin_path)
|
||||
|
||||
# Restore from backup
|
||||
import shutil
|
||||
shutil.copytree(backup_path, plugin_path)
|
||||
|
||||
# Remove backup metadata file from restored plugin
|
||||
metadata_file = os.path.join(plugin_path, '.backup_metadata.json')
|
||||
if os.path.exists(metadata_file):
|
||||
os.remove(metadata_file)
|
||||
|
||||
logging.writeToFile(f"Restored {plugin_name} from backup {backup_path}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error restoring {plugin_name} from backup: {str(e)}")
|
||||
return False
|
||||
|
||||
def _enrich_store_plugins(plugins):
|
||||
"""Enrich store plugins with installed/enabled status from local system"""
|
||||
enriched = []
|
||||
@@ -672,8 +914,22 @@ def _enrich_store_plugins(plugins):
|
||||
# Check if plugin is enabled (only if installed)
|
||||
if plugin['installed']:
|
||||
plugin['enabled'] = _is_plugin_enabled(plugin_dir)
|
||||
|
||||
# Check for updates by comparing versions
|
||||
installed_version = _get_installed_version(plugin_dir, plugin_install_dir)
|
||||
store_version = plugin.get('version', '0.0.0')
|
||||
|
||||
if installed_version and store_version:
|
||||
# Update available if store version is newer
|
||||
plugin['update_available'] = _compare_versions(store_version, installed_version) > 0
|
||||
plugin['installed_version'] = installed_version
|
||||
else:
|
||||
plugin['update_available'] = False
|
||||
plugin['installed_version'] = installed_version or 'Unknown'
|
||||
else:
|
||||
plugin['enabled'] = False
|
||||
plugin['update_available'] = False
|
||||
plugin['installed_version'] = None
|
||||
|
||||
# Ensure is_paid field exists and is properly set (default to False if not set or invalid)
|
||||
# Handle all possible cases: missing, None, empty string, string values, boolean
|
||||
@@ -907,6 +1163,244 @@ def fetch_plugin_store(request):
|
||||
'plugins': []
|
||||
}, status=500)
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def upgrade_plugin(request, plugin_name):
|
||||
"""Upgrade an installed plugin from GitHub store"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
# Check if plugin is installed
|
||||
pluginInstalled = '/usr/local/CyberCP/' + plugin_name
|
||||
if not os.path.exists(pluginInstalled):
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Plugin not installed: {plugin_name}'
|
||||
}, status=400)
|
||||
|
||||
# Get current version before upgrade
|
||||
installed_version = _get_installed_version(plugin_name, '/usr/local/CyberCP')
|
||||
|
||||
# Create automatic backup before upgrade
|
||||
backup_path, backup_info = _create_plugin_backup(plugin_name)
|
||||
if backup_path:
|
||||
logging.writeToFile(f"Created automatic backup for {plugin_name} before upgrade: {backup_path}")
|
||||
else:
|
||||
logging.writeToFile(f"Warning: Failed to create backup for {plugin_name}, continuing with upgrade anyway")
|
||||
|
||||
logging.writeToFile(f"Starting upgrade of {plugin_name} from version {installed_version}")
|
||||
|
||||
# Download and install plugin from GitHub (same as install_from_store)
|
||||
import tempfile
|
||||
import shutil
|
||||
import zipfile
|
||||
import io
|
||||
|
||||
# Create temporary directory
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
zip_path = os.path.join(temp_dir, plugin_name + '.zip')
|
||||
|
||||
try:
|
||||
# Download from GitHub
|
||||
repo_zip_url = 'https://github.com/master3395/cyberpanel-plugins/archive/refs/heads/main.zip'
|
||||
logging.writeToFile(f"Downloading plugin upgrade from: {repo_zip_url}")
|
||||
|
||||
repo_req = urllib.request.Request(
|
||||
repo_zip_url,
|
||||
headers={
|
||||
'User-Agent': 'CyberPanel-Plugin-Store/1.0',
|
||||
'Accept': 'application/zip'
|
||||
}
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(repo_req, timeout=30) as repo_response:
|
||||
repo_zip_data = repo_response.read()
|
||||
|
||||
# Extract plugin directory from repository ZIP
|
||||
repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data))
|
||||
|
||||
# Find plugin directory in ZIP
|
||||
plugin_prefix = f'cyberpanel-plugins-main/{plugin_name}/'
|
||||
plugin_files = [f for f in repo_zip.namelist() if f.startswith(plugin_prefix)]
|
||||
|
||||
if not plugin_files:
|
||||
raise Exception(f'Plugin {plugin_name} not found in GitHub repository')
|
||||
|
||||
logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub")
|
||||
|
||||
# Create plugin ZIP file from GitHub with correct structure
|
||||
plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED)
|
||||
|
||||
for file_path in plugin_files:
|
||||
relative_path = file_path[len(plugin_prefix):]
|
||||
if relative_path: # Skip directories
|
||||
file_data = repo_zip.read(file_path)
|
||||
arcname = os.path.join(plugin_name, relative_path)
|
||||
plugin_zip.writestr(arcname, file_data)
|
||||
|
||||
plugin_zip.close()
|
||||
repo_zip.close()
|
||||
|
||||
# Verify ZIP was created
|
||||
if not os.path.exists(zip_path):
|
||||
raise Exception(f'Failed to create plugin ZIP file')
|
||||
|
||||
logging.writeToFile(f"Created plugin ZIP: {zip_path}")
|
||||
|
||||
# Copy ZIP to current directory (pluginInstaller expects it in cwd)
|
||||
original_cwd = os.getcwd()
|
||||
os.chdir(temp_dir)
|
||||
|
||||
try:
|
||||
zip_file = plugin_name + '.zip'
|
||||
if not os.path.exists(zip_file):
|
||||
raise Exception(f'Zip file {zip_file} not found in temp directory')
|
||||
|
||||
logging.writeToFile(f"Upgrading plugin using pluginInstaller")
|
||||
|
||||
# Install using pluginInstaller (this will overwrite existing files)
|
||||
try:
|
||||
pluginInstaller.installPlugin(plugin_name)
|
||||
except Exception as install_error:
|
||||
error_msg = str(install_error)
|
||||
logging.writeToFile(f"pluginInstaller.installPlugin raised exception: {error_msg}")
|
||||
# Check if plugin directory exists despite the error
|
||||
if not os.path.exists(pluginInstalled):
|
||||
raise Exception(f'Plugin upgrade failed: {error_msg}')
|
||||
|
||||
# Wait for file system to sync
|
||||
import time
|
||||
time.sleep(3)
|
||||
|
||||
# Verify plugin was upgraded
|
||||
if not os.path.exists(pluginInstalled):
|
||||
raise Exception(f'Plugin upgrade failed: {pluginInstalled} does not exist after upgrade')
|
||||
|
||||
# Get new version
|
||||
new_version = _get_installed_version(plugin_name, '/usr/local/CyberCP')
|
||||
|
||||
logging.writeToFile(f"Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}")
|
||||
|
||||
backup_message = ''
|
||||
if backup_path:
|
||||
backup_message = f' Backup created at: {backup_info.get("timestamp", "unknown")}'
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}.{backup_message}',
|
||||
'backup_created': backup_path is not None,
|
||||
'backup_path': backup_path if backup_path else None
|
||||
})
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
error_msg = f'Failed to download plugin from GitHub: HTTP {e.code}'
|
||||
if e.code == 404:
|
||||
error_msg = f'Plugin {plugin_name} not found in GitHub repository'
|
||||
logging.writeToFile(f"Error upgrading {plugin_name}: {error_msg}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': error_msg
|
||||
}, status=500)
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error upgrading plugin {plugin_name}: {str(e)}")
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
logging.writeToFile(f"Traceback: {error_details}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["GET"])
|
||||
def get_plugin_backups(request, plugin_name):
|
||||
"""Get list of available backups for a plugin"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
backups = _get_plugin_backups(plugin_name)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'backups': backups,
|
||||
'count': len(backups)
|
||||
})
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error getting backups for {plugin_name}: {str(e)}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def revert_plugin(request, plugin_name):
|
||||
"""Revert a plugin to a previous version from backup"""
|
||||
mailUtilities.checkHome()
|
||||
|
||||
try:
|
||||
# Get backup path from request
|
||||
data = json.loads(request.body)
|
||||
backup_path = data.get('backup_path')
|
||||
|
||||
if not backup_path:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Backup path is required'
|
||||
}, status=400)
|
||||
|
||||
# Verify backup exists
|
||||
if not os.path.exists(backup_path):
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Backup not found: {backup_path}'
|
||||
}, status=404)
|
||||
|
||||
# Get backup version info
|
||||
metadata_file = os.path.join(backup_path, '.backup_metadata.json')
|
||||
backup_version = 'unknown'
|
||||
if os.path.exists(metadata_file):
|
||||
try:
|
||||
with open(metadata_file, 'r') as f:
|
||||
backup_info = json.load(f)
|
||||
backup_version = backup_info.get('version', 'unknown')
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.writeToFile(f"Reverting {plugin_name} to version {backup_version} from backup {backup_path}")
|
||||
|
||||
# Restore from backup
|
||||
if _restore_plugin_from_backup(plugin_name, backup_path):
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Plugin {plugin_name} reverted successfully to version {backup_version}'
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Failed to restore plugin from backup'
|
||||
}, status=500)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Invalid JSON data'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
logging.writeToFile(f"Error reverting plugin {plugin_name}: {str(e)}")
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
logging.writeToFile(f"Traceback: {error_details}")
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
@csrf_exempt
|
||||
@require_http_methods(["POST"])
|
||||
def install_from_store(request, plugin_name):
|
||||
|
||||
Reference in New Issue
Block a user