File size: 10,086 Bytes
3dab6f8
3424ff7
 
 
ef59b82
3424ff7
 
 
 
 
56a75e2
3dab6f8
3424ff7
 
ef59b82
 
 
3424ff7
3dab6f8
3424ff7
 
 
 
 
 
3dab6f8
3424ff7
3dab6f8
 
 
3424ff7
 
 
 
 
 
 
 
 
 
 
 
 
 
3dab6f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56a75e2
3dab6f8
 
 
 
3424ff7
56a75e2
3424ff7
 
3dab6f8
 
3424ff7
3dab6f8
 
 
 
 
56a75e2
3dab6f8
56a75e2
3dab6f8
 
 
3424ff7
3dab6f8
 
 
3424ff7
3dab6f8
3424ff7
 
 
 
 
3dab6f8
3424ff7
3dab6f8
3424ff7
3dab6f8
 
 
 
 
 
 
 
3424ff7
3dab6f8
3424ff7
 
 
 
 
 
 
 
 
56a75e2
 
3424ff7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3dab6f8
3424ff7
 
 
3dab6f8
56a75e2
3424ff7
 
 
 
 
56a75e2
3424ff7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef59b82
3424ff7
 
3dab6f8
3424ff7
56a75e2
3424ff7
 
 
56a75e2
3dab6f8
 
3424ff7
 
 
ef59b82
 
 
56a75e2
ef59b82
 
56a75e2
 
ef59b82
 
56a75e2
ef59b82
3424ff7
 
3dab6f8
 
 
 
 
 
 
 
3424ff7
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# 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)