整理文件夹及架构,加入打印机页面,octo反代有问题

This commit is contained in:
2026-04-14 00:11:00 +08:00
parent 1de35f21d7
commit 570af7c225
54 changed files with 939 additions and 292 deletions

View File

@@ -22,7 +22,7 @@ i18n_dict = {}
def load_i18n(app): def load_i18n(app):
global i18n_dict global i18n_dict
i18n_dir = os.path.join(app.root_path, '..', 'assets', 'i18n') i18n_dir = os.path.join(app.root_path, 'assets', 'i18n')
if os.path.exists(i18n_dir): if os.path.exists(i18n_dir):
for f in os.listdir(i18n_dir): for f in os.listdir(i18n_dir):
if f.endswith('.json'): if f.endswith('.json'):
@@ -50,9 +50,9 @@ def _t(key):
return key # fallback to the key itself return key # fallback to the key itself
def create_app(): def create_app():
app = Flask(__name__, static_url_path='/assets', static_folder='../assets') app = Flask(__name__, static_url_path='/assets', static_folder='assets')
app.config['SECRET_KEY'] = 'your-secret-key-change-it-in-production' app.config['SECRET_KEY'] = 'your-secret-key-change-it-in-production'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///aio_3d.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///../instance/aio_3d.db'
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'connect_args': {'timeout': 15}} app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {'connect_args': {'timeout': 15}}
app.config['UPLOAD_FOLDER'] = os.path.abspath(os.path.join(app.root_path, '..', 'uploads')) app.config['UPLOAD_FOLDER'] = os.path.abspath(os.path.join(app.root_path, '..', 'uploads'))
@@ -74,8 +74,10 @@ def create_app():
from . import models from . import models
db.create_all() db.create_all()
from .routes import main_bp, auth_bp, admin_bp from .routes.main_routes import main_bp
from .printer_routes import printer_bp from .routes.auth_routes import auth_bp
from .routes.admin_routes import admin_bp
from .routes.printer_routes import printer_bp
app.register_blueprint(main_bp) app.register_blueprint(main_bp)
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(admin_bp) app.register_blueprint(admin_bp)

View File

@@ -0,0 +1,23 @@
# Third-Party Licenses
This project uses the following third-party libraries, bundled in the `assets` folder.
## Frontend Libraries
1. **Bootstrap v5.3** (CSS/JS)
- License: MIT
- Source: https://getbootstrap.com/
2. **Bootstrap Icons**
- License: MIT
- Source: https://icons.getbootstrap.com/
3. **Three.js** (including Extra Controls / Loaders)
- License: MIT
- Source: https://threejs.org/
4. **GCode Preview**
- License: MIT
- Source: https://github.com/remcoder/gcode-preview
These libraries and their copyright notices belong to their respective creators. See individual source files or official repos for exact license texts.

View File

@@ -42,7 +42,7 @@
"GCode Preview": "GCode Preview", "GCode Preview": "GCode Preview",
"Delete": "Delete", "Delete": "Delete",
"No files uploaded yet.": "No files uploaded yet.", "No files uploaded yet.": "No files uploaded yet.",
"Drag & Drop STL file here or Click to Select": "Drag & Drop STL file here or Click to Select", "Drag & Drop STL files here or Click to Select": "Drag & Drop STL files here or Click to Select",
"Uploading...": "Uploading...", "Uploading...": "Uploading...",
"Simplifying": "Simplifying", "Simplifying": "Simplifying",
"Simplifying...": "Simplifying...", "Simplifying...": "Simplifying...",

View File

@@ -38,7 +38,7 @@
"GCode Preview": "GCode 预览", "GCode Preview": "GCode 预览",
"Delete": "删除", "Delete": "删除",
"No files uploaded yet.": "还没有上传文件。", "No files uploaded yet.": "还没有上传文件。",
"Drag & Drop STL file here or Click to Select": "将 STL 文件拖放到此处或点击选择", "Drag & Drop STL files here or Click to Select": "将 STL 文件拖放到此处或点击选择",
"Uploading...": "上传中...", "Uploading...": "上传中...",
"Simplifying": "简化中", "Simplifying": "简化中",
"Simplifying...": "正在简化...", "Simplifying...": "正在简化...",

131
app/routes/admin_routes.py Normal file
View File

@@ -0,0 +1,131 @@
import json
import trimesh
import uuid
import os
import configparser
from datetime import datetime
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 app.models import db, User, PrintFile, SystemConfig
from app.utils.tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task
from app import i18n_dict
# import trimesh.repair
from app.utils.stl_simplifier import simplify_stl
main_bp = Blueprint('main', __name__)
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
# Guest User Middleware
@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', methods=['GET', 'POST'])
def settings():
if request.method == 'POST':
# concurrent_slices = request.form.get('concurrent_slices')
offset_x = request.form.get('offset_x', '0')
offset_y = request.form.get('offset_y', '0')
proxy_skip_size_mb = request.form.get('proxy_skip_size_mb', '5.0')
default_infill = request.form.get('default_infill', '20')
default_support = request.form.get('default_support', 'false')
default_support_pattern = request.form.get('default_support_pattern', 'tree')
default_quality = request.form.get('default_quality', 'base_global_standard.inst.cfg')
# update or create config entries
config_items = [
('offset_x', offset_x),
('offset_y', offset_y),
('proxy_skip_size_mb', proxy_skip_size_mb),
('default_infill', default_infill),
('default_support', default_support),
('default_support_pattern', default_support_pattern),
('default_quality', default_quality)
]
for key, val in config_items:
conf = SystemConfig.query.filter_by(key=key).first()
if not conf:
conf = SystemConfig(key=key)
db.session.add(conf)
conf.value = val
db.session.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': True}), 200
flash('Settings updated successfully', 'success')
return redirect(url_for('admin.settings'))
configs = {c.key: c.value for c in SystemConfig.query.all()}
presets = get_quality_presets()
return render_template('admin/settings.html', configs=configs, presets=presets)
@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)
@admin_bp.route('/user/<int:user_id>/delete', methods=['POST'])
def delete_user(user_id):
user = User.query.get_or_404(user_id)
if user.id == current_user.id:
flash('You cannot delete yourself.', 'danger')
return redirect(url_for('admin.users'))
print_files = PrintFile.query.filter_by(user_id=user.id).all()
for print_file in print_files:
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)
proxy_path = stl_path + '.proxy.stl'
if os.path.exists(stl_path):
try: os.remove(stl_path)
except: pass
if os.path.exists(proxy_path):
try: os.remove(proxy_path)
except: pass
if os.path.exists(gcode_path):
try: os.remove(gcode_path)
except: pass
db.session.delete(print_file)
db.session.delete(user)
db.session.commit()
flash(f'User {user.username} and all their files have been deleted.', 'success')
return redirect(url_for('admin.users'))
def get_bed_dimensions():
try:
path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
w = data['overrides']['machine_width']['default_value']
h = data['overrides']['machine_depth']['default_value']
hd = data['overrides']['machine_height']['default_value']
return w, h, hd
except:
return 200, 200, 200
def get_quality_presets():
try:
path = os.path.join(current_app.root_path, '..', 'print_config', 'quality', 'creality', 'presets')
files = [f for f in os.listdir(path) if f.endswith('.inst.cfg')]
presets = []
for f in files:
# name = f.replace('.inst.cfg', '').replace('base_', '').replace('_', ' ')
name = f.replace('.inst.cfg', '')
presets.append((f, name))
presets.sort(key=lambda x: x[1])
return presets
except:
return []

46
app/routes/auth_routes.py Normal file
View File

