# Дизайн объектного хранилища
## Обзор
noa использует контентно-адресуемую модель хранения, вдохновлённую Git, но с
подключаемой архитектурой бэкенда. Объекты адресуются по хешу SHA-256
и хранятся как непрозрачные блобы.
## Типы объектов
### Блоб (Blob)
Сырое содержимое файла. Идентифицируется по `SHA256(content)`.
```rust
pub struct BlobId(pub String); // hex-закодированный SHA-256
```
Без дельта-сжатия. Каждое уникальное содержимое создаёт ровно один блоб.
Дублирующееся содержимое автоматически дедуплицируется по хешу.
### Дерево (Tree)
Список директории. Сопоставляет пути с дочерними записями (блобы или поддеревья).
```rust
pub struct TreeEntry {
pub name: String,
pub kind: TreeEntryKind, // Blob или Tree
pub hash: String, // SHA-256 дочернего элемента
}
pub struct TreeId(pub String); // SHA-256(msgpack(entries))
```
Деревья сериализуются в MessagePack для компактности и быстрой десериализации.
## Определение трейта
```rust
#[async_trait]
pub trait ObjectStore: Send + Sync {
async fn put_blob(&self, data: &[u8]) -> Result<BlobId>;
async fn get_blob(&self, id: &BlobId) -> Result<Vec<u8>>;
async fn put_tree(&self, entries: Vec<TreeEntry>) -> Result<TreeId>;
async fn get_tree(&self, id: &TreeId) -> Result<Vec<TreeEntry>>;
}
```
## Бэкенды
### RedbObjectStore (Локальный)
Использует встроенное хранилище ключ-значение [redb](https://github.com/cberner/redb).
- Две таблицы: `blobs` (ключ: байты хеша, значение: байты содержимого) и
`trees` (ключ: байты хеша, значение: msgpack записи)
- Чтение без копирования через файлы, отображаемые в память
- ACID-транзакции с автоматическим восстановлением после сбоев
- Один писатель, много читателей через MVCC
- Не требует внешнего демона
### MinioObjectStore (Удалённый)
Использует S3-совместимый API через `aws-sdk-s3`.
- Адресация в стиле путей: `<bucket>/blobs/<hash>`, `<bucket>/trees/<hash>`
- Поддерживает любой S3-совместимый бэкенд (MinIO, AWS S3, GCS и т.д.)
- Автоматические повторные попытки с экспоненциальной задержкой
- Подходит для распределённых развёртываний
## Проектные решения
### Почему SHA-256 вместо SHA-1?
Git использует SHA-1, который криптографически сломан (атака SHAttered, 2017).
SHA-256 устойчив к коллизиям и широко доступен.
### Почему без дельта-сжатия?
1. **Простота**: Дельта-сжатие (pack-файлы Git) значительно усложняет систему
(сопоставление скользящим окном, тонкие паки, цепочки дельт).
2. **Производительность записи**: Прямая запись блобов — O(1). Дельта-сжатие
требует чтения существующих объектов.
3. **Рабочая нагрузка ИИ-агентов**: Агенты часто перегенерируют целые файлы.
Старые версии эфемерны — цепочки дельт будут короткими и многочисленными.
4. **Разгрузка на бэкенд**: S3/MinIO обрабатывают дедупликацию на уровне хранения.
### Почему MessagePack для деревьев?
- На 30-50% компактнее JSON для данных с большим количеством бинарных данных
- Гибкая схема (не нужны определения protobuf)
- Поддержка в экосистеме Rust через `rmp-serde`
- Быстрая десериализация
### Почему redb вместо SQLite?
- **Безопасность типов**: redb использует обобщения Rust для определений таблиц
- **Производительность**: redb оптимизирован для рабочих нагрузок Rust (чтение без копирования)
- **Простота**: Одна зависимость, без связывания с C-библиотекой
- **Безопасность при сбоях**: Журнал упреждающей записи redb проще, чем режим WAL SQLite
Компромисс: у redb меньшее сообщество и меньше инструментов, чем у SQLite.
Для варианта использования noa (встроенное бинарное хранилище) этот компромисс оправдан.