import os import uuid import logging from flask import Flask, request, render_template, redirect, url_for, flash, session, jsonify, Response, make_response from werkzeug.utils import secure_filename from datetime import datetime import json # Need json for json.loads in export route # Import configuration and modules from .config import Config from .modules import analyzer, report_generator # Assuming gemini_api is used within analyzer # Configure logging logging.basicConfig(level=logging.INFO) def create_app(config_class=Config): """Factory pattern for creating the Flask application.""" app = Flask(__name__, instance_relative_config=False) app.config.from_object(config_class) logging.info(f"Upload folder set to: {app.config['UPLOAD_FOLDER']}") if not os.path.exists(app.config['UPLOAD_FOLDER']): logging.warning(f"Upload folder {app.config['UPLOAD_FOLDER']} does not exist. Attempting to create.") try: os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) except OSError as e: logging.error(f"Could not create upload folder: {e}") # Depending on requirements, might want to exit or handle differently def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS'] @app.context_processor def inject_now(): """Make 'now' available to all templates for the copyright year.""" return {'now': datetime.utcnow()} @app.route('/') def index(): """Render the main upload page.""" # Clear previous results from session if navigating back to index session.pop('analysis_result', None) session.pop('screenshot_filename', None) session.pop('analysis_error', None) return render_template('index.html') @app.route('/upload', methods=['POST']) def upload_and_analyze(): """Handle file upload or text input and trigger analysis.""" input_type = request.form.get('input_type') analysis_result = None image_path = None screenshot_filename = None text_input = None try: if input_type == 'screenshot': if 'screenshot' not in request.files: flash('No file part selected.', 'error') return redirect(url_for('index')) file = request.files['screenshot'] if file.filename == '': flash('No file selected for upload.', 'error') return redirect(url_for('index')) if file and allowed_file(file.filename): original_filename = secure_filename(file.filename) ext = os.path.splitext(original_filename)[1] unique_filename = f"{uuid.uuid4()}{ext}" image_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename) screenshot_filename = unique_filename # Keep filename for results page logging.info(f"Saving file to: {image_path}") file.save(image_path) logging.info("Starting analysis for screenshot...") analysis_result = analyzer.analyze_content(image_path=image_path) else: flash('Invalid file type. Allowed types: png, jpg, jpeg, gif, webp', 'error') return redirect(url_for('index')) elif input_type == 'text': text_input = request.form.get('article_text', '').strip() if not text_input: flash('No text pasted.', 'error') return redirect(url_for('index')) # Basic validation (e.g., length) # Using the maxlength from the textarea as a guide max_len = 15000 if len(text_input) > max_len: flash(f'Text input is too long (max {max_len} characters).', 'error') return redirect(url_for('index')) logging.info(f"Starting analysis for text input (length: {len(text_input)})...") analysis_result = analyzer.analyze_content(text_content=text_input) # No screenshot filename to store for text input screenshot_filename = None else: flash('Invalid input type selected.', 'error') return redirect(url_for('index')) # Common result handling logging.info("Analysis finished.") session['analysis_result'] = analysis_result session['screenshot_filename'] = screenshot_filename # Will be None for text input session.pop('analysis_error', None) return redirect(url_for('show_results')) except FileNotFoundError as e: logging.error(f"File not found during processing: {e}", exc_info=True) flash(f'Error: Could not find the uploaded file for analysis.', 'error') session['analysis_error'] = "File not found during processing." session.pop('analysis_result', None) # Clear potentially stale result return redirect(url_for('index')) # Redirect to index on file error except ValueError as e: # Catch potential value errors (e.g., no input from analyzer guard) logging.error(f"Value error during processing: {e}", exc_info=True) flash(f'Error: {e}', 'error') session['analysis_error'] = str(e) session.pop('analysis_result', None) return redirect(url_for('index')) except RuntimeError as e: logging.error(f"Runtime error during analysis: {e}", exc_info=True) flash(f'Error during analysis: {e}', 'error') session['analysis_error'] = str(e) session['screenshot_filename'] = screenshot_filename # Keep filename if available return redirect(url_for('show_results')) # Show error on results page except Exception as e: logging.exception("An unexpected error occurred during upload/analysis.") flash(f'An unexpected error occurred. Please try again.', 'error') session['analysis_error'] = "An unexpected server error occurred." session['screenshot_filename'] = screenshot_filename # Keep filename if available return redirect(url_for('show_results')) # Show error on results page finally: # Clean up the uploaded file only if it exists and we are *not* showing it on results page # (i.e., text input was used, or a non-runtime error occurred before results display) # Let's keep the image if there was a runtime/unexpected error for debugging context on results page. if image_path and os.path.exists(image_path) and input_type == 'text': try: os.remove(image_path) logging.info(f"Cleaned up unused file: {image_path}") except OSError as e: logging.error(f"Error removing unused file {image_path}: {e}") @app.route('/results') def show_results(): """Display the analysis results stored in the session.""" result = session.get('analysis_result') screenshot_filename = session.get('screenshot_filename') analysis_error = session.get('analysis_error') # Get error status too # If there was an error but we still have the result structure (e.g. from fallback parsing) # or if there's just an error message if analysis_error and not result: result = { "analysis_error": analysis_error } elif analysis_error and result: # Ensure the error message is part of the result dict if not already if 'analysis_error' not in result: result['analysis_error'] = analysis_error if not result and not analysis_error: flash('No analysis result found. Please upload a screenshot first.', 'warning') return redirect(url_for('index')) return render_template('results.html', result=result, screenshot_filename=screenshot_filename) # --- Export Routes --- # @app.route('/export/json') def export_json(): """Exports the analysis result as a JSON file.""" result = session.get('analysis_result') if not result: return jsonify({"error": "No analysis data found in session"}), 404 # Ensure result is serializable (report generator handles internal errors) json_string = report_generator.generate_json_report(result) # Create response response = make_response(json_string) response.headers['Content-Type'] = 'application/json' response.headers['Content-Disposition'] = 'attachment; filename=discover_analysis.json' return response @app.route('/export/pdf') def export_pdf(): """Exports the analysis result as a PDF file.""" result = session.get('analysis_result') if not result: flash('No analysis data found in session to generate PDF.', 'error') # Redirect back to results or index, depending on desired UX return redirect(url_for('show_results')) try: pdf_bytes = report_generator.generate_pdf_report(result) # Create response response = make_response(pdf_bytes) response.headers['Content-Type'] = 'application/pdf' response.headers['Content-Disposition'] = 'attachment; filename=discover_analysis_report.pdf' return response except Exception as e: logging.exception("Error generating or sending PDF report.") flash(f'Could not generate PDF report: {e}', 'error') return redirect(url_for('show_results')) return app # Allows running directly using `python -m app.app` or similar if __name__ == '__main__': app = create_app() # Use environment variables for host/port/debug (adjust defaults as needed) app.run(debug=os.getenv('FLASK_DEBUG', 'True').lower() == 'true', host=os.getenv('FLASK_HOST', '127.0.0.1'), port=int(os.getenv('FLASK_PORT', 5000)))