@@ -0,0 +1,46 @@
import json
import trimesh
import uuid
import os
import configparser
from datetime import datetime
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 app.models import db, User, PrintFile, SystemConfig
from app.utils.tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task
from app import i18n_dict
# import trimesh.repair
from app.utils.stl_simplifier import simplify_stl
main_bp = Blueprint('main', __name__)
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
# Guest User Middleware
@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('auth/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 ---

View File

@@ -8,11 +8,11 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash,
from flask_login import login_user, logout_user, login_required, current_user from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from .models import db, User, PrintFile, SystemConfig from app.models import db, User, PrintFile, SystemConfig
from .tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task from app.utils.tasks import merge_and_slice_task, slice_stl_task, simplify_stl_task
from app import i18n_dict from app import i18n_dict
# import trimesh.repair # import trimesh.repair
from stl_simplifier import simplify_stl from app.utils.stl_simplifier import simplify_stl
main_bp = Blueprint('main', __name__) main_bp = Blueprint('main', __name__)
@@ -49,7 +49,7 @@ def set_guest_cookie(response):
@main_bp.route('/') @main_bp.route('/')
def index(): def index():
return render_template('index.html') return render_template('slice/index.html')
@main_bp.route('/set_language/<lang>') @main_bp.route('/set_language/<lang>')
def set_language(lang): def set_language(lang):
@@ -66,72 +66,55 @@ def set_language(lang):
def files(): def files():
if request.method == 'POST': if request.method == 'POST':
if 'file' not in request.files: if 'file' not in request.files:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'No file part'}), 400
flash('No file part', 'danger') flash('No file part', 'danger')
return redirect(request.url) return redirect(request.url)
file = request.files['file']
if file.filename == '': uploaded_files = request.files.getlist('file')
flash('No selected file', 'danger') success_count = 0
return redirect(request.url)
if file and file.filename.lower().endswith('.stl'): for file in uploaded_files:
original_filename = file.filename # Do not use secure_filename to keep Chinese characters if file.filename == '':
ext = os.path.splitext(original_filename)[1].lower() continue
if not ext: if file and file.filename.lower().endswith('.stl'):
ext = '.stl' original_filename = file.filename # Do not use secure_filename to keep Chinese characters
timestamp = datetime.now().strftime('%Y%m%d%H%M%S') ext = os.path.splitext(original_filename)[1].lower()
unique_filename = f"{timestamp}_{uuid.uuid4().hex}{ext}" if not ext:
filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename) ext = '.stl'
file.save(filepath) timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
unique_filename = f"{timestamp}_{uuid.uuid4().hex}_{success_count}{ext}"
# try: filepath = os.path.join(current_app.config['UPLOAD_FOLDER'], unique_filename)
# mesh = trimesh.load(filepath) file.save(filepath)
# # Check for overlapping faces or if the mesh is not watertight
# # which can cause issues in CuraEngine
# needs_repair = False
# if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
# # needs_repair = True
# pass
# if needs_repair: print_file = PrintFile(
# # Attempt automatic repair filename=unique_filename,
original_filename=original_filename,
# trimesh.repair.fix_normals(mesh) file_type='stl',
# trimesh.repair.fix_inversion(mesh) user_id=current_user.id,
# trimesh.repair.fix_winding(mesh) status='simplifying' # Set to simplifying while proxy is generated
# trimesh.repair.fill_holes(mesh) )
db.session.add(print_file)
# # Re-check after repair db.session.commit()
# if not mesh.is_watertight or (len(mesh.faces) > 0 and len(mesh.intersecting_faces[0]) > 0):
# if request.headers.get('X-Requested-With') == 'XMLHttpRequest': # Start background simplification
# return jsonify({'success': False, 'error': 'Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually before slicing.'}), 400 simplify_stl_task(print_file.id, filepath)
# else: success_count += 1
# flash('Mesh has overlapping faces or is not watertight, and automatic repair failed! Please repair the model manually.', 'danger')
# os.remove(filepath) if success_count > 0:
# return redirect(request.url) if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# else: return jsonify({'success': True, 'count': success_count})
# # Repair succeeded, rewrite file flash(f'{success_count} file(s) uploaded successfully!', 'success')
# mesh.export(filepath) else:
# except Exception as e: if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
# pass return jsonify({'error': 'No valid files uploaded'}), 400
flash('No valid files uploaded', 'danger')
print_file = PrintFile( return redirect(url_for('main.files'))
filename=unique_filename,
original_filename=original_filename,
file_type='stl',
user_id=current_user.id,
status='simplifying' # Set to simplifying while proxy is generated
)
db.session.add(print_file)
db.session.commit()
# Start background simplification
simplify_stl_task(print_file.id, filepath)
flash('File uploaded successfully!', 'success')
return redirect(url_for('main.files'))
# Order by newest first # Order by newest first
user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all() user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all()
return render_template('files.html', files=user_files) return render_template('slice/files.html', files=user_files)
@main_bp.route('/api/files_status') @main_bp.route('/api/files_status')
@login_required @login_required
@@ -182,7 +165,7 @@ def preview_gcode(file_id):
configs = {c.key: c.value for c in SystemConfig.query.all()} configs = {c.key: c.value for c in SystemConfig.query.all()}
offset_x = float(configs.get('offset_x', '0.0')) offset_x = float(configs.get('offset_x', '0.0'))
offset_y = float(configs.get('offset_y', '0.0')) offset_y = float(configs.get('offset_y', '0.0'))
return render_template('gcode_preview.html', file=print_file, content=content, line_count=line_count, machine_width=w, machine_depth=h, machine_height=hd, offset_x=offset_x, offset_y=offset_y) return render_template('slice/gcode_preview.html', file=print_file, content=content, line_count=line_count, machine_width=w, machine_depth=h, machine_height=hd, offset_x=offset_x, offset_y=offset_y)
@main_bp.route('/delete_file/<int:file_id>', methods=['POST']) @main_bp.route('/delete_file/<int:file_id>', methods=['POST'])
@login_required @login_required
@@ -210,106 +193,6 @@ def delete_file(file_id):
# --- Auth Routes --- # --- 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', methods=['GET', 'POST'])
def settings():
if request.method == 'POST':
# concurrent_slices = request.form.get('concurrent_slices')
offset_x = request.form.get('offset_x', '0')
offset_y = request.form.get('offset_y', '0')
proxy_skip_size_mb = request.form.get('proxy_skip_size_mb', '5.0')
default_infill = request.form.get('default_infill', '20')
default_support = request.form.get('default_support', 'false')
default_support_pattern = request.form.get('default_support_pattern', 'tree')
default_quality = request.form.get('default_quality', 'base_global_standard.inst.cfg')
# update or create config entries
config_items = [
('offset_x', offset_x),
('offset_y', offset_y),
('proxy_skip_size_mb', proxy_skip_size_mb),
('default_infill', default_infill),
('default_support', default_support),
('default_support_pattern', default_support_pattern),
('default_quality', default_quality)
]
for key, val in config_items:
conf = SystemConfig.query.filter_by(key=key).first()
if not conf:
conf = SystemConfig(key=key)
db.session.add(conf)
conf.value = val
db.session.commit()
flash('Settings updated successfully', 'success')
return redirect(url_for('admin.settings'))
configs = {c.key: c.value for c in SystemConfig.query.all()}
presets = get_quality_presets()
return render_template('admin_settings.html', configs=configs, presets=presets)
@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)
@admin_bp.route('/user/<int:user_id>/delete', methods=['POST'])
def delete_user(user_id):
user = User.query.get_or_404(user_id)
if user.id == current_user.id:
flash('You cannot delete yourself.', 'danger')
return redirect(url_for('admin.users'))
print_files = PrintFile.query.filter_by(user_id=user.id).all()
for print_file in print_files:
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)
proxy_path = stl_path + '.proxy.stl'
if os.path.exists(stl_path):
try: os.remove(stl_path)
except: pass
if os.path.exists(proxy_path):
try: os.remove(proxy_path)
except: pass
if os.path.exists(gcode_path):
try: os.remove(gcode_path)
except: pass
db.session.delete(print_file)
db.session.delete(user)
db.session.commit()
flash(f'User {user.username} and all their files have been deleted.', 'success')
return redirect(url_for('admin.users'))
def get_bed_dimensions(): def get_bed_dimensions():
try: try:
path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json') path = os.path.join(current_app.root_path, '..', 'print_config', 'printers', 'creality_ender3v3se.def.json')
@@ -354,7 +237,7 @@ def plater():
user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all() user_files = PrintFile.query.filter_by(user_id=current_user.id, file_type='stl').order_by(PrintFile.created_at.desc()).all()
models = [{'id': f.id, 'name': f.original_filename, 'status': f.status, 'url': url_for('main.serve_proxy_file', file_id=f.id), 'transform_matrix': f.transform_matrix} for f in user_files] models = [{'id': f.id, 'name': f.original_filename, 'status': f.status, 'url': url_for('main.serve_proxy_file', file_id=f.id), 'transform_matrix': f.transform_matrix} for f in user_files]
return render_template('plater.html', w=w, h=h, hd=hd, presets=presets, last_quality=default_quality, models=models, offset_x=offset_x, offset_y=offset_y, default_infill=default_infill, default_support=default_support, default_support_pattern=default_support_pattern) return render_template('slice/plater.html', w=w, h=h, hd=hd, presets=presets, last_quality=default_quality, models=models, offset_x=offset_x, offset_y=offset_y, default_infill=default_infill, default_support=default_support, default_support_pattern=default_support_pattern)
@main_bp.route('/file/<int:file_id>') @main_bp.route('/file/<int:file_id>')
@login_required @login_required

