Plugin store: fix install for all plugins (nested repo + extraction)

- Find plugin in GitHub archive by any path segment (Category/pluginName)
- extractPlugin: prefer top-level dir matching plugin name; handle repo-root
  single dir by using plugin subdir when present (avoids wrong location)
- Add pluginHolder and pluginInstaller to upgrade recovery essential dirs

Fixes: Files extracted to wrong location (e.g. pm2Manager, README.md in root)
This commit is contained in:
master3395
2026-03-06 20:40:32 +01:00
committed by KraoESPfan1n
parent a6268d7f53
commit bbf0436c0d
3 changed files with 80 additions and 40 deletions

View File

@@ -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")

View File

@@ -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)

View File

@@ -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