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 loggingfrom flask import Flask, request, jsonify, Response import random import string import threading import time from datetime import datetime, timedelta import logging from gevent import sleep app = Flask(__name__) # Конфигурация TRANSFER_LIFETIME = timedelta(hours=6) CLEANUP_INTERVAL = 300 MAX_CHUNK_SIZE = 100 * 1024 * 1024 KEEP_ALIVE_INTERVAL = 25 TRANSFER_TIMEOUT = 3600 * 3 BUFFER_FLUSH_SIZE = 100 * 1024 * 1024 # 10MB # Логирование logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('FileTransfer') # Хранилище данных transfers = {} transfer_data = {} transfer_lock = threading.Lock() def cleanup_task(): """Фоновая очистка старых передач""" while True: time.sleep(CLEANUP_INTERVAL) with transfer_lock: now = datetime.now() to_delete = [ tid for tid, info in transfers.items() if info['completed'] and (now - info['completed_time']) > TRANSFER_LIFETIME ] for tid in to_delete: del transfers[tid] del transfer_data[tid] logger.info(f"Cleaned transfer: {tid}") threading.Thread(target=cleanup_task, daemon=True).start() @app.route('/download/', methods=['GET']) def download_file(transfer_id): """Потоковая отдача файла с оптимизацией буферизации""" def generate(): buffer = bytearray() last_flush = time.time() try: while True: with transfer_lock: transfer = transfers.get(transfer_id) if not transfer or (transfer['completed'] and not transfer_data.get(transfer_id)): break chunks = transfer_data.get(transfer_id, []) while chunks: buffer.extend(chunks.pop(0)) # Отправляем буфер при достижении размера или таймаута if (len(buffer) >= BUFFER_FLUSH_SIZE or (time.time() - last_flush) > KEEP_ALIVE_INTERVAL): yield bytes(buffer) buffer.clear() last_flush = time.time() # Асинхронное ожидание новых данных sleep(0.1) # Отправить оставшиеся данные if buffer: yield bytes(buffer) except Exception as e: logger.error(f"Download error: {transfer_id} - {str(e)}") finally: logger.info(f"Download finished: {transfer_id}") return Response( generate(), mimetype='application/octet-stream', headers={ 'Content-Disposition': f'attachment; filename="{transfers[transfer_id]["filename"]}"', 'Cache-Control': 'no-store' } ) if __name__ == '__main__': from gevent.pywsgi import WSGIServer logger.info("Starting optimized server...") http_server = WSGIServer(('0.0.0.0', 5000), app) http_server.serve_forever() 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 )