smodusermc commited on
Commit
7aff134
·
verified ·
1 Parent(s): af309f0

Update storage_handler.py

Browse files
Files changed (1) hide show
  1. storage_handler.py +147 -37
storage_handler.py CHANGED
@@ -1,105 +1,215 @@
 
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # storage_handler.py
2
  import os
3
  import base64
 
 
4
  import logging
5
+ from typing import Optional, BinaryIO, Dict, Any
6
 
7
+ from huggingface_hub import HfFileSystem, HfApi
8
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
9
 
10
  logger = logging.getLogger("StorageHandler")
11
 
12
  # ------------------------------------------------------------------------
13
+ # Configuration – all from environment variables
14
  # ------------------------------------------------------------------------
15
+ HF_TOKEN = os.environ.get("HF_TOKEN")
16
+ if not HF_TOKEN:
17
+ raise ValueError("HF_TOKEN environment variable is required")
18
+
19
+ BUCKET_ID = os.environ.get("INFINITY_CHAT_BUCKET", "infinitychat-data")
20
  FILE_ENCRYPTION_KEY_B64 = os.environ.get("FILE_ENCRYPTION_KEY", None)
21
  if FILE_ENCRYPTION_KEY_B64 is None:
 
22
  from cryptography.fernet import Fernet
23
  FILE_ENCRYPTION_KEY_B64 = Fernet.generate_key().decode()
24
+ logger.warning("FILE_ENCRYPTION_KEY not set – generated random key. Set it permanently!")
25
  FILE_ENCRYPTION_KEY = base64.urlsafe_b64decode(FILE_ENCRYPTION_KEY_B64.encode())
26
+ assert len(FILE_ENCRYPTION_KEY) == 32, "AES‑256 requires exactly 32 bytes"
27
 
28
+ # Get the bucket owner (your HF username)
29
  _api = HfApi(token=HF_TOKEN)
30
+ try:
31
+ OWNER = _api.whoami()["name"]
32
+ except Exception:
33
+ # Fallback: use the bucket ID prefix (if it contains a slash)
34
+ if "/" in BUCKET_ID:
35
+ OWNER, BUCKET_ID = BUCKET_ID.split("/", 1)
36
+ else:
37
+ raise ValueError("Cannot determine bucket owner. Set INFINITY_CHAT_BUCKET as 'owner/bucket-name'")
38
+
39
+ # Build the proper bucket URI prefix (Xet-native)
40
+ BUCKET_URI_PREFIX = f"hf://buckets/{OWNER}/{BUCKET_ID}"
41
 
42
+ # ------------------------------------------------------------------------
43
+ # HfFileSystem for low-level bucket operations (Xet-native, NO Git/LFS)
44
+ # ------------------------------------------------------------------------
45
+ _fs = HfFileSystem(token=HF_TOKEN)
46
 
47
  # ------------------------------------------------------------------------
48
+ # Bucket initialization (using the modern Hugging Face Hub API)
49
  # ------------------------------------------------------------------------
50
  def ensure_bucket():
51
+ """
52
+ Ensure the bucket exists using the native HF Bucket API.
53
+ The HfApi.create_bucket call does NOT use 'name' – it uses 'bucket_id'.
54
+ """
55
+ api = HfApi(token=HF_TOKEN)
56
+
57
+ # Modern API: create_bucket(bucket_id=..., private=True, exist_ok=True)
58
  try:
59
+ api.create_bucket(
60
+ bucket_id=f"{OWNER}/{BUCKET_ID}",
61
+ private=True,
62
+ exist_ok=True
63
+ )
64
+ logger.info(f"✅ Bucket '{OWNER}/{BUCKET_ID}' is ready (private).")
65
  except Exception as e:
66
+ # Some versions require the full ID
67
+ if "already exists" in str(e).lower():
68
+ logger.info(f"Bucket '{OWNER}/{BUCKET_ID}' already exists.")
69
+ # Try without the owner prefix
70
+ else:
71
+ try:
72
+ api.create_bucket(
73
+ bucket_id=BUCKET_ID,
74
+ private=True,
75
+ exist_ok=True
76
+ )
77
+ logger.info(f"✅ Bucket '{BUCKET_ID}' ready.")
78
+ global BUCKET_URI_PREFIX
79
+ BUCKET_URI_PREFIX = f"hf://buckets/{BUCKET_ID}"
80
+ except Exception as e2:
81
+ if "already exists" in str(e2).lower():
82
+ logger.info(f"Bucket '{BUCKET_ID}' already exists.")
83
+ else:
84
+ logger.error(f"Failed to create bucket: {e2}")
85
+ raise
86
 
87
+ # Initialize bucket
88
  ensure_bucket()
89
 
90
  # ------------------------------------------------------------------------
91
+ # Encryption / Decryption
92
  # ------------------------------------------------------------------------
93
  def encrypt_file(data: bytes) -> bytes:
94
+ """Encrypt binary data using AES‑256‑GCM. Output: nonce (12 B) || ciphertext."""
95
  aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
