# API хранилища fizmat.ai

Справка для модели и человека. Через API можно всё, что можно на сайте
в разделе «Хранилище»: заводить и править пакеты, части и блоки, класть
вложения любого вида, искать, импортировать и экспортировать пакеты,
раскладывать пакеты по папкам, делиться ими, предлагать в каталог.
Ключ видит ровно то, что его владелец: свои пакеты, те, которыми с ним
поделились, и каталог. Распознавание, индексация, теги каталога и
резервные копии — только ключу владельца сайта.

Этот текст отдаётся по адресу `GET /api/v1/help`, формат пакетов (как
резать книгу на блоки, какие бывают типы и атрибуты) — по
`GET /api/v1/help/uip-format`. Прочитайте оба, прежде чем что-то
заливать.

## 1. Основное

**Адрес:** `https://fizmat.ai/api/v1`

**Ключ:** каждый запрос несёт заголовок `Authorization: Bearer fz_…`.
Ключ выпускает себе каждый сам — меню аватара → «Ключи API»; он действует
от имени выпустившего, права те же, что у него на сайте. Ключ бывает
только для чтения (запись — `403`) и со сроком (вышел — `401`). Сколько
ключей, можно ли писать и сколько запросов в минуту — по подписке
(лишнее — `429`). Без ключа — `401`, с ключом человека, которому
хранилище закрыто, — `404`.

**Формат:** JSON в обе стороны (`Content-Type: application/json`),
кроме загрузки файлов — там `multipart/form-data`. Ответ всегда JSON,
какой бы `Accept` ни был. Кириллица — UTF-8; в ответах она в виде
`\uXXXX`-кодов, как принято в JSON, — любой парсер восстановит её сам.

**Ошибки:** `{"message": "…"}`; у ошибок валидации ещё `errors`
по полям. Коды: `401` нет ключа, `404` нет такой сущности (или нет
доступа), `413` слишком большой файл, `422` неверные данные,
`429` слишком часто (600 запросов в минуту на ключ), `500` — поломка
на сервере, повторите позже.

**Идентификаторы:**

- пакет — ULID (`01m30fwpnsxvqdvbhzpd4nmrsj`) в пути; в фильтрах поиска
  можно алиасом (`ru.bmstu.lizin-pyatkin`);
- часть — число `id`;
- блок — число `id` **или** свой идентификатор `block_id`
  (`thm-fourier`, `eq-3-12`) в `GET /packages/{package}/blocks/{id}`;
  в остальных путях `/blocks/{block}` — только число;
- вложение — число `id`; в тексте на него ссылаются как
  `uip://#asset:{asset_id}`;
- адрес блока для ссылок: `uip://{alias или ULID}#{block_id}`; внутри
  того же пакета — `uip://#{block_id}`.

**Порядок блоков.** Блоки лежат плоско в порядке чтения внутри части;
структуру держат разделы (`section`, уровни 1–4). Куда вставить —
параметр `after`: `id` блока, за которым встать; без него — в конец
части (`part`), а без части — в конец первой части пакета.

## 2. Кто я

```
GET /me
```

```json
{
  "user": {"id": 1, "name": "owner"},
  "super_admin": true,
  "package_types": {"article": "Статья", "book": "Книга", "lecture": "Лекция", "seminar": "Семинар",
                    "problem": "Задача", "problemset": "Задачник", "document": "Документ",
                    "note": "Заметка", "dataset": "Набор данных"},
  "block_types": {"section": "Раздел", "paragraph": "Абзац", "definition": "Определение", "…": "…"},
  "link_rels": {"cites": "ссылается на", "quotes": "цитирует", "prerequisite": "требует знания", "…": "…"},
  "ocr_modes": {"fast": "Быстро", "balanced": "Обычно", "accurate": "Точно"}
}
```

Словари здесь — истина: типы пакетов, типы блоков, виды связей и
режимы OCR берите отсюда, а не из памяти.

