diff --git a/.gitignore b/.gitignore index d93d7e3..1d81534 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__/ *.py[cod] *$py.class +# Command storage (temporary files) +command_storage/*.json + # Virtual environments .venv/ venv/ diff --git a/app.py b/app.py index fd92385..5f494e2 100644 --- a/app.py +++ b/app.py @@ -8,18 +8,22 @@ from flask import Flask, render_template, jsonify, send_from_directory, request app = Flask(__name__) -# In-memory storage for commands (thread-safe) -command_storage = {} +# File-based storage for commands (works across multiple workers) +STORAGE_DIR = Path(__file__).parent / 'command_storage' +STORAGE_DIR.mkdir(exist_ok=True) storage_lock = Lock() def cleanup_expired_commands(): - """Remove expired commands from storage""" - with storage_lock: - now = datetime.now(timezone.utc) - expired_keys = [key for key, value in command_storage.items() - if datetime.fromisoformat(value['expires_at']) < now] - for key in expired_keys: - del command_storage[key] + """Remove expired command files""" + now = datetime.now(timezone.utc) + for file_path in STORAGE_DIR.glob('*.json'): + try: + with open(file_path, 'r') as f: + data = json.load(f) + if datetime.fromisoformat(data['expires_at']) < now: + file_path.unlink() + except: + pass # Ignore corrupted files # Load projects from version.json def load_projects(): @@ -272,13 +276,18 @@ def store_command(): created_at = datetime.now(timezone.utc) expires_at = created_at + timedelta(minutes=30) - # Store command + # Prepare data + command_data = { + 'command': command, + 'created_at': created_at.isoformat(), + 'expires_at': expires_at.isoformat() + } + + # Store to file + storage_file = STORAGE_DIR / f'{command_uuid}.json' with storage_lock: - command_storage[command_uuid] = { - 'command': command, - 'created_at': created_at.isoformat(), - 'expires_at': expires_at.isoformat() - } + with open(storage_file, 'w', encoding='utf-8') as f: + json.dump(command_data, f) # Generate URL - use HTTPS if behind proxy scheme = request.headers.get('X-Forwarded-Proto', 'https' if request.is_secure else 'http') @@ -300,20 +309,23 @@ def retrieve_command(command_uuid): """Retrieve stored command by UUID (JSON response for plugin)""" cleanup_expired_commands() - # Keep lock for entire operation to prevent race conditions - with storage_lock: - command_data = command_storage.get(command_uuid) - - if not command_data: - return jsonify({'error': 'Command not found or expired'}), 404 + storage_file = STORAGE_DIR / f'{command_uuid}.json' + + if not storage_file.exists(): + return jsonify({'error': 'Command not found or expired'}), 404 + + try: + with open(storage_file, 'r', encoding='utf-8') as f: + command_data = json.load(f) # Check if expired if datetime.fromisoformat(command_data['expires_at']) < datetime.now(timezone.utc): - del command_storage[command_uuid] + storage_file.unlink() return jsonify({'error': 'Command expired'}), 410 - # Return a copy to avoid modification outside lock - return jsonify(dict(command_data)), 200 + return jsonify(command_data), 200 + except Exception as e: + return jsonify({'error': 'Failed to read command'}), 500 # Serve /versions as JSON @app.route('/versions') diff --git a/command_storage/.gitignore b/command_storage/.gitignore new file mode 100644 index 0000000..14ef347 --- /dev/null +++ b/command_storage/.gitignore @@ -0,0 +1,3 @@ +# This directory stores temporary command data +# Files are automatically deleted after 30 minutes +*.json