smodusermc commited on
Commit
e117f1c
Β·
verified Β·
1 Parent(s): d20b876

Update storage_handler.py

Browse files
Files changed (1) hide show
  1. storage_handler.py +115 -59
storage_handler.py CHANGED
@@ -5,6 +5,7 @@ import uuid
5
  import logging
6
  from typing import Optional, BinaryIO, Dict, Any, List
7
  import time
 
8
 
9
  from huggingface_hub import HfFileSystem, HfApi
10
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
@@ -64,7 +65,6 @@ _fs = HfFileSystem(token=HF_TOKEN)
64
  def ensure_bucket():
65
  """Create the private bucket if it doesn't exist."""
66
  global OWNER, BUCKET_URI, BUCKET_ID
67
-
68
  try:
69
  _api.create_bucket(
70
  bucket_id=f"{OWNER}/{BUCKET_ID}",
@@ -76,9 +76,8 @@ def ensure_bucket():
76
  error_str = str(e).lower()
77
  if "already exists" in error_str:
78
  logger.info(f"πŸ“¦ Bucket '{OWNER}/{BUCKET_ID}' already exists")
79
- elif "http" in error_str and ("403" in error_str or "401" in error_str):
80
  logger.warning(f"⚠️ Cannot create bucket - permission issue: {e}")
81
- logger.info("The bucket may already exist or you don't have permissions")
82
  else:
83
  try:
84
  _api.create_bucket(
@@ -95,31 +94,29 @@ def ensure_bucket():
95
  logger.info(f"πŸ“¦ Bucket '{BUCKET_ID}' already exists")
96
  else:
97
  logger.error(f"❌ Failed to create bucket: {e2}")
98
- logger.info("The app will still work but file uploads will fail")
99
 
100
  ensure_bucket()
101
 
102
- # storage_handler.py – add after ensure_bucket() call at bottom of file
103
- import threading
104
-
105
  def _sync_bucket_interval():
106
- """
107
- Periodic sync task: runs every 60 seconds.
108
- Uses HfFileSystem which already writes directly to the bucket,
109
- so this function logs stats and verifies the connection is alive.
110
- """
111
  def _sync():
112
  while True:
113
  time.sleep(60)
114
  try:
115
  stats = get_storage_stats()
116
- logger.info(f"πŸ“¦ Bucket sync heartbeat β€” {stats['file_count']} files / {stats['total_size_mb']} MB")
 
 
 
117
  except Exception as e:
118
- logger.warning(f"⚠️ Bucket sync heartbeat failed: {e}")
119
  t = threading.Thread(target=_sync, daemon=True)
120
  t.start()
121
 
122
  _sync_bucket_interval()
 
123
  # ------------------------------------------------------------------------
124
  # Encryption / Decryption
125
  # ------------------------------------------------------------------------
@@ -141,9 +138,6 @@ def verify_file_integrity(encrypted_blob: bytes) -> bool:
141
  # ------------------------------------------------------------------------
142
  # Path Utilities
143
  # ------------------------------------------------------------------------
144
- def _build_path(*parts: str) -> str:
145
- return "/".join(p.strip("/") for p in parts if p)
146
-
147
  def _bucket_path(remote_path: str) -> str:
148
  return f"{BUCKET_URI}/{remote_path.lstrip('/')}"
149
 
@@ -163,19 +157,16 @@ def _validate_path(remote_path: str) -> None:
163
  def store_file(remote_path: str, data: bytes, encrypt: bool = True) -> str:
164
  _validate_path(remote_path)
165
  try:
166
- if encrypt:
167
- encrypted_data = encrypt_bytes(data, remote_path.encode('utf-8'))
168
- else:
169
- encrypted_data = data
170
  full_uri = _bucket_path(remote_path)
171
  with _fs.open(full_uri, "wb") as f:
172
- f.write(encrypted_data)
173
  if not _fs.exists(full_uri):
174
  raise IOError(f"Failed to verify file was stored: {full_uri}")
175
- logger.debug(f"πŸ’Ύ Stored file: {remote_path} ({len(data)} bytes)")
176
  return remote_path
177
  except Exception as e:
178
- logger.error(f"❌ Failed to store file {remote_path}: {e}")
179
  raise IOError(f"Storage failed: {e}")
180
 
181
  def retrieve_file(remote_path: str, decrypt: bool = True) -> bytes:
@@ -185,21 +176,20 @@ def retrieve_file(remote_path: str, decrypt: bool = True) -> bytes:
185
  if not _fs.exists(full_uri):
186
  raise FileNotFoundError(f"File not found: {remote_path}")
187
  with _fs.open(full_uri, "rb") as f:
188
- encrypted_data = f.read()
189
- if not encrypted_data:
190
  raise IOError(f"Empty file: {remote_path}")
191
  if decrypt:
192
- if not verify_file_integrity(encrypted_data):
193
  raise IOError(f"Corrupted file: {remote_path}")
194
- decrypted_data = decrypt_bytes(encrypted_data, remote_path.encode('utf-8'))
195
- logger.debug(f"πŸ“‚ Retrieved file: {remote_path} ({len(decrypted_data)} bytes)")
196
- return decrypted_data
197
- else:
198
- return encrypted_data
199
  except FileNotFoundError:
200
  raise
201
  except Exception as e:
202
- logger.error(f"❌ Failed to retrieve file {remote_path}: {e}")
203
  raise IOError(f"Retrieval failed: {e}")
204
 
205
  def delete_file(remote_path: str) -> bool:
@@ -208,26 +198,25 @@ def delete_file(remote_path: str) -> bool:
208
  try:
209
  if _fs.exists(full_uri):
210
  _fs.rm(full_uri)
211
- logger.debug(f"πŸ—‘οΈ Deleted file: {remote_path}")
212
  return True
213
- else:
214
- logger.warning(f"⚠️ File not found for deletion: {remote_path}")
215
- return False
216
  except Exception as e:
217
- logger.error(f"❌ Failed to delete file {remote_path}: {e}")
218
  raise IOError(f"Deletion failed: {e}")
219
 
220
  def file_exists(remote_path: str) -> bool:
221
  _validate_path(remote_path)
222
- return _fs.exists(_bucket_path(remote_path))
 
 
 
223
 
224
  def list_files(prefix: str = "", recursive: bool = True) -> List[str]:
225
  search_path = _bucket_path(prefix) if prefix else BUCKET_URI
226
  try:
227
- if recursive:
228
- items = _fs.ls(search_path, detail=False, recursive=True)
229
- else:
230
- items = _fs.ls(search_path, detail=False)
231
  prefix_len = len(BUCKET_URI) + 1
232
  return [item[prefix_len:] for item in items if not _fs.isdir(item)]
233
  except FileNotFoundError:
@@ -241,10 +230,7 @@ def get_file_size(remote_path: str) -> Optional[int]:
241
  try:
242
  info = _fs.info(_bucket_path(remote_path))
243
  return info.get("size")
244
- except FileNotFoundError:
245
- return None
246
- except Exception as e:
247
- logger.error(f"Failed to get file size: {e}")
248
  return None
249
 
250
  def get_file_info(remote_path: str) -> Optional[Dict[str, Any]]:
@@ -265,13 +251,11 @@ def get_file_info(remote_path: str) -> Optional[Dict[str, Any]]:
265
  return None
266
 
267
  def store_file_stream(remote_path: str, data_stream: BinaryIO, encrypt: bool = True) -> str:
268
- data = data_stream.read()
269
- return store_file(remote_path, data, encrypt=encrypt)
270
 
271
  def retrieve_file_stream(remote_path: str, decrypt: bool = True) -> BinaryIO:
272
- data = retrieve_file(remote_path, decrypt=decrypt)
273
  import io
274
- return io.BytesIO(data)
275
 
276
  def get_storage_stats() -> Dict[str, Any]:
277
  try:
@@ -307,8 +291,7 @@ def create_backup(backup_prefix: str = "backups") -> str:
307
  for file_path in files:
308
  try:
309
  data = retrieve_file(file_path)
310
- backup_file_path = f"{backup_path}/{file_path}"
311
- store_file(backup_file_path, data, encrypt=True)
312
  except Exception as e:
313
  logger.error(f"Failed to backup {file_path}: {e}")
314
  logger.info(f"πŸ’Ύ Backup created at: {backup_path}")
@@ -317,16 +300,89 @@ def create_backup(backup_prefix: str = "backups") -> str:
317
  logger.error(f"❌ Backup failed: {e}")
318
  raise
319
 
320
- def close():
321
- if _fs:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  try:
323
- _fs.close()
324
- except:
325
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
 
 
 
327
  __all__ = [
328
  'store_file', 'retrieve_file', 'delete_file', 'file_exists',
329
  'list_files', 'get_file_size', 'get_file_info',
330
  'store_file_stream', 'retrieve_file_stream',
331
- 'get_storage_stats', 'create_backup', 'close'
 
 
332
  ]
 
5
  import logging
6
  from typing import Optional, BinaryIO, Dict, Any, List
7
  import time
8
+ import threading
9
 
10
  from huggingface_hub import HfFileSystem, HfApi
11
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
 
65
  def ensure_bucket():
66
  """Create the private bucket if it doesn't exist."""
67
  global OWNER, BUCKET_URI, BUCKET_ID
 
68
  try:
69
  _api.create_bucket(
70
  bucket_id=f"{OWNER}/{BUCKET_ID}",
 
76
  error_str = str(e).lower()
77
  if "already exists" in error_str:
78
  logger.info(f"πŸ“¦ Bucket '{OWNER}/{BUCKET_ID}' already exists")
79
+ elif "403" in error_str or "401" in error_str:
80
  logger.warning(f"⚠️ Cannot create bucket - permission issue: {e}")
 
81
  else:
82
  try:
83
  _api.create_bucket(
 
94
  logger.info(f"πŸ“¦ Bucket '{BUCKET_ID}' already exists")
95
  else:
96
  logger.error(f"❌ Failed to create bucket: {e2}")
 
97
 
98
  ensure_bucket()
99
 
100
+ # ------------------------------------------------------------------------
101
+ # Heartbeat sync
102
+ # ------------------------------------------------------------------------
103
  def _sync_bucket_interval():
 
 
 
 
 
104
  def _sync():
105
  while True:
106
  time.sleep(60)
107
  try:
108
  stats = get_storage_stats()
109
+ logger.info(
110
+ f"πŸ“¦ Bucket heartbeat β€” "
111
+ f"{stats['file_count']} files / {stats['total_size_mb']} MB"
112
+ )
113
  except Exception as e:
114
+ logger.warning(f"⚠️ Bucket heartbeat failed: {e}")
115
  t = threading.Thread(target=_sync, daemon=True)
116
  t.start()
117
 
118
  _sync_bucket_interval()
119
+
120
  # ------------------------------------------------------------------------
121
  # Encryption / Decryption
122
  # ------------------------------------------------------------------------
 
138
  # ------------------------------------------------------------------------
139
  # Path Utilities
140
  # ------------------------------------------------------------------------
 
 
 
141
  def _bucket_path(remote_path: str) -> str:
142
  return f"{BUCKET_URI}/{remote_path.lstrip('/')}"
143
 
 
157
  def store_file(remote_path: str, data: bytes, encrypt: bool = True) -> str:
158
  _validate_path(remote_path)
159
  try:
160
+ payload = encrypt_bytes(data, remote_path.encode('utf-8')) if encrypt else data
 
 
 
161
  full_uri = _bucket_path(remote_path)
162
  with _fs.open(full_uri, "wb") as f:
163
+ f.write(payload)
164
  if not _fs.exists(full_uri):
165
  raise IOError(f"Failed to verify file was stored: {full_uri}")
166
+ logger.debug(f"πŸ’Ύ Stored: {remote_path} ({len(data)} bytes)")
167
  return remote_path
168
  except Exception as e:
169
+ logger.error(f"❌ Failed to store {remote_path}: {e}")
170
  raise IOError(f"Storage failed: {e}")
171
 
172
  def retrieve_file(remote_path: str, decrypt: bool = True) -> bytes:
 
176
  if not _fs.exists(full_uri):
177
  raise FileNotFoundError(f"File not found: {remote_path}")
178
  with _fs.open(full_uri, "rb") as f:
179
+ payload = f.read()
180
+ if not payload:
181
  raise IOError(f"Empty file: {remote_path}")
182
  if decrypt:
183
+ if not verify_file_integrity(payload):
184
  raise IOError(f"Corrupted file: {remote_path}")
185
+ data = decrypt_bytes(payload, remote_path.encode('utf-8'))
186
+ logger.debug(f"πŸ“‚ Retrieved: {remote_path} ({len(data)} bytes)")
187
+ return data
188
+ return payload
 
189
  except FileNotFoundError:
190
  raise
191
  except Exception as e:
192
+ logger.error(f"❌ Failed to retrieve {remote_path}: {e}")
193
  raise IOError(f"Retrieval failed: {e}")
194
 
195
  def delete_file(remote_path: str) -> bool:
 
198
  try:
199
  if _fs.exists(full_uri):
200
  _fs.rm(full_uri)
201
+ logger.debug(f"πŸ—‘οΈ Deleted: {remote_path}")
202
  return True
203
+ logger.warning(f"⚠️ Not found for deletion: {remote_path}")
204
+ return False
 
205
  except Exception as e:
206
+ logger.error(f"❌ Failed to delete {remote_path}: {e}")
207
  raise IOError(f"Deletion failed: {e}")
208
 
209
  def file_exists(remote_path: str) -> bool:
210
  _validate_path(remote_path)
211
+ try:
212
+ return _fs.exists(_bucket_path(remote_path))
213
+ except Exception:
214
+ return False
215
 
216
  def list_files(prefix: str = "", recursive: bool = True) -> List[str]:
217
  search_path = _bucket_path(prefix) if prefix else BUCKET_URI
218
  try:
219
+ items = _fs.ls(search_path, detail=False, recursive=recursive)
 
 
 
220
  prefix_len = len(BUCKET_URI) + 1
221
  return [item[prefix_len:] for item in items if not _fs.isdir(item)]
222
  except FileNotFoundError:
 
230
  try:
231
  info = _fs.info(_bucket_path(remote_path))
232
  return info.get("size")
233
+ except Exception:
 
 
 
234
  return None
235
 
236
  def get_file_info(remote_path: str) -> Optional[Dict[str, Any]]:
 
251
  return None
252
 
253
  def store_file_stream(remote_path: str, data_stream: BinaryIO, encrypt: bool = True) -> str:
254
+ return store_file(remote_path, data_stream.read(), encrypt=encrypt)
 
255
 
256
  def retrieve_file_stream(remote_path: str, decrypt: bool = True) -> BinaryIO:
 
257
  import io
258
+ return io.BytesIO(retrieve_file(remote_path, decrypt=decrypt))
259
 
260
  def get_storage_stats() -> Dict[str, Any]:
261
  try:
 
291
  for file_path in files:
292
  try:
293
  data = retrieve_file(file_path)
294
+ store_file(f"{backup_path}/{file_path}", data, encrypt=True)
 
295
  except Exception as e:
296
  logger.error(f"Failed to backup {file_path}: {e}")
297
  logger.info(f"πŸ’Ύ Backup created at: {backup_path}")
 
300
  logger.error(f"❌ Backup failed: {e}")
301
  raise
302
 
303
+ # ------------------------------------------------------------------------
304
+ # Database Sync to/from Bucket
305
+ # ------------------------------------------------------------------------
306
+ DB_BUCKET_PATH = "database/infinitychat.db"
307
+ _db_sync_lock = threading.Lock()
308
+ _last_db_sync = 0
309
+ DB_SYNC_INTERVAL = 30 # seconds
310
+
311
+ def download_database(local_path: str) -> bool:
312
+ """
313
+ Download database from bucket to local path.
314
+ Returns True if database was found and downloaded.
315
+ """
316
+ try:
317
+ if file_exists(DB_BUCKET_PATH):
318
+ logger.info("πŸ“₯ Downloading database from bucket...")
319
+ data = retrieve_file(DB_BUCKET_PATH)
320
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
321
+ with open(local_path, 'wb') as f:
322
+ f.write(data)
323
+ logger.info(f"βœ… Database downloaded ({len(data):,} bytes)")
324
+ return True
325
+ else:
326
+ logger.info("πŸ“­ No existing database in bucket - will create fresh")
327
+ return False
328
+ except Exception as e:
329
+ logger.error(f"❌ Failed to download database: {e}")
330
+ return False
331
+
332
+ def upload_database(local_path: str) -> bool:
333
+ """
334
+ Upload local database file to bucket.
335
+ Returns True on success.
336
+ """
337
+ global _last_db_sync
338
+ with _db_sync_lock:
339
  try:
340
+ if not os.path.exists(local_path):
341
+ logger.warning(f"⚠️ Database not found at {local_path}, skipping upload")
342
+ return False
343
+ with open(local_path, 'rb') as f:
344
+ data = f.read()
345
+ if not data:
346
+ logger.warning("⚠️ Database file is empty, skipping upload")
347
+ return False
348
+ store_file(DB_BUCKET_PATH, data)
349
+ _last_db_sync = time.time()
350
+ logger.debug(f"☁️ Database synced to bucket ({len(data):,} bytes)")
351
+ return True
352
+ except Exception as e:
353
+ logger.error(f"❌ Failed to upload database: {e}")
354
+ return False
355
+
356
+ def start_db_sync(local_path: str):
357
+ """
358
+ Start background thread that periodically uploads the database to bucket.
359
+ """
360
+ def _sync_loop():
361
+ while True:
362
+ time.sleep(DB_SYNC_INTERVAL)
363
+ try:
364
+ upload_database(local_path)
365
+ except Exception as e:
366
+ logger.warning(f"⚠️ Periodic DB sync failed: {e}")
367
+
368
+ t = threading.Thread(target=_sync_loop, daemon=True)
369
+ t.start()
370
+ logger.info(f"πŸ”„ Database auto-sync started (every {DB_SYNC_INTERVAL}s)")
371
+
372
+ def close():
373
+ try:
374
+ _fs.close()
375
+ except Exception:
376
+ pass
377
 
378
+ # ------------------------------------------------------------------------
379
+ # Public API
380
+ # ------------------------------------------------------------------------
381
  __all__ = [
382
  'store_file', 'retrieve_file', 'delete_file', 'file_exists',
383
  'list_files', 'get_file_size', 'get_file_info',
384
  'store_file_stream', 'retrieve_file_stream',
385
+ 'get_storage_stats', 'create_backup', 'close',
386
+ 'download_database', 'upload_database', 'start_db_sync',
387
+ 'DB_BUCKET_PATH'
388
  ]