54 lines
1.9 KiB
Python
54 lines
1.9 KiB
Python
from __future__ import annotations
|
||
|
||
import base64
|
||
import hashlib
|
||
import logging
|
||
from dataclasses import dataclass, field, asdict
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
@dataclass
|
||
class ContentChunk:
|
||
"""
|
||
Модель чанка зашифрованного контента.
|
||
|
||
Все бинарные поля представлены в base64-строках для JSON-совместимости.
|
||
- chunk_hash: HEX(SHA-256(raw_encrypted_chunk_bytes)) — для дедупликации
|
||
- signature: base64-encoded Ed25519 подпись структуры чанка (детали в ChunkManager)
|
||
"""
|
||
chunk_id: str
|
||
content_id: str
|
||
chunk_index: int
|
||
chunk_hash: str # hex sha256(raw encrypted data)
|
||
encrypted_data: str # base64
|
||
signature: Optional[str] = None
|
||
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
|
||
|
||
def to_dict(self) -> dict:
|
||
return asdict(self)
|
||
|
||
@classmethod
|
||
def from_dict(cls, data: dict) -> "ContentChunk":
|
||
required = ["chunk_id", "content_id", "chunk_index", "chunk_hash", "encrypted_data"]
|
||
for f in required:
|
||
if f not in data:
|
||
raise ValueError(f"Missing required field in ContentChunk: {f}")
|
||
return cls(
|
||
chunk_id=data["chunk_id"],
|
||
content_id=data["content_id"],
|
||
chunk_index=int(data["chunk_index"]),
|
||
chunk_hash=data["chunk_hash"],
|
||
encrypted_data=data["encrypted_data"],
|
||
signature=data.get("signature"),
|
||
created_at=data.get("created_at") or datetime.utcnow().isoformat(),
|
||
)
|
||
|
||
def encrypted_bytes(self) -> bytes:
|
||
return base64.b64decode(self.encrypted_data)
|
||
|
||
@staticmethod
|
||
def compute_sha256_hex(buf: bytes) -> str:
|
||
return hashlib.sha256(buf).hexdigest() |