← Назад к вопросам

Как спроектируешь ООП для файлообменника?

3.0 Senior🔥 71 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проектирование ООП архитектуры файлообменника

Требования к системе

Файлообменник должен поддерживать:

  • загрузку и скачивание файлов
  • управление пользователями и правами доступа
  • версионирование и истории файлов
  • шифрование и безопасность
  • масштабируемость и производительность

Архитектурные слои

Использую onion architecture (чистая архитектура):

presentation/ → application/ → domain/ → infrastructure/
  (API)        (use cases)   (entities) (DB, storage)

Domain Layer — основные сущности

from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from uuid import UUID

class AccessLevel(Enum):
    VIEW = 'view'
    EDIT = 'edit'
    ADMIN = 'admin'

@dataclass
class User:
    id: UUID
    username: str
    email: str
    created_at: datetime
    
    def can_access(self, resource_id: UUID, level: AccessLevel) -> bool:
        # Логика проверки доступа
        pass

@dataclass
class FileMetadata:
    id: UUID
    owner_id: UUID
    name: str
    size: int
    mime_type: str
    created_at: datetime
    updated_at: datetime
    is_deleted: bool = False
    
    def is_valid_name(self) -> bool:
        return len(self.name) > 0 and len(self.name) <= 255

@dataclass
class FileShare:
    id: UUID
    file_id: UUID
    user_id: UUID
    access_level: AccessLevel
    created_at: datetime
    expires_at: datetime | None
    
    def is_expired(self) -> bool:
        if not self.expires_at:
            return False
        return datetime.now() > self.expires_at

@dataclass
class FileVersion:
    id: UUID
    file_id: UUID
    version_number: int
    storage_path: str
    created_at: datetime
    created_by: UUID
    checksum: str  # SHA256

Domain Services

class FileAccessService:
    """Бизнес-логика проверки доступа"""
    
    def __init__(self, share_repo):
        self.share_repo = share_repo
    
    def can_view_file(self, user_id: UUID, file_id: UUID) -> bool:
        share = self.share_repo.find_by_user_and_file(
            user_id, file_id
        )
        if not share:
            return False
        return not share.is_expired()
    
    def get_access_level(
        self, 
        user_id: UUID, 
        file_id: UUID
    ) -> AccessLevel | None:
        share = self.share_repo.find_by_user_and_file(
            user_id, file_id
        )
        return share.access_level if share else None

class FileStorageService:
    """Бизнес-логика хранения файлов"""
    
    def calculate_checksum(self, content: bytes) -> str:
        import hashlib
        return hashlib.sha256(content).hexdigest()
    
    def validate_file(
        self, 
        metadata: FileMetadata, 
        content: bytes
    ) -> bool:
        # Проверка размера, типа, вируса
        MAX_SIZE = 5 * 1024 * 1024 * 1024  # 5GB
        if metadata.size > MAX_SIZE:
            return False
        return metadata.is_valid_name()

Application Layer — Use Cases

class UploadFileUseCase:
    def __init__(
        self,
        file_repo,
        file_storage: FileStorageService,
        access_service: FileAccessService
    ):
        self.file_repo = file_repo
        self.file_storage = file_storage
        self.access_service = access_service
    
    def execute(
        self,
        user_id: UUID,
        filename: str,
        content: bytes,
        mime_type: str
    ) -> FileMetadata:
        # Валидация
        metadata = FileMetadata(
            id=UUID(),
            owner_id=user_id,
            name=filename,
            size=len(content),
            mime_type=mime_type,
            created_at=datetime.now(),
            updated_at=datetime.now()
        )
        
        if not self.file_storage.validate_file(metadata, content):
            raise ValueError('Invalid file')
        
        # Сохранение
        checksum = self.file_storage.calculate_checksum(content)
        storage_path = self.file_storage.save(
            metadata.id, content
        )
        
        # Создание версии
        version = FileVersion(
            id=UUID(),
            file_id=metadata.id,
            version_number=1,
            storage_path=storage_path,
            created_at=datetime.now(),
            created_by=user_id,
            checksum=checksum
        )
        
        # Сохранение в БД
        self.file_repo.save(metadata)
        self.file_repo.save_version(version)
        
        return metadata

