docs/FILES-ARCHITECTURE.md
Markov 1f2041e990 Design comprehensive files architecture for Team Board
- Project files structure: workspace/<project-slug>/files/ and /attachments/
- Database models: ProjectFile + extended Attachment
- Hybrid sync: inotify + periodic scanning for manual file additions
- MCP tools integration: 6 tools for agent file operations
- Security: validation, quotas, file type restrictions
- Features: thumbnails, text extraction, full-text search
- REST API endpoints for all file operations
- Detailed implementation plan and examples
2026-02-24 18:42:41 +01:00

1035 lines
42 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Files Architecture — Team Board
**Версия:** 1.0
**Дата:** 2026-02-24
**Автор:** Subagent (Research & Design)
---
## 1. Обзор
Team Board нуждается в файловой системе для двух типов файлов:
1. **Project Files** — файлы проекта (документация, код, ресурсы)
2. **Attachments** — файлы, прикреплённые к сообщениям и задачам
**Ключевые принципы:**
- Файлы хранятся на диске в настраиваемом workspace
- Структура папок по проектам: `workspace/<project-slug>/`
- Файлы, добавленные вручную, автоматически индексируются
- Агенты имеют полный доступ через MCP tools
- Единый API для всех операций с файлами
---
## 2. Структура директорий
### 2.1 Общая структура workspace
```
/data/team-board-workspace/
├── projects/ # Корневая папка проектов
│ ├── <project-slug>/ # Папка проекта (по slug)
│ │ ├── files/ # Проектные файлы (ручное добавление)
│ │ │ ├── docs/ # Документация
│ │ │ ├── assets/ # Ресурсы (изображения, шрифты)
│ │ │ ├── configs/ # Конфигурационные файлы
│ │ │ └── misc/ # Прочие файлы
│ │ ├── attachments/ # Файлы из сообщений/задач
│ │ │ ├── messages/ # По ID сообщения
│ │ │ │ ├── <message-id>/
│ │ │ │ │ ├── document.pdf
│ │ │ │ │ └── screenshot.png
│ │ │ └── tasks/ # По ID задачи
│ │ │ └── <task-id>/
│ │ │ ├── requirements.md
│ │ │ └── mockup.png
│ │ └── .meta/ # Метаданные (индекс файлов)
│ │ ├── files.index.json # Индекс проектных файлов
│ │ └── sync.log # Лог синхронизации
│ └── shared/ # Общие файлы (не привязаны к проекту)
│ ├── avatars/ # Аватары пользователей
│ ├── templates/ # Шаблоны документов
│ └── tmp/ # Временные файлы
├── .global/ # Глобальные настройки
│ ├── config.json # Конфигурация файловой системы
│ ├── mime-types.json # Настройки типов файлов
│ └── quotas.json # Квоты пользователей/проектов
└── trash/ # Корзина (soft delete)
└── <date>/
└── <original-path>
```
### 2.2 Пример для проекта "team-board"
```
/data/team-board-workspace/projects/team-board/
├── files/ # Проектные файлы
│ ├── docs/
│ │ ├── README.md # Добавлен вручную
│ │ ├── API.md # Добавлен вручную
│ │ └── diagrams/
│ │ └── architecture.png
│ ├── configs/
│ │ ├── docker-compose.yml
│ │ └── .env.example
│ └── assets/
│ ├── logo.png
│ └── icons/
├── attachments/ # Из сообщений/задач
│ ├── messages/
│ │ └── 550e8400-e29b-41d4-a716-446655440000/
│ │ ├── error-screenshot.png # Attachment к сообщению
│ │ └── logs.txt
│ └── tasks/
│ └── 550e8400-e29b-41d4-a716-446655440001/
│ └── technical-spec.pdf # Attachment к задаче
└── .meta/
├── files.index.json # Индекс проектных файлов
└── sync.log
```
### 2.3 Naming conventions
**Проекты:** `<project-slug>` (латиница, дефисы)
**Файлы:** Исходные имена сохраняются, конфликты решаются суффиксами
**Папки attachments:** UUID сообщения/задачи
**Временные файлы:** UUID + timestamp
---
## 3. Модели базы данных
### 3.1 Расширение существующей модели Attachment
```python
# Существующая модель (для attachments)
class Attachment(Base):
"""File attachment to message or task comment."""
__tablename__ = "attachments"
message_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("messages.id"), nullable=False)
filename: Mapped[str] = mapped_column(String(500), nullable=False)
mime_type: Mapped[str | None] = mapped_column(String(100))
size: Mapped[int] = mapped_column(Integer, default=0) # bytes
storage_path: Mapped[str] = mapped_column(String(1000), nullable=False) # относительный путь
message: Mapped["Message"] = relationship(back_populates="attachments")
```
### 3.2 Новая модель для проектных файлов
```python
class ProjectFile(Base):
"""File in project files/ directory — manually added or uploaded via API."""
__tablename__ = "project_files"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
project_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("projects.id"), nullable=False)
# Файл
filename: Mapped[str] = mapped_column(String(500), nullable=False) # исходное имя
relative_path: Mapped[str] = mapped_column(String(1000), nullable=False) # относительно project/files/
mime_type: Mapped[str | None] = mapped_column(String(100))
size: Mapped[int] = mapped_column(Integer, default=0) # bytes
checksum: Mapped[str | None] = mapped_column(String(64)) # SHA256 for deduplication
# Метаданные
source: Mapped[str] = mapped_column(String(20), default="manual") # manual | api | agent
uploaded_by: Mapped[str | None] = mapped_column(String(255)) # member slug
description: Mapped[str | None] = mapped_column(Text)
tags: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # для категоризации
# Флаги
is_indexed: Mapped[bool] = mapped_column(Boolean, default=True) # виден в API
is_searchable: Mapped[bool] = mapped_column(Boolean, default=True) # полнотекстовый поиск
# Временные метки
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) # когда последний раз сканировался
# Связи
project: Mapped["Project"] = relationship(back_populates="files")
```
### 3.3 Обновление модели Project
```python
class Project(Base):
# ... существующие поля ...
# Новая связь
files: Mapped[list["ProjectFile"]] = relationship(back_populates="project", cascade="all, delete-orphan")
```
### 3.4 Конфигурация файловой системы
```python
class FileSystemConfig(Base):
"""Global file system configuration."""
__tablename__ = "fs_config"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
# Пути
workspace_path: Mapped[str] = mapped_column(String(1000), nullable=False) # /data/team-board-workspace
# Лимиты
max_file_size_mb: Mapped[int] = mapped_column(Integer, default=100) # 100MB
max_files_per_project: Mapped[int] = mapped_column(Integer, default=1000)
max_total_size_gb: Mapped[int] = mapped_column(Integer, default=10) # 10GB на проект
# Разрешённые типы
allowed_extensions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # .pdf, .md, .png
blocked_extensions: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # .exe, .bat
# Автоматизация
auto_scan_interval: Mapped[int] = mapped_column(Integer, default=300) # секунды (5 минут)
enable_thumbnails: Mapped[bool] = mapped_column(Boolean, default=True)
enable_text_extraction: Mapped[bool] = mapped_column(Boolean, default=True) # для поиска
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now())
```
---
## 4. REST API Endpoints
### 4.1 Project Files API
```python
# GET /api/projects/{project_slug}/files
async def list_project_files(
project_slug: str,
path: Optional[str] = Query(None), # фильтр по пути
tags: Optional[List[str]] = Query(None), # фильтр по тегам
mime_type: Optional[str] = Query(None), # фильтр по типу
search: Optional[str] = Query(None), # поиск в содержимом
limit: int = Query(50, le=200),
offset: int = Query(0),
db: AsyncSession = Depends(get_db)
) -> List[ProjectFileOut]
# POST /api/projects/{project_slug}/files/upload
async def upload_project_file(
project_slug: str,
file: UploadFile,
path: Optional[str] = Form(None), # подпапка в files/
description: Optional[str] = Form(None),
tags: Optional[str] = Form(None), # JSON array
member_slug: str = Depends(get_current_member),
db: AsyncSession = Depends(get_db)
) -> ProjectFileOut
# GET /api/projects/{project_slug}/files/{file_id}
async def get_project_file(
project_slug: str,
file_id: str,
db: AsyncSession = Depends(get_db)
) -> ProjectFileOut
# GET /api/projects/{project_slug}/files/{file_id}/download
async def download_project_file(
project_slug: str,
file_id: str,
db: AsyncSession = Depends(get_db)
) -> FileResponse
# PUT /api/projects/{project_slug}/files/{file_id}
async def update_project_file(
project_slug: str,
file_id: str,
req: ProjectFileUpdate,
db: AsyncSession = Depends(get_db)
) -> ProjectFileOut
# DELETE /api/projects/{project_slug}/files/{file_id}
async def delete_project_file(
project_slug: str,
file_id: str,
db: AsyncSession = Depends(get_db)
) -> Dict[str, bool]
# POST /api/projects/{project_slug}/files/scan
async def scan_project_files(
project_slug: str,
db: AsyncSession = Depends(get_db)
) -> Dict[str, Any] # статистика сканирования
```
### 4.2 Attachments API (расширение существующего)
```python
# POST /api/messages/{message_id}/attachments
async def upload_message_attachment(
message_id: str,
file: UploadFile,
db: AsyncSession = Depends(get_db)
) -> AttachmentOut
# GET /api/messages/{message_id}/attachments
async def list_message_attachments(
message_id: str,
db: AsyncSession = Depends(get_db)
) -> List[AttachmentOut]
# GET /api/attachments/{attachment_id}/download
async def download_attachment(
attachment_id: str,
db: AsyncSession = Depends(get_db)
) -> FileResponse
# DELETE /api/attachments/{attachment_id}
async def delete_attachment(
attachment_id: str,
db: AsyncSession = Depends(get_db)
) -> Dict[str, bool]
# POST /api/tasks/{task_id}/attachments
async def upload_task_attachment(
task_id: str,
file: UploadFile,
db: AsyncSession = Depends(get_db)
) -> AttachmentOut
```
### 4.3 File System Management API
```python
# GET /api/admin/files/config
async def get_fs_config() -> FileSystemConfigOut
# PUT /api/admin/files/config
async def update_fs_config(req: FileSystemConfigUpdate) -> FileSystemConfigOut
# POST /api/admin/files/scan-all
async def scan_all_projects() -> Dict[str, Any] # глобальное сканирование
# GET /api/admin/files/stats
async def get_storage_stats() -> Dict[str, Any] # использование места, статистика
# POST /api/admin/files/cleanup
async def cleanup_orphaned_files() -> Dict[str, Any] # очистка сирых файлов
```
---
## 5. Синхронизация файловой системы и БД
### 5.1 Проблема синхронизации
**Вызов:** Файлы добавленные вручную в `files/` должны быть видны через API без перезапуска системы.
**Решения:**
1. **inotify** (Linux) — мониторинг файловой системы в реальном времени
2. **Periodic scan** — сканирование каждые N минут
3. **On-demand scan** — сканирование при API запросе
4. **Гибридный подход** (рекомендуется)
### 5.2 Гибридная реализация
```python
class FileSystemSyncService:
def __init__(self, workspace_path: str, db: AsyncSession):
self.workspace_path = workspace_path
self.db = db
self.observer = None # inotify observer
async def start_monitoring(self):
"""Запуск inotify мониторинга для активных проектов."""
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
class ProjectFileHandler(FileSystemEventHandler):
def __init__(self, sync_service):
self.sync_service = sync_service
def on_created(self, event):
if not event.is_directory:
asyncio.create_task(
self.sync_service.handle_file_created(event.src_path)
)
def on_deleted(self, event):
if not event.is_directory:
asyncio.create_task(
self.sync_service.handle_file_deleted(event.src_path)
)
def on_modified(self, event):
if not event.is_directory:
asyncio.create_task(
self.sync_service.handle_file_modified(event.src_path)
)
self.observer = Observer()
handler = ProjectFileHandler(self)
# Мониторим папки files/ всех активных проектов
projects = await self.get_active_projects()
for project in projects:
files_path = f"{self.workspace_path}/projects/{project.slug}/files"
if os.path.exists(files_path):
self.observer.schedule(handler, files_path, recursive=True)
self.observer.start()
async def handle_file_created(self, file_path: str):
"""Обработка создания файла."""
try:
project_slug, relative_path = self.extract_project_info(file_path)
if not project_slug:
return
# Проверяем, не дублируется ли файл
existing = await self.find_existing_file(project_slug, relative_path)
if existing:
return
# Создаём запись в БД
file_info = await self.analyze_file(file_path)
project_file = ProjectFile(
project_id=await self.get_project_id(project_slug),
filename=os.path.basename(file_path),
relative_path=relative_path,
mime_type=file_info.mime_type,
size=file_info.size,
checksum=file_info.checksum,
source="manual",
is_indexed=True,
indexed_at=datetime.now()
)
self.db.add(project_file)
await self.db.commit()
# Уведомляем через WebSocket о новом файле
await self.broadcast_file_event("file.created", project_slug, project_file)
except Exception as e:
logger.error(f"Error handling file creation {file_path}: {e}")
async def periodic_scan(self, project_slug: str = None):
"""Периодическое сканирование (fallback для inotify)."""
if project_slug:
projects = [await self.get_project(project_slug)]
else:
projects = await self.get_active_projects()
for project in projects:
await self.scan_project_files(project)
async def scan_project_files(self, project: Project):
"""Сканирование файлов проекта."""
files_path = f"{self.workspace_path}/projects/{project.slug}/files"
if not os.path.exists(files_path):
return
# Сканируем файловую систему
fs_files = {}
for root, dirs, files in os.walk(files_path):
for file in files:
full_path = os.path.join(root, file)
relative_path = os.path.relpath(full_path, files_path)
stat = os.stat(full_path)
fs_files[relative_path] = {
'size': stat.st_size,
'mtime': stat.st_mtime,
'full_path': full_path
}
# Получаем файлы из БД
db_files = await self.get_project_files_dict(project.id)
# Находим новые файлы
for relative_path, file_info in fs_files.items():
if relative_path not in db_files:
await self.create_db_file_record(project, relative_path, file_info)
# Находим удалённые файлы
for relative_path in db_files:
if relative_path not in fs_files:
await self.mark_file_as_deleted(db_files[relative_path])
# Находим изменённые файлы
for relative_path in set(fs_files.keys()) & set(db_files.keys()):
db_file = db_files[relative_path]
fs_file = fs_files[relative_path]
if fs_file['size'] != db_file.size:
await self.update_file_record(db_file, fs_file)
```
### 5.3 Стратегии по типам операций
| Операция | Метод | Когда |
|----------|-------|-------|
| **Добавление файла** | inotify → немедленная индексация | Real-time |
| **Удаление файла** | inotify → пометка как deleted | Real-time |
| **Изменение файла** | inotify → обновление метаданных | Real-time |
| **Fallback scan** | Periodic (каждые 5 минут) | Если inotify недоступен |
| **Full rescan** | On-demand (API или cron) | При несоответствиях |
| **Startup scan** | При запуске сервиса | Синхронизация после рестарта |
### 5.4 Индексация и метаданные
```python
class FileIndexer:
"""Извлечение метаданных и индексация контента."""
async def analyze_file(self, file_path: str) -> FileAnalysis:
"""Полный анализ файла."""
mime_type = self.detect_mime_type(file_path)
size = os.path.getsize(file_path)
checksum = await self.calculate_checksum(file_path)
# Извлекаем текст для поиска (если возможно)
text_content = None
if self.is_text_extractable(mime_type):
text_content = await self.extract_text(file_path, mime_type)
# Генерируем thumbnail для изображений
thumbnail_path = None
if self.is_image(mime_type):
thumbnail_path = await self.generate_thumbnail(file_path)
return FileAnalysis(
mime_type=mime_type,
size=size,
checksum=checksum,
text_content=text_content,
thumbnail_path=thumbnail_path
)
async def extract_text(self, file_path: str, mime_type: str) -> str:
"""Извлечение текста для полнотекстового поиска."""
if mime_type == 'application/pdf':
return await self.extract_pdf_text(file_path)
elif mime_type.startswith('text/'):
return await self.read_text_file(file_path)
elif mime_type in ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']:
return await self.extract_doc_text(file_path)
# ... другие форматы
return None
```
---
## 6. Интеграция с агентами (MCP Tools)
### 6.1 File Tools для MCP
```python
# В picogent/src/tools/files.ts
export const listProjectFilesTool: ToolDefinition = {
name: 'list_project_files',
title: 'Список файлов проекта',
description: 'Получить список всех файлов в проекте с фильтрацией',
inputSchema: {
type: 'object',
properties: {
project_slug: { type: 'string', description: 'Slug проекта' },
path: { type: 'string', description: 'Фильтр по пути (например, docs/)' },
search: { type: 'string', description: 'Поиск в содержимом файлов' },
tags: { type: 'array', items: { type: 'string' }, description: 'Фильтр по тегам' },
mime_type: { type: 'string', description: 'Фильтр по типу файла' }
},
required: ['project_slug']
},
async handler(params, context) {
const files = await context.trackerClient.listProjectFiles(params);
return {
content: [{
type: 'text',
text: `Найдено файлов: ${files.length}\n${files.map(f => `- ${f.relative_path} (${f.size} bytes)`).join('\n')}`
}],
structuredContent: files
};
}
};
export const uploadProjectFileTool: ToolDefinition = {
name: 'upload_project_file',
title: 'Загрузить файл в проект',
description: 'Загрузить файл в папку files/ проекта',
inputSchema: {
type: 'object',
properties: {
project_slug: { type: 'string', description: 'Slug проекта' },
filename: { type: 'string', description: 'Имя файла' },
content: { type: 'string', description: 'Содержимое файла (base64 для бинарных)' },
path: { type: 'string', description: 'Подпапка в files/ (например, docs/)' },
description: { type: 'string', description: 'Описание файла' },
tags: { type: 'array', items: { type: 'string' }, description: 'Теги для категоризации' }
},
required: ['project_slug', 'filename', 'content']
},
async handler(params, context) {
const result = await context.trackerClient.uploadProjectFile(params);
return {
content: [{
type: 'text',
text: `✅ Файл ${result.filename} загружен в ${result.relative_path}`
}],
structuredContent: result
};
}
};
export const downloadProjectFileTool: ToolDefinition = {
name: 'download_project_file',
title: 'Скачать файл из проекта',
description: 'Скачать содержимое файла проекта',
inputSchema: {
type: 'object',
properties: {
project_slug: { type: 'string', description: 'Slug проекта' },
file_id: { type: 'string', description: 'ID файла' }
},
required: ['project_slug', 'file_id']
},
async handler(params, context) {
const fileData = await context.trackerClient.downloadProjectFile(params.project_slug, params.file_id);
return {
content: [{
type: 'text',
text: `📄 Файл ${fileData.filename} (${fileData.size} bytes):\n\n${fileData.content}`
}],
structuredContent: fileData
};
}
};
export const uploadAttachmentTool: ToolDefinition = {
name: 'upload_attachment',
title: 'Прикрепить файл к сообщению',
description: 'Загрузить файл как attachment к сообщению или задаче',
inputSchema: {
type: 'object',
properties: {
message_id: { type: 'string', description: 'ID сообщения (для чата)' },
task_id: { type: 'string', description: 'ID задачи (для комментария)' },
filename: { type: 'string', description: 'Имя файла' },
content: { type: 'string', description: 'Содержимое файла (base64)' }
},
required: ['filename', 'content']
},
async handler(params, context) {
// Одно из полей должно быть заполнено
if (!params.message_id && !params.task_id) {
throw new Error('Необходимо указать message_id или task_id');
}
const result = await context.trackerClient.uploadAttachment(params);
return {
content: [{
type: 'text',
text: `📎 Файл ${result.filename} прикреплён`
}],
structuredContent: result
};
}
};
export const scanProjectFilesTool: ToolDefinition = {
name: 'scan_project_files',
title: 'Сканировать файлы проекта',
description: 'Принудительно сканировать директорию проекта и синхронизировать с БД',
inputSchema: {
type: 'object',
properties: {
project_slug: { type: 'string', description: 'Slug проекта' }
},
required: ['project_slug']
},
async handler(params, context) {
const result = await context.trackerClient.scanProjectFiles(params.project_slug);
return {
content: [{
type: 'text',
text: `🔄 Сканирование завершено:\n- Найдено: ${result.found}\n- Добавлено: ${result.added}\n- Обновлено: ${result.updated}\n- Удалено: ${result.deleted}`
}],
structuredContent: result
};
}
};
```
### 6.2 Типичные сценарии использования агентами
**1. Изучение документации проекта:**
```typescript
// Агент получил задачу и изучает контекст
const projectFiles = await tools.list_project_files({
project_slug: "team-board",
path: "docs/",
search: "API"
});
for (const file of projectFiles.filter(f => f.filename.endsWith('.md'))) {
const content = await tools.download_project_file({
project_slug: "team-board",
file_id: file.id
});
// Анализируем содержимое...
}
```
**2. Создание технической документации:**
```typescript
// Агент создаёт техническую спецификацию по результатам работы
await tools.upload_project_file({
project_slug: "team-board",
filename: "FILES-ARCHITECTURE.md",
content: btoa(documentContent), // base64 encoding
path: "docs/",
description: "Архитектура файловой системы",
tags: ["documentation", "architecture"]
});
```
**3. Прикрепление файлов к отчётам:**
```typescript
// Агент выполнил задачу и прикрепляет лог или скриншот
await tools.upload_attachment({
task_id: "550e8400-e29b-41d4-a716-446655440001",
filename: "test-results.log",
content: btoa(testResults)
});
```
---
## 7. Лимиты, квоты и безопасность
### 7.1 Ограничения по размеру и типам
```python
class FileValidationService:
def __init__(self, config: FileSystemConfig):
self.config = config
async def validate_upload(self, file: UploadFile, project: Project, member: Member) -> None:
"""Валидация загружаемого файла."""
# Проверка размера файла
if file.size > self.config.max_file_size_mb * 1024 * 1024:
raise HTTPException(
status_code=413,
detail=f"Файл слишком большой. Максимум: {self.config.max_file_size_mb}MB"
)
# Проверка расширения
extension = os.path.splitext(file.filename)[1].lower()
if extension in self.config.blocked_extensions:
raise HTTPException(
status_code=400,
detail=f"Тип файла {extension} запрещён"
)
if (self.config.allowed_extensions and
extension not in self.config.allowed_extensions):
raise HTTPException(
status_code=400,
detail=f"Тип файла {extension} не разрешён"
)
# Проверка квоты проекта
current_usage = await self.get_project_storage_usage(project.id)
max_size_bytes = self.config.max_total_size_gb * 1024 * 1024 * 1024
if current_usage + file.size > max_size_bytes:
raise HTTPException(
status_code=413,
detail=f"Превышена квота проекта ({self.config.max_total_size_gb}GB)"
)
# Проверка количества файлов
file_count = await self.get_project_file_count(project.id)
if file_count >= self.config.max_files_per_project:
raise HTTPException(
status_code=413,
detail=f"Превышено максимальное количество файлов ({self.config.max_files_per_project})"
)
async def scan_file_security(self, file_path: str) -> SecurityScanResult:
"""Базовое сканирование безопасности."""
result = SecurityScanResult(safe=True, warnings=[])
# Проверка на исполняемые файлы
if self.is_executable(file_path):
result.warnings.append("Файл может содержать исполняемый код")
# Проверка размера (защита от zip-bomb)
if os.path.getsize(file_path) > 500 * 1024 * 1024: # 500MB
result.safe = False
result.warnings.append("Файл слишком большой для обработки")
# Проверка на подозрительные расширения в архивах
if file_path.endswith(('.zip', '.tar', '.gz')):
suspicious_files = await self.scan_archive_contents(file_path)
if suspicious_files:
result.warnings.extend(suspicious_files)
return result
```
### 7.2 Дефолтные лимиты
| Ограничение | Значение | Настраиваемо |
|-------------|----------|--------------|
| **Размер файла** | 100MB | ✅ |
| **Файлов на проект** | 1000 | ✅ |
| **Общий размер проекта** | 10GB | ✅ |
| **Разрешённые типы** | Список расширений | ✅ |
| **Запрещённые типы** | .exe, .bat, .sh, .scr | ✅ |
### 7.3 Безопасность
**Защита путей:**
```python
def sanitize_file_path(user_path: str) -> str:
"""Очистка пути от path traversal атак."""
# Удаляем .. и другие опасные конструкции
clean_path = os.path.normpath(user_path)
clean_path = clean_path.replace('..', '').replace('~', '')
# Убираем ведущие слеши
clean_path = clean_path.lstrip('/')
return clean_path
```
**Контроль доступа:**
- Агенты видят только файлы проектов, к которым у них есть доступ
- Attachments доступны только участникам чата/задачи
- Администраторы имеют полный доступ
---
## 8. Превью изображений и обработка
### 8.1 Генерация превью
```python
class ThumbnailService:
def __init__(self, workspace_path: str):
self.workspace_path = workspace_path
self.thumbnail_sizes = [(150, 150), (300, 300)] # small, medium
async def generate_thumbnails(self, file_path: str, file_id: str) -> List[str]:
"""Генерация превью для изображений."""
if not self.is_image_file(file_path):
return []
thumbnails = []
try:
from PIL import Image
with Image.open(file_path) as image:
# Конвертируем в RGB если нужно
if image.mode in ('RGBA', 'LA', 'P'):
image = image.convert('RGB')
for size in self.thumbnail_sizes:
thumbnail = image.copy()
thumbnail.thumbnail(size, Image.Resampling.LANCZOS)
# Путь для превью
thumb_filename = f"{file_id}_{size[0]}x{size[1]}.jpg"
thumb_path = os.path.join(
self.workspace_path,
"shared/thumbnails",
thumb_filename
)
# Создаём директорию если не существует
os.makedirs(os.path.dirname(thumb_path), exist_ok=True)
# Сохраняем превью
thumbnail.save(thumb_path, 'JPEG', quality=85)
thumbnails.append(f"shared/thumbnails/{thumb_filename}")
except Exception as e:
logger.error(f"Error generating thumbnails for {file_path}: {e}")
return thumbnails
def is_image_file(self, file_path: str) -> bool:
"""Проверка, является ли файл изображением."""
mime_type = mimetypes.guess_type(file_path)[0]
return mime_type and mime_type.startswith('image/')
```
### 8.2 Обновление модели для превью
```python
class ProjectFile(Base):
# ... существующие поля ...
# Превью
thumbnail_paths: Mapped[list[str]] = mapped_column(ARRAY(String), default=list) # пути к превью
has_thumbnails: Mapped[bool] = mapped_column(Boolean, default=False)
# Извлечённый текст для поиска
extracted_text: Mapped[str | None] = mapped_column(Text)
is_text_extracted: Mapped[bool] = mapped_column(Boolean, default=False)
```
---
## 9. Ответы на исследовательские вопросы
### 1. Как организовать структуру директорий?
**Решение:** Двухуровневая структура:
- `workspace/projects/<project-slug>/files/` — проектные файлы (ручное добавление)
- `workspace/projects/<project-slug>/attachments/` — файлы из сообщений/задач
- Подпапки по типу контента: docs/, assets/, configs/
- Метаданные в `.meta/` для каждого проекта
### 2. Как синхронизировать файлы положенные руками с БД?
**Решение:** Гибридный подход:
- **inotify** для мониторинга файловой системы в реальном времени
- **Periodic scan** (каждые 5 минут) как fallback
- **On-demand scan** через API endpoint
- **Startup scan** при запуске сервиса
### 3. Как привязывать attachments к сообщениям/задачам?
**Решение:** Используем существующую модель `Attachment`:
- Поле `message_id` для привязки к сообщению
- Сообщения могут быть привязаны к чату (`chat_id`) или задаче (`task_id`)
- Файлы хранятся в `attachments/messages/<message-id>/` или `attachments/tasks/<task-id>/`
### 4. Нужна ли БД таблица для файлов?
**Решение:** Да, нужно две таблицы:
- `project_files` — для файлов в папке `files/` (новая)
- `attachments` — для attachments в сообщениях (существующая)
- Обе содержат метаданные, пути, размеры, checksums
- БД обеспечивает быстрый поиск, фильтрацию, авторизацию
### 5. Как агенты будут скачивать/загружать файлы через MCP tools?
**Решение:** 6 MCP tools для файлов:
- `list_project_files` — список с фильтрацией
- `upload_project_file` — загрузка в files/
- `download_project_file` — скачивание с декодированием
- `upload_attachment` — прикрепление к сообщению/задаче
- `scan_project_files` — принудительная синхронизация
- Base64 encoding для передачи бинарных данных
### 6. Лимиты: размер файла, типы, квоты?
**Решение:** Настраиваемые лимиты через `FileSystemConfig`:
- Размер файла: 100MB (по умолчанию)
- Файлов на проект: 1000
- Общий размер проекта: 10GB
- Разрешённые/запрещённые расширения
- Валидация и security scanning при загрузке
### 7. Превью для изображений?
**Решение:** Автоматическая генерация превью:
- При загрузке изображений создаются thumbnails (150x150, 300x300)
- Хранятся в `shared/thumbnails/`
- Пути сохраняются в `thumbnail_paths` поле
- Доступны через отдельный API endpoint
---
## 10. Примеры использования
### 10.1 Пользователь добавляет файл вручную
```bash
# Пользователь кладёт файл
cp ~/documents/api-spec.md /data/team-board-workspace/projects/team-board/files/docs/
# Система автоматически (через inotify):
# 1. Обнаруживает новый файл
# 2. Анализирует (MIME, размер, checksum)
# 3. Создаёт запись в project_files
# 4. Извлекает текст для поиска
# 5. Уведомляет через WebSocket
# Агент получает уведомление и может сразу прочитать файл
await tools.list_project_files({
project_slug: "team-board",
search: "API"
});
```
### 10.2 Агент загружает результат работы
```typescript
// Агент создал диаграмму архитектуры
const diagram = await generateArchitectureDiagram();
await tools.upload_project_file({
project_slug: "team-board",
filename: "architecture-diagram.png",
content: btoa(diagram), // base64
path: "docs/diagrams/",
description: "Диаграмма архитектуры файловой системы",
tags: ["diagram", "architecture"]
});
// Прикрепляем к задаче как attachment
await tools.upload_attachment({
task_id: taskId,
filename: "architecture-diagram.png",
content: btoa(diagram)
});
```
### 10.3 Поиск и анализ файлов проекта
```typescript
// Агент ищет все документы по API
const apiDocs = await tools.list_project_files({
project_slug: "team-board",
search: "API endpoint",
path: "docs/"
});
// Читает содержимое для анализа
for (const doc of apiDocs) {
const content = await tools.download_project_file({
project_slug: "team-board",
file_id: doc.id
});
// Анализирует API endpoints
const endpoints = parseApiDocumentation(content.content);
// ...
}
```
---
## Заключение
Предложенная архитектура файловой системы для Team Board обеспечивает:
**Простоту использования** — файлы можно добавлять вручную и через API
**Автоматическую синхронизацию** — inotify + periodic scan
**Богатый API** — REST endpoints + MCP tools для агентов
**Безопасность** — валидация, квоты, контроль доступа
**Масштабируемость** — индексация, поиск, превью
**Интеграцию** — единая модель для attachments и project files
**Ключевые преимущества:**
- Агенты получают полный доступ к файлам проекта через 6 MCP tools
- Пользователи могут добавлять файлы вручную без перезапуска
- Автоматическая индексация и поиск по содержимому
- Превью изображений и метаданные
- Настраиваемые лимиты и безопасность
Архитектура готова к реализации и может быть развёрнута поэтапно, начиная с базового функционала и постепенно добавляя продвинутые возможности.