## 3. Пакеты

| Метод и путь | Что делает |
|---|---|
| `GET /packages` | список, свежие сверху; фильтры `q=` (по названию/алиасу), `type=`, `tags[]=` (id тегов), `scope=own\|shared\|catalog`, `folder=<id>\|root`. Порциями: `per_page=` (до 500, по умолчанию 200), `page=`; в ответе `next` — номер следующей порции, `null` — последняя |
| `POST /packages` | завести пакет |
| `GET /packages/{package}` | паспорт с частями и вложениями |
| `PATCH /packages/{package}` | поправить паспорт (любые из полей) |
| `DELETE /packages/{package}` | удалить пакет со всеми блоками, вложениями и связями — **в корзину** на 30 дней (раздел 12) |
| `PUT /packages/{package}/tags` | поставить теги целиком: `{"tags": [1, 5]}` |
| `GET /packages/{package}/export?format=uip` | скачать `.uip` (zip); `format=md` — один Markdown; `no-assets=1` — без вложений |

Поля паспорта (`POST`/`PATCH`):

```json
{
  "title": "Расчёт тонкостенных конструкций",
  "type": "book",
  "alias": "ru.bmstu.lizin-pyatkin",
  "subtitle": "Учебное пособие",
  "authors": ["Лизин В. Т.", "Пяткин В. А."],
  "langs": ["ru"],
  "tags": [3, 7]
}
```

Обязательны при создании только `title` и `type`. `alias` — строчная
латиница, цифры, точка, дефис, уникален во всём хранилище; по нему
строятся адреса `uip://`; начинается с вашего ника и точки (`ник.имя`,
см. раздел 12а). `source_id` в ответе — из какого пакета сделана копия
(«Добавить к себе» из каталога или «Копия себе»), у своих — `null`. Ответ — `{"package": {…}}` в той же форме,
что и `GET /packages/{package}`:

```json
{
  "package": {
    "id": "01m30fwpnsxvqdvbhzpd4nmrsj", "alias": "ru.bmstu.lizin-pyatkin",
    "uri": "uip://ru.bmstu.lizin-pyatkin", "title": "…", "subtitle": "…",
    "type": "book", "type_label": "Книга", "authors": ["…"], "langs": ["ru"],
    "tags": [{"id": 3, "title": "Сопротивление материалов", "type": "Предмет"}],
    "blocks": 3550, "created_at": "…", "updated_at": "…",
    "url": "https://fizmat.ai/storage/p/01m30fwpnsxvqdvbhzpd4nmrsj",
    "source_id": null,
    "parts": [{"id": 12, "title": "ГЛАВА 1. …", "position": 1, "blocks": 240}],
    "assets": [{"id": 5, "asset_id": "p55-fig1", "reference": "#asset:p55-fig1", "name": "fig1.png",
                "mime": "image/png", "bytes": 48213, "sha256": "…", "pages": null, "indexable": false,
                "content_url": "https://fizmat.ai/api/v1/assets/5/content"}]
  }
}
```

## 4. Части

Часть — глава или том; у нового пакета первая часть заводится сама при
первом блоке. Ориентир: 100–400 блоков на часть.

| Метод и путь | Тело | Что делает |
|---|---|---|
| `GET /packages/{package}/parts` | — | список `{id, title, position, blocks}` |
| `POST /packages/{package}/parts` | `{"title": "ГЛАВА 2. …"}` | новая часть в конец |
| `PATCH /parts/{part}` | `{"title": "…"}` | переименовать |
| `POST /parts/{part}/move` | `{"after": 12}` или `{}` | переставить за часть `after`, без него — в начало |
| `POST /blocks/{block}/split-part` | `{"title": "ГЛАВА 2. …"}` | разрезать часть по этому блоку: он и всё, что ниже, уезжают в новую часть сразу за нынешней |
| `DELETE /parts/{part}` | — | удалить **со всеми блоками**; последнюю часть удалить нельзя (`422`) |