class DownloadFileUseCase:
    def __init__(self, file_repo, access_service):
        self.file_repo = file_repo
        self.access_service = access_service
    
    def execute(
        self,
        user_id: UUID,
        file_id: UUID
    ) -> bytes:
        # Проверка доступа
        if not self.access_service.can_view_file(user_id, file_id):
            raise PermissionError('No access')
        
        # Получение файла
        metadata = self.file_repo.find_by_id(file_id)
        if not metadata or metadata.is_deleted:
            raise FileNotFoundError()
        
        # Загрузка из хранилища
        content = self.file_repo.load(metadata.id)
        return content

class ShareFileUseCase:
    def __init__(self, share_repo, file_repo, access_service):
        self.share_repo = share_repo
        self.file_repo = file_repo
        self.access_service = access_service
    
    def execute(
        self,
        owner_id: UUID,
        file_id: UUID,
        target_user_id: UUID,
        access_level: AccessLevel,
        expires_in_days: int | None = None
    ) -> FileShare:
        # Проверка, что владелец
        file_meta = self.file_repo.find_by_id(file_id)
        if file_meta.owner_id != owner_id:
            raise PermissionError('Not owner')
        
        # Создание share
        from datetime import timedelta
        expires_at = None
        if expires_in_days:
            expires_at = datetime.now() + timedelta(days=expires_in_days)
        
        share = FileShare(
            id=UUID(),
            file_id=file_id,
            user_id=target_user_id,
            access_level=access_level,
            created_at=datetime.now(),
            expires_at=expires_at
        )
        
        self.share_repo.save(share)
        return share

Infrastructure Layer — Repositories

from abc import ABC, abstractmethod

class FileRepository(ABC):
    @abstractmethod
    def save(self, metadata: FileMetadata) -> None:
        pass
    
    @abstractmethod
    def find_by_id(self, file_id: UUID) -> FileMetadata | None:
        pass
    
    @abstractmethod
    def save_version(self, version: FileVersion) -> None:
        pass

class SQLAlchemyFileRepository(FileRepository):
    def __init__(self, session):
        self.session = session
    
    def save(self, metadata: FileMetadata) -> None:
        file_model = FileModel(
            id=metadata.id,
            owner_id=metadata.owner_id,
            name=metadata.name,
            size=metadata.size,
            mime_type=metadata.mime_type,
            created_at=metadata.created_at,
            updated_at=metadata.updated_at,
            is_deleted=metadata.is_deleted
        )
        self.session.add(file_model)
        self.session.commit()

class S3FileStorage:
    """Хранение файлов в S3"""
    
    def __init__(self, bucket_name: str):
        import boto3
        self.s3 = boto3.client('s3')
        self.bucket = bucket_name
    
    def save(self, file_id: UUID, content: bytes) -> str:
        key = f'files/{file_id}'
        self.s3.put_object(
            Bucket=self.bucket,
            Key=key,
            Body=content
        )
        return key
    
    def load(self, file_id: UUID) -> bytes:
        key = f'files/{file_id}'
        response = self.s3.get_object(Bucket=self.bucket, Key=key)
        return response['Body'].read()

Presentation Layer — API

from fastapi import APIRouter, HTTPException, Depends

router = APIRouter(prefix='/api/v1/files')

@router.post('/upload')
async def upload_file(
    filename: str,
    content: bytes,
    current_user: User = Depends(get_current_user),
    use_case: UploadFileUseCase = Depends()
):
    try:
        metadata = use_case.execute(
            current_user.id,
            filename,
            content,
            'application/octet-stream'
        )
        return {'file_id': metadata.id}
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))

@router.get('/{file_id}/download')
async def download_file(
    file_id: UUID,
    current_user: User = Depends(get_current_user),
    use_case: DownloadFileUseCase = Depends()
):
    try:
        content = use_case.execute(current_user.id, file_id)
        return {'content': content}
    except PermissionError:
        raise HTTPException(status_code=403)

@router.post('/{file_id}/share')
async def share_file(
    file_id: UUID,
    target_user_id: UUID,
    access_level: AccessLevel,
    current_user: User = Depends(get_current_user),
    use_case: ShareFileUseCase = Depends()
):
    share = use_case.execute(
        current_user.id,
        file_id,
        target_user_id,
        access_level
    )
    return {'share_id': share.id}

Ключевые принципы

  • DDD (Domain-Driven Design): Бизнес-логика в domain layer
  • SOLID: Каждый класс отвечает за одно
  • Инверсия зависимостей: Repositories через DI
  • Разделение ответственности: API не знает о БД
  • Тестируемость: Mock объекты для unit-тестов

Этот подход обеспечивает масштабируемость, тестируемость и поддерживаемость системы.

Как спроектируешь ООП для файлообменника? | PrepBro