from flask import Flask, request, jsonify, Response import random import string import threading import time from collections import deque from datetime import datetime, timedelta import logging app = Flask(__name__) # Конфигурация TRANSFER_LIFETIME = timedelta(hours=1) # Время хранения завершенных передач CLEANUP_INTERVAL = 300 # Интервал очистки (5 минут) MAX_CHUNK_SIZE = 100 * 1024 * 1024 # 100MB KEEP_ALIVE_INTERVAL = 20 # Интервал keep-alive (секунды) TRANSFER_TIMEOUT = 3600 # Таймаут передачи (1 час) # Логирование logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Хранилище данных transfers = {} transfer_data = {} transfer_lock = threading.Lock() def cleanup_task(): """Фоновая задача для очистки старых передач""" while True: time.sleep(CLEANUP_INTERVAL) with transfer_lock: now = datetime.now() to_delete = [] for transfer_id, info in transfers.items(): if info['completed'] and (now - info['completed_time']) > TRANSFER_LIFETIME: to_delete.append(transfer_id) logger.info(f"Cleaning up transfer: {transfer_id}") for transfer_id in to_delete: del transfers[transfer_id] del transfer_data[transfer_id] # Запуск фоновой задачи threading.Thread(target=cleanup_task, daemon=True).start() def generate_short_id(length=8): """Генерация уникального ID для передачи""" while True: token = ''.join(random.choices(string.ascii_letters + string.digits, k=length)) if token not in transfers: return token @app.route('/create_transfer', methods=['POST']) def create_transfer(): """Создание новой передачи""" try: data = request.json if not data or 'filename' not in data: return jsonify({'error': 'Missing required fields'}), 400 transfer_id = generate_short_id() with transfer_lock: transfers[transfer_id] = { 'filename': data['filename'], 'filesize': data.get('filesize', 0), 'completed': False, 'created_time': datetime.now(), 'completed_time': None, 'last_activity': datetime.now() } transfer_data[transfer_id] = deque(maxlen=1000) logger.info(f"Created new transfer: {transfer_id}") return jsonify({'transfer_id': transfer_id}) except Exception as e: logger.error(f"Error creating transfer: {str(e)}") return jsonify({'error': str(e)}), 500 @app.route('/upload/', methods=['POST']) def upload_chunk(transfer_id): """Загрузка чанка данных""" try: if transfer_id not in transfers: return jsonify({'error': 'Invalid transfer ID'}), 404 chunk = request.data if len(chunk) > MAX_CHUNK_SIZE: return jsonify({'error': 'Chunk size too large'}), 413 with transfer_lock: if transfers[transfer_id]['completed']: return jsonify({'error': 'Transfer already completed'}), 400 transfer_data[transfer_id].append(chunk) transfers[transfer_id]['last_activity'] = datetime.now() if request.headers.get('X-Transfer-Complete') == 'true': transfers[transfer_id]['completed'] = True transfers[transfer_id]['completed_time'] = datetime.now() logger.info(f"Transfer completed: {transfer_id}") return jsonify({'status': 'chunk accepted'}) except Exception as e: logger.error(f"Error uploading chunk: {str(e)}") return jsonify({'error': str(e)}), 500 @app.route('/download/', methods=['GET']) def download_file(transfer_id): """Скачивание файла""" def generate(): try: index = 0 last_activity = time.time() while True: with transfer_lock: # Проверка таймаута if time.time() - last_activity > TRANSFER_TIMEOUT: logger.warning(f"Transfer timeout: {transfer_id}") break transfer = transfers.get(transfer_id) if not transfer: logger.warning(f"Transfer not found: {transfer_id}") break chunks = transfer_data.get(transfer_id, []) # Отправка доступных чанков while index < len(chunks): yield chunks[index] last_activity = time.time() index += 1 if transfer['completed']: logger.info(f"Transfer finished: {transfer_id}") break # Отправка keep-alive if time.time() - last_activity > KEEP_ALIVE_INTERVAL: yield b'\0' # Пустой чанк для поддержания соединения last_activity = time.time() time.sleep(0.5) except Exception as e: logger.error(f"Download error: {str(e)}") if transfer_id not in transfers: return jsonify({'error': 'Transfer not found'}), 404 transfer = transfers[transfer_id] return Response( generate(), mimetype='application/octet-stream', headers={ 'Content-Disposition': f'attachment; filename="{transfer["filename"]}"', 'Cache-Control': 'no-store', 'Transfer-Encoding': 'chunked' } ) if __name__ == '__main__': # Конфигурация сервера app.run( host='0.0.0.0', port=5000, threaded=True, debug=False )