diff --git a/AstraOS/Frontend/HTML/ets2_dlcs.html b/AstraOS/Frontend/HTML/ets2_dlcs.html index 9b8eef8..9eb0409 100644 --- a/AstraOS/Frontend/HTML/ets2_dlcs.html +++ b/AstraOS/Frontend/HTML/ets2_dlcs.html @@ -8,12 +8,17 @@

ETS2 DLCs

+
+ Große DLC-Pakete werden im Hintergrund als ZIP vorbereitet. Der Download steht erst bereit, wenn der Status Bereit ist. +
+
+ @@ -23,12 +28,26 @@ + {% else %} - + {% endfor %} diff --git a/AstraOS/astra_os.py b/AstraOS/astra_os.py index 4cf2a69..34cc15a 100644 --- a/AstraOS/astra_os.py +++ b/AstraOS/astra_os.py @@ -7,7 +7,7 @@ from datetime import timedelta, datetime from functools import wraps from bson.objectid import ObjectId import paramiko -from flask import jsonify, send_file, after_this_request +from flask import jsonify, send_file import logging from flask_socketio import SocketIO, emit, join_room, leave_room import subprocess @@ -18,7 +18,6 @@ import ipaddress import socket import ssl import zipfile -import tempfile from dotenv import load_dotenv load_dotenv() async_mode = "eventlet" @@ -38,7 +37,11 @@ except Exception as e: os.makedirs(BOT_UPLOAD_FOLDER, exist_ok=True) print(f"WARNUNG: Konnte '/opt/astraos/Discord_Bots' nicht erstellen ({e}), benutze lokalen Ordner: {BOT_UPLOAD_FOLDER}") app.config['BOT_UPLOAD_FOLDER'] = BOT_UPLOAD_FOLDER +ETS2_DLC_ARCHIVE_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ets2_dlc_archives') +os.makedirs(ETS2_DLC_ARCHIVE_FOLDER, exist_ok=True) running_bots = {} +ets2_dlc_build_locks = {} +ets2_dlc_build_locks_guard = threading.Lock() class SuppressServerLoadFilter(logging.Filter): def filter(self, record): msg = record.getMessage() @@ -275,6 +278,156 @@ def add_log(action, details=None): } logs_collection.insert_one(log_entry) +def _ets2_archive_filename(package_id: str, package_name: str): + safe_name = secure_filename(package_name or 'ets2_dlc_package') or 'ets2_dlc_package' + return f'{package_id}_{safe_name}.zip' + + +def _ets2_archive_path(package_id: str, package_name: str): + return os.path.join(ETS2_DLC_ARCHIVE_FOLDER, _ets2_archive_filename(package_id, package_name)) + + +def _get_ets2_build_lock(package_id: str): + with ets2_dlc_build_locks_guard: + lock = ets2_dlc_build_locks.get(package_id) + if lock is None: + lock = threading.Lock() + ets2_dlc_build_locks[package_id] = lock + return lock + + +def _set_ets2_archive_state(package_id: str, **fields): + if ets2_dlcs_collection is None: + return + ets2_dlcs_collection.update_one({'_id': ObjectId(package_id)}, {'$set': fields}) + + +def _build_ets2_archive(package_id: str): + if ets2_dlcs_collection is None: + return + + package = ets2_dlcs_collection.find_one({'_id': ObjectId(package_id)}) + if not package: + return + + folder_path = package.get('folder_path') + if not folder_path or not os.path.isdir(folder_path): + _set_ets2_archive_state( + package_id, + archive_status='error', + archive_error='Ordnerpfad ist nicht mehr verfügbar.', + archive_generated_at=None, + archive_path=None + ) + return + + archive_path = _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) + temp_archive_path = f'{archive_path}.tmp' + + try: + if os.path.exists(temp_archive_path): + try: + os.remove(temp_archive_path) + except Exception: + pass + + _set_ets2_archive_state( + package_id, + archive_status='building', + archive_error=None, + archive_path=archive_path, + archive_generated_at=None + ) + + with zipfile.ZipFile(temp_archive_path, 'w', compression=zipfile.ZIP_STORED, allowZip64=True) as zipf: + has_content = False + for root, _, files in os.walk(folder_path): + for filename in files: + file_path = os.path.join(root, filename) + arcname = os.path.relpath(file_path, folder_path) + zipf.write(file_path, arcname, compress_type=zipfile.ZIP_STORED) + has_content = True + + if not has_content: + zipf.writestr('EMPTY_PACKAGE.txt', 'Dieses DLC-Paket enthaelt aktuell keine Dateien.') + + os.replace(temp_archive_path, archive_path) + _set_ets2_archive_state( + package_id, + archive_status='ready', + archive_error=None, + archive_path=archive_path, + archive_generated_at=datetime.utcnow() + ) + except Exception as e: + try: + if os.path.exists(temp_archive_path): + os.remove(temp_archive_path) + except Exception: + pass + _set_ets2_archive_state( + package_id, + archive_status='error', + archive_error=str(e), + archive_generated_at=None, + archive_path=archive_path + ) + + +def _schedule_ets2_archive_build(package_id: str): + lock = _get_ets2_build_lock(package_id) + if not lock.acquire(blocking=False): + return False + + def runner(): + try: + _build_ets2_archive(package_id) + finally: + try: + lock.release() + except Exception: + pass + with ets2_dlc_build_locks_guard: + ets2_dlc_build_locks.pop(package_id, None) + + thread = threading.Thread(target=runner, daemon=True) + thread.start() + return True + + +def _format_ets2_dlcs_for_view(packages): + for package in packages: + dlc_date = package.get('dlc_date') + try: + package['dlc_date_display'] = datetime.strptime(dlc_date, '%Y-%m-%d').strftime('%d.%m.%Y') if dlc_date else 'N/A' + except ValueError: + package['dlc_date_display'] = str(dlc_date) if dlc_date else 'N/A' + + package_id = str(package['_id']) + archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) + archive_ready = package.get('archive_status') == 'ready' and os.path.isfile(archive_path) + folder_path = package.get('folder_path') + + if not archive_ready and folder_path and os.path.isdir(folder_path): + package.setdefault('archive_status', 'pending') + package['download_ready'] = False + package['archive_status_display'] = 'Wird erstellt' + package['archive_path'] = archive_path + _schedule_ets2_archive_build(package_id) + elif archive_ready: + package['download_ready'] = True + package['archive_status_display'] = 'Bereit' + package['archive_path'] = archive_path + else: + package['download_ready'] = False + package['archive_status_display'] = 'Fehler' + package['archive_path'] = archive_path + + if package.get('archive_status') == 'error' and package.get('archive_error'): + package['archive_status_display'] = f"Fehler: {package.get('archive_error')}" + + return packages + def get_user_permissions(user_id): user = accounts_collection.find_one({'_id': ObjectId(user_id)}) if not user or 'role_id' not in user: @@ -997,13 +1150,7 @@ def ets2_dlcs(): flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('dashboard')) - packages = list(ets2_dlcs_collection.find().sort('created_at', -1)) - for package in packages: - dlc_date = package.get('dlc_date') - try: - package['dlc_date_display'] = datetime.strptime(dlc_date, '%Y-%m-%d').strftime('%d.%m.%Y') if dlc_date else 'N/A' - except ValueError: - package['dlc_date_display'] = str(dlc_date) if dlc_date else 'N/A' + packages = _format_ets2_dlcs_for_view(list(ets2_dlcs_collection.find().sort('created_at', -1))) return render_template( 'ets2_dlcs.html', active_page='ets2_dlcs', @@ -1043,15 +1190,20 @@ def create_ets2_dlc_package(): flash("Ein DLC-Paket mit diesem Namen existiert bereits.", "danger") return redirect(url_for('ets2_dlcs')) - ets2_dlcs_collection.insert_one({ + package_id = ets2_dlcs_collection.insert_one({ 'name': package_name, 'folder_path': folder_path, 'dlc_date': dlc_date, 'created_by': session.get('username'), - 'created_at': datetime.utcnow() - }) + 'created_at': datetime.utcnow(), + 'archive_status': 'pending', + 'archive_path': None, + 'archive_generated_at': None, + 'archive_error': None + }).inserted_id add_log("ETS2 DLC-Paket erstellt", f"Name: {package_name}, Pfad: {folder_path}") - flash("DLC-Paket wurde erfolgreich erstellt.", "success") + _schedule_ets2_archive_build(str(package_id)) + flash("DLC-Paket wurde erstellt. Das Archiv wird im Hintergrund vorbereitet.", "success") return redirect(url_for('ets2_dlcs')) @@ -1074,6 +1226,13 @@ def delete_ets2_dlc_package(package_id): flash("DLC-Paket nicht gefunden.", "danger") return redirect(url_for('ets2_dlcs')) + archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) + if package.get('archive_status') == 'ready' and os.path.isfile(archive_path): + try: + os.remove(archive_path) + except Exception: + pass + ets2_dlcs_collection.delete_one({'_id': package_object_id}) add_log("ETS2 DLC-Paket gelöscht", f"Name: {package.get('name', 'N/A')}") flash("DLC-Paket wurde gelöscht.", "success") @@ -1099,46 +1258,18 @@ def download_ets2_dlc_package(package_id): flash("DLC-Paket nicht gefunden.", "danger") return redirect(url_for('ets2_dlcs')) - folder_path = package.get('folder_path') - if not folder_path or not os.path.isdir(folder_path): - flash("Der gespeicherte Ordnerpfad ist ungültig oder nicht mehr vorhanden.", "danger") + archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) + if package.get('archive_status') != 'ready' or not os.path.isfile(archive_path): + if package.get('folder_path') and os.path.isdir(package.get('folder_path')): + _schedule_ets2_archive_build(package_id) + flash("Das DLC-Archiv wird noch erstellt. Bitte in ein paar Augenblicken erneut versuchen.", "info") + else: + flash("Das gespeicherte Archiv ist nicht verfügbar.", "danger") return redirect(url_for('ets2_dlcs')) - temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix='.zip') - temp_zip_path = temp_zip.name - temp_zip.close() - - try: - with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: - has_content = False - for root, _, files in os.walk(folder_path): - for filename in files: - has_content = True - file_path = os.path.join(root, filename) - arcname = os.path.relpath(file_path, folder_path) - zipf.write(file_path, arcname) - - if not has_content: - zipf.writestr('EMPTY_PACKAGE.txt', 'Dieses DLC-Paket enthaelt aktuell keine Dateien.') - except Exception as e: - try: - os.remove(temp_zip_path) - except Exception: - pass - flash(f"Fehler beim Erstellen des Downloads: {e}", "danger") - return redirect(url_for('ets2_dlcs')) - - @after_this_request - def cleanup_temp_file(response): - try: - os.remove(temp_zip_path) - except Exception: - pass - return response - safe_name = secure_filename(package.get('name', 'ets2_dlc_package')) or 'ets2_dlc_package' add_log("ETS2 DLC-Paket heruntergeladen", f"Name: {package.get('name', 'N/A')}") - return send_file(temp_zip_path, as_attachment=True, download_name=f"{safe_name}.zip") + return send_file(archive_path, as_attachment=True, download_name=f"{safe_name}.zip", conditional=True) def stream_bot_output(bot_id, process): def stream_to_socket(pipe, log_type):
Name DLC DatumStatus Erstellt von Aktionen
{{ package.name }} {{ package.dlc_date_display }} + + {{ package.archive_status_display }} + + {{ package.created_by or 'N/A' }}
+ {% if package.download_ready %} Download + {% else %} + + Download in Vorbereitung + + {% endif %} {% if can_manage_ets2_dlcs %}
@@ -42,7 +61,7 @@
Noch keine DLC-Pakete vorhanden.Noch keine DLC-Pakete vorhanden.