View File

@@ -1,8 +1,12 @@
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app, Response from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for, current_app, Response
from flask_login import login_required, current_user from flask_login import login_required, current_user
from websockets.sync.client import connect as ws_connect
import websockets.exceptions
import threading
import requests import requests
from .models import SystemConfig, db from urllib.parse import urlparse
from .octoprint_client import OctoPrintClient from app.models import SystemConfig, db
from app.utils.octoprint_client import OctoPrintClient
printer_bp = Blueprint('printer', __name__, url_prefix='/printer') printer_bp = Blueprint('printer', __name__, url_prefix='/printer')
@@ -118,6 +122,8 @@ def octo_config():
conf_key.value = apikey conf_key.value = apikey
db.session.commit() db.session.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': True}), 200
flash("OctoPrint settings updated", "success") flash("OctoPrint settings updated", "success")
return redirect(url_for('printer.octo_config')) return redirect(url_for('printer.octo_config'))
@@ -135,6 +141,8 @@ def octo_embed():
embed_url = url_for('printer.octo_proxy') if url and url.value else None embed_url = url_for('printer.octo_proxy') if url and url.value else None
return render_template('printer/octo_embed.html', embed_url=embed_url) return render_template('printer/octo_embed.html', embed_url=embed_url)
@printer_bp.route('/proxy', defaults={'path': ''}, websocket=True)
@printer_bp.route('/proxy/<path:path>', websocket=True)
@printer_bp.route('/proxy', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @printer_bp.route('/proxy', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@printer_bp.route('/proxy/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) @printer_bp.route('/proxy/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@login_required @login_required
@@ -146,29 +154,153 @@ def octo_proxy(path):
if not url_config or not url_config.value: if not url_config or not url_config.value:
return "OctoPrint URL not configured", 404 return "OctoPrint URL not configured", 404
from urllib.parse import urlparse
base_url = url_config.value.rstrip('/') base_url = url_config.value.rstrip('/')
# print("----- REQUEST HEADERS -----")
# for k, v in request.headers:
# print(f"{k}: {v}")
# print("----- END HEADERS -----")
# --- WebSocket Proxy Logic ---
if request.headers.get('Upgrade', '').lower() == 'websocket':
from flask_sock import Server, ConnectionClosed
# Check if environment supports WebSockets
try:
ws = Server(request.environ)
except Exception as e:
env_keys = sorted(list(request.environ.keys()))
print(f"FAILED. ENV KEYS: {env_keys}")
return f"WebSocket Upgrade Failed: {str(e)}", 400
def handle_ws():
if base_url.startswith('https://'):
ws_base = base_url.replace('https://', 'wss://', 1)
else:
ws_base = base_url.replace('http://', 'ws://', 1)
target_url = f"{ws_base}/{path}"
if request.query_string:
target_url = f"{target_url}?{request.query_string.decode('utf-8')}"
print(f"WS Proxy Query String: {request.query_string.decode('utf-8')}")
# Copy most headers, especially Origin to pass CORS
ws_headers = {}
for k, v in request.headers:
if k.lower() not in ['host', 'connection', 'upgrade', 'sec-websocket-key', 'sec-websocket-version', 'sec-websocket-extensions', 'content-length']:
ws_headers[k] = v
# Match Tornado's expectations for Origin to avoid 400 Bad Request
parsed_base = urlparse(base_url)
ws_headers['Host'] = parsed_base.netloc
if 'Origin' in request.headers:
ws_headers['Origin'] = base_url
if 'Referer' in request.headers:
ws_headers['Referer'] = f"{base_url}/{path}"
ws_headers['X-Real-IP'] = request.remote_addr
forwarded_for = request.headers.get('X-Forwarded-For', '')
ws_headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
ws_headers['X-Forwarded-Proto'] = request.scheme
ws_headers['X-Forwarded-Host'] = request.host
ws_headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
ws_headers['X-Script-Name'] = '/printer/proxy'
print(f"WS Proxy Connecting to: {target_url}")
try:
remote_ws = ws_connect(target_url, additional_headers=ws_headers)
print("WS Proxy connected to remote.")
except Exception as e:
import traceback
traceback.print_exc()
print(f"Remote WS Connection Error: {e}")
ws.close(1011, str(e))
return
def recv_loop():
print("WS recv_loop started")
try:
for message in remote_ws:
ws.send(message)
except Exception as e:
print("WS recv error:", e)
finally:
try: remote_ws.close()
except: pass
try: ws.close()
except: pass
print("WS recv_loop ended")
t = threading.Thread(target=recv_loop)
t.daemon = True
t.start()
print("WS Entering client receive loop")
try:
while True:
data = ws.receive()
if data is None:
break
remote_ws.send(data)
except Exception as e:
print("WS send error:", e)
finally:
try: remote_ws.close()
except: pass
try: ws.close()
except: pass
print("WS client loop ended")
try:
handle_ws()
except ConnectionClosed:
print("WS Connection Closed")
except Exception as e:
print("WS Error in handle_ws:", e)
finally:
try: ws.close()
except: pass
class WebSocketResponse(Response):
def __call__(self, *args, **kwargs):
print("WS Response __call__")
if getattr(ws, 'mode', 'werkzeug') == 'werkzeug':
return super().__call__(*args, **kwargs)
return []
return WebSocketResponse()
# --- Standard HTTP Proxy Logic ---
# from urllib.parse import urlparse
target_url = f"{base_url}/{path}" target_url = f"{base_url}/{path}"
if request.query_string: if request.query_string:
target_url = f"{target_url}?{request.query_string.decode('utf-8')}" target_url = f"{target_url}?{request.query_string.decode('utf-8')}"
# Build headers for reverse proxy, masking origin/referer to avoid CSRF # Build headers for reverse proxy based on nginx config reference
parsed_base = urlparse(base_url) parsed_base = urlparse(base_url)
headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'content-length']} headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'content-length']}
headers['Host'] = parsed_base.netloc
if 'Origin' in headers: # NGINX equivalent proxy headers
headers['Origin'] = base_url headers['Host'] = request.host
if 'Referer' in headers: headers['X-Real-IP'] = request.remote_addr
headers['Referer'] = f"{base_url}/{path}" headers['X-Real-Port'] = str(request.environ.get('REMOTE_PORT', ''))
headers['X-Forwarded-For'] = request.remote_addr forwarded_for = request.headers.get('X-Forwarded-For', '')
headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
headers['X-Forwarded-Protocol'] = request.scheme
headers['X-Script-Name'] = "/printer/proxy"
headers['X-Forwarded-Host'] = request.host headers['X-Forwarded-Host'] = request.host
headers['X-Forwarded-Proto'] = request.scheme headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
headers['X-Script-Name'] = '/printer/proxy' headers['REMOTE-HOST'] = request.remote_addr
if request.headers.get('Upgrade'):
headers['Upgrade'] = request.headers.get('Upgrade')
if request.headers.get('Connection'):
headers['Connection'] = request.headers.get('Connection')
try: try:
# proxy_connect_timeout 60s, proxy_read_timeout 600s
resp = requests.request( resp = requests.request(
method=request.method, method=request.method,
url=target_url, url=target_url,
@@ -176,7 +308,8 @@ def octo_proxy(path):
data=request.get_data(), data=request.get_data(),
cookies=request.cookies, cookies=request.cookies,
allow_redirects=False, allow_redirects=False,
stream=True stream=True,
timeout=(60, 600)
) )
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
return f"Proxy connection error: {str(e)}", 502 return f"Proxy connection error: {str(e)}", 502
@@ -191,3 +324,4 @@ def octo_proxy(path):
yield chunk yield chunk
return Response(generate(), resp.status_code, response_headers) return Response(generate(), resp.status_code, response_headers)

