Spaces:
Running
Running
Furqan Ahmad Rao
feat: implement in-memory ZIP creation and enhance file management functionality
cfda7db
| """ | |
| File Manager Module - Handles saving and managing generated MVP files | |
| """ | |
| import os | |
| import io | |
| import shutil | |
| import zipfile | |
| import tempfile | |
| import re | |
| from typing import Dict, Optional, Tuple | |
| from datetime import datetime | |
| from .mcp_http_clients import FileManagerMCPClient | |
| def sanitize_markdown(content: str) -> str: | |
| """ | |
| Sanitize markdown content by removing invisible/control characters. | |
| Args: | |
| content: Raw markdown content | |
| Returns: | |
| Sanitized markdown content | |
| """ | |
| if not content: | |
| return content | |
| # Remove common invisible characters (zero-width spaces, BOM, etc.) | |
| # Keep newlines, tabs, and standard spaces | |
| sanitized = content.replace('\ufeff', '') # BOM | |
| sanitized = sanitized.replace('\u200b', '') # Zero-width space | |
| sanitized = sanitized.replace('\u200c', '') # Zero-width non-joiner | |
| sanitized = sanitized.replace('\u200d', '') # Zero-width joiner | |
| sanitized = sanitized.replace('\ufffe', '') # Reverse BOM | |
| # Remove any other control characters except newline, carriage return, and tab | |
| sanitized = ''.join(char for char in sanitized if ord(char) >= 32 or char in '\n\r\t') | |
| # Normalize line endings to \n | |
| sanitized = sanitized.replace('\r\n', '\n').replace('\r', '\n') | |
| return sanitized | |
| class FileManager: | |
| """Manages saving MVP files - uses in-memory ZIP for HF Spaces compatibility""" | |
| def __init__(self, output_dir: str = "outputs"): | |
| """ | |
| Initialize file manager | |
| Args: | |
| output_dir: Directory to save files (default: outputs/) - NOT USED in production | |
| """ | |
| self.output_dir = output_dir | |
| self.mcp_client = FileManagerMCPClient() | |
| # No longer creating output directory - we use temp files instead | |
| def create_zip_in_memory(self, files: Dict[str, str], idea: str) -> str: | |
| """ | |
| Create a temporary ZIP file from generated content. | |
| The ZIP file is stored in system temp directory and will be auto-cleaned. | |
| Args: | |
| files: Dictionary with file contents (keys: features_md, architecture_md, etc.) | |
| idea: The startup idea (used for naming) | |
| Returns: | |
| Path to temporary ZIP file (will be auto-deleted by OS) | |
| """ | |
| # Prepare file content | |
| zip_content = {} | |
| # File mapping | |
| file_names = { | |
| "overview_md": "overview.md", | |
| "features_md": "features.md", | |
| "architecture_md": "architecture.md", | |
| "design_md": "design.md", | |
| "user_flow_md": "user_flow.md", | |
| "roadmap_md": "roadmap.md", | |
| "business_model_md": "business_model.md", | |
| "testing_plan_md": "testing_plan.md" | |
| } | |
| # Add all markdown files (sanitized) | |
| for key, filename in file_names.items(): | |
| if key in files: | |
| # Sanitize content before writing | |
| zip_content[filename] = sanitize_markdown(files[key]) | |
| # Add README | |
| readme_content = f"""# MVP Blueprint | |
| **Idea:** {idea} | |
| **Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | |
| **Files:** | |
| - overview.md - MVP overview and usage guide | |
| - features.md - Feature specifications | |
| - architecture.md - Technical architecture | |
| - design.md - Design philosophy | |
| - user_flow.md - User journey maps | |
| - roadmap.md - 6-week launch plan | |
| - business_model.md - Business model specification | |
| - testing_plan.md - Testing plan and QA | |
| --- | |
| *Generated by MVP Agent* | |
| """ | |
| zip_content["README.md"] = readme_content | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| safe_idea = "".join(c for c in idea[:30] if c.isalnum() or c in (" ", "-", "_")).strip() | |
| safe_idea = safe_idea.replace(" ", "_") or "mvp" | |
| zip_filename = f'mvp_{safe_idea}_{timestamp}.zip' | |
| # Try MCP first | |
| try: | |
| resp = self.mcp_client.create_zip_from_memory(zip_content, zip_filename) | |
| if resp.get("success") and resp.get("path"): | |
| print(f"✅ Successfully created ZIP via MCP: {resp.get('path')}") | |
| return resp.get("path") | |
| else: | |
| print(f"⚠️ MCP ZIP creation failed: {resp.get('message')}. Falling back to local.") | |
| except Exception as e: | |
| print(f"⚠️ MCP ZIP creation error: {e}. Falling back to local.") | |
| # Fallback: Create temp file locally | |
| temp_zip = tempfile.NamedTemporaryFile( | |
| mode='w+b', | |
| suffix='.zip', | |
| prefix=f'mvp_{safe_idea}_{timestamp}_', | |
| delete=False # We'll let Gradio handle deletion after download | |
| ) | |
| temp_zip.close() # Close it so we can write to it with zipfile | |
| # Create ZIP in the temp file with atomic write | |
| with zipfile.ZipFile(temp_zip.name, 'w', zipfile.ZIP_DEFLATED) as zipf: | |
| for filename, content in zip_content.items(): | |
| zipf.writestr(filename, content) | |
| print(f"✅ Successfully created ZIP locally: {temp_zip.name}") | |
| return temp_zip.name | |
| def save_mvp_files(self, files: Dict[str, str], idea: str) -> Dict[str, str]: | |
| """ | |
| Create in-memory ZIP file for download. | |
| NO persistent storage - perfect for Hugging Face Spaces. | |
| Args: | |
| files: Dictionary with file contents (keys: features_md, architecture_md, etc.) | |
| idea: The startup idea (used for naming) | |
| Returns: | |
| Dictionary with 'zip' key pointing to temporary file path | |
| """ | |
| # Create temporary ZIP file | |
| zip_path = self.create_zip_in_memory(files, idea) | |
| return { | |
| "zip": zip_path, | |
| "directory": "temp", # No actual directory | |
| } | |
| def get_latest_mvp_dir(self) -> str: | |
| """ | |
| Deprecated - not used with in-memory ZIP approach. | |
| Returns empty string. | |
| """ | |
| return "" | |
| # Singleton instance | |
| _file_manager = None | |
| def get_file_manager() -> FileManager: | |
| """Get or create the file manager singleton""" | |
| global _file_manager | |
| if _file_manager is None: | |
| _file_manager = FileManager() | |
| return _file_manager | |