smodusermc commited on
Commit
a6cc470
·
verified ·
1 Parent(s): c5af485

Create storage_handler.py

Browse files
Files changed (1) hide show
  1. storage_handler.py +105 -0
storage_handler.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import tempfile
4
+ from typing import Optional, BinaryIO
5
+ import logging
6
+
7
+ from huggingface_hub import HfFileSystem, HfApi, create_bucket
8
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
9
+
10
+ logger = logging.getLogger("StorageHandler")
11
+
12
+ # ------------------------------------------------------------------------
13
+ # Configuration
14
+ # ------------------------------------------------------------------------
15
+ HF_TOKEN = os.environ["HF_TOKEN"] # required
16
+ BUCKET_NAME = os.environ.get("INFINITY_CHAT_BUCKET", "infinitychat-data")
17
+ FILE_ENCRYPTION_KEY_B64 = os.environ.get("FILE_ENCRYPTION_KEY", None)
18
+ if FILE_ENCRYPTION_KEY_B64 is None:
19
+ # Auto‑generate (for production, set env var permanently)
20
+ from cryptography.fernet import Fernet
21
+ FILE_ENCRYPTION_KEY_B64 = Fernet.generate_key().decode()
22
+ FILE_ENCRYPTION_KEY = base64.urlsafe_b64decode(FILE_ENCRYPTION_KEY_B64.encode())
23
+ assert len(FILE_ENCRYPTION_KEY) == 32, "AES‑256 requires a 32‑byte key"
24
+
25
+ # Fetch the bucket owner (username) from the token
26
+ _api = HfApi(token=HF_TOKEN)
27
+ OWNER = _api.whoami()["name"]
28
+ del _api
29
+
30
+ # Bucket URI prefix
31
+ BUCKET_URI_PREFIX = f"hf://buckets/{OWNER}/{BUCKET_NAME}"
32
+
33
+ # ------------------------------------------------------------------------
34
+ # Bucket lifecycle
35
+ # ------------------------------------------------------------------------
36
+ def ensure_bucket():
37
+ """Create the private bucket if it does not exist (idempotent)."""
38
+ try:
39
+ create_bucket(name=BUCKET_NAME, token=HF_TOKEN, private=True, exist_ok=True)
40
+ logger.info(f"Bucket '{BUCKET_NAME}' ready (owner: {OWNER}).")
41
+ except Exception as e:
42
+ logger.error(f"Failed to ensure bucket: {e}")
43
+ raise
44
+
45
+ # Initialise on import
46
+ ensure_bucket()
47
+
48
+ # ------------------------------------------------------------------------
49
+ # Encryption helpers
50
+ # ------------------------------------------------------------------------
51
+ def encrypt_file(data: bytes) -> bytes:
52
+ """AES‑256‑GCM: nonce (12 B) || ciphertext (incl. tag)"""
53
+ aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
54
+ nonce = os.urandom(12)
55
+ ciphertext = aesgcm.encrypt(nonce, data, None)
56
+ return nonce + ciphertext
57
+
58
+ def decrypt_file(encrypted_blob: bytes) -> bytes:
59
+ nonce = encrypted_blob[:12]
60
+ ciphertext = encrypted_blob[12:]
61
+ aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
62
+ return aesgcm.decrypt(nonce, ciphertext, None)
63
+
64
+ # ------------------------------------------------------------------------
65
+ # Core file operations via HfFileSystem
66
+ # ------------------------------------------------------------------------
67
+ _fs = HfFileSystem(token=HF_TOKEN)
68
+
69
+ def _bucket_path(remote_path: str) -> str:
70
+ """Constructs the full hf://buckets/... URI."""
71
+ return f"{BUCKET_URI_PREFIX}/{remote_path}"
72
+
73
+ def store_file(remote_path: str, data: bytes) -> str:
74
+ """Encrypt and write data directly into the bucket. Returns the remote path."""
75
+ encrypted = encrypt_file(data)
76
+ full_uri = _bucket_path(remote_path)
77
+ with _fs.open(full_uri, "wb") as f:
78
+ f.write(encrypted)
79
+ return remote_path
80
+
81
+ def retrieve_file(remote_path: str) -> bytes:
82
+ """Read from bucket and decrypt."""
83
+ full_uri = _bucket_path(remote_path)
84
+ with _fs.open(full_uri, "rb") as f:
85
+ encrypted = f.read()
86
+ return decrypt_file(encrypted)
87
+
88
+ def delete_file(remote_path: str):
89
+ """Remove a file from the bucket."""
90
+ full_uri = _bucket_path(remote_path)
91
+ try:
92
+ _fs.rm(full_uri)
93
+ except FileNotFoundError:
94
+ pass
95
+
96
+ # ------------------------------------------------------------------------
97
+ # Helper for reassembled chunked uploads (used by app.py)
98
+ # ------------------------------------------------------------------------
99
+ def store_file_stream(remote_path: str, data_stream: BinaryIO) -> str:
100
+ """
101
+ Read the entire stream, encrypt, and write to bucket.
102
+ Useful for handling reassembled chunks without a single byte array.
103
+ """
104
+ data = data_stream.read()
105
+ return store_file(remote_path, data)