66import requests as http_requests
77import logging
88from logging .handlers import RotatingFileHandler
9+ from redis import Redis
910from fetcher import GradeFetcher
10- import time
11- import threading
12- import secrets
13- import string
14- SHARED_FOLDER = 'shared_grades'
15- CLEANUP_INTERVAL = 600 # 10 minutes
16- FILE_LIFETIME = 7200 # 2 hours
11+ from app .services .share_service import ShareService , is_valid_share_id
12+ from app .services .share_store_redis import RedisShareStore
13+
14+ SHARE_TTL_SECONDS = int (os .environ .get ('SHARE_TTL_SECONDS' , '7200' ))
15+ REDIS_URL = os .environ .get ('REDIS_URL' , 'redis://redis:6379/0' )
1716
1817# Configure logging
1918logging .basicConfig (level = logging .INFO )
@@ -82,35 +81,8 @@ def verify_turnstile(token, remote_ip, context=''):
8281 return True , None
8382
8483
85- if not os .path .exists (SHARED_FOLDER ):
86- os .makedirs (SHARED_FOLDER )
87-
88- def cleanup_thread ():
89- """Background thread to clean up old shared files."""
90- while True :
91- try :
92- now = time .time ()
93- for filename in os .listdir (SHARED_FOLDER ):
94- file_path = os .path .join (SHARED_FOLDER , filename )
95- if os .path .isfile (file_path ):
96- if now - os .path .getmtime (file_path ) > FILE_LIFETIME :
97- try :
98- os .remove (file_path )
99- logger .info (f"Deleted expired file: { filename } " )
100- except Exception as e :
101- logger .error (f"Error deleting file { filename } : { e } " )
102- except Exception as e :
103- logger .error (f"Error in cleanup thread: { e } " )
104- time .sleep (CLEANUP_INTERVAL )
105-
106- # Start cleanup thread
107- # In debug mode, Flask's reloader spawns two processes. We only start the thread
108- # in the child (WERKZEUG_RUN_MAIN='true') to avoid running it twice.
109- # In production (gunicorn), WERKZEUG_RUN_MAIN is not set, so it always starts.
110- _is_reloader_parent = app .debug and os .environ .get ('WERKZEUG_RUN_MAIN' ) != 'true'
111- if not _is_reloader_parent :
112- threading .Thread (target = cleanup_thread , daemon = True ).start ()
113- logger .info ("Cleanup thread started" )
84+ redis_client = Redis .from_url (REDIS_URL )
85+ share_service = ShareService (store = RedisShareStore (redis_client ), ttl_seconds = SHARE_TTL_SECONDS )
11486
11587@app .route ('/' )
11688def index ():
@@ -276,15 +248,7 @@ def create_share_link():
276248 if not ok :
277249 return err
278250
279- # Generate 15-char random URL-safe ID with special chars
280- # Using A-Z, a-z, 0-9, -, _, ., ~
281- chars = string .ascii_letters + string .digits + "-_.~"
282- share_id = '' .join (secrets .choice (chars ) for _ in range (15 ))
283-
284- file_path = os .path .join (SHARED_FOLDER , f"{ share_id } .json" )
285-
286- with open (file_path , 'w' , encoding = 'utf-8' ) as f :
287- json .dump (data , f , ensure_ascii = False )
251+ share_id = share_service .create_share (data )
288252
289253 return jsonify ({'success' : True , 'id' : share_id })
290254 except Exception as e :
@@ -294,17 +258,13 @@ def create_share_link():
294258@app .route ('/api/share/<share_id>' , methods = ['GET' ])
295259def get_shared_grades (share_id ):
296260 try :
297- # Basic validation for ID (alphanumeric + special chars)
298- if not all (c in string .ascii_letters + string .digits + "-_.~" for c in share_id ):
261+ if not is_valid_share_id (share_id ):
299262 return jsonify ({'error' : 'Invalid ID format' }), 400
300263
301- file_path = os . path . join ( SHARED_FOLDER , f" { share_id } .json" )
302- if not os . path . exists ( file_path ) :
264+ data = share_service . get_share ( share_id )
265+ if data is None :
303266 return jsonify ({'error' : 'Link expired or not found' }), 404
304267
305- with open (file_path , 'r' , encoding = 'utf-8' ) as f :
306- data = json .load (f )
307-
308268 return jsonify ({'success' : True , 'data' : data })
309269 except Exception as e :
310270 logger .error (f"Error reading share: { e } " )
0 commit comments