import json from base58 import b58decode from sqlalchemy import Column, BigInteger, Integer, String, ForeignKey, DateTime, JSON, Boolean from sqlalchemy.orm import relationship from datetime import datetime from app.core.logger import make_log from app.core._config import UPLOADS_DIR, PROJECT_HOST import os from app.core.content.content_id import ContentId from app.core.content.audio import AudioContentMixin from app.core.content.image import ImageContentMixin # from app.core.models.content.indexation_mixins import NodeStorageIndexationMixin from .base import AlchemyBase class StoredContent(AlchemyBase, AudioContentMixin): __tablename__ = 'node_storage' id = Column(Integer, autoincrement=True, primary_key=True) type = Column(String(32), nullable=False) hash = Column(String(64), nullable=False, unique=True) # base58 content_id = Column(String(512), nullable=True) # base58 onchain_index = Column(BigInteger, nullable=True, default=None) status = Column(String(32), nullable=True) filename = Column(String(1024), nullable=False) meta = Column(JSON, nullable=False, default={}) user_id = Column(Integer, ForeignKey('users.id'), nullable=True) owner_address = Column(String(1024), nullable=True) btfs_cid = Column(String(1024), nullable=True) # На самом деле это CID контента в High качестве ipfs_cid = Column(String(1024), nullable=True) # На самом деле это CID контента в Low качестве telegram_cid = Column(String(1024), nullable=True) codebase_version = Column(Integer, nullable=True) created = Column(DateTime, nullable=False, default=0) updated = Column(DateTime, nullable=False, default=0) disabled = Column(DateTime, nullable=False, default=0) disabled_by = Column(Integer, ForeignKey('users.id'), nullable=True, default=None) encrypted = Column(Boolean, nullable=False, default=False) decrypted_content_id = Column(Integer, ForeignKey('node_storage.id'), nullable=True, default=None) key_id = Column(Integer, ForeignKey('known_keys.id'), nullable=True, default=None) user = relationship('User', uselist=False, foreign_keys=[user_id]) key = relationship('KnownKey', uselist=False, foreign_keys=[key_id]) decrypted_content = relationship('StoredContent', uselist=False, foreign_keys=[decrypted_content_id]) @property def cid(self) -> ContentId: return ContentId( content_hash=b58decode(self.hash), onchain_index=self.onchain_index, accept_type=self.meta.get('content_type', 'image/jpeg') ) @property def filepath(self) -> str: return os.path.join(UPLOADS_DIR, self.hash) @property def web_url(self) -> str: return f"{PROJECT_HOST}/api/v1.5/storage/{self.cid.serialize_v2(include_accept_type=True)}" @property def decrypt_possible(self) -> bool: if self.encrypted is False: return True return bool(self.key_id or self.decrypted_content_id) def open_content(self, db_session, content_type=None): try: # Получение StoredContent в прод-виде с указанием типа данных и доступный для перевода в другие форматы decrypted_content = self if not self.encrypted else None encrypted_content = self if self.encrypted else None content_type = None if not decrypted_content: decrypted_content = db_session.query(StoredContent).filter(StoredContent.id == self.decrypted_content_id).first() else: encrypted_content = db_session.query(StoredContent).filter(StoredContent.decrypted_content_id == self.id).first() assert decrypted_content, "Can't get decrypted content" assert encrypted_content, "Can't get encrypted content" content_type = content_type or decrypted_content.json_format()['content_type'] content_type, content_encoding = content_type.split('/') return { 'encrypted_content': encrypted_content, 'decrypted_content': decrypted_content, 'content_type': content_type or 'application/x-binary' } except BaseException as e: make_log("NodeStorage.open_content", f"Can't open content: {self.id} {e}", level='warning') raise e async def open_content_async(self, db_session, content_type=None): from sqlalchemy import select try: decrypted_content = self if not self.encrypted else None encrypted_content = self if self.encrypted else None if not decrypted_content: decrypted_content = (await db_session.execute(select(StoredContent).where(StoredContent.id == self.decrypted_content_id))).scalars().first() else: encrypted_content = (await db_session.execute(select(StoredContent).where(StoredContent.decrypted_content_id == self.id))).scalars().first() assert decrypted_content, "Can't get decrypted content" assert encrypted_content, "Can't get encrypted content" _ct = content_type or decrypted_content.json_format()['content_type'] content_type = _ct.split('/')[0] if _ct else 'application' return { 'encrypted_content': encrypted_content, 'decrypted_content': decrypted_content, 'content_type': content_type or 'application/x-binary' } except BaseException as e: make_log("NodeStorage.open_content_async", f"Can't open content: {self.id} {e}", level='warning') raise e def json_format(self): extra_fields = {} if self.type.startswith('local'): extra_fields['filename'] = self.filename extra_fields['encrypted'] = self.encrypted elif self.type.startswith('onchain'): extra_fields['onchain_index'] = self.onchain_index extra_fields['owner_address'] = self.owner_address for k in [ 'item_address', 'license_type', 'metadata_cid', 'content_cid', 'cover_cid', 'license' ]: extra_fields[k] = self.meta.get(k, None) try: spec_field_updated = self.updated.isoformat() if isinstance(self.updated, datetime) else ( datetime.fromisoformat(self.updated).isoformat() if isinstance(self.updated, str) else None ) except BaseException as e: make_log("StoredContent.json_format", f"[{self.id}] Can't convert updated field: {self.updated} {e}", level='debug') spec_field_updated = datetime(1970, 1, 1).isoformat() try: spec_field_created = self.created.isoformat() if isinstance(self.created, datetime) else ( datetime.fromisoformat(self.created).isoformat() if isinstance(self.created, str) else None ) except BaseException as e: make_log("StoredContent.json_format", f"[{self.id}] Can't convert created field: {self.created} {e}", level='debug') spec_field_created = datetime(1970, 1, 1).isoformat() return { **extra_fields, "hash": self.hash, "cid": self.cid.serialize_v2(), "content_type": self.meta.get('content_type', 'application/x-binary'), "status": self.status, "created": spec_field_created, "updated": spec_field_updated, } def metadata_json(self, db_session): metadata_cid = self.meta.get('metadata_cid') if metadata_cid: metadata_content = StoredContent.from_cid(db_session, metadata_cid) with open(metadata_content.filepath, 'r') as f: return json.loads(f.read()) @classmethod def from_cid(cls, db_session, content_id): if isinstance(content_id, str): cid = ContentId.deserialize(content_id) else: cid = content_id content = db_session.query(StoredContent).filter(StoredContent.hash == cid.content_hash_b58).first() assert content, "Content not found" return content @classmethod async def from_cid_async(cls, db_session, content_id): from sqlalchemy import select if isinstance(content_id, str): cid = ContentId.deserialize(content_id) else: cid = content_id result = await db_session.execute(select(StoredContent).where(StoredContent.hash == cid.content_hash_b58)) content = result.scalars().first() assert content, "Content not found" return content