smodusermc commited on
Commit
acb50d4
·
verified ·
1 Parent(s): aefdaa1

Update storage_handler.py

Browse files
Files changed (1) hide show
  1. storage_handler.py +12 -224
storage_handler.py CHANGED
@@ -4,7 +4,6 @@ import hashlib
4
  import uuid
5
  import logging
6
  from typing import Optional, BinaryIO, Dict, Any, List
7
- from contextlib import contextmanager
8
  import time
9
 
10
  from huggingface_hub import HfFileSystem, HfApi
@@ -44,20 +43,8 @@ try:
44
  OWNER = owner_info["name"]
45
  except Exception as e:
46
  logger.warning(f"Could not determine HF username: {e}")
47
- # Try to extract from token or use a default
48
- try:
49
- # Token might be hf_xxxx format, extract first part
50
- token_parts = HF_TOKEN.split("_")
51
- if len(token_parts) >= 2:
52
- OWNER = token_parts[1][:10] # Use part of token as identifier
53
- else:
54
- OWNER = "infinitychat"
55
- except:
56
- OWNER = "infinitychat"
57
-
58
- logger.info(f"Using owner: {OWNER}")
59
 
60
- # Handle bucket name with or without owner prefix
61
  if "/" in BUCKET_NAME:
62
  OWNER, BUCKET_ID = BUCKET_NAME.split("/", 1)
63
  else:
@@ -99,7 +86,6 @@ def ensure_bucket():
99
  private=True,
100
  exist_ok=True
101
  )
102
- # now we can safely reassign globals
103
  OWNER = ""
104
  BUCKET_URI = f"hf://buckets/{BUCKET_ID}"
105
  logger.info(f"✅ Bucket '{BUCKET_ID}' is ready (alternative format)")
@@ -111,73 +97,36 @@ def ensure_bucket():
111
  logger.error(f"❌ Failed to create bucket: {e2}")
112
  logger.info("The app will still work but file uploads will fail")
113
 
114
- # Initialize on import
115
  ensure_bucket()
116
 
117
  # ------------------------------------------------------------------------
118
  # Encryption / Decryption
119
  # ------------------------------------------------------------------------
120
  def encrypt_bytes(data: bytes, aad: Optional[bytes] = None) -> bytes:
121
- """
122
- Encrypt binary data with AES-256-GCM.
123
-
124
- Args:
125
- data: Plaintext bytes to encrypt
126
- aad: Additional authenticated data (optional)
127
-
128
- Returns:
129
- nonce (12 bytes) + ciphertext + tag (16 bytes) = 12 + len(data) + 16
130
- """
131
  aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
132
  nonce = os.urandom(12)
133
  ciphertext = aesgcm.encrypt(nonce, data, aad or b"")
134
  return nonce + ciphertext
135
 
136
  def decrypt_bytes(encrypted_blob: bytes, aad: Optional[bytes] = None) -> bytes:
137
- """
138
- Decrypt data encrypted with encrypt_bytes().
139
-
140
- Args:
141
- encrypted_blob: nonce (12 bytes) + ciphertext + tag (16 bytes)
142
- aad: Additional authenticated data used during encryption
143
-
144
- Returns:
145
- Decrypted plaintext bytes
146
- """
147
  aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
148
  nonce = encrypted_blob[:12]
149
  ciphertext = encrypted_blob[12:]
150
  return aesgcm.decrypt(nonce, ciphertext, aad or b"")
151
 
152
  def verify_file_integrity(encrypted_blob: bytes) -> bool:
153
- """
154
- Verify that encrypted blob has valid format.
155
-
156
- Returns:
157
- True if format appears valid (nonce + ciphertext + tag)
158
- """
159
- if len(encrypted_blob) < 29: # 12 (nonce) + 1 (minimum data) + 16 (tag)
160
- return False
161
- return True
162
 
163
  # ------------------------------------------------------------------------
164
  # Path Utilities
165
  # ------------------------------------------------------------------------
