from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, session, make_response, send_file, abort, jsonify from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename from .models import db, User, PrintFile, SystemConfig import os import uuid import configparser from datetime import datetime from .tasks import slice_stl_task main_bp = Blueprint('main', __name__) def get_quality_presets(): preset_dir = os.path.join(current_app.root_path, '..', 'print_config', 'presets', 'creality', 'base') presets = [] if os.path.exists(preset_dir): for f in os.listdir(preset_dir): if f.startswith('base_global_') and f.endswith('.inst.cfg'): config = configparser.ConfigParser() try: config.read(os.path.join(preset_dir, f)) name = config.get('general', 'name', fallback=f) presets.append((f, name)) except Exception as e: pass # Custom sort order or alphanumeric return sorted(presets, key=lambda x: x[1]) auth_bp = Blueprint('auth', __name__, url_prefix='/auth') admin_bp = Blueprint('admin', __name__, url_prefix='/admin') # Guest User Middleware @main_bp.before_app_request def assign_guest_cookie(): if not current_user.is_authenticated: guest_id = request.cookies.get('guest_id') if not guest_id: guest_id = str(uuid.uuid4()) user = User(username=f'guest_{guest_id[:8]}', is_guest=True, guest_cookie_id=guest_id) db.session.add(user) db.session.commit() login_user(user) # We will set the cookie in the response after request, see below request.guest_id_to_set = guest_id else: user = User.query.filter_by(guest_cookie_id=guest_id).first() if user: login_user(user) @main_bp.after_app_request def set_guest_cookie(response): if hasattr(request, 'guest_id_to_set'): response.set_cookie('guest_id', request.guest_id_to_set, max_age=60*60*24*365) # 1 year return response # --- Main Routes --- @main_bp.route('/') def index(): return render_template('index.html') @main_bp.route('/set_language/') def set_language(lang): from app import i18n_dict if lang not in i18n_dict: lang = 'en' # return to previous page response = make_response(redirect(request.referrer or url_for('main.index'))) response.set_cookie('lang', lang, max_age=60*60*24*365) return response @main_bp.route('/slice', methods=['GET', 'POST']) @login_required def slice_page(): if request.method == 'POST': if 'file' not in request.files: flash('No file part', 'danger') return redirect(request.url) file = request.files['file'] if file.filename == '': flash('No selected file', 'danger') return redirect(request.url) if file and file.filename.lower().endswith('.stl'): original_filename = file.filename # Do not use secure_filename to keep Chinese characters ext = os.path.splitext(original_filename)[1].lower() if not ext: ext = '.stl' timestamp = datetime.now().strftime('%Y%m%d%H%M%S') unique_filename = f"{timestamp}_{uuid.uuid4().hex}{ext}" filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename) file.save(filepath) print_file = PrintFile( filename=unique_filename, original_filename=original_filename, file_type='stl', user_id=current_user.id, status='waiting' ) db.session.add(print_file) db.session.commit() # Start slicing task quality_preset = request.form.get('quality', 'base_global_standard.inst.cfg') slice_stl_task(print_file.id, filepath, quality_preset) flash('File uploaded and slicing started!', 'success') response = make_response(redirect(url_for('main.files'))) response.set_cookie('last_quality_preset', quality_preset, max_age=60*60*24*365) return response presets = get_quality_presets() last_quality = request.cookies.get('last_quality_preset', 'base_global_standard.inst.cfg') return render_template('slice.html', presets=presets, last_quality=last_quality) @main_bp.route('/files') @login_required def files(): # Order by newest first user_files = PrintFile.query.filter_by(user_id=current_user.id).order_by(PrintFile.created_at.desc()).all() return render_template('files.html', files=user_files) @main_bp.route('/api/files_status') @login_required def files_status(): files = PrintFile.query.filter_by(user_id=current_user.id).all() return jsonify({str(f.id): f.status for f in files}) @main_bp.route('/download/') @login_required def download_gcode(file_id): print_file = PrintFile.query.get_or_404(file_id) if print_file.user_id != current_user.id and not current_user.is_admin: abort(403) if print_file.status != 'sliced': flash('File is not ready yet.', 'warning') return redirect(url_for('main.files')) gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode' filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename) if os.path.exists(filepath): safe_name = print_file.original_filename.rsplit('.', 1)[0] + '.gcode' return send_file(filepath, as_attachment=True, download_name=safe_name) flash('GCode file not found. It might have been deleted.', 'danger') return redirect(url_for('main.files')) @main_bp.route('/preview_gcode/') @login_required def preview_gcode(file_id): print_file = PrintFile.query.get_or_404(file_id) if print_file.user_id != current_user.id and not current_user.is_admin: abort(403) gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode' filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename) content = "File not found or not ready." line_count = 0 if os.path.exists(filepath): with open(filepath, 'r') as f: lines = f.readlines() line_count = len(lines) content = "".join(lines[:500]) # Preview first 500 lines if line_count > 500: content += f"\n... \n[Preview truncated. Total lines: {line_count}. Please download to view full file.]" return render_template('gcode_preview.html', file=print_file, content=content, line_count=line_count) @main_bp.route('/delete_file/', methods=['POST']) @login_required def delete_file(file_id): print_file = PrintFile.query.get_or_404(file_id) if print_file.user_id != current_user.id and not current_user.is_admin: abort(403) stl_path = os.path.join(current_app.config['UPLOAD_FOLDER'], print_file.filename) gcode_filename = print_file.filename.rsplit('.', 1)[0] + '.gcode' gcode_path = os.path.join(current_app.config['UPLOAD_FOLDER'], gcode_filename) if os.path.exists(stl_path): os.remove(stl_path) if os.path.exists(gcode_path): os.remove(gcode_path) db.session.delete(print_file) db.session.commit() flash(f"Deleted {print_file.original_filename} successfully.", 'success') return redirect(url_for('main.files')) # --- Auth Routes --- @auth_bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') user = User.query.filter_by(username=username, is_guest=False).first() if user and check_password_hash(user.password_hash, password): login_user(user) return redirect(url_for('main.index')) flash('Invalid username or password', 'danger') return render_template('login.html') @auth_bp.route('/logout') @login_required def logout(): logout_user() response = make_response(redirect(url_for('main.index'))) response.delete_cookie('guest_id') # Optionally clear guest cookie return response # --- Admin Routes --- @admin_bp.before_request def require_admin(): if not current_user.is_authenticated or not current_user.is_admin: flash('Admin access required', 'danger') return redirect(url_for('main.index')) @admin_bp.route('/settings') def settings(): configs = SystemConfig.query.all() return render_template('admin_settings.html', configs=configs) @admin_bp.route('/users') def users(): all_users = User.query.order_by(User.created_at.desc()).all() return render_template('admin_users.html', users=all_users)