96
  nonce = os.urandom(12)
97
  ciphertext = aesgcm.encrypt(nonce, data, None)
98
  return nonce + ciphertext
99
 
100
  def decrypt_file(encrypted_blob: bytes) -> bytes:
101
+ """Decrypt data produced by encrypt_file()."""
102
  nonce = encrypted_blob[:12]
103
  ciphertext = encrypted_blob[12:]
104
  aesgcm = AESGCM(FILE_ENCRYPTION_KEY)
105
  return aesgcm.decrypt(nonce, ciphertext, None)
106
 
107
  # ------------------------------------------------------------------------
108
+ # Core Bucket Operations (Xet-native via HfFileSystem)
109
  # ------------------------------------------------------------------------
 
 
110
  def _bucket_path(remote_path: str) -> str:
111
+ """Construct the full hf://buckets/... URI."""
112
  return f"{BUCKET_URI_PREFIX}/{remote_path}"
113
 
114
  def store_file(remote_path: str, data: bytes) -> str:
115
+ """
116
+ Encrypt data and write it to the bucket using HfFileSystem.
117
+ Returns: the remote path (for DB storage).
118
+ """
119
+ encrypted_data = encrypt_file(data)
120
  full_uri = _bucket_path(remote_path)
121
+
122
  with _fs.open(full_uri, "wb") as f:
123
+ f.write(encrypted_data)
124
+
125
+ logger.debug(f"Stored file: {full_uri}")
126
  return remote_path
127
 
128
  def retrieve_file(remote_path: str) -> bytes:
129
+ """
130
+ Read from bucket using HfFileSystem and decrypt.
131
+ """
132
  full_uri = _bucket_path(remote_path)
133
+
134
  with _fs.open(full_uri, "rb") as f:
135
+ encrypted_data = f.read()
136
+
137
+ return decrypt_file(encrypted_data)
138
 
139
  def delete_file(remote_path: str):
140
+ """
141
+ Delete a file from the bucket using HfFileSystem.
142
+ """
143
  full_uri = _bucket_path(remote_path)
144
  try:
145
  _fs.rm(full_uri)
146
+ logger.debug(f"Deleted file: {full_uri}")
147
  except FileNotFoundError:
148
+ logger.warning(f"File not found for deletion: {full_uri}")
149
+ except Exception as e:
150
+ logger.error(f"Failed to delete file {full_uri}: {e}")
151
+
152
+ def list_files(prefix: str = "") -> list:
153
+ """
154
+ List files in the bucket under a given prefix.
155
+ Returns list of full URIs.
156
+ """
157
+ search_path = f"{BUCKET_URI_PREFIX}/{prefix}" if prefix else BUCKET_URI_PREFIX
158
+ try:
159
+ return _fs.ls(search_path)
160
+ except FileNotFoundError:
161
+ return []
162
+
163
+ def file_exists(remote_path: str) -> bool:
164
+ """Check if a file exists in the bucket."""
165
+ full_uri = _bucket_path(remote_path)
166
+ return _fs.exists(full_uri)
167
+
168
+ def get_file_size(remote_path: str) -> Optional[int]:
169
+ """Get the size of a file in bytes (compressed)."""
170
+ full_uri = _bucket_path(remote_path)
171
+ try:
172
+ info = _fs.info(full_uri)
173
+ return info.get("size")
174
+ except Exception:
175
+ return None
176
 
177
  # ------------------------------------------------------------------------
178
+ # Stream-based store (for reassembled chunks)
179
  # ------------------------------------------------------------------------
180
  def store_file_stream(remote_path: str, data_stream: BinaryIO) -> str:
181
  """
182
  Read the entire stream, encrypt, and write to bucket.
 
183
  """
184
  data = data_stream.read()
185
+ return store_file(remote_path, data)
186
+
187
+ # ------------------------------------------------------------------------
188
+ # Bucket info (for monitoring / health checks)
189
+ # ------------------------------------------------------------------------
190
+ def get_bucket_info() -> Dict[str, Any]:
191
+ """Return information about the bucket."""
192
+ api = HfApi(token=HF_TOKEN)
193
+ try:
194
+ # Try the modern API first
195
+ info = api.bucket_info(bucket_id=f"{OWNER}/{BUCKET_ID}")
196
+ return {
197
+ "id": f"{OWNER}/{BUCKET_ID}",
198
+ "exists": True,
199
+ "private": info.get("private", True),
200
+ "total_files": len(list_files()),
201
+ }
202
+ except Exception:
203
+ return {
204
+ "id": f"{OWNER}/{BUCKET_ID}",
205
+ "exists": False,
206
+ "note": "Cannot fetch bucket info"
207
+ }
208
+
209
+ # ------------------------------------------------------------------------
210
+ # Clean shutdown
211
+ # ------------------------------------------------------------------------
212
+ def close():
213
+ """Clean up resources (HfFileSystem)."""
214
+ if _fs:
215
+ _fs.close()