View File

@@ -9,7 +9,7 @@
<div class="card-body"> <div class="card-body">
<h5>{{ _('CuraEngine Configurations') }}</h5> <h5>{{ _('CuraEngine Configurations') }}</h5>
<hr> <hr>
<form method="POST" action="{{ url_for('admin.settings') }}"> <form id="settingsForm" onsubmit="submitSettings(event)">
<div class="mb-3"> <div class="mb-3">
<label for="offset_x" class="form-label">{{ _('Plater Origin Offset X (mm)') }}</label> <label for="offset_x" class="form-label">{{ _('Plater Origin Offset X (mm)') }}</label>
<input type="number" class="form-control" name="offset_x" id="offset_x" value="{{ configs.get('offset_x', '0') }}"> <input type="number" class="form-control" name="offset_x" id="offset_x" value="{{ configs.get('offset_x', '0') }}">
@@ -70,8 +70,44 @@
</select> </select>
</div> </div>
<button type="submit" class="btn btn-primary">{{ _('Save Settings') }}</button> <button type="submit" class="btn btn-primary" id="btn-save-settings">{{ _('Save Settings') }}</button>
</form> </form>
</div> </div>
</div> </div>
<script>
function submitSettings(event) {
event.preventDefault();
const form = document.getElementById('settingsForm');
const formData = new FormData(form);
const btn = document.getElementById('btn-save-settings');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Saving...';
fetch("{{ url_for('admin.settings') }}", {
method: "POST",
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (response.ok) {
window.showToast("{{ _('Settings updated successfully') }}", "success");
} else {
window.showToast("{{ _('Error updating settings') }}", "danger");
}
})
.catch(error => {
console.error('Error:', error);
window.showToast("{{ _('Network error') }}", "danger");
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = originalText;
});
}
</script>
{% endblock %} {% endblock %}

View File

@@ -32,7 +32,7 @@
</td> </td>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td> <td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td> <td>
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="POST" class="d-inline" onsubmit="return confirm('{{ _('WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?') }}');"> <form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="POST" class="d-inline" onsubmit="event.preventDefault(); window.customConfirm('{{ _('WARNING: Are you sure you want to permanently delete this user AND ALL their uploaded files and G-codes?') }}', () => { this.submit(); });">
<button type="submit" class="btn btn-sm btn-outline-danger" {% if user.id == current_user.id %}disabled{% endif %}>{{ _('Delete') }}</button> <button type="submit" class="btn btn-sm btn-outline-danger" {% if user.id == current_user.id %}disabled{% endif %}>{{ _('Delete') }}</button>
</form> </form>
</td> </td>

View File