## 5. Блоки

| Метод и путь | Что делает |
|---|---|
| `GET /packages/{package}/blocks` | все блоки пакета в порядке чтения; `?part=12` — одной части |
| `GET /packages/{package}/blocks/{id}` | один блок по `id` или `block_id` |
| `GET /blocks/{block}` | один блок по `id` |
| `POST /packages/{package}/blocks` | создать |
| `PATCH /blocks/{block}` | изменить любые из: `title`, `body`, `type`, `level`, `attrs`, `indexable` |
| `POST /blocks/{block}/move` | `{"after": 345, "part": 12, "with_range": true}` — переставить; `with_range` двигает раздел вместе с содержимым |
| `DELETE /blocks/{block}?with_range=1` | удалить; `with_range` — раздел с содержимым, иначе содержимое остаётся и переходит заголовку выше |
| `GET /blocks/{block}/versions` | история правок тела |
| `POST /blocks/{block}/versions/{version}/restore` | вернуть версию |
| `POST /blocks/{block}/run` | выполнить ячейку `ipynb` (вместе с ячейками части до неё) или собрать заново рисунок `tikz` |

Блок в ответах:

```json
{
  "id": 345, "block_id": "thm-3-1", "uri": "uip://ru.bmstu.lizin-pyatkin#thm-3-1",
  "package_id": "01m30…", "part_id": 12,
  "type": "theorem", "level": null, "title": "Теорема 3.1",
  "body": "При стационарном режиме поток через плоскую стенку постоянен.",
  "attrs": {"page": "41"}, "number": null, "position": 1200.0,
  "indexable": true, "tokens": 31,
  "breadcrumb": ["ГЛАВА 3. …", "3.1. Гладкие оболочки"], "updated_at": "…",
  "rev": "5f0c…"
}
```

`number` — номер формулы `(3.12)` или источника `[1]`: свой из книги
(`attrs.tag`) либо порядковый в части. `level` — уровень раздела 1–4,
у остальных типов `null`.

Создание — `POST /packages/{package}/blocks`:

```json
{
  "type": "equation",
  "part": 12,
  "after": 344,
  "title": null,
  "body": "q = -\\lambda \\nabla T",
  "attrs": {"tag": "3.12", "page": "41"},
  "level": null
}
```

`type` обязателен. `level` — только для `section` (1–4; без него — как
у ближайшего заголовка выше). Тело до 256 КБ. `block_id` назначается
сам по типу и заголовку (`thm-3-1`, `eq-…`, `blk-…`) и потом не меняется —
на него можно ссылаться сразу после создания.

Правка — `PATCH /blocks/{block}`; присылайте только то, что меняете.
`attrs` заменяются целиком, поэтому читайте блок, меняйте нужное поле и
отправляйте весь объект. Каждая правка тела попадает в историю версий.

Чтобы не стереть чужую правку (блок правят вместе, на сайте и через API),
присылайте `rev` — отпечаток блока из последнего чтения:
`PATCH /blocks/{block} {"body": "…", "rev": "5f0c…"}`. Если блок с тех пор
изменили, ответ — `409` с `{conflict: true, by: "@ник", block: {…}}`:
возьмите свежий блок, наложите свою правку и пришлите снова. Без `rev`
правка записывается как есть.

Типы и атрибуты — коротко (полностью в `/help/uip-format`, раздел 6):