166
  def _build_path(*parts: str) -> str:
167
- """Build a clean path by joining parts."""
168
  return "/".join(p.strip("/") for p in parts if p)
169
 
170
  def _bucket_path(remote_path: str) -> str:
171
- """Construct the full hf://buckets/... URI."""
172
  return f"{BUCKET_URI}/{remote_path.lstrip('/')}"
173
 
174
  def _validate_path(remote_path: str) -> None:
175
- """
176
- Validate that a path is safe to use.
177
-
178
- Raises:
179
- ValueError: If path contains dangerous patterns
180
- """
181
  if not remote_path:
182
  raise ValueError("Path cannot be empty")
183
  if ".." in remote_path.split("/"):
@@ -191,90 +140,41 @@ def _validate_path(remote_path: str) -> None:
191
  # Core File Operations
192
  # ------------------------------------------------------------------------
193
  def store_file(remote_path: str, data: bytes, encrypt: bool = True) -> str:
194
- """
195
- Store a file in the bucket with optional encryption.
196
-
197
- Args:
198
- remote_path: Path within bucket (e.g., "avatars/user123.jpg")
199
- data: File content as bytes
200
- encrypt: Whether to encrypt data before storing (default: True)
201
-
202
- Returns:
203
- The remote path for later retrieval
204
-
205
- Raises:
206
- ValueError: If path is invalid
207
- IOError: If storage fails
208
- """
209
  _validate_path(remote_path)
210
-
211
  try:
212
- # Encrypt if requested
213
  if encrypt:
214
  encrypted_data = encrypt_bytes(data, remote_path.encode('utf-8'))
215
  else:
216
  encrypted_data = data
217
-
218
- # Ensure parent directory exists (HfFileSystem handles this)
219
  full_uri = _bucket_path(remote_path)
220
-
221
- # Write to bucket
222
  with _fs.open(full_uri, "wb") as f:
223
  f.write(encrypted_data)
224
-
225
- # Verify file was written
226
  if not _fs.exists(full_uri):
227
  raise IOError(f"Failed to verify file was stored: {full_uri}")
228
-
229
  logger.debug(f"💾 Stored file: {remote_path} ({len(data)} bytes)")
230
  return remote_path
231
-
232
  except Exception as e:
233
  logger.error(f"❌ Failed to store file {remote_path}: {e}")
234
  raise IOError(f"Storage failed: {e}")
235
 
236
  def retrieve_file(remote_path: str, decrypt: bool = True) -> bytes:
237
- """
238
- Retrieve and optionally decrypt a file from the bucket.
239
-
240
- Args:
241
- remote_path: Path within bucket
242
- decrypt: Whether to decrypt data (default: True)
243
-
244
- Returns:
245
- File content as bytes
246
-
247
- Raises:
248
- FileNotFoundError: If file doesn't exist
249
- IOError: If retrieval fails
250
- """
251
  _validate_path(remote_path)
252
-
253
  full_uri = _bucket_path(remote_path)
254
-
255
  try:
256
- # Check if file exists
257
  if not _fs.exists(full_uri):
258
  raise FileNotFoundError(f"File not found: {remote_path}")
259
-
260
- # Read from bucket
261
  with _fs.open(full_uri, "rb") as f:
262
  encrypted_data = f.read()
263
-
264
  if not encrypted_data:
265
  raise IOError(f"Empty file: {remote_path}")
266
-
267
- # Decrypt if requested
268
  if decrypt:
269
  if not verify_file_integrity(encrypted_data):
270
  raise IOError(f"Corrupted file: {remote_path}")
271
-
272
  decrypted_data = decrypt_bytes(encrypted_data, remote_path.encode('utf-8'))
273
  logger.debug(f"📂 Retrieved file: {remote_path} ({len(decrypted_data)} bytes)")
274
  return decrypted_data
275
  else:
276
  return encrypted_data
277
-
278
  except FileNotFoundError:
279
  raise
280
  except Exception as e:
