99Now includes remote plugin installation from GitHub repositories.
1010"""
1111
12- from fastapi import APIRouter , HTTPException , Depends , status
12+ from fastapi import APIRouter , HTTPException , Depends , status , File , UploadFile , Form
1313from sqlalchemy .ext .asyncio import AsyncSession
14- from typing import Dict , Any , Optional
14+ from typing import Dict , Any , Optional , Union
1515from pathlib import Path
1616import importlib .util
1717import json
1818import structlog
19+ import tempfile
20+ import shutil
1921from pydantic import BaseModel
2022
2123# Import the remote installer
@@ -45,11 +47,19 @@ def _get_error_suggestions(step: str, error_message: str) -> list:
4547 "Verify the release contains downloadable assets" ,
4648 "Ensure the release archive format is supported (tar.gz, zip)"
4749 ])
50+ elif step == 'file_extraction' :
51+ suggestions .extend ([
52+ "Ensure the uploaded file is a valid archive (ZIP, TAR.GZ)" ,
53+ "Check that the file is not corrupted" ,
54+ "Verify the file size is within limits (100MB max)" ,
55+ "Try re-uploading the file if extraction fails"
56+ ])
4857 elif step == 'plugin_validation' :
4958 suggestions .extend ([
5059 "Ensure the plugin contains a 'lifecycle_manager.py' file" ,
5160 "Check that the lifecycle manager extends BaseLifecycleManager" ,
52- "Verify the plugin structure follows BrainDrive plugin standards"
61+ "Verify the plugin structure follows BrainDrive plugin standards" ,
62+ "Make sure the archive contains a valid BrainDrive plugin"
5363 ])
5464 elif step == 'lifecycle_manager_install' :
5565 suggestions .extend ([
@@ -66,8 +76,8 @@ def _get_error_suggestions(step: str, error_message: str) -> list:
6676 else :
6777 suggestions .extend ([
6878 "Check the server logs for more detailed error information" ,
69- "Ensure the plugin repository follows BrainDrive plugin standards" ,
70- "Try installing a different version of the plugin"
79+ "Ensure the plugin follows BrainDrive plugin standards" ,
80+ "Try installing a different version or format of the plugin"
7181 ])
7282
7383 return suggestions
@@ -77,6 +87,12 @@ class RemoteInstallRequest(BaseModel):
7787 repo_url : str
7888 version : str = "latest"
7989
90+ class UnifiedInstallRequest (BaseModel ):
91+ method : str # 'github' or 'local-file'
92+ repo_url : Optional [str ] = None
93+ version : Optional [str ] = "latest"
94+ filename : Optional [str ] = None
95+
8096class UpdateCheckResponse (BaseModel ):
8197 plugin_id : str
8298 current_version : str
@@ -733,6 +749,186 @@ async def get_plugin_info(plugin_slug: str):
733749 )
734750
735751# Remote plugin installation endpoints
752+ @router .post ("/install" )
753+ async def install_plugin_unified (
754+ method : str = Form (...),
755+ repo_url : Optional [str ] = Form (None ),
756+ version : Optional [str ] = Form ("latest" ),
757+ filename : Optional [str ] = Form (None ),
758+ file : Optional [UploadFile ] = File (None ),
759+ current_user = Depends (get_current_user ),
760+ db : AsyncSession = Depends (get_db )
761+ ):
762+ """
763+ Unified plugin installation endpoint supporting both GitHub and local file methods.
764+
765+ For GitHub installation:
766+ - method: 'github'
767+ - repo_url: GitHub repository URL
768+ - version: Version to install (optional, defaults to 'latest')
769+
770+ For local file installation:
771+ - method: 'local-file'
772+ - file: Archive file (ZIP, RAR, TAR.GZ)
773+ - filename: Original filename
774+ """
775+ try :
776+ logger .info (f"Unified plugin installation requested by user { current_user .id } " )
777+ logger .info (f"Method: { method } " )
778+
779+ if method == 'github' :
780+ if not repo_url :
781+ raise HTTPException (
782+ status_code = status .HTTP_400_BAD_REQUEST ,
783+ detail = "repo_url is required for GitHub installation"
784+ )
785+
786+ logger .info (f"GitHub installation - Repository URL: { repo_url } , Version: { version } " )
787+
788+ # Use the remote installer to install the plugin
789+ result = await remote_installer .install_from_url (
790+ repo_url = repo_url ,
791+ user_id = current_user .id ,
792+ version = version or "latest"
793+ )
794+
795+ if result ['success' ]:
796+ return {
797+ "status" : "success" ,
798+ "message" : f"Plugin installed successfully from { repo_url } " ,
799+ "data" : {
800+ "plugin_id" : result .get ('plugin_id' ),
801+ "plugin_slug" : result .get ('plugin_slug' ),
802+ "modules_created" : result .get ('modules_created' , []),
803+ "plugin_directory" : result .get ('plugin_directory' ),
804+ "source" : "github" ,
805+ "repo_url" : repo_url ,
806+ "version" : version or "latest"
807+ }
808+ }
809+ else :
810+ # Enhanced error response with suggestions
811+ error_details = result .get ('details' , {})
812+ step = error_details .get ('step' , 'unknown' )
813+ error_message = result .get ('error' , 'Installation failed' )
814+ suggestions = _get_error_suggestions (step , error_message )
815+
816+ raise HTTPException (
817+ status_code = status .HTTP_400_BAD_REQUEST ,
818+ detail = {
819+ "message" : error_message ,
820+ "details" : error_details ,
821+ "suggestions" : suggestions
822+ }
823+ )
824+
825+ elif method == 'local-file' :
826+ if not file :
827+ raise HTTPException (
828+ status_code = status .HTTP_400_BAD_REQUEST ,
829+ detail = "file is required for local file installation"
830+ )
831+
832+ logger .info (f"Local file installation - Filename: { filename } , Size: { file .size if hasattr (file , 'size' ) else 'unknown' } " )
833+
834+ # Validate file size (100MB limit)
835+ MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
836+ if hasattr (file , 'size' ) and file .size > MAX_FILE_SIZE :
837+ raise HTTPException (
838+ status_code = status .HTTP_400_BAD_REQUEST ,
839+ detail = f"File size ({ file .size } bytes) exceeds maximum allowed size ({ MAX_FILE_SIZE } bytes)"
840+ )
841+
842+ # Validate file format
843+ if filename :
844+ supported_formats = ['.zip' , '.rar' , '.tar.gz' , '.tgz' ]
845+ file_ext = None
846+ filename_lower = filename .lower ()
847+ for ext in supported_formats :
848+ if filename_lower .endswith (ext ):
849+ file_ext = ext
850+ break
851+
852+ if not file_ext :
853+ raise HTTPException (
854+ status_code = status .HTTP_400_BAD_REQUEST ,
855+ detail = f"Unsupported file format. Supported formats: { ', ' .join (supported_formats )} "
856+ )
857+
858+ # Save uploaded file to temporary location
859+ import tempfile
860+ import shutil
861+ temp_dir = Path (tempfile .mkdtemp ())
862+ temp_file_path = temp_dir / (filename or "uploaded_plugin" )
863+
864+ try :
865+ # Write uploaded file to temporary location
866+ with open (temp_file_path , "wb" ) as buffer :
867+ shutil .copyfileobj (file .file , buffer )
868+
869+ logger .info (f"File saved to temporary location: { temp_file_path } " )
870+
871+ # Use the remote installer to install from file
872+ result = await remote_installer .install_from_file (
873+ file_path = temp_file_path ,
874+ user_id = current_user .id ,
875+ filename = filename
876+ )
877+
878+ if result ['success' ]:
879+ return {
880+ "status" : "success" ,
881+ "message" : f"Plugin '{ filename } ' installed successfully from local file" ,
882+ "data" : {
883+ "plugin_id" : result .get ('plugin_id' ),
884+ "plugin_slug" : result .get ('plugin_slug' ),
885+ "modules_created" : result .get ('modules_created' , []),
886+ "plugin_directory" : result .get ('plugin_directory' ),
887+ "source" : "local-file" ,
888+ "filename" : filename ,
889+ "file_size" : temp_file_path .stat ().st_size if temp_file_path .exists () else 0
890+ }
891+ }
892+ else :
893+ # Enhanced error response with suggestions
894+ error_details = result .get ('details' , {})
895+ step = error_details .get ('step' , 'unknown' )
896+ error_message = result .get ('error' , 'Installation failed' )
897+ suggestions = _get_error_suggestions (step , error_message )
898+
899+ raise HTTPException (
900+ status_code = status .HTTP_400_BAD_REQUEST ,
901+ detail = {
902+ "message" : error_message ,
903+ "details" : error_details ,
904+ "suggestions" : suggestions
905+ }
906+ )
907+
908+ finally :
909+ # Clean up temporary file
910+ try :
911+ if temp_file_path .exists ():
912+ temp_file_path .unlink ()
913+ temp_dir .rmdir ()
914+ except Exception as cleanup_error :
915+ logger .warning (f"Failed to clean up temporary file: { cleanup_error } " )
916+
917+ else :
918+ raise HTTPException (
919+ status_code = status .HTTP_400_BAD_REQUEST ,
920+ detail = f"Unsupported installation method: { method } . Supported methods: 'github', 'local-file'"
921+ )
922+
923+ except HTTPException :
924+ raise
925+ except Exception as e :
926+ logger .error (f"Unexpected error during unified plugin installation: { e } " )
927+ raise HTTPException (
928+ status_code = status .HTTP_500_INTERNAL_SERVER_ERROR ,
929+ detail = f"Internal server error during plugin installation: { str (e )} "
930+ )
931+
736932@router .post ("/install-from-url" )
737933async def install_plugin_from_repository (
738934 request : RemoteInstallRequest ,
0 commit comments