diff --git a/pluginHolder/views.py b/pluginHolder/views.py index c3527e181..977098540 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -54,6 +54,34 @@ RESERVED_PLUGIN_DIRS = frozenset([ 'websiteFunctions', 'aiScanner', 'dns', 'help', 'installed', ]) +def _find_plugin_prefix_in_archive(namelist, plugin_name): + """ + Find the path prefix for a plugin inside a GitHub archive (e.g. repo-main/pluginName/ or repo-main/Category/pluginName/). + Returns (top_level, plugin_prefix) or (None, None) if not found. + """ + top_level = None + for name in namelist: + if '/' in name: + top_level = name.split('/')[0] + break + if not top_level: + return None, None + plugin_name_lower = plugin_name.lower() + # Check every path: find one that has a segment equal to plugin_name (e.g. .../pm2Manager/ or .../snappymailAdmin/) + for name in namelist: + if '/' not in name: + continue + parts = name.split('/') + # parts[0] = top_level, then we need a segment that matches plugin_name + for i in range(1, len(parts)): + if parts[i].lower() == plugin_name_lower: + # Plugin folder is at top_level/parts[1]/.../parts[i]/ + prefix_parts = [top_level] + parts[1:i + 1] + plugin_prefix = '/'.join(prefix_parts) + '/' + return top_level, plugin_prefix + return top_level, None + + def _get_plugin_source_path(plugin_name): """Return the full path to a plugin's source directory, or None if not found.""" for base in PLUGIN_SOURCE_PATHS: @@ -1439,29 +1467,10 @@ def upgrade_plugin(request, plugin_name): repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data)) namelist = repo_zip.namelist() - # Discover top-level folder (GitHub uses repo-name-branch, e.g. cyberpanel-plugins-main) - top_level = None - for name in namelist: - if '/' in name: - top_level = name.split('/')[0] - break - elif name and not name.endswith('/'): - top_level = name - break + # Find plugin folder (supports flat repo or nested e.g. Category/pluginName) + top_level, plugin_prefix = _find_plugin_prefix_in_archive(namelist, plugin_name) if not top_level: raise Exception('GitHub archive has no recognizable structure') - - # Find plugin folder in ZIP (case-insensitive: repo may have RedisManager vs redisManager) - plugin_prefix = None - plugin_name_lower = plugin_name.lower() - for name in namelist: - if '/' not in name: - continue - parts = name.split('/') - if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower: - # Use the actual casing from the ZIP for reading - plugin_prefix = f'{top_level}/{parts[1]}/' - break if not plugin_prefix: sample = namelist[:15] if len(namelist) > 15 else namelist logging.writeToFile(f"Plugin {plugin_name} not in archive. Top-level={top_level}, sample paths: {sample}") @@ -1697,23 +1706,10 @@ def install_from_store(request, plugin_name): repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data)) namelist = repo_zip.namelist() - # Discover top-level folder and find plugin (case-insensitive) - top_level = None - for name in namelist: - if '/' in name: - top_level = name.split('/')[0] - break + # Find plugin folder (supports flat repo or nested e.g. Category/pluginName) + top_level, plugin_prefix = _find_plugin_prefix_in_archive(namelist, plugin_name) if not top_level: raise Exception('GitHub archive has no recognizable structure') - plugin_prefix = None - plugin_name_lower = plugin_name.lower() - for name in namelist: - if '/' not in name: - continue - parts = name.split('/') - if len(parts) >= 2 and parts[0] == top_level and parts[1].lower() == plugin_name_lower: - plugin_prefix = f'{top_level}/{parts[1]}/' - break if not plugin_prefix: repo_zip.close() logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source") diff --git a/pluginInstaller/pluginInstaller.py b/pluginInstaller/pluginInstaller.py index 67d0c160e..e29fc6010 100644 --- a/pluginInstaller/pluginInstaller.py +++ b/pluginInstaller/pluginInstaller.py @@ -82,15 +82,58 @@ class pluginInstaller: with zipfile.ZipFile(pathToPlugin, 'r') as zf: zf.extractall(extract_dir) top_level = os.listdir(extract_dir) + plugin_name_lower = pluginName.lower() + # Prefer a top-level directory whose name matches pluginName (case-insensitive) + matching_dir = None + for name in top_level: + if os.path.isdir(os.path.join(extract_dir, name)) and name.lower() == plugin_name_lower: + matching_dir = name + break if len(top_level) == 1: single = os.path.join(extract_dir, top_level[0]) if os.path.isdir(single): - # One top-level directory: use it as the plugin dir (move or rename to pluginName) - shutil.move(single, pluginPath) + # One top-level directory + single_name = top_level[0] + # If it's the repo root (e.g. cyberpanel-plugins-main), check for plugin subdir + if single_name.lower() != plugin_name_lower: + plugin_subdir = os.path.join(single, pluginName) + if not os.path.isdir(plugin_subdir): + # Try case-insensitive subdir match + for entry in os.listdir(single): + if os.path.isdir(os.path.join(single, entry)) and entry.lower() == plugin_name_lower: + plugin_subdir = os.path.join(single, entry) + break + if os.path.isdir(plugin_subdir): + # Use the plugin subdir as the plugin content (avoid nesting repo root) + shutil.move(plugin_subdir, pluginPath) + shutil.rmtree(single, ignore_errors=True) + else: + shutil.move(single, pluginPath) + else: + shutil.move(single, pluginPath) else: # Single file at root os.makedirs(pluginPath, exist_ok=True) shutil.move(single, os.path.join(pluginPath, top_level[0])) + elif matching_dir: + # Multiple items: one is a dir matching pluginName - use it as plugin, put rest inside pluginPath + os.makedirs(pluginPath, exist_ok=True) + src_match = os.path.join(extract_dir, matching_dir) + # Move the matching plugin dir to pluginPath (replace if exists) + if os.path.exists(pluginPath): + shutil.rmtree(pluginPath) + shutil.move(src_match, pluginPath) + for name in top_level: + if name == matching_dir: + continue + src = os.path.join(extract_dir, name) + dst = os.path.join(pluginPath, name) + if os.path.exists(dst): + if os.path.isdir(dst): + shutil.rmtree(dst) + else: + os.remove(dst) + shutil.move(src, dst) else: # Multiple items or empty: place everything inside pluginName/ os.makedirs(pluginPath, exist_ok=True) diff --git a/upgrade_modules/06_components.sh b/upgrade_modules/06_components.sh index b76d62850..b7eab9ad5 100644 --- a/upgrade_modules/06_components.sh +++ b/upgrade_modules/06_components.sh @@ -37,12 +37,13 @@ Pre_Upgrade_Required_Components() { # Check if CyberCP directory exists but is incomplete/damaged echo -e "[$(date +"%Y-%m-%d %H:%M:%S")] Checking CyberCP directory integrity..." | tee -a /var/log/cyberpanel_upgrade_debug.log -# Define essential CyberCP components +# Define essential CyberCP components (do not add /usr/local/CyberCP/manage - it does not exist; see usmannasir/cyberpanel#1721) CYBERCP_ESSENTIAL_DIRS=( "/usr/local/CyberCP/CyberCP" "/usr/local/CyberCP/plogical" "/usr/local/CyberCP/websiteFunctions" - "/usr/local/CyberCP/manage" + "/usr/local/CyberCP/pluginHolder" + "/usr/local/CyberCP/pluginInstaller" ) CYBERCP_MISSING=0