plugin distribution system

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-09 21:38:14 +00:00
parent cf8181d1f9
commit 80f1709a2e
22 changed files with 2804 additions and 35 deletions

View File

@@ -0,0 +1,321 @@
"""
Plugin Distribution System Utilities
Helper functions for plugin management, ZIP creation, and versioning.
"""
import os
import hashlib
import zipfile
import shutil
import logging
from pathlib import Path
from typing import Optional, Tuple
from django.conf import settings
logger = logging.getLogger(__name__)
# Base paths for plugin storage
PLUGINS_ROOT = Path('/data/app/igny8/plugins')
WORDPRESS_SOURCE = PLUGINS_ROOT / 'wordpress' / 'source'
WORDPRESS_DIST = PLUGINS_ROOT / 'wordpress' / 'dist'
SHOPIFY_SOURCE = PLUGINS_ROOT / 'shopify' / 'source'
SHOPIFY_DIST = PLUGINS_ROOT / 'shopify' / 'dist'
CUSTOM_SOURCE = PLUGINS_ROOT / 'custom-site' / 'source'
CUSTOM_DIST = PLUGINS_ROOT / 'custom-site' / 'dist'
PLATFORM_PATHS = {
'wordpress': {
'source': WORDPRESS_SOURCE,
'dist': WORDPRESS_DIST,
},
'shopify': {
'source': SHOPIFY_SOURCE,
'dist': SHOPIFY_DIST,
},
'custom': {
'source': CUSTOM_SOURCE,
'dist': CUSTOM_DIST,
},
}
def get_source_path(platform: str, plugin_slug: str) -> Path:
"""Get the source directory path for a plugin."""
paths = PLATFORM_PATHS.get(platform)
if not paths:
raise ValueError(f"Unknown platform: {platform}")
return paths['source'] / plugin_slug
def get_dist_path(platform: str) -> Path:
"""Get the distribution directory path for a platform."""
paths = PLATFORM_PATHS.get(platform)
if not paths:
raise ValueError(f"Unknown platform: {platform}")
return paths['dist']
def calculate_checksum(file_path: str) -> str:
"""Calculate SHA256 checksum of a file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
def parse_version_to_code(version_string: str) -> int:
"""
Convert version string to numeric code for comparison.
'1.0.0' -> 10000
'1.0.1' -> 10001
'1.2.3' -> 10203
'2.0.0' -> 20000
"""
try:
parts = version_string.split('.')
major = int(parts[0]) if len(parts) > 0 else 0
minor = int(parts[1]) if len(parts) > 1 else 0
patch = int(parts[2]) if len(parts) > 2 else 0
return major * 10000 + minor * 100 + patch
except (ValueError, IndexError):
return 0
def code_to_version(version_code: int) -> str:
"""Convert version code back to version string."""
major = version_code // 10000
minor = (version_code % 10000) // 100
patch = version_code % 100
return f"{major}.{minor}.{patch}"
def update_php_version(file_path: str, new_version: str) -> bool:
"""
Update version numbers in a PHP plugin file.
Updates both the plugin header and any $version variables.
"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
import re
# Update "Version: X.X.X" in plugin header
content = re.sub(
r'(Version:\s*)\d+\.\d+\.\d+',
f'\\g<1>{new_version}',
content
)
# Update IGNY8_BRIDGE_VERSION constant
content = re.sub(
r"(define\s*\(\s*'IGNY8_BRIDGE_VERSION'\s*,\s*')[^']+(')",
f"\\g<1>{new_version}\\g<2>",
content
)
# Update $version variable
content = re.sub(
r"(\$version\s*=\s*')[^']+(')",
f"\\g<1>{new_version}\\g<2>",
content
)
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return True
except Exception as e:
logger.error(f"Failed to update PHP version: {e}")
return False
def create_plugin_zip(
platform: str,
plugin_slug: str,
version: str,
update_version: bool = True
) -> Tuple[Optional[str], Optional[str], Optional[int]]:
"""
Create a ZIP file for plugin distribution.
Args:
platform: Target platform ('wordpress', 'shopify', 'custom')
plugin_slug: Plugin slug (e.g., 'igny8-wp-bridge')
version: Version string (e.g., '1.0.1')
update_version: Whether to update version numbers in source files
Returns:
Tuple of (file_path, checksum, file_size) or (None, None, None) on error
"""
try:
source_path = get_source_path(platform, plugin_slug)
dist_path = get_dist_path(platform)
if not source_path.exists():
logger.error(f"Source path does not exist: {source_path}")
return None, None, None
# Ensure dist directory exists
dist_path.mkdir(parents=True, exist_ok=True)
# Create temp directory for packaging
import tempfile
temp_dir = tempfile.mkdtemp()
package_dir = Path(temp_dir) / plugin_slug
try:
# Copy source files to temp directory
shutil.copytree(source_path, package_dir)
# Update version in main plugin file if requested
if update_version and platform == 'wordpress':
main_file = package_dir / f"{plugin_slug}.php"
# Handle renamed plugin file
if not main_file.exists():
main_file = package_dir / "igny8-bridge.php"
if main_file.exists():
update_php_version(str(main_file), version)
# Remove unwanted files
patterns_to_remove = [
'**/__pycache__',
'**/*.pyc',
'**/.git',
'**/.gitignore',
'**/tests',
'**/tester',
'**/.DS_Store',
]
for pattern in patterns_to_remove:
for path in package_dir.glob(pattern):
if path.is_dir():
shutil.rmtree(path)
else:
path.unlink()
# Create ZIP file
zip_filename = f"{plugin_slug}-v{version}.zip"
zip_path = dist_path / zip_filename
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(package_dir):
for file in files:
file_path = Path(root) / file
arcname = file_path.relative_to(temp_dir)
zipf.write(file_path, arcname)
# Create/update latest symlink
latest_link = dist_path / f"{plugin_slug}-latest.zip"
if latest_link.exists() or latest_link.is_symlink():
latest_link.unlink()
latest_link.symlink_to(zip_filename)
# Calculate checksum and file size
checksum = calculate_checksum(str(zip_path))
file_size = zip_path.stat().st_size
logger.info(f"Created plugin ZIP: {zip_path} ({file_size} bytes)")
return zip_filename, checksum, file_size
finally:
# Clean up temp directory
shutil.rmtree(temp_dir, ignore_errors=True)
except Exception as e:
logger.exception(f"Failed to create plugin ZIP: {e}")
return None, None, None
def get_plugin_file_path(platform: str, file_name: str) -> Optional[Path]:
"""Get the full path to a plugin ZIP file."""
dist_path = get_dist_path(platform)
file_path = dist_path / file_name
if file_path.exists():
return file_path
return None
def verify_checksum(file_path: str, expected_checksum: str) -> bool:
"""Verify a file's checksum matches expected value."""
actual_checksum = calculate_checksum(file_path)
return actual_checksum == expected_checksum
def get_installed_version_from_header(file_path: str) -> Optional[str]:
"""Extract version from a PHP plugin file header."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
# Read first 8KB which should contain the header
content = f.read(8192)
import re
match = re.search(r'Version:\s*(\d+\.\d+\.\d+)', content)
if match:
return match.group(1)
return None
except Exception as e:
logger.error(f"Failed to extract version from {file_path}: {e}")
return None
def list_available_versions(platform: str, plugin_slug: str) -> list:
"""List all available versions for a plugin."""
dist_path = get_dist_path(platform)
versions = []
import re
pattern = re.compile(rf'^{re.escape(plugin_slug)}-v(\d+\.\d+\.\d+)\.zip$')
for file in dist_path.iterdir():
if file.is_file():
match = pattern.match(file.name)
if match:
versions.append({
'version': match.group(1),
'version_code': parse_version_to_code(match.group(1)),
'file_name': file.name,
'file_size': file.stat().st_size,
})
# Sort by version_code descending
versions.sort(key=lambda x: x['version_code'], reverse=True)
return versions
def cleanup_old_versions(platform: str, plugin_slug: str, keep_count: int = 5) -> int:
"""
Remove old version ZIP files, keeping the most recent ones.
Args:
platform: Target platform
plugin_slug: Plugin slug
keep_count: Number of recent versions to keep
Returns:
Number of versions removed
"""
versions = list_available_versions(platform, plugin_slug)
removed = 0
if len(versions) > keep_count:
versions_to_remove = versions[keep_count:]
dist_path = get_dist_path(platform)
for version_info in versions_to_remove:
file_path = dist_path / version_info['file_name']
try:
file_path.unlink()
removed += 1
logger.info(f"Removed old version: {file_path}")
except Exception as e:
logger.error(f"Failed to remove {file_path}: {e}")
return removed