AstraOS Ver. 1.23:
All checks were successful
Deploy AstraOS Panel / deploy (push) Successful in 12s

Fix:
- ETS2 Download Timeout gefixt
This commit is contained in:
MrWhiff 2026-04-19 12:48:51 +02:00
parent fd977fcc01
commit ea64ca0dcd
2 changed files with 200 additions and 50 deletions

View file

@ -8,12 +8,17 @@
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">ETS2 DLCs</h2>
</header>
<div class="text-sm text-gray-400">
Große DLC-Pakete werden im Hintergrund als ZIP vorbereitet. Der Download steht erst bereit, wenn der Status <span class="text-white font-semibold">Bereit</span> ist.
</div>
<div class="bg-black/40 rounded-xl backdrop-blur-lg overflow-hidden {% if maintenance_active %}border border-yellow-500/30{% else %}border border-purple-500/20{% endif %}">
<table class="min-w-full divide-y {% if maintenance_active %}divide-yellow-500/30{% else %}divide-purple-500/20{% endif %}">
<thead class="bg-black/20">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">DLC Datum</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Erstellt von</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-300 uppercase tracking-wider">Aktionen</th>
</tr>
@ -23,12 +28,26 @@
<tr class="transition {% if maintenance_active %}hover:bg-yellow-500/5{% else %}hover:bg-purple-500/5{% endif %}">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-white">{{ package.name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ package.dlc_date_display }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">
<span class="px-2 py-1 rounded-full text-xs font-semibold
{% if package.archive_status == 'ready' %}bg-green-500/20 text-green-300
{% elif package.archive_status == 'building' or package.archive_status == 'pending' %}bg-yellow-500/20 text-yellow-300
{% else %}bg-red-500/20 text-red-300{% endif %}">
{{ package.archive_status_display }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">{{ package.created_by or 'N/A' }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right">
<div class="flex items-center justify-end gap-2">
{% if package.download_ready %}
<a href="{{ url_for('download_ets2_dlc_package', package_id=package._id) }}" class="px-3 py-2 text-sm font-semibold rounded-lg transition text-white {% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
Download
</a>
{% else %}
<span class="px-3 py-2 text-sm font-semibold rounded-lg bg-gray-700 text-gray-300 cursor-not-allowed">
Download in Vorbereitung
</span>
{% endif %}
{% if can_manage_ets2_dlcs %}
<form action="{{ url_for('delete_ets2_dlc_package', package_id=package._id) }}" method="POST" onsubmit="return confirm('Möchtest du dieses DLC-Paket wirklich löschen?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
@ -42,7 +61,7 @@
</tr>
{% else %}
<tr>
<td colspan="5" class="px-6 py-8 text-center text-gray-400">Noch keine DLC-Pakete vorhanden.</td>
<td colspan="6" class="px-6 py-8 text-center text-gray-400">Noch keine DLC-Pakete vorhanden.</td>
</tr>
{% endfor %}
</tbody>

View file

@ -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):