@@ -27,6 +27,23 @@ async def init_db():
2727 created_at REAL NOT NULL
2828 )
2929 """ )
30+ # projects table: persist project metadata + file contents across restarts
31+ await _db .execute ("""
32+ CREATE TABLE IF NOT EXISTS projects (
33+ id TEXT PRIMARY KEY,
34+ data TEXT NOT NULL,
35+ created_at REAL NOT NULL
36+ )
37+ """ )
38+ # rate limiting: daily LLM call count per IP
39+ await _db .execute ("""
40+ CREATE TABLE IF NOT EXISTS rate_limits (
41+ ip TEXT NOT NULL,
42+ date TEXT NOT NULL,
43+ count INTEGER NOT NULL DEFAULT 0,
44+ PRIMARY KEY (ip, date)
45+ )
46+ """ )
3047 await _db .commit ()
3148
3249
@@ -60,3 +77,68 @@ async def put(key: str, value: dict | list):
6077 (key , data , time .time ()),
6178 )
6279 await _db .commit ()
80+
81+
82+ async def save_project (project_id : str , data : dict ):
83+ """Persist project data (metadata + file contents) to SQLite."""
84+ if _db is None :
85+ return
86+ payload = json .dumps (data , ensure_ascii = False )
87+ await _db .execute (
88+ "INSERT OR REPLACE INTO projects (id, data, created_at) VALUES (?, ?, ?)" ,
89+ (project_id , payload , time .time ()),
90+ )
91+ await _db .commit ()
92+
93+
94+ _DAILY_LIMIT = 20
95+
96+
97+ async def check_rate_limit (ip : str ) -> tuple [bool , int ]:
98+ """Check if IP is within daily free limit.
99+
100+ Returns (allowed, remaining).
101+ """
102+ if _db is None :
103+ return True , _DAILY_LIMIT
104+
105+ today = time .strftime ("%Y-%m-%d" )
106+ async with _db .execute (
107+ "SELECT count FROM rate_limits WHERE ip = ? AND date = ?" , (ip , today )
108+ ) as cursor :
109+ row = await cursor .fetchone ()
110+
111+ current = row [0 ] if row else 0
112+ remaining = max (0 , _DAILY_LIMIT - current )
113+ return current < _DAILY_LIMIT , remaining
114+
115+
116+ async def increment_rate_limit (ip : str ):
117+ """Bump the daily usage counter for an IP."""
118+ if _db is None :
119+ return
120+ today = time .strftime ("%Y-%m-%d" )
121+ await _db .execute (
122+ """INSERT INTO rate_limits (ip, date, count) VALUES (?, ?, 1)
123+ ON CONFLICT (ip, date) DO UPDATE SET count = count + 1""" ,
124+ (ip , today ),
125+ )
126+ await _db .commit ()
127+
128+
129+ async def load_project (project_id : str ) -> dict | None :
130+ """Load project data from SQLite. Returns None if not found or expired."""
131+ if _db is None :
132+ return None
133+ async with _db .execute (
134+ "SELECT data, created_at FROM projects WHERE id = ?" , (project_id ,)
135+ ) as cursor :
136+ row = await cursor .fetchone ()
137+ if row is None :
138+ return None
139+ data , created_at = row
140+ if time .time () - created_at > _TTL_SECONDS :
141+ await _db .execute ("DELETE FROM projects WHERE id = ?" , (project_id ,))
142+ await _db .commit ()
143+ return None
144+ return json .loads (data )
0 commit comments