| тип | заголовок | attrs |
|---|---|---|
| `section` | да | — (уровень в `level`) |
| `paragraph`, `hint`, `figure`, `ipynb` | нет | `page` |
| `definition`, `axiom`, `theorem`, `law`, `lemma`, `proof`, `example`, `remark`, `table`, `solution`, `answer` | да | `page` |
| `equation` | нет | `tag` (номер из книги), `page` |
| `code` | да | `lang` (`python`, `matlab`, `c`, …), `page` |
| `tikz` | нет | `tikz` — ставит сервер: `{image, error, src}`, руками не трогать |
| `quote` | нет | `author`, `page` |
| `problem` | да | `solution`, `answer`, `page` |
| `reference` | нет | `kind` (`book`/`article`/`standard`/`web`/`package`) и поля описания по ГОСТ, см. 6.7 |

Тело — Markdown: формулы `$…$` (KaTeX), картинки
`![подпись](uip://#asset:p55-fig1)`, ссылки на блоки
`[текст](uip://#eq-fourier)` (подпись заменится номером формулы или
источника). У `figure` в теле только картинки, у `reference` тело
пустое, у `code` и `ipynb` — код без ```-забора, у `tikz` — код рисунка.

**TikZ.** Тело блока `tikz` — код рисунка: достаточно содержимого
`tikzpicture` (окружение сервер поставит сам), можно и целиком
`\begin{tikzpicture}…`, `tikzcd`, `circuitikz`, `pgfpicture` или даже
документ с `\documentclass` — преамбула возьмётся из него. Строки
`\usetikzlibrary`, `\usepackage`, `\pgfplotsset`, `\tikzset`,
`\definecolor`, `\newcommand` можно писать прямо в теле. Кириллица,
amsmath, pgfplots есть. Сервер собирает рисунок в SVG (latex → dvisvgm)
при каждой правке тела (`PATCH`) и кладёт его вложением пакета; в
`attrs.tikz.image` — ссылка на картинку, в `attrs.tikz.error` — лог
ошибки, если не собралось (прежняя картинка тогда остаётся). Только что
созданный (`POST …/blocks`) или импортированный рисунок ещё не собран — `POST /blocks/{id}/run`
собирает заново и отвечает `{ok, outputs: {image, error, ms}, block}`.

**Формулы** в `equation` — без `$$`: тело — сама формула. Обёртку
`$$…$$` или `\[…\]` сервер снимет, но лучше её не писать.

**Несколько правок разом** — `POST /packages/{package}/edit` с `{"ops": [...]}`
(до 40 действий, всё или ничего, каждое — в журнал пакета). Блоки — по
`block_id`. Действия:

| op | поля | что делает |
|---|---|---|
| `replace` | `block`, `find`, `replace`, `field?` | заменить кусок текста (ровно как в блоке, ровно одно вхождение) в `body`, `title` или поле attrs (`solution`, `answer`…) |
| `update` | `block`, `title?`, `body?`, `solution?`, `answer?`, `level?`, `type?` | задать поля блока целиком |
| `insert` | `type`, `title?`, `body?`, `solution?`, `answer?`, `after?` или `part?` | новый блок после `after` или в начало части |
| `delete` | `block` | удалить блок |
| `move` | `block`, `after?`, `part?` | переставить блок |
| `add_part` | `title` | новая часть |
| `rename_part` | `part`, `title` | переименовать часть |
| `delete_part` | `part` | удалить часть вместе с её блоками (последнюю — нельзя) |
| `move_part` | `part`, `after_part?` | поставить часть после другой (без неё — первой) |
| `set_package` | `title?`, `subtitle?`, `package_type?` | свойства пакета |
| `tags` | `tags?`, `remove?` | повесить и снять теги: «Род: Тег» или «Тег»; своих нет — заведутся; теги каталога — только владельцу сайта |
| `folder` | `folder` | переложить пакет в папку (имя или путь «А/Б»; нет — создастся) |
| `delete_package` | — | пакет в корзину; только владельцу и только последним действием |

```json
{"ops": [{"op": "replace", "block": "prb-19", "field": "answer", "find": "Q_x=-DA", "replace": "Q_x=DA"}]}
```

Ответ: `{ok, message, count}`; ошибка в любом действии — `422` и пакет не тронут.

## 6. Массовая заливка: импорт Markdown

Собирать книгу по одному блоку — сотни запросов. Быстрее прислать
Markdown в разметке UIP (`/help/uip-format`): разделы `#…####`,
огороженные блоки `::: {.theorem title="…"} … :::`, абзацы пустыми
строками.