@@ -21,8 +21,18 @@
.card { border: none; border-radius: 0.75rem; overflow: hidden; } .card { border: none; border-radius: 0.75rem; overflow: hidden; }
.card-header { border-bottom: 1px solid rgba(0,0,0,.05); background-color: transparent; } .card-header { border-bottom: 1px solid rgba(0,0,0,.05); background-color: transparent; }
.toast-container { margin-bottom: 20px; margin-right: 20px; } .toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1055; width: auto; max-width: 90%; pointer-events: none; }
.toast { border-radius: 0.5rem; box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15); opacity: 0.95; } .toast { border-radius: 0.5rem; box-shadow: 0 0.5rem 1rem rgba(0,0,0,.25); opacity: 1 !important; pointer-events: auto; }
/* 页面切换动画 Page Transition */
@keyframes pageFadeInSlide {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
main { animation: pageFadeInSlide 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
/* 提升 Accordion 折叠栏动画更平滑 */
.collapsing { transition: height 0.35s cubic-bezier(0.25, 0.8, 0.25, 1) !important; }
</style> </style>
</head> </head>
<body> <body>
@@ -150,12 +160,12 @@
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 mt-4 bg-light min-vh-100 pb-5"> <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 mt-4 bg-light min-vh-100 pb-5">
<!-- Toast Notification Container --> <!-- Toast Notification Container -->
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1055;"> <div class="toast-container" id="global-toast-container">
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
{% for category, message in messages %} {% for category, message in messages %}
{% set toast_class = 'bg-success text-white' if category == 'success' else 'bg-danger text-white' if category == 'danger' else 'bg-warning text-dark' if category == 'warning' else 'bg-primary text-white' %} {% set toast_class = 'bg-success text-white' if category == 'success' else 'bg-danger text-white' if category == 'danger' else 'bg-warning text-dark' if category == 'warning' else 'bg-primary text-white' %}
<div class="toast align-items-center border-0 {{ toast_class }}" role="alert" aria-live="assertive" aria-atomic="true"> <div class="toast align-items-center border-0 {{ toast_class }} mb-2" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex"> <div class="d-flex">
<div class="toast-body fw-medium"> <div class="toast-body fw-medium">
{{ message }} {{ message }}
@@ -173,6 +183,41 @@
</div> </div>
</div> </div>
<!-- Global Custom Alert Modal -->
<div class="modal fade" id="globalAlertModal" tabindex="-1" aria-hidden="true" style="z-index: 1060;">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-warning text-dark py-2">
<h6 class="modal-title fw-bold" id="globalAlertTitle"><i class="bi bi-exclamation-triangle-fill me-2"></i>{{ _('Notice') }}</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 text-center py-4 text-break" id="globalAlertMessage">
</div>
<div class="modal-footer border-0 p-2 justify-content-center bg-light">
<button type="button" class="btn btn-warning px-4 rounded-pill fw-bold" data-bs-dismiss="modal">{{ _('OK') }}</button>
</div>
</div>
</div>
</div>
<!-- Global Custom Confirm Modal -->
<div class="modal fade" id="globalConfirmModal" tabindex="-1" aria-hidden="true" style="z-index: 1060;">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white py-2">
<h6 class="modal-title fw-bold"><i class="bi bi-question-circle-fill me-2"></i>{{ _('Confirm') }}</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body fs-6 text-center py-4 text-break" id="globalConfirmMessage">
</div>
<div class="modal-footer border-0 p-2 justify-content-center bg-light">
<button type="button" class="btn btn-outline-secondary px-4 rounded-pill" data-bs-dismiss="modal">{{ _('Cancel') }}</button>
<button type="button" class="btn btn-primary px-4 rounded-pill fw-bold" id="globalConfirmBtn">{{ _('Yes') }}</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script> <script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script> <script>
// Initialize Toasts automatically // Initialize Toasts automatically
@@ -182,6 +227,53 @@
return new bootstrap.Toast(toastEl, { delay: 3000 }).show() return new bootstrap.Toast(toastEl, { delay: 3000 }).show()
}); });
}); });
// Global Utility: Show Toast dynamically
window.showToast = function(msg, type='success', duration=3000) {
const container = document.getElementById('global-toast-container');
const toastClass = type === 'success' ? 'bg-success text-white' :
type === 'danger' ? 'bg-danger text-white' :
type === 'warning' ? 'bg-warning text-dark' : 'bg-primary text-white';
const html = `
<div class="toast align-items-center border-0 ${toastClass} mb-2" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body fw-medium">${msg}</div>
<button type="button" class="btn-close ${type==='warning'?'':'btn-close-white'} me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
const ts = container.lastElementChild;
new bootstrap.Toast(ts, { autohide: true, delay: duration }).show();
ts.addEventListener('hidden.bs.toast', () => { ts.remove(); });
};
// Override default alert
window.customAlert = function(msg, title) {
document.getElementById('globalAlertMessage').innerHTML = String(msg).replace(/\n/g, '<br>');
if(title) document.getElementById('globalAlertTitle').innerHTML = '<i class="bi bi-info-circle-fill me-2"></i>' + title;
else document.getElementById('globalAlertTitle').innerHTML = '<i class="bi bi-exclamation-triangle-fill me-2"></i>Notice';
new bootstrap.Modal(document.getElementById('globalAlertModal')).show();
};
// Override default confirm
window.customConfirm = function(msg, onConfirm) {
document.getElementById('globalConfirmMessage').innerHTML = String(msg).replace(/\n/g, '<br>');
const modalEl = document.getElementById('globalConfirmModal');
const modal = new bootstrap.Modal(modalEl);
// Clear previous event listener bindings
const elClone = document.getElementById('globalConfirmBtn').cloneNode(true);
document.getElementById('globalConfirmBtn').parentNode.replaceChild(elClone, document.getElementById('globalConfirmBtn'));
document.getElementById('globalConfirmBtn').addEventListener('click', function() {
modal.hide();
if(onConfirm) onConfirm();
});
modal.show();
};
</script> </script>
</body> </body>
</html> </html>

View File

@@ -52,10 +52,13 @@
<script> <script>
function sendCommand(cmdName) { function sendCommand(cmdName) {
if ((cmdName === 'cancel' || cmdName === 'home') && !confirm("Are you sure you want to perform this action?")) { if (cmdName === 'cancel' || cmdName === 'home') {
return; window.customConfirm("{{ _('Are you sure you want to perform this action?') }}", () => doSendCommand(cmdName));
} else {
doSendCommand(cmdName);
} }
}
function doSendCommand(cmdName) {
fetch('{{ url_for("printer.api_command") }}', { fetch('{{ url_for("printer.api_command") }}', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -64,24 +67,15 @@ function sendCommand(cmdName) {
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if(data.success) { if(data.success) {
flashMessage("success", "Command " + cmdName + " sent."); window.showToast("{{ _('Command') }} " + cmdName + " {{ _('sent.') }}", "success");
} else { } else {
flashMessage("danger", "Control failed: " + data.error); window.customAlert("{{ _('Control failed: ') }}" + data.error);
} }
}) })
.catch(err => { .catch(err => {
flashMessage("danger", "Network Error: " + err); window.customAlert("{{ _('Network Error: ') }}" + err);
}); });
} }
function flashMessage(type, text) {
const container = document.querySelector('.toast-container');
if(!container) return alert(text);
const toast = document.createElement('div');
toast.className = `toast align-items-center border-0 bg-${type} text-white show`;
toast.innerHTML = `<div class="d-flex"><div class="toast-body fw-medium">${text}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
container.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -10,7 +10,7 @@
<i class="bi bi-link-45deg me-1"></i>{{ _('Connection Settings') }} <i class="bi bi-link-45deg me-1"></i>{{ _('Connection Settings') }}
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" action="{{ url_for('printer.octo_config') }}"> <form id="octoConfigForm" onsubmit="submitConfig(event)">
<div class="mb-3"> <div class="mb-3">
<label for="octoprint_url" class="form-label fw-bold">{{ _('OctoPrint Base URL') }}</label> <label for="octoprint_url" class="form-label fw-bold">{{ _('OctoPrint Base URL') }}</label>
<div class="input-group mb-3 shadow-sm"> <div class="input-group mb-3 shadow-sm">
@@ -34,9 +34,45 @@
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary px-4 rounded-pill shadow-sm"><i class="bi bi-save2 me-2"></i>{{ _('Save Connection Settings') }}</button> <button type="submit" class="btn btn-primary px-4 rounded-pill shadow-sm" id="btn-save-octo"><i class="bi bi-save2 me-2"></i>{{ _('Save Connection Settings') }}</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<script>
function submitConfig(event) {
event.preventDefault();
const form = document.getElementById('octoConfigForm');
const formData = new FormData(form);
const btn = document.getElementById('btn-save-octo');
const originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Saving...';
fetch("{{ url_for('printer.octo_config') }}", {
method: "POST",
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (response.ok) {
window.showToast("{{ _('OctoPrint settings updated') }}", "success");
} else {
window.showToast("{{ _('Error saving settings') }}", "danger");
}
})
.catch(error => {
console.error('Error:', error);
window.showToast("{{ _('Network error') }}", "danger");
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = originalText;
});
}
</script>
{% endblock %} {% endblock %}

View File

@@ -39,23 +39,25 @@
<script> <script>
function printFile(origin, path) { function printFile(origin, path) {
if(!confirm("{{ _('Send this file to print immediately?') }}\n\n" + path)) return; window.customConfirm("{{ _('Send this file to print immediately?') }}<br><small>" + path + "</small>", () => {
fetch('{{ url_for("printer.api_print_file") }}', {
fetch('{{ url_for("printer.api_print_file") }}', { method: 'POST',
method: 'POST', headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ origin: origin, path: path })
body: JSON.stringify({ origin: origin, path: path }) })
}) .then(response => response.json())
.then(response => response.json()) .then(data => {
.then(data => { if(data.success) {
if(data.success) { window.showToast("{{ _('Print starting! Going to dashboard...') }}", "success");
alert("{{ _('Print starting! Going to dashboard...') }}"); setTimeout(() => {
window.location.href = "{{ url_for('printer.status') }}"; window.location.href = "{{ url_for('printer.status') }}";
} else { }, 1500);
alert("Error: " + data.error); } else {
} window.customAlert("Error: " + data.error);
}) }
.catch(err => alert("Error: " + err)); })
.catch(err => window.customAlert("Error: " + err));
});
} }
</script> </script>
{% endif %} {% endif %}

View File

@@ -84,8 +84,13 @@
<script> <script>
function sendCmd(cmd) { function sendCmd(cmd) {
if(cmd === 'cancel' && !confirm("{{ _('Are you sure you want to cancel the print?') }}")) return; if(cmd === 'cancel') {
window.customConfirm("{{ _('Are you sure you want to cancel the print?') }}", () => doSendCmd(cmd));
} else {
doSendCmd(cmd);
}
}
function doSendCmd(cmd) {
fetch('{{ url_for("printer.api_command") }}', { fetch('{{ url_for("printer.api_command") }}', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -96,7 +101,7 @@ function sendCmd(cmd) {
if(data.success) { if(data.success) {
window.location.reload(); window.location.reload();
} else { } else {
alert("Error: " + data.error); window.customAlert("Error: " + data.error);
} }
}); });
} }

View File

