/ config_portal / backend / plugin_catalog_server.py
plugin_catalog_server.py
  1  from flask import Flask, jsonify, request, send_from_directory
  2  import os
  3  import shutil
  4  import logging
  5  from pathlib import Path
  6  
  7  from .plugin_catalog.scraper import PluginScraper
  8  from .plugin_catalog.registry_manager import RegistryManager
  9  from .plugin_catalog.constants import PLUGIN_CATALOG_TEMP_DIR
 10  
 11  logger = logging.getLogger(__name__)
 12  if not logger.hasHandlers():
 13      logging.basicConfig(
 14          level=logging.INFO,
 15          format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 16      )
 17  
 18  registry_manager = RegistryManager()
 19  plugin_scraper = PluginScraper()
 20  
 21  
 22  def create_plugin_catalog_app(shared_config=None):
 23      current_dir = Path(__file__).parent
 24      static_folder_path = (
 25          Path(__file__).resolve().parent.parent / "frontend" / "static" / "client"
 26      )
 27      app = Flask(__name__, static_folder=str(static_folder_path), static_url_path="")
 28      app.config["SHARED_CONFIG"] = shared_config
 29  
 30      logger.info("Performing initial plugin scrape on startup...")
 31      try:
 32          initial_registries = registry_manager.get_all_registries()
 33          plugin_scraper.get_all_plugins(initial_registries, force_refresh=True)
 34          logger.info(
 35              f"Initial scrape complete. Found {len(plugin_scraper.plugin_cache)} plugins."
 36          )
 37      except Exception as e:
 38          logger.error(f"Error during initial plugin scrape: {e}", exc_info=True)
 39  
 40      @app.route("/")
 41      def serve_index():
 42          return send_from_directory(app.static_folder, "index.html")
 43  
 44      @app.route("/assets/<path:filename>")
 45      def serve_assets(filename):
 46          assets_dir = Path(app.static_folder) / "assets"
 47          return send_from_directory(str(assets_dir), filename)
 48  
 49      @app.route("/api/plugin_catalog/plugins", methods=["GET"])
 50      def get_plugins_api():
 51          search_query = request.args.get("search", "").lower()
 52  
 53          if not plugin_scraper.is_cache_populated:
 54              logger.info(
 55                  "Plugin cache not populated, attempting to refresh for /api/plugin_catalog/plugins request."
 56              )
 57              all_regs = registry_manager.get_all_registries()
 58              plugin_scraper.get_all_plugins(all_regs, force_refresh=True)
 59  
 60          plugins_to_filter = plugin_scraper.plugin_cache
 61  
 62          if search_query:
 63              plugins_to_filter = [
 64                  p
 65                  for p in plugins_to_filter
 66                  if search_query in p.pyproject.name.lower()
 67                  or (
 68                      p.pyproject.description
 69                      and search_query in p.pyproject.description.lower()
 70                  )
 71              ]
 72          return jsonify([p.model_dump(exclude_none=True) for p in plugins_to_filter])
 73  
 74      @app.route("/api/plugin_catalog/plugins/<plugin_id>/details", methods=["GET"])
 75      def get_plugin_details_api(plugin_id: str):
 76          details = plugin_scraper.get_plugin_details(plugin_id)
 77          if details:
 78              return jsonify(details.model_dump(exclude_none=True))
 79          return jsonify({"error": "Plugin not found", "status": "failure"}), 404
 80  
 81      @app.route("/api/plugin_catalog/plugins/install", methods=["POST"])
 82      def install_plugin_api():
 83          data = request.json
 84          if not data:
 85              return (
 86                  jsonify({"error": "Request body must be JSON", "status": "failure"}),
 87                  400,
 88              )
 89  
 90          plugin_id = data.get("pluginId")
 91          component_name = data.get("componentName")
 92  
 93          if not plugin_id or not component_name:
 94              return (
 95                  jsonify(
 96                      {
 97                          "error": "pluginId and componentName are required",
 98                          "status": "failure",
 99                      }
100                  ),
101                  400,
102              )
103  
104          plugin_info = plugin_scraper.get_plugin_details(plugin_id)
105          if not plugin_info:
106              return (
107                  jsonify({"error": "Plugin not found to install", "status": "failure"}),
108                  404,
109              )
110  
111          success, message = plugin_scraper.install_plugin_cli(
112              plugin_info, component_name
113          )
114          if success:
115              return jsonify({"message": message, "status": "success"})
116          else:
117              return jsonify({"error": message, "status": "failure"}), 500
118  
119      @app.route("/api/plugin_catalog/registries", methods=["GET"])
120      def get_registries_api():
121          registries = registry_manager.get_all_registries()
122          return jsonify([r.model_dump(exclude_none=True) for r in registries])
123  
124      @app.route("/api/plugin_catalog/registries", methods=["POST"])
125      def add_registry_api():
126          data = request.json
127          if not data:
128              return (
129                  jsonify({"error": "Request body must be JSON", "status": "failure"}),
130                  400,
131              )
132  
133          path_or_url = data.get("path_or_url")
134          name = data.get("name")
135  
136          if not path_or_url:
137              return (
138                  jsonify({"error": "path_or_url is required", "status": "failure"}),
139                  400,
140              )
141  
142          success, is_update = registry_manager.add_registry(path_or_url, name=name)
143          
144          if success:
145              action = "updated" if is_update else "added"
146              logger.info(
147                  f"Registry '{path_or_url}' (Name: {name if name else 'N/A'}) {action} by user."
148              )
149              
150              # For re-added registries, clear the git cache to force fresh clone
151              if is_update:
152                  registry_id = registry_manager._generate_registry_id(path_or_url)
153                  plugin_scraper.clear_git_cache(registry_id)
154                  logger.info(f"Cleared git cache for re-added registry: {registry_id}")
155              
156              # Always force refresh plugins, and use fresh clone for re-added registries
157              all_regs = registry_manager.get_all_registries()
158              plugin_scraper.get_all_plugins(all_regs, force_refresh=True, force_fresh_clone=is_update)
159              
160              message = f"Registry {action} and plugins refreshed."
161              return jsonify({
162                  "message": message,
163                  "status": "success",
164                  "is_update": is_update
165              })
166          else:
167              return (
168                  jsonify({
169                      "error": "Failed to add registry (invalid path/URL or file system error)",
170                      "status": "failure"
171                  }),
172                  400,
173              )
174  
175      @app.route("/api/plugin_catalog/registries/refresh", methods=["POST"])
176      def refresh_registries_api():
177          logger.info("Refreshing all plugin registries via API call...")
178          all_regs = registry_manager.get_all_registries()
179          plugin_scraper.get_all_plugins(all_regs, force_refresh=True)
180          logger.info(
181              f"Refresh complete. Found {len(plugin_scraper.plugin_cache)} plugins."
182          )
183          return jsonify(
184              {
185                  "message": f"Registries refreshed. Found {len(plugin_scraper.plugin_cache)} plugins.",
186                  "status": "success",
187              }
188          )
189  
190      @app.route("/api/shutdown", methods=["POST"])
191      def shutdown_api():
192          shared_config = app.config.get("SHARED_CONFIG")
193          if shared_config is not None:
194              shared_config["status"] = "shutdown_requested"
195  
196          temp_dir_path = Path(os.path.expanduser(PLUGIN_CATALOG_TEMP_DIR))
197          if temp_dir_path.exists():
198              logger.info(f"Cleaning up temporary directory: {temp_dir_path}")
199              try:
200                  shutil.rmtree(temp_dir_path)
201              except Exception as e:
202                  logger.error(
203                      f"Error cleaning up temporary directory {temp_dir_path}: {e}"
204                  )
205  
206          func = request.environ.get("werkzeug.server.shutdown")
207          if func is None:
208              logger.warning(
209                  "Werkzeug server shutdown function not found. Attempting os._exit(0)."
210              )
211              os._exit(0)
212          try:
213              func()
214              logger.info("Server shutdown initiated.")
215          except Exception as e:
216              logger.error(f"Error during server shutdown: {e}. Forcing exit.")
217              os._exit(1)
218          return jsonify({"status": "success", "message": "Server shutting down"})
219  
220      return app
221  
222  
223  if __name__ == "__main__":
224      app = create_plugin_catalog_app()
225      print(
226          "Starting Plugin Catalog Flask app directly for testing on http://127.0.0.1:5003"
227      )
228      app.run(host="127.0.0.1", port=5003, debug=True)