@@ -282,19 +182,8 @@ def retrieve_file(remote_path: str, decrypt: bool = True) -> bytes:
282
  raise IOError(f"Retrieval failed: {e}")
283
 
284
  def delete_file(remote_path: str) -> bool:
285
- """
286
- Delete a file from the bucket.
287
-
288
- Args:
289
- remote_path: Path within bucket
290
-
291
- Returns:
292
- True if file was deleted, False if it didn't exist
293
- """
294
  _validate_path(remote_path)
295
-
296
  full_uri = _bucket_path(remote_path)
297
-
298
  try:
299
  if _fs.exists(full_uri):
300
  _fs.rm(full_uri)
@@ -307,52 +196,27 @@ def delete_file(remote_path: str) -> bool:
307
  logger.error(f"❌ Failed to delete file {remote_path}: {e}")
308
  raise IOError(f"Deletion failed: {e}")
309
 
310
- # ------------------------------------------------------------------------
311
- # Bulk Operations
312
- # ------------------------------------------------------------------------
 
313
  def list_files(prefix: str = "", recursive: bool = True) -> List[str]:
314
- """
315
- List files in the bucket under a given prefix.
316
-
317
- Args:
318
- prefix: Optional path prefix to filter by
319
- recursive: Whether to list files recursively
320
-
321
- Returns:
322
- List of file paths (relative to bucket root)
323
- """
324
  search_path = _bucket_path(prefix) if prefix else BUCKET_URI
325
-
326
  try:
327
  if recursive:
328
  items = _fs.ls(search_path, detail=False, recursive=True)
329
  else:
330
  items = _fs.ls(search_path, detail=False)
331
-
332
- # Convert full URIs to relative paths
333
- prefix_len = len(BUCKET_URI) + 1 # +1 for trailing slash
334
  return [item[prefix_len:] for item in items if not _fs.isdir(item)]
335
-
336
  except FileNotFoundError:
337
  return []
338
  except Exception as e:
339
- logger.error(f"❌ Failed to list files: {e}")
340
  return []
341
 
342
- def file_exists(remote_path: str) -> bool:
343
- """Check if a file exists in the bucket."""
344
- _validate_path(remote_path)
345
- return _fs.exists(_bucket_path(remote_path))
346
-
347
  def get_file_size(remote_path: str) -> Optional[int]:
348
- """
349
- Get the size of a file in bytes.
350
-
351
- Returns:
352
- File size in bytes, or None if file doesn't exist
353
- """
354
  _validate_path(remote_path)
355
-
356
  try:
357
  info = _fs.info(_bucket_path(remote_path))
358
  return info.get("size")
@@ -363,14 +227,7 @@ def get_file_size(remote_path: str) -> Optional[int]:
363
  return None
364
 
365
  def get_file_info(remote_path: str) -> Optional[Dict[str, Any]]:
366
- """
367
- Get detailed information about a file.
368
-
369
- Returns:
370
- Dict with file metadata, or None if file doesn't exist
371
- """
372
  _validate_path(remote_path)
373
-
374
  try:
375
  info = _fs.info(_bucket_path(remote_path))
