import os import gradio as gr from pathlib import Path import tempfile import zipfile import shutil from typing import List, Set, Dict, Tuple class WebProjectAnalyzer: def __init__(self): # Diretórios que devem ser completamente ignorados self.ignored_dirs = { '__pycache__', '.git', '.vscode', '.idea', 'node_modules', 'vendor', 'dist', 'build', 'cache', 'logs', 'tmp', 'coverage', '.github', '.circleci', '.docker', 'venv', 'env', '.venv', 'virtualenv', 'bin', 'include', 'lib', 'lib64', 'site-packages', 'dist-packages', '.gitlab', '.svn', '.hg', '.pytest_cache', '.mypy_cache', '__MACOSX', '.next', '.nuxt', 'out', 'target', '.gradle', '.mvn', 'bower_components', 'jspm_packages', 'web_modules', '.serverless', '.fusebox', '.dynamodb', '.tern-port', '.yarn', '.pnp', 'eggs', 'wheels', 'htmlcov', '.tox', '.hypothesis', '.ruff_cache', 'var', 'sdist', 'develop-eggs', '.sass-cache', '.cache', 'public/packs', 'public/assets' } # Extensões relevantes para projetos web (PRINCIPAIS) self.relevant_extensions = { # Frontend '.html', '.htm', '.css', '.scss', '.sass', '.less', '.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte', # Backend '.py', '.rb', '.php', '.java', '.go', '.rs', # Config importantes '.json', '.yml', '.yaml', '.toml', '.xml', # Docs '.md', '.txt', # SQL '.sql', # Outros '.env.example', '.gitignore', '.dockerignore' } # Arquivos de configuração CRÍTICOS (sempre incluir) self.critical_files = { 'package.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'requirements.txt', 'Pipfile', 'Pipfile.lock', 'pyproject.toml', 'composer.json', 'composer.lock', 'Gemfile', 'Gemfile.lock', 'go.mod', 'go.sum', 'Cargo.toml', 'Cargo.lock', 'pom.xml', 'build.gradle', 'settings.gradle', 'CMakeLists.txt', 'Makefile', 'tsconfig.json', 'webpack.config.js', 'vite.config.js', 'rollup.config.js', 'docker-compose.yml', 'Dockerfile', '.dockerignore', '.env.example', '.gitignore', '.eslintrc', '.prettierrc', 'README.md', 'LICENSE', 'setup.py', 'setup.cfg', 'manage.py', 'wsgi.py', 'asgi.py', 'app.py', 'main.py', 'index.html', 'index.js', 'index.ts', 'App.js', 'App.tsx', 'next.config.js', 'nuxt.config.js', 'vue.config.js', 'angular.json', 'nest-cli.json', 'vercel.json', 'netlify.toml' } # Padrões de nomes de arquivos a ignorar self.ignored_file_patterns = { '.min.js', '.min.css', '.map', '.log', '.pyc', '.pyo', '.pyd', '.so', '.dll', '.dylib', '.class', '.jar', '.war', '.ear', '.tar', '.gz', '.exe', '.bin', '.dat', '.db', '.sqlite', '.sqlite3', 'thumbs.db', '.DS_Store', 'desktop.ini' } def should_ignore(self, name: str, path: str, is_dir: bool) -> bool: """Determina se um arquivo/diretório deve ser ignorado""" # Ignora diretórios específicos if is_dir and name in self.ignored_dirs: return True # Ignora arquivos críticos? NÃO! if name in self.critical_files: return False # Ignora arquivos ocultos (exceto importantes) if name.startswith('.') and name not in self.critical_files: if not name.endswith('.example') and name not in ['.gitignore', '.dockerignore', '.env.example']: return True # Ignora arquivos temporários if name.endswith('~') or name.startswith('~') or name.startswith('#'): return True # Ignora padrões específicos (exceto .lock de configs) for pattern in self.ignored_file_patterns: if pattern in name and name not in self.critical_files: return True # Ignora arquivos muito grandes (>10MB) if not is_dir: try: if os.path.getsize(path) > 10 * 1024 * 1024: return True except: pass return False def is_relevant_file(self, name: str) -> bool: """Determina se um arquivo é relevante para mostrar""" # Arquivos críticos sempre são relevantes if name in self.critical_files: return True # Ignora padrões indesejados (exceto configs) for pattern in self.ignored_file_patterns: if pattern in name and name not in self.critical_files: return False # Verifica extensões relevantes _, ext = os.path.splitext(name) return ext.lower() in self.relevant_extensions def get_file_icon(self, filename: str) -> str: """Retorna ícone apropriado para o tipo de arquivo""" _, ext = os.path.splitext(filename) # Arquivos específicos if filename in ['package.json', 'package-lock.json']: return '📦' if filename in ['requirements.txt', 'Pipfile']: return '🐍' if filename in ['docker-compose.yml', 'Dockerfile']: return '🐳' if filename == 'README.md': return '📖' if filename in ['.gitignore', '.dockerignore']: return '🚫' if filename.startswith('.env'): return '🔐' # Por extensão icon_map = { '.py': '🐍', '.js': '📜', '.jsx': '⚛️', '.ts': '📘', '.tsx': '⚛️', '.html': '🌐', '.htm': '🌐', '.css': '🎨', '.scss': '🎨', '.less': '🎨', '.php': '🐘', '.java': '☕', '.rb': '💎', '.json': '📋', '.xml': '📄', '.sql': '🗃️', '.md': '📖', '.txt': '📄', '.yml': '⚙️', '.yaml': '⚙️', '.toml': '⚙️', '.vue': '💚', '.svelte': '🟢', '.rs': '🦀', '.go': '🐹' } return icon_map.get(ext, '📄') def extract_zip(self, zip_path: str) -> str: """Extrai arquivo ZIP e retorna o diretório""" try: # Cria diretório temporário temp_dir = tempfile.mkdtemp(prefix='project_') print(f"📦 Extraindo ZIP para: {temp_dir}") # Extrai ZIP with zipfile.ZipFile(zip_path, 'r') as zip_ref: zip_ref.extractall(temp_dir) # Verifica se há uma única pasta raiz (comum em ZIPs do GitHub) items = os.listdir(temp_dir) if len(items) == 1 and os.path.isdir(os.path.join(temp_dir, items[0])): root_dir = os.path.join(temp_dir, items[0]) print(f"📁 Pasta raiz encontrada: {items[0]}") return root_dir return temp_dir except Exception as e: print(f"❌ Erro ao extrair ZIP: {e}") raise def analyze_structure(self, root_path: str, max_depth: int = 4) -> Tuple[str, Dict, List[str]]: """Analisa a estrutura do projeto""" if not root_path: return "❌ Nenhuma pasta para analisar.", {"total_files": 0, "relevant_files": 0, "total_size": 0}, [] if not os.path.exists(root_path): return "❌ Pasta não encontrada.", {"total_files": 0, "relevant_files": 0, "total_size": 0}, [] if not os.path.isdir(root_path): return "❌ Caminho inválido.", {"total_files": 0, "relevant_files": 0, "total_size": 0}, [] structure_output = [] relevant_files = [] stats = { 'total_files': 0, 'relevant_files': 0, 'total_size': 0 } def analyze_directory(current_path: str, level: int = 0, prefix: str = ''): if level > max_depth: return try: items = sorted(os.listdir(current_path)) dirs = [] files = [] # Separa diretórios e arquivos for item in items: full_path = os.path.join(current_path, item) is_dir = os.path.isdir(full_path) if self.should_ignore(item, full_path, is_dir): continue if is_dir: dirs.append(item) else: stats['total_files'] += 1 if self.is_relevant_file(item): files.append(item) stats['relevant_files'] += 1 relevant_files.append(full_path) try: stats['total_size'] += os.path.getsize(full_path) except: pass # Renderiza arquivos primeiro for i, file in enumerate(files): is_last = (i == len(files) - 1) and len(dirs) == 0 connector = '└── ' if is_last else '├── ' icon = self.get_file_icon(file) structure_output.append(f"{prefix}{connector}{icon} {file}") # Renderiza diretórios depois for i, dir_name in enumerate(dirs): is_last = i == len(dirs) - 1 connector = '└── ' if is_last else '├── ' structure_output.append(f"{prefix}{connector}📁 {dir_name}/") # Recursão extension = ' ' if is_last else '│ ' full_path = os.path.join(current_path, dir_name) analyze_directory(full_path, level + 1, prefix + extension) except PermissionError: structure_output.append(f"{prefix}🚫 [Acesso negado]") except Exception as e: structure_output.append(f"{prefix}⚠️ [Erro: {str(e)}]") try: # Nome da pasta raiz project_name = os.path.basename(root_path) structure_output.append(f"📁 {project_name}/") analyze_directory(root_path, 0, '') if not structure_output or len(structure_output) == 1: return "ℹ️ Nenhum arquivo relevante encontrado.", stats, relevant_files structure_text = '\n'.join(structure_output) return structure_text, stats, relevant_files except Exception as e: return f"❌ Erro ao analisar: {str(e)}", stats, [] def create_structure_report(self, folder_name: str, structure: str, stats: Dict) -> str: """Cria relatório completo da estrutura""" total_files = stats['total_files'] relevant_files = stats['relevant_files'] total_size = stats['total_size'] from datetime import datetime current_time = datetime.now().strftime('%d/%m/%Y %H:%M:%S') report = f"""# 📊 ANÁLISE DO PROJETO: `{folder_name}` ## 📍 Informações - **Projeto**: `{folder_name}` - **Data da Análise**: {current_time} ## 📈 Estatísticas | Métrica | Valor | |---------|-------| | 📁 Total de arquivos escaneados | {total_files} | | ✅ Arquivos relevantes | {relevant_files} | | 💾 Tamanho total | {total_size / 1024 / 1024:.2f} MB | | 🎯 Taxa de relevância | {(relevant_files/total_files*100) if total_files > 0 else 0:.1f}% | --- ## 🌳 Estrutura do Projeto ``` {structure} ``` --- ## 💡 Legenda - 📁 Diretório - 🐍 Python - 📜 JavaScript - ⚛️ React (JSX/TSX) - 📘 TypeScript - 🌐 HTML - 🎨 CSS/SCSS - 📦 Package Config - 🐳 Docker - 📖 Documentação - 🚫 Git/Docker Ignore - 🔐 Environment --- **✨ Apenas arquivos estruturais e de configuração principais foram incluídos** """ return report def process_zip(self, zip_file, max_depth: int = 4): """Processa arquivo ZIP do projeto""" if not zip_file: return ( "**📁 Status**: Aguardando upload de arquivo ZIP...", "ℹ️ Faça upload do arquivo ZIP do seu projeto para análise...", None ) temp_dir = None try: # Obtém o caminho do arquivo if hasattr(zip_file, 'name'): zip_path = zip_file.name else: zip_path = str(zip_file) print(f"📦 Processando ZIP: {zip_path}") # Verifica se é um ZIP if not zip_path.lower().endswith('.zip'): return "❌ Por favor, envie um arquivo ZIP do projeto.", "", None # Extrai ZIP temp_dir = self.extract_zip(zip_path) # Analisa estrutura structure, stats, relevant_files = self.analyze_structure(temp_dir, max_depth) # Nome do projeto (do ZIP) project_name = Path(zip_path).stem # Cria relatório report = self.create_structure_report(project_name, structure, stats) # Salva relatório report_file = tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') report_file.write(report) report_file.close() return report, structure, report_file.name except zipfile.BadZipFile: return "❌ Arquivo ZIP inválido ou corrompido.", "", None except Exception as e: error_msg = f"❌ Erro ao processar: {str(e)}" import traceback traceback.print_exc() return error_msg, "", None finally: # Limpa diretório temporário if temp_dir and os.path.exists(temp_dir): try: shutil.rmtree(temp_dir) print(f"🧹 Limpeza: {temp_dir}") except: pass def create_interface(): """Cria a interface Gradio""" analyzer = WebProjectAnalyzer() with gr.Blocks( title="🔍 Web Project Structure Analyzer", theme=gr.themes.Soft(primary_hue="purple"), ) as demo: gr.Markdown(""" # 🔍 Web Project Structure Analyzer ### Analise a estrutura do seu projeto web automaticamente! **✨ Recursos:** - 📦 Upload de arquivo ZIP do projeto completo - 🎯 Filtragem inteligente (ignora node_modules, .git, cache, etc) - 📊 Estatísticas detalhadas - 💾 Download do relatório em Markdown - 🌳 Visualização em árvore ASCII --- """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 📦 Upload do Projeto (ZIP)") zip_input = gr.File( label="Arraste o arquivo ZIP ou clique para selecionar", file_count="single", file_types=[".zip"], type="filepath", height=200 ) gr.Markdown(""" **💡 Como usar:** 1. **Comprima seu projeto em ZIP:** - Windows: Clique direito na pasta → "Enviar para" → "Pasta compactada" - Mac: Clique direito na pasta → "Comprimir" - Linux: `zip -r meu-projeto.zip meu-projeto/` 2. **Upload:** - Arraste o arquivo .zip aqui - OU clique e selecione o arquivo 3. **Automático:** - A análise inicia automaticamente! - Aguarde alguns segundos **🎯 Download do GitHub:** - Vá no repositório - Clique em "Code" → "Download ZIP" - Faça upload aqui! """) max_depth = gr.Slider( minimum=1, maximum=8, value=5, step=1, label="🔍 Profundidade de Análise", info="Níveis de subpastas a escanear" ) analyze_btn = gr.Button( "🚀 Re-Analisar", variant="secondary", size="lg" ) gr.Markdown(""" --- ### ℹ️ O que é analisado? **✅ INCLUÍDO:** - 🐍 Código fonte (.py, .js, .html, .css, .php) - ⚙️ Arquivos de config (package.json, requirements.txt) - 📖 Documentação (README.md, LICENSE) - 🐳 Docker (Dockerfile, docker-compose.yml) **🚫 IGNORADO AUTOMATICAMENTE:** - 📦 node_modules, vendor - 🗂️ .git, .vscode, .idea - 🐍 venv, __pycache__ - 🏗️ dist, build, cache - 📊 logs, tmp - 🗑️ Arquivos temporários """) with gr.Column(scale=2): gr.Markdown("### 📊 Resultado da Análise") report_output = gr.Markdown( value="""**📁 Status**: Aguardando upload... **💡 Dica Rápida:** 1. Comprima sua pasta de projeto em ZIP 2. Arraste o arquivo aqui 3. Pronto! Análise automática. **🎯 Para projetos do GitHub:** - Code → Download ZIP → Upload aqui""", label="Relatório" ) structure_output = gr.Textbox( label="🌳 Estrutura em Árvore (Copie e Cole)", lines=22, max_lines=30, value="ℹ️ A estrutura do projeto aparecerá aqui após o upload...", show_copy_button=True, container=True ) download_output = gr.File( label="💾 Download do Relatório Completo (Markdown)", visible=False ) # Eventos def analyze_wrapper(zip_file, depth): if not zip_file: return ( "**📁 Status**: Aguardando upload...", "ℹ️ Faça upload de um arquivo ZIP para começar...", gr.File(visible=False) ) report, structure, file_path = analyzer.process_zip(zip_file, depth) if file_path: return report, structure, gr.File(value=file_path, visible=True) else: return report, structure, gr.File(visible=False) # Upload automático zip_input.change( fn=analyze_wrapper, inputs=[zip_input, max_depth], outputs=[report_output, structure_output, download_output] ) # Botão manual analyze_btn.click( fn=analyze_wrapper, inputs=[zip_input, max_depth], outputs=[report_output, structure_output, download_output] ) # Quando muda a profundidade max_depth.change( fn=analyze_wrapper, inputs=[zip_input, max_depth], outputs=[report_output, structure_output, download_output] ) gr.Markdown(""" --- ### 🎯 Exemplos de Uso **React App:** ```bash cd meu-react-app zip -r meu-react-app.zip . -x "node_modules/*" ".git/*" ``` **Python/Django:** ```bash cd meu-projeto-django zip -r meu-projeto.zip . -x "venv/*" ".git/*" "*.pyc" ``` **Node.js API:** ```bash cd minha-api zip -r minha-api.zip . -x "node_modules/*" ".git/*" ``` --- ### 📋 Formatos Suportados - ✅ Arquivos .zip padrão - ✅ ZIP do GitHub ("Download ZIP") - ✅ ZIP com ou sem pasta raiz - ✅ Projetos de qualquer tamanho --- **Made with ❤️ using Gradio 5.49.1+ | Open Source** """) return demo if __name__ == "__main__": demo = create_interface() demo.launch()