diff --git a/plogical/CyberCPLogFileWriter.py b/plogical/CyberCPLogFileWriter.py index 2babc8527..d91d2f8ee 100644 --- a/plogical/CyberCPLogFileWriter.py +++ b/plogical/CyberCPLogFileWriter.py @@ -7,6 +7,11 @@ import smtplib class CyberCPLogFileWriter: fileName = "/home/cyberpanel/error-logs.txt" + @staticmethod + def get_current_timestamp(): + """Return current timestamp in same format used for log lines (for errorSanitizer etc).""" + return time.strftime("%m.%d.%Y_%H-%M-%S") + @staticmethod def AddFromHeader(sender, message): try: diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 4746f88af..0bda088bc 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -887,6 +887,27 @@ def _get_installed_version(plugin_dir, plugin_install_dir): return None +def _sync_meta_xml_from_github(plugin_name, plugin_install_dir='/usr/local/CyberCP'): + """ + Fetch meta.xml from GitHub raw (main) and overwrite installed meta.xml. + Ensures installed version matches store even when archive ZIP is cached/stale. + Returns True if synced, False on non-fatal failure (logged). + """ + meta_url = f'{GITHUB_RAW_BASE}/{plugin_name}/meta.xml' + meta_path = os.path.join(plugin_install_dir, plugin_name, 'meta.xml') + try: + req = urllib.request.Request(meta_url, headers={'User-Agent': 'CyberPanel-Plugin-Store/1.0'}) + with urllib.request.urlopen(req, timeout=10) as resp: + content = resp.read() + if content: + with open(meta_path, 'wb') as f: + f.write(content) + logging.writeToFile(f"Synced meta.xml for {plugin_name} from GitHub raw") + return True + except Exception as e: + logging.writeToFile(f"Could not sync meta.xml for {plugin_name} from GitHub: {str(e)}") + return False + def _create_plugin_backup(plugin_name, plugin_install_dir='/usr/local/CyberCP'): """ Create a backup of a plugin before upgrade @@ -1362,22 +1383,48 @@ def upgrade_plugin(request, plugin_name): # Extract plugin directory from repository ZIP repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data)) + namelist = repo_zip.namelist() - # 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)] + # 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 + 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}") + raise Exception(f'Plugin {plugin_name} not found in GitHub repository (checked under {top_level}/)') + + plugin_files = [f for f in namelist if f.startswith(plugin_prefix)] if not plugin_files: + logging.writeToFile(f"Plugin {plugin_name}: no files under prefix {plugin_prefix}") 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") + logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub (prefix {plugin_prefix})") - # Create plugin ZIP file from GitHub with correct structure + # Create plugin ZIP with correct structure: plugin_name/... for install to /usr/local/CyberCP/plugin_name/ 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 + if relative_path: # Skip directory-only entries file_data = repo_zip.read(file_path) arcname = os.path.join(plugin_name, relative_path) plugin_zip.writestr(arcname, file_data) @@ -1420,7 +1467,10 @@ def upgrade_plugin(request, plugin_name): if not os.path.exists(pluginInstalled): raise Exception(f'Plugin upgrade failed: {pluginInstalled} does not exist after upgrade') - # Get new version + # Sync meta.xml from GitHub raw so version matches store (archive ZIP can be cached/stale) + _sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP') + + # Get new version (now reflects meta.xml from main) new_version = _get_installed_version(plugin_name, '/usr/local/CyberCP') logging.writeToFile(f"Plugin {plugin_name} upgraded successfully from {installed_version} to {new_version}") @@ -1593,32 +1643,46 @@ def install_from_store(request, plugin_name): # Extract plugin directory from repository ZIP repo_zip = zipfile.ZipFile(io.BytesIO(repo_zip_data)) + namelist = repo_zip.namelist() - # 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: + # 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 + 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") use_local_fallback = True else: - logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub") - - # Create plugin ZIP file from GitHub with correct structure - # The ZIP must contain plugin_name/ directory structure for proper extraction - plugin_zip = zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) - - for file_path in plugin_files: - # Remove the repository root prefix - relative_path = file_path[len(plugin_prefix):] - if relative_path: # Skip directories - file_data = repo_zip.read(file_path) - # Add plugin_name prefix to maintain directory structure - arcname = os.path.join(plugin_name, relative_path) - plugin_zip.writestr(arcname, file_data) - - plugin_zip.close() - repo_zip.close() + plugin_files = [f for f in namelist if f.startswith(plugin_prefix)] + if not plugin_files: + repo_zip.close() + logging.writeToFile(f"Plugin {plugin_name} not found in GitHub repository, trying local source") + use_local_fallback = True + else: + logging.writeToFile(f"Found {len(plugin_files)} files for plugin {plugin_name} in GitHub") + 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: + 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() except Exception as github_error: logging.writeToFile(f"GitHub download failed for {plugin_name}: {str(github_error)}, trying local source") use_local_fallback = True @@ -1694,6 +1758,9 @@ def install_from_store(request, plugin_name): raise Exception(f'Plugin installation failed: Files extracted to wrong location. Found {found_root_files} in /usr/local/CyberCP/ root instead of {pluginInstalled}/') raise Exception(f'Plugin installation failed: {pluginInstalled} does not exist after installation') + # Sync meta.xml from GitHub raw so version matches store + _sync_meta_xml_from_github(plugin_name, '/usr/local/CyberCP') + logging.writeToFile(f"Plugin {plugin_name} installed successfully") # Set plugin to enabled by default after installation