376
  return {
@@ -386,60 +243,25 @@ def get_file_info(remote_path: str) -> Optional[Dict[str, Any]]:
386
  logger.error(f"Failed to get file info: {e}")
387
  return None
388
 
389
- # ------------------------------------------------------------------------
390
- # Stream-based Operations
391
- # ------------------------------------------------------------------------
392
  def store_file_stream(remote_path: str, data_stream: BinaryIO, encrypt: bool = True) -> str:
393
- """
394
- Store file from a binary stream.
395
-
396
- Args:
397
- remote_path: Path within bucket
398
- data_stream: Binary stream to read from
399
- encrypt: Whether to encrypt data
400
-
401
- Returns:
402
- The remote path
403
- """
404
  data = data_stream.read()
405
  return store_file(remote_path, data, encrypt=encrypt)
406
 
407
  def retrieve_file_stream(remote_path: str, decrypt: bool = True) -> BinaryIO:
408
- """
409
- Retrieve file as a binary stream.
410
-
411
- Args:
412
- remote_path: Path within bucket
413
- decrypt: Whether to decrypt data
414
-
415
- Returns:
416
- BinaryIO object with file content
417
- """
418
  data = retrieve_file(remote_path, decrypt=decrypt)
419
  import io
420
  return io.BytesIO(data)
421
 
422
- # ------------------------------------------------------------------------
423
- # Storage Statistics
424
- # ------------------------------------------------------------------------
425
  def get_storage_stats() -> Dict[str, Any]:
426
- """
427
- Get storage statistics.
428
-
429
- Returns:
430
- Dict with storage information
431
- """
432
  try:
433
  files = list_files()
434
  total_size = 0
435
  file_count = 0
436
-
437
  for f in files:
438
  size = get_file_size(f)
439
  if size is not None:
440
  total_size += size
441
  file_count += 1
442
-
443
  return {
444
  "bucket": f"{OWNER}/{BUCKET_ID}",
445
  "file_count": file_count,
@@ -456,22 +278,9 @@ def get_storage_stats() -> Dict[str, Any]:
456
  "error": str(e)
457
  }
458
 
459
- # ------------------------------------------------------------------------
460
- # Backup and Maintenance
461
- # ------------------------------------------------------------------------
462
  def create_backup(backup_prefix: str = "backups") -> str:
463
- """
464
- Create a backup of all files in the bucket.
465
-
466
- Args:
467
- backup_prefix: Path prefix for backup location
468
-
469
- Returns:
470
- Backup path
471
- """
472
  timestamp = int(time.time())
473
  backup_path = f"{backup_prefix}/backup_{timestamp}"
474
-
475
  try:
476
  files = list_files()
477
  for file_path in files:
@@ -481,43 +290,22 @@ def create_backup(backup_prefix: str = "backups") -> str:
481
  store_file(backup_file_path, data, encrypt=True)
482
  except Exception as e:
483
  logger.error(f"Failed to backup {file_path}: {e}")
484
-
485
  logger.info(f"💾 Backup created at: {backup_path}")
486
  return backup_path
487
  except Exception as e:
488
  logger.error(f"❌ Backup failed: {e}")
489
  raise
490
 
491
- # ------------------------------------------------------------------------
492
- # Cleanup
493
- # ------------------------------------------------------------------------
494
  def close():
495
- """Clean up resources."""
496
  if _fs:
497
  try:
498
  _fs.close()
499
  except:
500
  pass
501
 
502
- # ------------------------------------------------------------------------
503
- # Export public API
504
- # ------------------------------------------------------------------------
505
  __all__ = [
506
- 'store_file',
507
- 'retrieve_file',
508
- 'delete_file',
509
- 'list_files',
510
- 'file_exists',
511
- 'get_file_size',
512
- 'get_file_info',
513
- 'store_file_stream',
514
- 'retrieve_file_stream',
515
- 'get_storage_stats',
516
- 'create_backup',
517
- 'encrypt_bytes',
518
- 'decrypt_bytes',
519
- 'close',
520
- 'OWNER',
521
- 'BUCKET_ID',
522
- 'BUCKET_URI'
523
  ]
 
4
  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
 
43
  OWNER = owner_info["name"]
44
  except Exception as e:
45
  logger.warning(f"Could not determine HF username: {e}")
46
+ OWNER = "infinitychat"
 
 
 
 
 
 
 
 
 
 
 
47
 
 
48
  if "/" in BUCKET_NAME:
49
  OWNER, BUCKET_ID = BUCKET_NAME.split("/", 1)
50
  else:
 
86
  private=True,
87
  exist_ok=True
88
  )
 
89
  OWNER = ""
90
  BUCKET_URI = f"hf://buckets/{BUCKET_ID}"
91
  logger.info(f"✅ Bucket '{BUCKET_ID}' is ready (alternative format)")
 
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
  # ------------------------------------------------------------------------