```
POST /packages/{package}/import
Content-Type: application/json

{"markdown": "# 3.1. Гладкие оболочки {#sec-3-1}\n\nТекст…\n\n::: {#eq-1 .equation tag=\"3.1\"}\nq = -\\lambda \\nabla T\n:::\n", "part": 12}
```

Ответ: `{"ok": true, "blocks": 57, "sections": 4}`. Без `part` — новая
часть; заголовок `# … {.part}` внутри текста заводит части сам. Можно
прислать файлом: `multipart`, поле `file` (до 8 МБ). Импорт **добавляет**
блоки в конец, ничего не стирает: чтобы заменить часть, удалите её и
импортируйте заново.

Пакет целиком из файла — `POST /packages/import`, `multipart`:
`files[]` — один `.uip` (zip с `uip.yaml`, `content/*.md`, `assets/`)
или один `.md`, либо несколько файлов папки с их путями в `paths[]`.
Ответ — `201` и `{"package": {…}}`. Вложения из архива подхватываются
вместе с манифестом.

## 7. Вложения (файлы любого вида)

Вложение — любой файл: картинка к рисунку, PDF-источник, таблица данных,
архив. Хранится по содержимому (sha256): одинаковый файл в двух пакетах
лежит один раз. До 200 МБ.

| Метод и путь | Что делает |
|---|---|
| `GET /packages/{package}/assets` | список |
| `POST /packages/{package}/assets` | загрузить |
| `GET /assets/{asset}` | описание |
| `GET /assets/{asset}/content` | сам файл (на S3 — редирект на временную ссылку; следуйте за ним) |
| `PATCH /assets/{asset}` | `asset_id`, `name`, `indexable` |
| `DELETE /assets/{asset}` | убрать из пакета; объект удаляется, если больше нигде не нужен |

Загрузка — двумя способами:

```
# multipart: поле file, необязательно name, mime, indexable
curl -H "Authorization: Bearer fz_…" -F "file=@fig1.png" -F "name=p55-fig1.png" \
     https://fizmat.ai/api/v1/packages/{package}/assets

# сырое тело: имя в ?name=, тип в Content-Type
curl -H "Authorization: Bearer fz_…" -H "Content-Type: image/png" --data-binary @fig1.png \
     "https://fizmat.ai/api/v1/packages/{package}/assets?name=p55-fig1.png"
```

Ответ `201`: `{"asset": {"id": 5, "asset_id": "p55-fig1", "reference": "#asset:p55-fig1", …}}`.
Если такой файл в пакете уже есть — `200` с `"duplicate": true`.
`asset_id` делается из имени файла (латиница, цифры, дефис); именно на
него ссылаются в тексте: `![Рис. 3.4](uip://#asset:p55-fig1)`. Хотите
свой — переименуйте через `PATCH`. `indexable` — попадает ли текст PDF
в поиск; по умолчанию только у PDF.

## 8. Связи между блоками

Типизированные рёбра графа: «требует знания», «выводится из», «цитирует».
Ссылки в тексте (`uip://…`) становятся рёбрами `cites` сами; здесь —
то, чего в тексте нет.

| Метод и путь | Тело |
|---|---|
| `GET /blocks/{block}/links` | — → `{"outgoing": […], "incoming": […]}` |
| `POST /blocks/{block}/links` | `{"rel": "prerequisite", "target_uri": "uip://ru.gost.2789-73#sec-1", "strength": 0.8, "note": "…"}` |
| `POST /links/{link}/verify` | — (переключает отметку «проверено») |
| `DELETE /links/{link}` | — |
| `POST /packages/{package}/links/reresolve` | — (пересчитать, куда ведут ссылки: целевой пакет мог появиться позже) |