@@ -9,8 +9,8 @@
<div class="card-body"> <div class="card-body">
<div id="drop-zone" class="border border-2 border-primary rounded p-4 text-center bg-white" style="border-style: dashed !important; cursor: pointer; transition: all 0.3s ease;"> <div id="drop-zone" class="border border-2 border-primary rounded p-4 text-center bg-white" style="border-style: dashed !important; cursor: pointer; transition: all 0.3s ease;">
<i class="bi bi-cloud-arrow-up display-4 text-primary mb-2"></i> <i class="bi bi-cloud-arrow-up display-4 text-primary mb-2"></i>
<h5 class="text-secondary fw-normal">{{ _('Drag & Drop STL file here or Click to Select') }}</h5> <h5 class="text-secondary fw-normal">{{ _('Drag & Drop STL files here or Click to Select') }}</h5>
<input type="file" id="file" name="file" accept=".stl" class="d-none"> <input type="file" id="file" name="file" accept=".stl" class="d-none" multiple>
</div> </div>
<div id="upload-progress-container" class="mt-3 d-none"> <div id="upload-progress-container" class="mt-3 d-none">
@@ -69,7 +69,7 @@
<a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-sm btn-outline-primary shadow-sm" title="{{ _('Download GCode') }}"><i class="bi bi-download"></i></a> <a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-sm btn-outline-primary shadow-sm" title="{{ _('Download GCode') }}"><i class="bi bi-download"></i></a>
<a href="{{ url_for('main.preview_gcode', file_id=file.id) }}" class="btn btn-sm btn-outline-info shadow-sm" title="{{ _('GCode Preview') }}"><i class="bi bi-eye"></i></a> <a href="{{ url_for('main.preview_gcode', file_id=file.id) }}" class="btn btn-sm btn-outline-info shadow-sm" title="{{ _('GCode Preview') }}"><i class="bi bi-eye"></i></a>
{% endif %} {% endif %}
<form action="{{ url_for('main.delete_file', file_id=file.id) }}" method="POST" onsubmit="return confirm('{{ _('Are you sure you want to delete this file?') }}');"> <form action="{{ url_for('main.delete_file', file_id=file.id) }}" method="POST" onsubmit="event.preventDefault(); window.customConfirm('{{ _('Are you sure you want to delete this file?') }}', () => { this.submit(); });">
<button type="submit" class="btn btn-sm btn-outline-danger shadow-sm" title="{{ _('Delete') }}"><i class="bi bi-trash3"></i></button> <button type="submit" class="btn btn-sm btn-outline-danger shadow-sm" title="{{ _('Delete') }}"><i class="bi bi-trash3"></i></button>
</form> </form>
</div> </div>
@@ -147,7 +147,7 @@ document.addEventListener('DOMContentLoaded', function() {
actionsHtml += `<a href="${previewUrl}" class="btn btn-sm btn-outline-info shadow-sm" title="{{ _('GCode Preview') }}"><i class="bi bi-eye"></i></a>\n`; actionsHtml += `<a href="${previewUrl}" class="btn btn-sm btn-outline-info shadow-sm" title="{{ _('GCode Preview') }}"><i class="bi bi-eye"></i></a>\n`;
} }
const deleteUrl = `{{ url_for('main.delete_file', file_id=999999999) }}`.replace('999999999', id); const deleteUrl = `{{ url_for('main.delete_file', file_id=999999999) }}`.replace('999999999', id);
actionsHtml += `<form action="${deleteUrl}" method="POST" onsubmit="return confirm('{{ _('Are you sure you want to delete this file?') }}');"> actionsHtml += `<form action="${deleteUrl}" method="POST" onsubmit="event.preventDefault(); window.customConfirm('{{ _('Are you sure you want to delete this file?') }}', () => { this.submit(); });">
<button type="submit" class="btn btn-sm btn-outline-danger shadow-sm" title="{{ _('Delete') }}"><i class="bi bi-trash3"></i></button> <button type="submit" class="btn btn-sm btn-outline-danger shadow-sm" title="{{ _('Delete') }}"><i class="bi bi-trash3"></i></button>
</form>`; </form>`;
actionsTd.innerHTML = actionsHtml; actionsTd.innerHTML = actionsHtml;
@@ -199,25 +199,32 @@ document.addEventListener('DOMContentLoaded', function() {
dropZone.addEventListener('drop', e => { dropZone.addEventListener('drop', e => {
const files = e.dataTransfer.files; const files = e.dataTransfer.files;
if (files.length) { if (files.length) {
handleFileUpload(files[0]); handleFileUpload(files);
} }
}); });
fileInput.addEventListener('change', () => { fileInput.addEventListener('change', () => {
if (fileInput.files.length) { if (fileInput.files.length) {
handleFileUpload(fileInput.files[0]); handleFileUpload(fileInput.files);
} }
}); });
function handleFileUpload(file) { function handleFileUpload(files) {
if (!file.name.toLowerCase().endsWith('.stl')) { const formData = new FormData();
alert('{{ _("Please upload a valid .stl file!") }}'); let hasValidFile = false;
for (let i = 0; i < files.length; i++) {
if (files[i].name.toLowerCase().endsWith('.stl')) {
formData.append('file', files[i]);
hasValidFile = true;
}
}
if (!hasValidFile) {
window.customAlert('{{ _("Please upload valid .stl files!") }}');
return; return;
} }
const formData = new FormData();
formData.append('file', file);
progressContainer.classList.remove('d-none'); progressContainer.classList.remove('d-none');
dropZone.classList.add('d-none'); dropZone.classList.add('d-none');
progressBar.style.width = '0%'; progressBar.style.width = '0%';
@@ -244,7 +251,7 @@ document.addEventListener('DOMContentLoaded', function() {
try { try {
let response = JSON.parse(xhr.responseText); let response = JSON.parse(xhr.responseText);
if (response.error) { if (response.error) {
alert('{{ _("Validation Failed") }}:\n' + response.error); window.customAlert('{{ _("Validation Failed") }}:\n' + response.error);
progressContainer.classList.add('d-none'); progressContainer.classList.add('d-none');
dropZone.classList.remove('d-none'); dropZone.classList.remove('d-none');
return; return;
@@ -252,14 +259,14 @@ document.addEventListener('DOMContentLoaded', function() {
} catch(e) { } catch(e) {
console.log('No JSON error response'); console.log('No JSON error response');
} }
alert('{{ _("Upload failed.") }}'); window.customAlert('{{ _("Upload failed.") }}');
progressContainer.classList.add('d-none'); progressContainer.classList.add('d-none');
dropZone.classList.remove('d-none'); dropZone.classList.remove('d-none');
} }
}; };
xhr.onerror = function() { xhr.onerror = function() {
alert('{{ _("Upload error.") }}'); window.customAlert('{{ _("Upload error.") }}');
progressContainer.classList.add('d-none'); progressContainer.classList.add('d-none');
dropZone.classList.remove('d-none'); dropZone.classList.remove('d-none');
}; };

View File

@@ -52,13 +52,15 @@
</div> </div>
<!-- Sidebar --> <!-- Sidebar -->
<div class="col-md-3 h-100 d-flex flex-column pb-3" style="overflow-y: auto; overflow-x: hidden;"> <div class="col-md-3 h-100 d-flex flex-column pb-3">
<div class="card shadow-sm mb-3"> <!-- Accordion wrapper for options -->
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer; z-index: 10;" data-bs-toggle="collapse" data-bs-target="#collapseModels" aria-expanded="true"> <div class="accordion flex-grow-1" id="platerSidebarAccordion" style="overflow-y: auto; overflow-x: hidden; padding-right: 5px;">
<span><i class="bi bi-layers-fill me-2"></i>{{ _('Available Models') }}</span> <div class="card shadow-sm mb-3">
<i class="bi bi-chevron-bar-contract"></i> <div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer; z-index: 10;" data-bs-toggle="collapse" data-bs-target="#collapseModels" aria-expanded="true">
</div> <span><i class="bi bi-layers-fill me-2"></i>{{ _('Available Models') }}</span>
<div id="collapseModels" class="collapse show"> <i class="bi bi-chevron-bar-contract"></i>
</div>
<div id="collapseModels" class="collapse show" data-bs-parent="#platerSidebarAccordion">
<div class="list-group list-group-flush" id="model-list" style="min-height: 160px; max-height: max(250px, 35vh); overflow-y: auto;"> <div class="list-group list-group-flush" id="model-list" style="min-height: 160px; max-height: max(250px, 35vh); overflow-y: auto;">
{% for model in models %} {% for model in models %}
<button id="add-model-btn-{{ model.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-matrix="{{ model.transform_matrix or '' }}" onclick="addModelToPlate(this, {{ model.id }}, '{{ model.url }}', '{{ model.name }}', '{{ model.status }}')"> <button id="add-model-btn-{{ model.id }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" data-matrix="{{ model.transform_matrix or '' }}" onclick="addModelToPlate(this, {{ model.id }}, '{{ model.url }}', '{{ model.name }}', '{{ model.status }}')">
@@ -73,11 +75,11 @@
</div> </div>
<div class="card shadow-sm mb-3 flex-shrink-0"> <div class="card shadow-sm mb-3 flex-shrink-0">
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseSettings" aria-expanded="true"> <div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center collapsed" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseSettings" aria-expanded="false">
<span><i class="bi bi-sliders me-2"></i>{{ _('Other Settings') }}</span> <span><i class="bi bi-sliders me-2"></i>{{ _('Other Settings') }}</span>
<i class="bi bi-chevron-bar-contract"></i> <i class="bi bi-chevron-bar-contract"></i>
</div> </div>
<div id="collapseSettings" class="collapse show"> <div id="collapseSettings" class="collapse" data-bs-parent="#platerSidebarAccordion">
<div class="card-body py-2"> <div class="card-body py-2">
<div class="mb-2"> <div class="mb-2">
<label for="infill-density" class="form-label text-secondary small mb-1">{{ _('Infill Density') }} (%)</label> <label for="infill-density" class="form-label text-secondary small mb-1">{{ _('Infill Density') }} (%)</label>
@@ -110,12 +112,12 @@
</div> </div>
</div> </div>
<div class="card shadow-sm flex-shrink-0"> <div class="card shadow-sm flex-shrink-0 mb-3">
<div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseQuality" aria-expanded="true"> <div class="card-header bg-light fw-bold text-secondary d-flex justify-content-between align-items-center collapsed" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#collapseQuality" aria-expanded="false">
<span><i class="bi bi-gear-wide-connected me-2"></i>{{ _('Quality Profile') }}</span> <span><i class="bi bi-gear-wide-connected me-2"></i>{{ _('Quality Profile') }}</span>
<i class="bi bi-chevron-bar-contract"></i> <i class="bi bi-chevron-bar-contract"></i>
</div> </div>
<div id="collapseQuality" class="collapse show"> <div id="collapseQuality" class="collapse" data-bs-parent="#platerSidebarAccordion">
<div class="card-body"> <div class="card-body">
<div class="mb-3"> <div class="mb-3">
<select class="form-select bg-light" id="quality"> <select class="form-select bg-light" id="quality">
@@ -124,16 +126,16 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<hr>
<div class="d-flex justify-content-between">
<button class="btn btn-outline-danger btn-sm" onclick="clearPlate()"><i class="bi bi-trash me-1"></i>{{ _('Clear Board') }}</button>
<button class="btn btn-primary" onclick="mergeAndSlice()" id="btn-merge"><i class="bi bi-gear-fill me-2" id="merge-icon"></i><span id="merge-text">{{ _('Merge & Slice') }}</span></button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <!-- End of accordion wrapper -->
<div class="mt-auto pt-3 border-top d-flex flex-column gap-2 mb-1">
<button class="btn btn-outline-danger w-100" onclick="clearPlate()"><i class="bi bi-trash me-2"></i>{{ _('Clear Board') }}</button>
<button class="btn btn-primary w-100 py-2 fs-5 shadow-sm" onclick="mergeAndSlice()" id="btn-merge"><i class="bi bi-gear-fill me-2" id="merge-icon"></i><span id="merge-text">{{ _('Merge & Slice') }}</span></button>
</div>
</div> </div>
</div> </div>
@@ -764,7 +766,7 @@ function loadSTL(fileId, url, name, status, matrixData, callback) {
}, undefined, function (error) { }, undefined, function (error) {
console.error(error); console.error(error);
if (callback) callback(); if (callback) callback();
alert("{{ _('Error loading STL model file.') }}"); window.customAlert("{{ _('Error loading STL model file.') }}");
}); });
} }
@@ -819,12 +821,12 @@ function mergeAndSlice() {
selectModels([]); // Detach any active model to bake transformProxy world coordinates into its local matrix properties selectModels([]); // Detach any active model to bake transformProxy world coordinates into its local matrix properties
if (loadedModels.length === 0) { if (loadedModels.length === 0) {
alert("{{ _('Please add at least one model to the build plate.') }}"); window.customAlert("{{ _('Please add at least one model to the build plate.') }}");
return; return;
} }
if (checkBounds()) { if (checkBounds()) {
alert("{{ _('One or more models are outside the print area. Please adjust them before slicing.') }}"); window.customAlert("{{ _('One or more models are outside the print area. Please adjust them before slicing.') }}");
return; return;
} }
@@ -842,17 +844,18 @@ function mergeAndSlice() {
if (isEdit) { if (isEdit) {
// Just checking if we want to warn // Just checking if we want to warn
if (loadedModels.length === 1 && loadedModels[0].userData.status === 'sliced') { if (loadedModels.length === 1 && loadedModels[0].userData.status === 'sliced') {
if (!confirm("{{ _('This model has already been sliced. The existing GCode will be overwritten. Continue?') }}")) { window.customConfirm("{{ _('This model has already been sliced. The existing GCode will be overwritten. Continue?') }}", doMergeAndSlice);
return; return;
}
} else if (window.isCompositeEdit) { } else if (window.isCompositeEdit) {
if (!confirm("{{ _('You are editing a composite model. The existing composite will be updated and re-sliced. Continue?') }}")) { window.customConfirm("{{ _('You are editing a composite model. The existing composite will be updated and re-sliced. Continue?') }}", doMergeAndSlice);
return; return;
}
} }
} }
const pieces = loadedModels.map(m => { doMergeAndSlice();
function doMergeAndSlice() {
const pieces = loadedModels.map(m => {
m.updateMatrixWorld(true); m.updateMatrixWorld(true);
const mat = m.matrixWorld.clone(); const mat = m.matrixWorld.clone();
if (m.userData.geomTrans) { if (m.userData.geomTrans) {
@@ -893,18 +896,19 @@ function mergeAndSlice() {
if(data.success) { if(data.success) {
window.location.href = "{{ url_for('main.files') }}"; window.location.href = "{{ url_for('main.files') }}";
} else { } else {
alert("{{ _('Error:') }} " + data.error); window.customAlert("{{ _('Error:') }} " + data.error);
btn.disabled = false; btn.disabled = false;
icon.className = 'bi bi-gear-fill me-2'; icon.className = 'bi bi-gear-fill me-2';
text.innerText = '{{ _("Merge & Slice") }}'; text.innerText = '{{ _("Merge & Slice") }}';
} }
}) })
.catch(err => { .catch(err => {
alert("{{ _('Error:') }} " + String(err)); window.customAlert("{{ _('Error:') }} " + String(err));
btn.disabled = false; btn.disabled = false;
icon.className = 'bi bi-gear-fill me-2'; icon.className = 'bi bi-gear-fill me-2';
text.innerText = '{{ _("Merge & Slice") }}'; text.innerText = '{{ _("Merge & Slice") }}';
}); });
}
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {

View File

@@ -1,14 +1,21 @@
from huey import SqliteHuey from huey import SqliteHuey
import subprocess import subprocess
import os import os
from .models import db, PrintFile, SystemConfig from app.models import db, PrintFile, SystemConfig
from .conf_parse import ConfParse from app.utils.conf_parse import ConfParse
import json import json
import uuid import uuid
import configparser import configparser
huey = SqliteHuey(filename='huey_queue.db') import os
# Ensure instance directory exists
instance_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), '..', 'instance')
os.makedirs(instance_dir, exist_ok=True)
huey_db_path = os.path.join(instance_dir, 'huey_queue.db')
huey = SqliteHuey(filename=huey_db_path)
@huey.task() @huey.task()
@@ -216,7 +223,7 @@ def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None,
from app import create_app from app import create_app
app = create_app() app = create_app()
with app.app_context(): with app.app_context():
from .models import PrintFile, db from app.models import PrintFile, db
print_file = PrintFile.query.get(file_id) print_file = PrintFile.query.get(file_id)
if not print_file: if not print_file:
return return
@@ -224,7 +231,7 @@ def merge_and_slice_task(file_id, inputs, merged_filepath, quality_preset=None,
db.session.remove() db.session.remove()
try: try:
from stl_merger import merge_stls from app.utils.stl_merger import merge_stls
merge_stls(inputs, merged_filepath) merge_stls(inputs, merged_filepath)
# Now trigger the regular slicing task # Now trigger the regular slicing task
@@ -244,9 +251,9 @@ def simplify_stl_task(file_id, filepath):
from app import create_app from app import create_app
app = create_app() app = create_app()
with app.app_context(): with app.app_context():
from .models import PrintFile, SystemConfig, db from app.models import PrintFile, SystemConfig, db
import os import os
from stl_simplifier import simplify_stl from app.utils.stl_simplifier import simplify_stl
print_file = PrintFile.query.get(file_id) print_file = PrintFile.query.get(file_id)
if not print_file: if not print_file:

178
patch_proxy.py Normal file
View File

@@ -0,0 +1,178 @@
import re
with open('app/routes/printer_routes.py', 'r', encoding='utf-8') as f:
content = f.read()
# Find everything between @sock.route('/proxy', bp=printer_bp) and def octo_proxy(path):
start_str = "@sock.route('/proxy', bp=printer_bp)"
end_str = "@printer_bp.route('/proxy', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])"
pre_content = content[:content.find(start_str)]
post_content = content[content.find(end_str):]
new_proxy = """@printer_bp.route('/proxy', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@printer_bp.route('/proxy/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
@login_required
def octo_proxy(path):
if not current_user.is_admin:
return "Unauthorized", 403
url_config = SystemConfig.query.filter_by(key='octoprint_url').first()
if not url_config or not url_config.value:
return "OctoPrint URL not configured", 404
base_url = url_config.value.rstrip('/')
# --- WebSocket Proxy Logic ---
if request.headers.get('Upgrade', '').lower() == 'websocket':
from flask_sock import Server, ConnectionClosed
# Check if environment supports WebSockets
try:
ws = Server(request.environ)
except Exception as e:
return "WebSocket Upgrade Failed", 400
def handle_ws():
if base_url.startswith('https://'):
ws_base = base_url.replace('https://', 'wss://', 1)
else:
ws_base = base_url.replace('http://', 'ws://', 1)
target_url = f"{ws_base}/{path}"
if request.query_string:
target_url = f"{target_url}?{request.query_string.decode('utf-8')}"
# Forward essential headers like NGINX Proxy
headers = {
'Host': request.host,
'X-Real-IP': request.remote_addr,
}
forwarded_for = request.headers.get('X-Forwarded-For', '')
headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
headers['X-Forwarded-Proto'] = request.scheme
headers['X-Forwarded-Host'] = request.host
headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
if request.headers.get('Cookie'):
headers['Cookie'] = request.headers.get('Cookie')
try:
remote_ws = ws_connect(target_url, additional_headers=headers)
except Exception as e:
ws.close(1011, str(e))
return
def recv_loop():
try:
for message in remote_ws:
ws.send(message)
except Exception:
pass
finally:
try: remote_ws.close()
except: pass
try: ws.close()
except: pass
t = threading.Thread(target=recv_loop)
t.daemon = True
t.start()
try:
while True:
data = ws.receive()
if data is None:
break
remote_ws.send(data)
except Exception:
pass
finally:
try: remote_ws.close()
except: pass
try: ws.close()
except: pass
try:
handle_ws()
except ConnectionClosed:
pass
except Exception:
pass
finally:
try: ws.close()
except: pass
class WebSocketResponse(Response):
def __call__(self, *args, **kwargs):
if getattr(ws, 'mode', 'werkzeug') == 'werkzeug':
return super().__call__(*args, **kwargs)
return []
return WebSocketResponse()
# --- Standard HTTP Proxy Logic ---
from urllib.parse import urlparse
target_url = f"{base_url}/{path}"
if request.query_string:
target_url = f"{target_url}?{request.query_string.decode('utf-8')}"
# Build headers for reverse proxy based on nginx config reference
parsed_base = urlparse(base_url)
headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'content-length']}
# NGINX equivalent proxy headers
headers['Host'] = request.host
headers['X-Real-IP'] = request.remote_addr
headers['X-Real-Port'] = str(request.environ.get('REMOTE_PORT', ''))
forwarded_for = request.headers.get('X-Forwarded-For', '')
headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
headers['X-Forwarded-Protocol'] = request.scheme
headers['X-Script-Name'] = "/printer/proxy"
headers['X-Forwarded-Host'] = request.host
headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
headers['REMOTE-HOST'] = request.remote_addr
if request.headers.get('Upgrade'):
headers['Upgrade'] = request.headers.get('Upgrade')
if request.headers.get('Connection'):
headers['Connection'] = request.headers.get('Connection')
try:
# proxy_connect_timeout 60s, proxy_read_timeout 600s
resp = requests.request(
method=request.method,
url=target_url,
headers=headers,
data=request.get_data(),
cookies=request.cookies,
allow_redirects=False,
stream=True,
timeout=(60, 600)
)
except requests.exceptions.RequestException as e:
return f"Proxy connection error: {str(e)}", 502
# Strip headers that might break the iframe or framing
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection', 'x-frame-options', 'content-security-policy']
response_headers = [(name, value) for (name, value) in resp.headers.items() if name.lower() not in excluded_headers]
def generate():
for chunk in resp.iter_content(chunk_size=8192):
if chunk:
yield chunk
return Response(generate(), resp.status_code, response_headers)
"""
post_end_str = " return Response(generate(), resp.status_code, response_headers)"
post_end_idx = post_content.find(post_end_str) + len(post_end_str)
final_content = pre_content + new_proxy + post_content[post_end_idx:]
with open('app/routes/printer_routes.py', 'w', encoding='utf-8') as f:
f.write(final_content)
print("Patched!")

45
patch_ws.py Normal file
View File

@@ -0,0 +1,45 @@
import re
with open('app/routes/printer_routes.py', 'r', encoding='utf-8') as f:
content = f.read()
# Replace the WS Proxy header logic
target_str = """ ws_headers['Host'] = request.host
ws_headers['X-Real-IP'] = request.remote_addr
forwarded_for = request.headers.get('X-Forwarded-For', '')
ws_headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
ws_headers['X-Forwarded-Proto'] = request.scheme
ws_headers['X-Forwarded-Host'] = request.host
ws_headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))"""
replacement_str = """ ws_headers['Host'] = request.host
ws_headers['X-Real-IP'] = request.remote_addr
forwarded_for = request.headers.get('X-Forwarded-For', '')
ws_headers['X-Forwarded-For'] = f"{forwarded_for}, {request.remote_addr}" if forwarded_for else request.remote_addr
ws_headers['X-Forwarded-Proto'] = request.scheme
ws_headers['X-Forwarded-Host'] = request.host
ws_headers['X-Forwarded-Port'] = str(request.environ.get('SERVER_PORT', '80'))
ws_headers['X-Script-Name'] = '/printer/proxy'"""
new_content = content.replace(target_str, replacement_str)
origin_str = """ # Mask Origin/Referer to bypass Octoprint CSRF if needed
if 'Origin' in ws_headers:
ws_headers['Origin'] = base_url
if 'Referer' in ws_headers:
ws_headers['Referer'] = f"{base_url}/{path}\""""
replacement_origin = """ # Match Tornado's expectations for Origin to avoid 400 Bad Request
parsed_base = urlparse(base_url)
ws_headers['Host'] = parsed_base.netloc
if 'Origin' in ws_headers:
ws_headers['Origin'] = base_url
if 'Referer' in ws_headers:
ws_headers['Referer'] = f"{base_url}/{path}\""""
new_content = new_content.replace(origin_str, replacement_origin)
with open('app/routes/printer_routes.py', 'w', encoding='utf-8') as f:
f.write(new_content)
print('Patched!')

View File

@@ -1,8 +1,9 @@
Flask==3.0.2 Flask>=3.0.0
Flask-SQLAlchemy==3.1.1 Flask-SQLAlchemy>=3.1.0
Flask-Login==0.6.3 Flask-Login>=0.6.0
Flask-Babel==4.0.0 Flask-Babel>=4.0.0
huey==2.5.0 huey>=2.5.0
Werkzeug==3.0.1 Werkzeug>=3.0.0
requests trimesh>=4.0.0
httpx requests>=2.31.0
httpx>=0.25.0

View File

@@ -1,4 +1,4 @@
from app.tasks import huey from app.utils.tasks import huey
from app import create_app from app import create_app
app = create_app() app = create_app()

2
test_server.py Normal file
View File

@@ -0,0 +1,2 @@
import flask_sock
print(dir(flask_sock))

3
test_sock.py Normal file
View File

@@ -0,0 +1,3 @@
from flask_sock import Sock
import inspect
print(inspect.getsource(Sock.__init__))

3
test_sock2.py Normal file
View File

@@ -0,0 +1,3 @@
from flask_sock import Sock
import inspect
print(inspect.getsource(Sock.route))

3
test_sock3.py Normal file
View File

@@ -0,0 +1,3 @@
from flask_sock import Sock
import inspect
print(inspect.getsource(Sock.init_app))

3
test_sock4.py Normal file
View File

@@ -0,0 +1,3 @@
from flask_sock import Sock
import inspect
print(inspect.getsource(Sock.__init__))

3
test_sock5.py Normal file
View File

@@ -0,0 +1,3 @@
from flask_sock import Sock
import inspect
print(dir(Sock))

4
test_sock_init.py Normal file
View File

@@ -0,0 +1,4 @@
import inspect
import flask_sock
from flask_sock import Server
print(Server)