103
  # Encryption / Decryption
104
  # ------------------------------------------------------------------------
105
  def encrypt_bytes(data: bytes, aad: Optional[bytes] = None) -> bytes:
 
 
 
 
 
 
 
 
 
 
106
  aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
107
  nonce = os.urandom(12)
108
  ciphertext = aesgcm.encrypt(nonce, data, aad or b"")
109
  return nonce + ciphertext
110
 
111
  def decrypt_bytes(encrypted_blob: bytes, aad: Optional[bytes] = None) -> bytes:
 
 
 
 
 
 
 
 
 
 
112
  aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
113
  nonce = encrypted_blob[:12]
114
  ciphertext = encrypted_blob[12:]
115
  return aesgcm.decrypt(nonce, ciphertext, aad or b"")
116
 
117
  def verify_file_integrity(encrypted_blob: bytes) -> bool:
118
+ return len(encrypted_blob) >= 29
 
 
 
 
 
 
 
 
119
 
120
  # ------------------------------------------------------------------------
121
  # Path Utilities
122
  # ------------------------------------------------------------------------
123
  def _build_path(*parts: str) -> str:
 
124
  return "/".join(p.strip("/") for p in parts if p)
125
 
126
  def _bucket_path(remote_path: str) -> str:
 
127
  return f"{BUCKET_URI}/{remote_path.lstrip('/')}"
128
 
129
  def _validate_path(remote_path: str) -> None:
 
 
 
 
 
 
130
  if not remote_path:
131
  raise ValueError("Path cannot be empty")
132
  if ".." in remote_path.split("/"):
 
140
  # Core File Operations
141
  # ------------------------------------------------------------------------
142
  def store_file(remote_path: str, data: bytes, encrypt: bool = True) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  _validate_path(remote_path)
 
144
  try:
 
145
  if encrypt:
146
  encrypted_data = encrypt_bytes(data, remote_path.encode('utf-8'))
147
  else:
148
  encrypted_data = data
 
 
149
  full_uri = _bucket_path(remote_path)
 
 
150
  with _fs.open(full_uri, "wb") as f:
151
  f.write(encrypted_data)
 
 
152
  if not _fs.exists(full_uri):
153
  raise IOError(f"Failed to verify file was stored: {full_uri}")
 
154
  logger.debug(f"💾 Stored file: {remote_path} ({len(data)} bytes)")
155
  return remote_path
 
156
  except Exception as e:
157
  logger.error(f"❌ Failed to store file {remote_path}: {e}")
158
  raise IOError(f"Storage failed: {e}")
159
 
160
  def retrieve_file(remote_path: str, decrypt: bool = True) -> bytes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  _validate_path(remote_path)
 
162
  full_uri = _bucket_path(remote_path)
 
163
  try:
 
164
  if not _fs.exists(full_uri):
165
  raise FileNotFoundError(f"File not found: {remote_path}")
 
 
166
  with _fs.open(full_uri, "rb") as f:
167
  encrypted_data = f.read()
 
168
  if not encrypted_data:
169
  raise IOError(f"Empty file: {remote_path}")
 
 
170
  if decrypt:
171
  if not verify_file_integrity(encrypted_data):
172
  raise IOError(f"Corrupted file: {remote_path}")
 
173
  decrypted_data = decrypt_bytes(encrypted_data, remote_path.encode('utf-8'))
174
  logger.debug(f"📂 Retrieved file: {remote_path} ({len(decrypted_data)} bytes)")
175
  return decrypted_data
176
  else:
177
  return encrypted_data
 
178
  except FileNotFoundError:
179
  raise
180
  except Exception as e:
 
182
  raise IOError(f"Retrieval failed: {e}")
183
 
184
  def delete_file(remote_path: str) -> bool:
 
 
 
 
 
 
 
 
 
185
  _validate_path(remote_path)
 
186
  full_uri = _bucket_path(remote_path)
 
187
  try:
188
  if _fs.exists(full_uri):