Виды `rel` — из `GET /me`. Битую ссылку завести можно: она хранится
неразрешённой, пока не появится цель.

## 9. Поиск

```
GET /search?q=тепловой+поток&limit=20
GET /search?q=…&package=ru.bmstu.lizin-pyatkin
GET /search?q=…&types[]=book&blocks[]=theorem&blocks[]=definition&tags[]=3
```

Гибрид: по смыслу (векторы) и по словам, слитые в один список. `package` —
ULID или алиас; `types[]` — типы пакетов; `blocks[]` — типы блоков;
`tags[]` — id тегов (внутри рода — любой из, между родами — все).
`limit` до 100.

```json
{
  "dense": true, "lexical": true,
  "hits": [{
    "score": 0.0325, "similarity": 0.81, "found_by": ["смысл", "слова"],
    "breadcrumb": ["ГЛАВА 3. …", "3.1. …"],
    "package": {"id": "01m30…", "alias": "ru.bmstu.lizin-pyatkin", "title": "…", "type": "book"},
    "block": {"id": 345, "block_id": "def-teplovoy-potok", "type": "definition", "title": "…", "body": "…", "…": "…"}
  }]
}
```

`dense: false` значит, что векторов у запроса не было (эмбеддер не
установлен) и искали только по словам.

**Индекс.** Новые и изменённые блоки попадают в векторный поиск после
индексации; она идёт по расписанию сама, но после большой заливки можно
подтолкнуть:

| Метод и путь | Что делает |
|---|---|
| `GET /embeddings` | `{"pending": 120, "stored": 4800, "blocks": 4920}` — сколько ждёт |
| `POST /embeddings/run` | посчитать одну пачку; зовите, пока `pending` не станет 0 |
| `POST /embeddings/rebuild` | сбросить индекс целиком (пересчёт всего — долго; только если поменялась модель) |

Поиск по словам работает сразу, без индексации.

## 10. Теги

Тег вешается на пакет целиком; теги собраны в роды («Предмет», «Курс»).

| Метод и путь | Тело |
|---|---|
| `GET /tags` | — → `{"types": [{"id": 1, "title": "Предмет", "color": "sky", "tags": [{"id": 3, "title": "Физика"}]}]}` |
| `POST /tag-types` | `{"title": "Курс", "color": "amber"}` (цвета: rose, amber, lime, emerald, sky, blue, violet, stone) |
| `PATCH /tag-types/{type}` | то же |
| `DELETE /tag-types/{type}` | только пустой род |
| `POST /tag-types/{type}/tags` | `{"title": "Теплотехника"}` |
| `PATCH /tags/{tag}` | `{"title": "…"}` |
| `DELETE /tags/{tag}` | только тег, который не стоит ни на одном пакете |
| `PUT /packages/{package}/tags` | `{"tags": [3, 7]}` — теги пакета целиком |

## 11. Распознавание книг (OCR)

PDF или скан отдаётся облачному конвертеру (Datalab marker), из ответа
собирается пакет: части по главам, формулы, таблицы, рисунки. Платно
(центы за страницу), идёт минуты.

```
POST /ocr   multipart: file (pdf/png/jpeg/webp), title, mode (fast|balanced|accurate),
            page_range ("1-40, 55"), chapters (1/0 — делить на части по главам),
            typing (1/0 — угадывать типы блоков), package_id + part_id (дописать в существующий пакет)
```

Ответ `201`: `{"job": {"id": 7, "status": "queued", "label": "В очереди", "active": true, …}}`.

| Метод и путь | Что делает |
|---|---|
| `GET /ocr` | последние задания со статусом (`queued` → `processing` → `importing` → `done` / `failed`), стоимостью и пакетом |
| `POST /ocr/{job}/rebuild` | пересобрать пакет из сохранённого Markdown — бесплатно (`chapters`, `typing` можно поменять) |
| `DELETE /ocr/{job}` | убрать задание |

