"""Client download management - upload builds, serve downloads.""" import os from pathlib import Path from flask import request, jsonify, send_from_directory, current_app from app.api import api_bp # Supported platforms and their file extensions PLATFORMS = { 'linux': {'name': 'Linux', 'icon': 'pi-desktop', 'extensions': ['.AppImage', '.deb', '']}, 'windows': {'name': 'Windows', 'icon': 'pi-microsoft', 'extensions': ['.msi', '.exe']}, 'mac': {'name': 'macOS', 'icon': 'pi-apple', 'extensions': ['.dmg']}, 'android': {'name': 'Android', 'icon': 'pi-android', 'extensions': ['.apk']}, 'ios': {'name': 'iOS', 'icon': 'pi-apple', 'extensions': ['.ipa']}, } def _clients_dir(): """Get the client downloads directory.""" base = Path(current_app.config.get('UPLOAD_PATH', '/app/data/files')).parent d = base / 'client-downloads' d.mkdir(parents=True, exist_ok=True) return d def _verify_build_token(): """Verify the build upload token from header or query param.""" expected = os.environ.get('BUILD_UPLOAD_TOKEN', '') if not expected: return False token = request.headers.get('X-Build-Token', '') or request.args.get('build_token', '') return token == expected # --- Public: list available clients --- @api_bp.route('/clients', methods=['GET']) def list_clients(): """List available client downloads (public, no auth needed).""" clients_dir = _clients_dir() available = [] for platform, info in PLATFORMS.items(): platform_dir = clients_dir / platform if not platform_dir.exists(): continue files = sorted(platform_dir.iterdir(), key=lambda f: f.stat().st_mtime, reverse=True) if not files: continue # Take the newest file latest = files[0] available.append({ 'platform': platform, 'name': info['name'], 'icon': info['icon'], 'filename': latest.name, 'size': latest.stat().st_size, 'updated_at': latest.stat().st_mtime, 'download_url': f'/api/clients/{platform}/download', }) return jsonify({ 'clients': available, 'has_clients': len(available) > 0, }), 200 @api_bp.route('/clients//download', methods=['GET']) def download_client(platform): """Download the latest client for a platform (public, no auth).""" if platform not in PLATFORMS: return jsonify({'error': 'Unbekannte Plattform'}), 404 clients_dir = _clients_dir() platform_dir = clients_dir / platform if not platform_dir.exists(): return jsonify({'error': 'Kein Client fuer diese Plattform verfuegbar'}), 404 files = sorted(platform_dir.iterdir(), key=lambda f: f.stat().st_mtime, reverse=True) if not files: return jsonify({'error': 'Kein Client verfuegbar'}), 404 latest = files[0] return send_from_directory(str(platform_dir), latest.name, as_attachment=True) # --- Build upload (authenticated with BUILD_UPLOAD_TOKEN) --- @api_bp.route('/clients//upload', methods=['POST']) def upload_client(platform): """Upload a new client build. Authenticated with BUILD_UPLOAD_TOKEN.""" if not _verify_build_token(): return jsonify({'error': 'Ungueltiger Build-Token'}), 403 if platform not in PLATFORMS: return jsonify({'error': 'Unbekannte Plattform'}), 404 if 'file' not in request.files: return jsonify({'error': 'Keine Datei gesendet'}), 400 upload = request.files['file'] if not upload.filename: return jsonify({'error': 'Leerer Dateiname'}), 400 clients_dir = _clients_dir() platform_dir = clients_dir / platform platform_dir.mkdir(parents=True, exist_ok=True) # Remove old files for this platform (keep only latest) for old_file in platform_dir.iterdir(): old_file.unlink() dest = platform_dir / upload.filename upload.save(str(dest)) return jsonify({ 'message': f'{PLATFORMS[platform]["name"]} Client hochgeladen', 'filename': upload.filename, 'size': dest.stat().st_size, }), 200