189
  _fs.rm(full_uri)
 
196
  logger.error(f"❌ Failed to delete file {remote_path}: {e}")
197
  raise IOError(f"Deletion failed: {e}")
198
 
199
+ def file_exists(remote_path: str) -> bool:
200
+ _validate_path(remote_path)
201
+ return _fs.exists(_bucket_path(remote_path))
202
+
203
  def list_files(prefix: str = "", recursive: bool = True) -> List[str]:
 
 
 
 
 
 
 
 
 
 
204
  search_path = _bucket_path(prefix) if prefix else BUCKET_URI
 
205
  try:
206
  if recursive:
207
  items = _fs.ls(search_path, detail=False, recursive=True)
208
  else:
209
  items = _fs.ls(search_path, detail=False)
210
+ prefix_len = len(BUCKET_URI) + 1
 
 
211
  return [item[prefix_len:] for item in items if not _fs.isdir(item)]
 
212
  except FileNotFoundError:
213
  return []
214
  except Exception as e:
215
+ logger.error(f"Failed to list files: {e}")
216
  return []
217
 
 
 
 
 
 
218
  def get_file_size(remote_path: str) -> Optional[int]:
 
 
 
 
 
 
219
  _validate_path(remote_path)
 
220
  try:
221
  info = _fs.info(_bucket_path(remote_path))
222
  return info.get("size")
 
227
  return None
228
 
229
  def get_file_info(remote_path: str) -> Optional[Dict[str, Any]]:
 
 
 
 
 
 
230
  _validate_path(remote_path)
 
231
  try:
232
  info = _fs.info(_bucket_path(remote_path))
233
  return {
 
243
  logger.error(f"Failed to get file info: {e}")
244
  return None
245
 
 
 
 
246
  def store_file_stream(remote_path: str, data_stream: BinaryIO, encrypt: bool = True) -> str:
 
 
 
 
 
 
 
 
 
 
 
247
  data = data_stream.read()
248
  return store_file(remote_path, data, encrypt=encrypt)
249
 
250
  def retrieve_file_stream(remote_path: str, decrypt: bool = True) -> BinaryIO:
 
 
 
 
 
 
 
 
 
 
251
  data = retrieve_file(remote_path, decrypt=decrypt)
252
  import io
253
  return io.BytesIO(data)
254
 
 
 
 
255
  def get_storage_stats() -> Dict[str, Any]:
 
 
 
 
 
 
256
  try:
257
  files = list_files()
258
  total_size = 0
259
  file_count = 0
 
260
  for f in files:
261
  size = get_file_size(f)
262
  if size is not None:
263
  total_size += size
264
  file_count += 1
 
265
  return {
266
  "bucket": f"{OWNER}/{BUCKET_ID}",
267
  "file_count": file_count,
 
278
  "error": str(e)
279
  }
280
 
 
 
 
281
  def create_backup(backup_prefix: str = "backups") -> str:
 
 
 
 
 
 
 
 
 
282
  timestamp = int(time.time())
283
  backup_path = f"{backup_prefix}/backup_{timestamp}"
 
284
  try:
285
  files = list_files()
286
  for file_path in files:
 
290
  store_file(backup_file_path, data, encrypt=True)
291
  except Exception as e:
292
  logger.error(f"Failed to backup {file_path}: {e}")
 
293
  logger.info(f"💾 Backup created at: {backup_path}")
294
  return backup_path
295
  except Exception as e:
296
  logger.error(f"❌ Backup failed: {e}")
297
  raise
298
 
 
 
 
299
  def close():
 
300
  if _fs:
301
  try:
302
  _fs.close()
303
  except:
304
  pass
305
 
 
 
 
306
  __all__ = [
307
+ 'store_file', 'retrieve_file', 'delete_file', 'file_exists',
308
+ 'list_files', 'get_file_size', 'get_file_info',
309
+ 'store_file_stream', 'retrieve_file_stream',
310
+ 'get_storage_stats', 'create_backup', 'close'
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  ]