Опрашивайте `GET /ocr` раз в полминуты, пока `active`. Готовый пакет —
в `job.package`; дальше правьте его как любой другой.

## 12. Корзина и резервные копии

**Корзина.** `DELETE /packages/{package}` не стирает пакет, а переносит
его в корзину целиком — с частями, блоками, историей правок, связями,
вложениями и тегами. Там он лежит 30 дней и возвращается на прежнее место
с прежними адресами (`uip://…`, номера блоков), потом уходит насовсем.

| Метод и путь | Что делает |
|---|---|
| `GET /trash` | `{"days": 30, "trash": [{"id": 4, "package_id": "01m…", "alias": "…", "title": "…", "blocks": 444, "assets": 46, "deleted_by": "…", "deleted_at": "…", "expires_at": "…"}]}` |
| `POST /trash/{id}/restore` | вернуть → `{"package": {…}, "copy": false, "missing_files": 0}` |
| `DELETE /trash/{id}` | стереть насовсем (только ключ владельца сайта) |

`copy: true` — пакет с таким id уже есть (его завели заново), и
возвращённый встал рядом копией: новый id, без алиаса, «(восстановлен)» в
названии. `missing_files` — сколько файлов вложений не нашлось ни в
основном бакете, ни во втором.

**Резервные копии** — только для ключа владельца сайта (`super_admin: true`
в `/me`): в архиве лежит выгрузка базы и `.env`. Копия делается каждую ночь
и лежит во втором бакете, отдельно от данных; в ней, кроме выгрузки всей
базы, — снимок каждого пакета по отдельности, поэтому пакет можно вернуть
из копии один, не откатывая остальное.

| Метод и путь | Что делает |
|---|---|
| `GET /backups` | `{"keep": 3, "backups": [{"name": "fizmat-2026-09-22-0300", "made_at": "…", "note": "…", "size": 1234567, "tables": {…}, "packages": [{"id": "…", "title": "…", "blocks": 444}], "archive": true}]}` |
| `POST /backups` | сделать копию сейчас → `201` и манифест |
| `GET /backups/{name}/download` | скачать `tar.gz` |
| `GET /backups/{name}/packages` | какие пакеты в копии: `{"packages": [{"id", "alias", "title", "type", "parts", "blocks", "assets"}]}` |
| `POST /backups/{name}/packages/{id}/restore` | вернуть один пакет из копии → `201`, ответ как у корзины |
| `DELETE /backups/{name}` | удалить |

Копии старше заданного числа (`keep`) уходят сами после каждой новой. В
копиях, сделанных до 23.09.2026, снимков пакетов нет — из них
восстанавливается только база целиком (на сервере: `deploy/restore.sh`).

## 12а. Моё хранилище: папки, доступ, каталог

Всё это — то же, что на сайте в разделе «Хранилище».

**Какие пакеты:** `GET /packages` — всё моё хранилище; `?scope=own` —
только свои, `?scope=shared` — те, которыми со мной поделились,
`?scope=catalog` — весь каталог. `?folder=<id>` — пакеты одной папки,
`?folder=root` — свои без папки. У каждого пакета в списке — `folder_id`
(null — в корне).

**Папки** (личные, у каждого свои; имена на одном уровне не повторяются):

```
GET    /folders                         → {folders: [{id, name, parent_id, position}]}
POST   /folders        {name, parent_id?}             — 422, если такое имя рядом уже есть
PATCH  /folders/{id}   {name}
DELETE /folders/{id}                    — в корзину со всем содержимым
POST   /move           {folder_id|null, items: [{type: "package"|"folder", id}]}
POST   /trash/{id}/restore              — папка из корзины возвращается целиком
```

**Личные теги** (видны только вам):

```
POST   /my-tag-types             {title, color}
POST   /my-tag-types/{id}/tags   {title}
DELETE /my-tags/{id}
PUT    /packages/{id}/my-tags    {tags: [id…]}   — свои теги на пакете целиком
```

**Поделиться** (владельцу пакета):

```
GET    /packages/{id}/share                    → {owner, people, link}
POST   /packages/{id}/share/people   {who: "@ник", role: "viewer"|"editor"}
PATCH  /packages/{id}/share/people/{grant}   {role}
DELETE /packages/{id}/share/people/{grant}
POST   /packages/{id}/share/link     {role: "viewer"|"editor"|null}
```

Читателю и редактору: `POST /packages/{id}/copy` — копия себе,
`POST /packages/{id}/leave` — покинуть.

**Каталог.** Пакеты каталога — `GET /packages?scope=catalog`.

```
POST /packages/{id}/catalog           {tags: [id тегов каталога…]}
                                      — предложить свой пакет (нужен alias) или его новую версию
POST /packages/{id}/catalog/retract   — автору: отозвать заявку или снять пакет с каталога
POST /packages/{id}/catalog/add       → {package, url} — «Добавить к себе»
POST /packages/{id}/catalog/remove    — убрать пакет каталога из своего списка
```

В каталог уходит копия пакета, её проверяют модераторы; `{id}` у `add` —
пакет каталога. `add` кладёт в ваше хранилище свою копию: её можно
править, у неё `source_id` — пакет каталога; ещё раз — ещё одна копия.
Отзыв каталога не трогает копии, уже добавленные другими.

Модераторам (`{id}` — кандидат или пакет каталога):

```
POST /packages/{id}/catalog/approve
POST /packages/{id}/catalog/reject     {note?}   — отклонить заявку
POST /packages/{id}/catalog/withdraw   {note?}   — снять пакет с каталога
```

**Alias** пакета — `ник.имя`: после вашего ника и точки латиница в нижнем
регистре, цифры, `_` и `-`.

## 13. Типичные сценарии

**Залить книгу из Markdown.**
1. `POST /packages` — паспорт (`title`, `type: book`, `alias`, `authors`).
2. Картинки: `POST /packages/{p}/assets` на каждую; запомните `asset_id`.
3. `POST /packages/{p}/import` с Markdown главы — по запросу на главу
   (`# ГЛАВА 1. … {.part}` первой строкой, или без него, тогда каждая
   заливка — новая часть).
4. `GET /packages/{p}/blocks?part=…` — проверить, что нарезалось как
   задумано; поправить `PATCH /blocks/{id}`.
5. `POST /embeddings/run`, пока `pending` не 0.

**Найти и поправить.** `GET /search?q=…` → `hits[].block.id` →
`GET /blocks/{id}` → `PATCH /blocks/{id}` с новым `body`.

**Перенести блок в другую часть.** `POST /blocks/{id}/move` с
`{"part": 13, "after": null}` — в начало части 13.

**Скопировать пакет на другой сайт.** `GET /packages/{p}/export` →
файл `.uip` → там `POST /packages/import`.

**Вернуть удалённый пакет.** `GET /trash` → `POST /trash/{id}/restore`.
Если его уже нет в корзине — `GET /backups` → выбрать копию, где он есть
(`packages`), → `POST /backups/{name}/packages/{id}/restore`.

## 14. Ограничения

- 600 запросов в минуту на ключ.
- Вложение — до 200 МБ; Markdown в `import` — до 4 МБ текстом, до 8 МБ файлом; `.uip` — до 200 МБ.
- Тело блока — до 256 КБ, `attrs` — до 256 КБ.
- Блоки в `GET /packages/{p}/blocks` отдаются все разом: у книги на
  4000 блоков это несколько мегабайт — берите по частям (`?part=`).
- Ключ не истекает; отозвать — на странице «Доступ → API».
