imageediting / app.py
Ezmary's picture
Update app.py
3dab6f8 verified
# 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)