+
+
+
+ +
+ {% include sidebar.html %} +
+ +
+
+ + +
+ +
+
+
+ +
+ + +
+ +
+
+

Carregando...

+ +

...

+ +
+
+
+ +
+ +
+
+
+
+ + + +
+ + +
+
+ + + + +
NomeDataAções
+ +
+
+ + + + + +
+ +
+
+ + + \ No newline at end of file diff --git a/scripts/handler/upload_server.py b/scripts/handler/upload_server.py index 11c18217..a2ac5b33 100755 --- a/scripts/handler/upload_server.py +++ b/scripts/handler/upload_server.py @@ -8,10 +8,12 @@ import json import shutil import time import io +import threading from flask import Flask, request, jsonify, send_file, redirect from flask_cors import CORS from werkzeug.utils import secure_filename +from datetime import datetime from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt @@ -43,8 +45,15 @@ class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(150), unique=True, nullable=False) password = db.Column(db.String(150), nullable=False) - # Novo campo para dizer se é chefe ou não is_admin = db.Column(db.Boolean, default=False) + + # Detalhes do usuário + bio = db.Column(db.String(240), default="") + tags = db.Column(db.String(500), default="") # Vamos salvar separado por vírgula + avatar_url = db.Column(db.String(255), default="/assets/img/default_avatar.png") + cover_url = db.Column(db.String(255), default="/assets/img/default_cover.jpg") + + projects = db.relationship('Project', backref='owner', lazy=True) class SecureModelView(ModelView): def is_accessible(self): @@ -67,6 +76,16 @@ class SecureIndexView(AdminIndexView): return super(SecureIndexView, self).index() +# Tabela de Projetos +class Project(db.Model): + id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String(255), nullable=False) # Nome do arquivo físico .mmp + display_name = db.Column(db.String(255), nullable=False) # Nome "bonito" para exibir + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # Chave estrangeira ligando ao usuário + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + # Cria o banco na inicialização se não existir with app.app_context(): db.create_all() @@ -93,11 +112,17 @@ def run_jekyll_build(): print("Iniciando build do Jekyll...") command = [BUNDLE_PATH, "exec", "jekyll", "build", "--destination", "/var/www/html/trens/mmpSearch/"] try: - # Passamos env=env_vars para que ele ache o ruby3.2 - subprocess.run(command, check=True, cwd=BASE_DATA, capture_output=True, text=True, env=env_vars) - print("Jekyll Build Sucesso!") - except subprocess.CalledProcessError as e: - print(f"ERRO no Jekyll Build: {e.stderr}") + # Redirecionamos a saída para DEVNULL para não encher o buffer e travar + subprocess.Popen( + command, + cwd=BASE_DATA, + env=env_vars, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + print("Jekyll Build iniciado em segundo plano (background).") + except Exception as e: + print(f"Erro ao iniciar Jekyll: {e}") def load_manifest_keys(): """Carrega as pastas de fábrica (keys do manifesto).""" @@ -212,15 +237,38 @@ def update_xml_paths_exact(mmp_filename, replacements): print(f"Erro crítico ao editar XML: {e}") return False -def process_and_build(filename): - """Encapsula a chamada do main.py""" - result = process_single_file(filename) - if result["success"]: - rebuild_indexes() - run_jekyll_build() +def run_heavy_tasks_in_background(): + """Esta função roda isolada sem travar o usuário""" + print("--- [BACKGROUND] Iniciando reconstrução de índices ---") + try: + # 1. Isso é Python puro e PESADO (aqui é onde estava travando) + rebuild_indexes() + + # 2. Isso gera os manifestos (Python puro) generate_manifests(SRC_MMPSEARCH) + + # 3. Isso chama o subprocesso do Jekyll (Externo) + # Mantém sua função original que usa subprocess + run_jekyll_build() + + print("--- [BACKGROUND] Tarefas concluídas com sucesso ---") + except Exception as e: + print(f"--- [BACKGROUND] Erro: {e} ---") + +def process_and_build(filename): + """Função chamada pela rota de upload""" + # Processamento inicial do arquivo (rápido) + result = process_single_file(filename) + + if result["success"]: + # Em vez de chamar rebuild_indexes() direto, criamos a Thread + # O Flask vai responder o return abaixo imediatamente, + # enquanto a thread continua rodando no servidor. + task_thread = threading.Thread(target=run_heavy_tasks_in_background) + task_thread.start() + return jsonify({ - "message": "Sucesso! Projeto processado.", + "message": "Sucesso! O projeto foi recebido. O site será atualizado em instantes.", "data": result["data"] }), 200 else: @@ -267,6 +315,84 @@ def check_auth(): return jsonify({"logged_in": True, "user": current_user.username}) return jsonify({"logged_in": False}) +# No upload_server.py, adicione esta rota +@app.route('/api/user/update', methods=['POST']) +@login_required +def update_profile(): + data = request.json + new_username = data.get('username') + + # Verificar disponibilidade do username se ele mudou + if new_username and new_username != current_user.username: + existing = User.query.filter_by(username=new_username).first() + if existing: + return jsonify({"error": "Nome de usuário já está em uso."}), 400 + current_user.username = new_username + + if 'bio' in data: + current_user.bio = data['bio'][:240] # Corta em 240 chars + + if 'tags' in data: + # Limpa e formata as tags + current_user.tags = data['tags'] + + try: + db.session.commit() + return jsonify({"message": "Perfil atualizado!"}), 200 + except Exception as e: + return jsonify({"error": "Erro ao salvar."}), 500 + +# Atualize a rota user_profile antiga para retornar os novos dados +@app.route('/api/user/profile', methods=['GET']) +@login_required +def user_profile(): + user_projects = [] + for p in current_user.projects: + user_projects.append({ + "id": p.id, + "display_name": p.display_name, + "filename": p.filename, + "created_at": p.created_at.strftime("%d/%m/%Y"), + "download_link": f"/api/download/{p.filename}" + }) + + return jsonify({ + "username": current_user.username, + "bio": current_user.bio, + "tags": current_user.tags, + "avatar": current_user.avatar_url, + "cover": current_user.cover_url, + "projects": user_projects, + "samples": [], # Placeholder enquanto não cria a tabela Samples + "recordings": [] # Placeholder enquanto não cria a tabela Recordings + }) + + + +@app.route('/api/project/', methods=['DELETE']) +@login_required +def delete_project(project_id): + project = Project.query.get_or_404(project_id) + + # Segurança: Só o dono ou admin pode apagar + if project.owner != current_user and not current_user.is_admin: + return jsonify({"error": "Permissão negada"}), 403 + + try: + # 1. Remove do Banco + db.session.delete(project) + db.session.commit() + + # 2. Remove o arquivo físico (Opcional, mas recomendado para não lotar o disco) + file_path = os.path.join(MMP_FOLDER, project.filename) + if os.path.exists(file_path): + os.remove(file_path) + + # Nota: Você pode querer rodar o rebuild_indexes() aqui também se apagar o arquivo + + return jsonify({"message": "Projeto removido com sucesso"}), 200 + except Exception as e: + return jsonify({"error": str(e)}), 500 # ============================================================================== # ROTAS PRINCIPAIS # ============================================================================== @@ -355,7 +481,18 @@ def upload_file(): return jsonify({"error": "Falha crítica na descompactação."}), 400 elif ext == ".mmp" and upload_path != final_mmp_path: shutil.move(upload_path, final_mmp_path) - + + # Registra no banco de dados vinculado ao usuário logado + new_project = Project( + filename=final_mmp_filename, + display_name=clean_name, # Ou name_without_ext se preferir o original + owner=current_user # Flask-Login pega o user atual + ) + db.session.add(new_project) + db.session.commit() + print(f"Projeto {clean_name} associado ao usuário {current_user.username}") + # ----------------------------- + # is_clean, missing_files = analyze_mmp_dependencies(final_mmp_filename) if not is_clean: