# app.py (نسخه نهایی، پایدار و بهینه شده) import os import json import time from flask import Flask, request, jsonify, render_template, Response, stream_with_context from flask_cors import CORS import logging import threading from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError import requests import tempfile # --- تنظیمات اصلی --- DATASET_REPO = "Ezmary/Karbaran-rayegan-tedad" DATASET_FILENAME = "image_usage_data.json" USAGE_LIMIT = 5 HF_TOKEN = os.environ.get("HF_TOKEN") TEMP_DIR = "/app/tmp" # <<< تغییر کلیدی: استفاده از پوشه موقت امن # --- راه‌اندازی Flask و لاگ‌ها --- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') app = Flask(__name__) CORS(app) # --- مدیریت داده‌های کاربران و قفل‌ها --- usage_data_cache = [] cache_lock = threading.Lock() # قفل برای دسترسی به حافظه کش data_changed = threading.Event() # برای اطلاع از وجود تغییرات برای ذخیره persistence_lock = threading.Lock() # <<< تغییر کلیدی: قفل جدید برای اتمی کردن عملیات ذخیره‌سازی >>> api = None if not HF_TOKEN: logging.error("CRITICAL: Secret 'HF_TOKEN' not found. Cannot access the private dataset.") else: api = HfApi(token=HF_TOKEN) logging.info("HfApi initialized successfully.") def load_initial_data(): global usage_data_cache with cache_lock: if not api: return try: logging.info(f"Attempting to load data from '{DATASET_REPO}/{DATASET_FILENAME}'...") # <<< تغییر کلیدی: دانلود در پوشه موقت برای جلوگیری از مشکلات دسترسی >>> with tempfile.TemporaryDirectory(dir=TEMP_DIR) as tmp_download_dir: local_path = hf_hub_download( repo_id=DATASET_REPO, filename=DATASET_FILENAME, repo_type="dataset", token=HF_TOKEN, force_download=True, cache_dir=tmp_download_dir ) with open(local_path, 'r', encoding='utf-8') as f: content = f.read() data_from_hub = json.loads(content) if content else [] logging.info(f"Loaded {len(data_from_hub)} records from {DATASET_FILENAME}.") # --- حذف خودکار رکوردهای قدیمی‌تر از ۶ ماه --- now = time.time() six_months_ago = now - (6 * 30 * 24 * 60 * 60) cleaned_data = [user for user in data_from_hub if user.get('last_seen', 0) > six_months_ago] pruned_count = len(data_from_hub) - len(cleaned_data) if pruned_count > 0: logging.info(f"Pruned {pruned_count} user records older than 6 months.") data_changed.set() # <<< تغییر کلیدی: فقط فلگ را ست می‌کنیم، ترد پس‌زمینه ذخیره می‌کند >>> usage_data_cache = cleaned_data except json.JSONDecodeError: # <<< تغییر کلیدی: مدیریت فایل خراب برای جلوگیری از کرش و صفحه سفید >>> logging.error(f"CRITICAL: Failed to decode JSON from '{DATASET_FILENAME}'. The file is likely corrupted. Starting fresh.") usage_data_cache = [] except (RepositoryNotFoundError, EntryNotFoundError): logging.warning(f"Dataset file '{DATASET_FILENAME}' not found. A new one will be created.") usage_data_cache = [] except Exception as e: logging.error(f"Failed to load initial data: {e}", exc_info=True) usage_data_cache = [] def persist_data_to_hub(): # <<< تغییر کلیدی: این تابع اکنون کاملاً Thread-Safe و امن است >>> with persistence_lock: if not data_changed.is_set() or not api: return with cache_lock: data_to_write = list(usage_data_cache) data_changed.clear() temp_filepath = None try: with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=TEMP_DIR, delete=False, suffix='.json') as temp_f: temp_filepath = temp_f.name json.dump(data_to_write, temp_f, ensure_ascii=False, indent=2) logging.info(f"Change detected, persisting {len(data_to_write)} records to Hub...") api.upload_file( path_or_fileobj=temp_filepath, path_in_repo=DATASET_FILENAME, repo_id=DATASET_REPO, repo_type="dataset", commit_message="Update image editor usage data [automated]" ) logging.info(f"Successfully persisted data to Hub.") except Exception as e: logging.error(f"CRITICAL: Failed to persist data to Hub: {e}", exc_info=True) data_changed.set() # اگر آپلود ناموفق بود، فلگ را دوباره ست می‌کنیم finally: if temp_filepath and os.path.exists(temp_filepath): os.remove(temp_filepath) def background_persister(): # <<< تغییر کلیدی: این ترد اصلی برای ذخیره‌سازی است و هر ۳۰ ثانیه اجرا می‌شود >>> while True: time.sleep(30) persist_data_to_hub() # --- روت‌های API --- @app.route('/') def index(): return render_template('index.html') def get_user_identifier(data): fingerprint = data.get('fingerprint') if fingerprint: return str(fingerprint) if request.headers.getlist("X-Forwarded-For"): return request.headers.getlist("X-Forwarded-For")[0].split(',')[0].strip() return request.remote_addr @app.route('/api/check-credit', methods=['POST']) def check_credit(): data = request.get_json() if not data: return jsonify({"error": "Invalid request"}), 400 user_id = get_user_identifier(data) if not user_id: return jsonify({"error": "User identifier is required."}), 400 with cache_lock: now = time.time() one_week_seconds = 7 * 24 * 60 * 60 user_record = next((user for user in usage_data_cache if user.get('id') == user_id), None) credits_remaining = USAGE_LIMIT limit_reached = False reset_timestamp = 0 if user_record: user_record['last_seen'] = now # آپدیت زمان آخرین فعالیت if user_record.get('week_start', 0) < (now - one_week_seconds): user_record['count'] = 0 user_record['week_start'] = now data_changed.set() # اگر هفته ریست شد، باید ذخیره شود credits_remaining = max(0, USAGE_LIMIT - user_record.get('count', 0)) if credits_remaining == 0: limit_reached = True reset_timestamp = user_record.get('week_start', now) + one_week_seconds return jsonify({"credits_remaining": credits_remaining, "limit_reached": limit_reached, "reset_timestamp": reset_timestamp}) @app.route('/api/use-credit', methods=['POST']) def use_credit(): data = request.get_json() if not data: return jsonify({"error": "Invalid request"}), 400 user_id = get_user_identifier(data) if not user_id: return jsonify({"error": "User identifier is required."}), 400 with cache_lock: now = time.time() one_week_seconds = 7 * 24 * 60 * 60 user_record = next((user for user in usage_data_cache if user.get('id') == user_id), None) if user_record: if user_record.get('week_start', 0) < (now - one_week_seconds): user_record['count'] = 0 user_record['week_start'] = now if user_record.get('count', 0) >= USAGE_LIMIT: reset_timestamp = user_record.get('week_start', now) + one_week_seconds return jsonify({"status": "limit_reached", "credits_remaining": 0, "reset_timestamp": reset_timestamp}), 429 user_record['count'] += 1 user_record['last_seen'] = now else: user_record = {"id": user_id, "count": 1, "week_start": now, "last_seen": now} usage_data_cache.append(user_record) credits_remaining = USAGE_LIMIT - user_record['count'] # <<< تغییر کلیدی: به جای ایجاد ترد جدید، فقط فلگ تغییرات را فعال می‌کنیم >>> data_changed.set() return jsonify({"status": "success", "credits_remaining": credits_remaining}) @app.route('/api/proxy') def proxy_image(): image_url = request.args.get('url') if not image_url: return "URL parameter is missing.", 400 try: req = requests.get(image_url, stream=True, timeout=20) req.raise_for_status() return Response(stream_with_context(req.iter_content(chunk_size=4096)), content_type=req.headers.get('content-type', 'image/png')) except requests.exceptions.RequestException as e: logging.error(f"Could not proxy image from {image_url}: {e}") return f"Failed to retrieve image.", 502 # --- اجرای برنامه --- if __name__ != '__main__': # <<< تغییر کلیدی: افزایش پایداری در استارت‌آپ >>> try: load_initial_data() persister_thread = threading.Thread(target=background_persister, daemon=True) persister_thread.start() logging.info("Application startup complete.") except Exception as e: logging.critical(f"A critical error occurred during application startup: {e}", exc_info=True) if __name__ == '__main__': port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port)